Merge "Make ImeOptions.hintLocales non-nullable" into androidx-main
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentDialogTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentDialogTest.kt
index b914f18..9517064e 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentDialogTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentDialogTest.kt
@@ -31,7 +31,6 @@
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import leakcanary.DetectLeaksAfterTestSuccess
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -44,7 +43,6 @@
@get:Rule
val rule = DetectLeaksAfterTestSuccess()
- @Ignore("b/286303870")
@Test
fun testLifecycle() {
withUse(ActivityScenario.launch(EmptyContentActivity::class.java)) {
diff --git a/activity/integration-tests/testapp/build.gradle b/activity/integration-tests/testapp/build.gradle
index 24d7a2e..ce12c4a 100644
--- a/activity/integration-tests/testapp/build.gradle
+++ b/activity/integration-tests/testapp/build.gradle
@@ -35,6 +35,7 @@
implementation("androidx.core:core-splashscreen:1.0.0")
// Manually align dependencies across debugRuntime and debugAndroidTestRuntime.
+ androidTestImplementation(project(":annotation:annotation"))
androidTestImplementation("androidx.annotation:annotation-experimental:1.4.0")
androidTestImplementation(libs.kotlinStdlib)
diff --git a/appcompat/integration-tests/receive-content-testapp/build.gradle b/appcompat/integration-tests/receive-content-testapp/build.gradle
index 0a9360b..118297f 100644
--- a/appcompat/integration-tests/receive-content-testapp/build.gradle
+++ b/appcompat/integration-tests/receive-content-testapp/build.gradle
@@ -35,6 +35,7 @@
implementation(libs.material)
// Align dependencies in debugRuntimeClasspath and debugAndroidTestRuntimeClasspath.
+ androidTestImplementation(project(":annotation:annotation"))
androidTestImplementation("androidx.annotation:annotation-experimental:1.4.0")
androidTestImplementation("androidx.lifecycle:lifecycle-common:2.6.1")
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
index ae1c025..583f424 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
@@ -226,6 +226,9 @@
private fun checkAgpVersion() {
val agpVersion = project.agpVersion()
+ if (agpVersion.previewType == "dev") {
+ return // Skip version check for androidx-studio-integration branch
+ }
if (agpVersion < minAgpVersionInclusive) {
throw GradleException(
"""
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
index 561d71c..7eda4fc 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
@@ -34,6 +34,7 @@
import perfetto.protos.TraceConfig
import perfetto.protos.TraceConfig.BufferConfig
import perfetto.protos.TraceConfig.BufferConfig.FillPolicy
+import perfetto.protos.TrackEventConfig
/**
* Configuration for Perfetto trace recording.
@@ -333,7 +334,12 @@
TraceConfig.DataSource(DataSourceConfig("android.gpu.memory")),
TraceConfig.DataSource(DataSourceConfig("android.surfaceflinger.frame")),
TraceConfig.DataSource(DataSourceConfig("android.surfaceflinger.frametimeline")),
- TraceConfig.DataSource(DataSourceConfig("track_event")) // required by tracing-perfetto
+ TraceConfig.DataSource(DataSourceConfig(
+ "track_event",
+ track_event_config = TrackEventConfig(
+ enabled_categories = listOf("*") // required by tracing-perfetto
+ )
+ ))
)
if (stackSamplingConfig != null) {
dataSources += stackSamplingSource(
diff --git a/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml b/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml
new file mode 100644
index 0000000..dcdc3f1
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha12" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha12)" variant="all" version="8.4.0-alpha12">
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" val xcodeGenUri = when (val uri = project.findProperty(XCODEGEN_DOWNLOAD_URI)) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="WithTypeWithoutConfigureEach"
+ message="Avoid passing a closure to withType, use withType().configureEach instead"
+ errorLine1=" project.plugins.withType(KotlinMultiplatformPluginWrapper::class.java) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt"/>
+ </issue>
+
+</issues>
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/FetchXCodeGenTask.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/FetchXCodeGenTask.kt
index 6b8d010..b1c0f80 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/FetchXCodeGenTask.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/FetchXCodeGenTask.kt
@@ -19,6 +19,7 @@
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
+import java.io.Serializable
import java.net.URI
import java.util.zip.ZipInputStream
import org.gradle.api.DefaultTask
@@ -120,8 +121,14 @@
}
fun xcodeGenBinary(): RegularFile {
- return RegularFile {
- findXcodeGen()
+ return LocalRegularFile(findXcodeGen())
+ }
+
+ companion object {
+ class LocalRegularFile(private val file: File) : RegularFile, Serializable {
+ override fun getAsFile(): File {
+ return file.absoluteFile
+ }
}
}
}
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/InstrumentationResultsRunListener.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/InstrumentationResultsRunListener.kt
index 49a7697..8556260 100644
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/InstrumentationResultsRunListener.kt
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/InstrumentationResultsRunListener.kt
@@ -17,6 +17,7 @@
package androidx.benchmark.junit4
import android.os.Bundle
+import android.util.Log
import androidx.annotation.RestrictTo
import androidx.benchmark.InstrumentationResults
import androidx.test.internal.runner.listener.InstrumentationRunListener
@@ -27,16 +28,16 @@
* Used to register files to copy at the end of the entire test run in CI.
*
* See [InstrumentationResults.runEndResultBundle]
- *
*/
@Suppress("unused", "RestrictedApiAndroidX") // referenced by inst arg at runtime
@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class InstrumentationResultsRunListener : InstrumentationRunListener() {
+class InstrumentationResultsRunListener : InstrumentationRunListener() {
override fun instrumentationRunFinished(
streamResult: PrintStream?,
resultBundle: Bundle,
junitResults: Result?
) {
+ Log.d("Benchmark", "InstrumentationResultsRunListener#instrumentationRunFinished")
resultBundle.putAll(InstrumentationResults.runEndResultBundle)
}
}
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/SideEffectRunListener.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/SideEffectRunListener.kt
index fa14e63..ee34dc9 100644
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/SideEffectRunListener.kt
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/SideEffectRunListener.kt
@@ -16,6 +16,7 @@
package androidx.benchmark.junit4
+import android.util.Log
import androidx.annotation.RestrictTo
import androidx.benchmark.DisableDexOpt
import androidx.benchmark.DisablePackages
@@ -27,6 +28,7 @@
/**
* Enables the use of side-effects that reduce the noise during a benchmark run.
*/
+@Suppress("unused") // referenced by inst arg at runtime
@RestrictTo(RestrictTo.Scope.LIBRARY)
class SideEffectRunListener : RunListener() {
private val delegate: RunListenerDelegate = RunListenerDelegate(
@@ -38,11 +40,13 @@
override fun testRunStarted(description: Description) {
super.testRunStarted(description)
+ Log.d("Benchmark", "SideEffectRunListener#onTestRunStarted")
delegate.onTestRunStarted()
}
override fun testRunFinished(result: Result) {
super.testRunFinished(result)
+ Log.d("Benchmark", "SideEffectRunListener#onTestRunFinished")
delegate.onTestRunFinished()
}
}
diff --git a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/InstrumentationResultsRunListener.kt b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/InstrumentationResultsRunListener.kt
new file mode 100644
index 0000000..d93e294
--- /dev/null
+++ b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/InstrumentationResultsRunListener.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.junit4
+
+import android.os.Bundle
+import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.benchmark.InstrumentationResults
+import androidx.test.internal.runner.listener.InstrumentationRunListener
+import java.io.PrintStream
+import org.junit.runner.Result
+
+/**
+ * Used to register files to copy at the end of the entire test run in CI.
+ *
+ * See [InstrumentationResults.runEndResultBundle]
+ */
+@Suppress("unused", "RestrictedApiAndroidX") // referenced by inst arg at runtime
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class InstrumentationResultsRunListener : InstrumentationRunListener() {
+ override fun instrumentationRunFinished(
+ streamResult: PrintStream?,
+ resultBundle: Bundle,
+ junitResults: Result?
+ ) {
+ Log.d("Benchmark", "InstrumentationResultsRunListener#instrumentationRunFinished")
+ resultBundle.putAll(InstrumentationResults.runEndResultBundle)
+ }
+}
diff --git a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/SideEffectRunListener.kt b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/SideEffectRunListener.kt
index 3aaa557..17ddf62 100644
--- a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/SideEffectRunListener.kt
+++ b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/SideEffectRunListener.kt
@@ -16,6 +16,7 @@
package androidx.benchmark.macro.junit4
+import android.util.Log
import androidx.annotation.RestrictTo
import androidx.benchmark.DisableDexOpt
import androidx.benchmark.DisablePackages
@@ -27,6 +28,7 @@
/**
* Enables the use of side-effects that reduce the noise during a macro benchmark run.
*/
+@Suppress("unused") // referenced by inst arg at runtime
@RestrictTo(RestrictTo.Scope.LIBRARY)
class SideEffectRunListener : RunListener() {
private val delegate: RunListenerDelegate = RunListenerDelegate(
@@ -38,11 +40,13 @@
override fun testRunStarted(description: Description) {
super.testRunStarted(description)
+ Log.d("Benchmark", "SideEffectRunListener#onTestRunStarted")
delegate.onTestRunStarted()
}
override fun testRunFinished(result: Result) {
super.testRunFinished(result)
+ Log.d("Benchmark", "SideEffectRunListener#onTestRunFinished")
delegate.onTestRunFinished()
}
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
index edefac4..2237b10 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
@@ -363,4 +363,14 @@
)
scope.dropKernelPageCache() // shouldn't crash
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33)
+ fun cancelBackgroundDexopt() {
+ val scope = MacrobenchmarkScope(
+ Packages.TARGET,
+ launchWithClearTask = false
+ )
+ scope.cancelBackgroundDexopt() // shouldn't crash
+ }
}
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
index 014480c..fbff27a 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt
@@ -186,7 +186,8 @@
rowOf(
"name" to "activityStart",
"ts" to 186975009436431L,
- "dur" to 29580628L)
+ "dur" to 29580628L
+ )
),
actual = query(
"SELECT name,ts,dur FROM slice WHERE name LIKE \"activityStart\""
@@ -255,11 +256,13 @@
val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
val startups = PerfettoTraceProcessor.runServer {
loadTrace(PerfettoTrace(traceFile.absolutePath)) {
- query("""
+ query(
+ """
INCLUDE PERFETTO MODULE android.startup.startups;
SELECT * FROM android_startups;
- """.trimIndent()).toList()
+ """.trimIndent()
+ ).toList()
}
}
// minimal validation, just verifying query worked
@@ -364,6 +367,24 @@
assertTrue(!isRunning())
}
+ @Test
+ fun testParseTracesWithProcessTracks() {
+ assumeTrue(isAbiSupported())
+ val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+ PerfettoTraceProcessor.runSingleSessionServer(traceFile.absolutePath) {
+ val slices = querySlices("launching:%", packageName = null)
+ assertEquals(
+ expected = listOf(
+ Slice(
+ name = "launching: androidx.benchmark.integration.macrobenchmark.target",
+ ts = 186974946587883,
+ dur = 137401159
+ )
+ ), slices
+ )
+ }
+ }
+
@LargeTest
@Test
fun parseLongTrace() {
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 a1bb59d..de30795 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
@@ -227,9 +227,15 @@
// Capture if the app being benchmarked is a system app.
scope.isSystemApp = applicationInfo.isSystemApp()
scope.launchWithMethodTracing = launchWithMethodTracing
+
// Ensure the device is awake
scope.device.wakeUp()
+ // Stop Background Dexopt during a Macrobenchmark to improve stability.
+ if (Build.VERSION.SDK_INT >= 33) {
+ scope.cancelBackgroundDexopt()
+ }
+
// Always kill the process at beginning of test
scope.killProcess()
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
index 3cc10550..5834fe3 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
@@ -168,12 +168,12 @@
packageName
))
) {
- isMethodTracing = true
- val tracePath = methodTraceRecordPath(packageName)
- "--start-profiler \"$tracePath\" --streaming"
- } else {
- ""
- }
+ isMethodTracing = true
+ val tracePath = methodTraceRecordPath(packageName)
+ "--start-profiler \"$tracePath\" --streaming"
+ } else {
+ ""
+ }
val cmd = "am start $profileArgs -W \"$uri\""
Log.d(TAG, "Starting activity with command: $cmd")
@@ -228,9 +228,11 @@
Thread.sleep(100)
}
}
- throw IllegalStateException("Unable to confirm activity launch completion $lastFrameStats" +
- " Please report a bug with the output of" +
- " `adb shell dumpsys gfxinfo $packageName framestats`")
+ throw IllegalStateException(
+ "Unable to confirm activity launch completion $lastFrameStats" +
+ " Please report a bug with the output of" +
+ " `adb shell dumpsys gfxinfo $packageName framestats`"
+ )
}
/**
@@ -483,6 +485,32 @@
}
}
+ /**
+ * Cancels the job responsible for running background `dexopt`.
+ *
+ * Background `dexopt` is a CPU intensive operation that can interfere with benchmarks.
+ * By cancelling this job, we ensure that this operation will not interfere with the benchmark,
+ * and we get stable numbers.
+ */
+ @RequiresApi(33)
+ internal fun cancelBackgroundDexopt() {
+ val result = if (Build.VERSION.SDK_INT >= 34) {
+ Shell.executeScriptCaptureStdout("pm bg-dexopt-job --cancel")
+ } else {
+ // This command is deprecated starting Android U, and is just an alias for the
+ // command above. More info in the link below.
+ // https://cs.android.com/android/platform/superproject/main/+/main:art/libartservice/service/java/com/android/server/art/ArtShellCommand.java;l=123;drc=93f35d39de15c555b0ddea16121b0ee3f0aa9f91
+ Shell.executeScriptCaptureStdout("pm cancel-bg-dexopt-job")
+ }
+ // We expect one of the following messages in stdout.
+ val expected = listOf("Success", "Background dexopt job cancelled")
+ if (expected.none { it == result.trim() }) {
+ throw IllegalStateException(
+ "Failed to cancel background dexopt job, result: '$result'"
+ )
+ }
+ }
+
internal companion object {
fun getShaderCachePath(packageName: String): String {
val context = InstrumentationRegistry.getInstrumentation().context
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 13876b5..38076ff 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
@@ -94,15 +94,23 @@
private fun findEndRenderTimeForUiFrame(
uiSlices: List<Slice>,
rtSlices: List<Slice>,
+ predicateErrorLabel: String,
predicate: (Slice) -> Boolean
): Long {
// find first UI slice that corresponds with the predicate
- val uiSlice = uiSlices.first(predicate)
+ val uiSlice = uiSlices.firstOrNull(predicate)
+
+ check(uiSlice != null) { "No Choreographer#doFrame $predicateErrorLabel" }
// find corresponding rt slice
- val rtSlice = rtSlices.first { rtSlice ->
+ val rtSlice = rtSlices.firstOrNull { rtSlice ->
rtSlice.ts > uiSlice.ts
}
+
+ check(rtSlice != null) {
+ "No RT frame slice associated with UI thread frame slice $predicateErrorLabel"
+ }
+
return rtSlice.endTs
}
@@ -140,7 +148,7 @@
val rtSlices = groupedData.getOrElse(StartupSliceType.FrameRenderThread) { listOf() }
if (uiSlices.isEmpty() || rtSlices.isEmpty()) {
- Log.d("Benchmark", "No UI / RT slices seen, not reporting startup.")
+ Log.w("Benchmark", "No UI / RT slices seen, not reporting startup.")
return null
}
@@ -151,13 +159,23 @@
val launchingSlice = groupedData[StartupSliceType.Launching]?.firstOrNull {
// verify full name only on API 23+, since before package name not specified
(captureApiLevel < 23 || it.name == "launching: $targetPackageName")
- } ?: return null
+ } ?: run {
+ Log.w("Benchmark", "No launching slice seen, not reporting startup.")
+ return null
+ }
startTs = if (captureApiLevel >= 29) {
// Starting on API 29, expect to see 'notify started' system_server slice
val notifyStartedSlice = groupedData[StartupSliceType.NotifyStarted]?.lastOrNull {
it.ts < launchingSlice.ts
- } ?: return null
+ } ?: run {
+ Log.w(
+ "Benchmark",
+ "No launchObserverNotifyIntentStarted slice seen before launching: " +
+ "slice, not reporting startup."
+ )
+ return null
+ }
notifyStartedSlice.ts
} else {
launchingSlice.ts
@@ -167,15 +185,26 @@
// both because on some platforms the launching slice may not wait for renderthread, but
// also because this allows us to make the guarantee that timeToInitialDisplay ==
// timeToFirstDisplay when they are the same frame.
- initialDisplayTs = findEndRenderTimeForUiFrame(uiSlices, rtSlices) { uiSlice ->
+ initialDisplayTs = findEndRenderTimeForUiFrame(
+ uiSlices = uiSlices,
+ rtSlices = rtSlices,
+ predicateErrorLabel = "after launching slice"
+ ) { uiSlice ->
uiSlice.ts > launchingSlice.ts
}
} else {
// Prior to API 29, hot starts weren't traced with the launching slice, so we do a best
// guess - the time taken to Activity#onResume, and then produce the next frame.
startTs = groupedData[StartupSliceType.ActivityResume]?.first()?.ts
- ?: return null
- initialDisplayTs = findEndRenderTimeForUiFrame(uiSlices, rtSlices) { uiSlice ->
+ ?: run {
+ Log.w("Benchmark", "No activityResume slice, not reporting startup.")
+ return null
+ }
+ initialDisplayTs = findEndRenderTimeForUiFrame(
+ uiSlices = uiSlices,
+ rtSlices = rtSlices,
+ predicateErrorLabel = "after activityResume"
+ ) { uiSlice ->
uiSlice.ts > startTs
}
}
@@ -185,7 +214,11 @@
val reportFullyDrawnEndTs: Long? = reportFullyDrawnSlice?.let {
// find first uiSlice with end after reportFullyDrawn (reportFullyDrawn may happen
// during or before a given frame)
- findEndRenderTimeForUiFrame(uiSlices, rtSlices) { uiSlice ->
+ findEndRenderTimeForUiFrame(
+ uiSlices = uiSlices,
+ rtSlices = rtSlices,
+ predicateErrorLabel = "ends after reportFullyDrawn"
+ ) { uiSlice ->
uiSlice.endTs > reportFullyDrawnSlice.ts
}
}
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
index 1960762..1974f7c 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/PerfettoTraceProcessor.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/perfetto/PerfettoTraceProcessor.kt
@@ -374,11 +374,11 @@
},
postfix = ")"
) {
- "slice.name LIKE \"$it\""
+ "slice_name LIKE \"$it\""
}
val innerJoins = if (packageName != null) {
"""
- INNER JOIN thread_track on slice.track_id = thread_track.id
+ INNER JOIN thread_track ON slice.track_id = thread_track.id
INNER JOIN thread USING(utid)
INNER JOIN process USING(upid)
""".trimMargin()
@@ -386,15 +386,32 @@
""
}
+ val processTrackInnerJoins = """
+ INNER JOIN process_track ON slice.track_id = process_track.id
+ INNER JOIN process USING(upid)
+ """.trimIndent()
+
return query(
query = """
- SELECT slice.name,ts,dur
+ SELECT slice.name AS slice_name,ts,dur
FROM slice
$innerJoins
WHERE $whereClause
+ UNION
+ SELECT process_track.name AS slice_name,ts,dur
+ FROM slice
+ $processTrackInnerJoins
+ WHERE $whereClause
ORDER BY ts
- """.trimMargin()
- ).toSlices()
+ """.trimIndent()
+ ).map { row ->
+ // Using an explicit mapper here to account for the aliasing of `slice_name`
+ Slice(
+ name = row.string("slice_name"),
+ ts = row.long("ts"),
+ dur = row.long("dur")
+ )
+ }.toList()
}
}
diff --git a/benchmark/gradle-plugin/lint-baseline.xml b/benchmark/gradle-plugin/lint-baseline.xml
index e8cbd60..b42bedc 100644
--- a/benchmark/gradle-plugin/lint-baseline.xml
+++ b/benchmark/gradle-plugin/lint-baseline.xml
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.3.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-beta01)" variant="all" version="8.3.0-beta01">
+<issues format="6" by="lint 8.4.0-alpha12" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha12)" variant="all" version="8.4.0-alpha12">
<issue
id="EagerGradleConfiguration"
- message="Avoid using eager method findByName"
+ message="Avoid using method findByName"
errorLine1=" if (project.rootProject.tasks.findByName("lockClocks") == null) {"
errorLine2=" ~~~~~~~~~~">
<location
@@ -12,11 +12,29 @@
<issue
id="EagerGradleConfiguration"
- message="Avoid using eager method findByName"
+ message="Avoid using method findByName"
errorLine1=" if (project.rootProject.tasks.findByName("unlockClocks") == null) {"
errorLine2=" ~~~~~~~~~~">
<location
file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
</issue>
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" if (!project.findProperty(ADDITIONAL_TEST_OUTPUT_KEY).toString().toBoolean()) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" project.findProperty("androidx.benchmark.lockClocks.cores")?.toString() ?: """
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
+ </issue>
+
</issues>
diff --git a/benchmark/integration-tests/macrobenchmark/build.gradle b/benchmark/integration-tests/macrobenchmark/build.gradle
index 07b8c7f..ec90c5b 100644
--- a/benchmark/integration-tests/macrobenchmark/build.gradle
+++ b/benchmark/integration-tests/macrobenchmark/build.gradle
@@ -57,7 +57,7 @@
androidComponents { beforeVariants(selector().all()) { enabled = buildType == 'release' } }
dependencies {
- implementation(project(":benchmark:benchmark-junit4"))
+ implementation(project(":benchmark:benchmark-common"))
implementation(project(":benchmark:benchmark-macro-junit4"))
implementation(project(":internal-testutils-macrobenchmark"))
implementation(project(":tracing:tracing-ktx"))
diff --git a/biometric/biometric/src/main/res/values-b+sr+Latn/strings.xml b/biometric/biometric/src/main/res/values-b+sr+Latn/strings.xml
index de4ee97..68e7ac4 100644
--- a/biometric/biometric/src/main/res/values-b+sr+Latn/strings.xml
+++ b/biometric/biometric/src/main/res/values-b+sr+Latn/strings.xml
@@ -33,15 +33,15 @@
<string name="use_fingerprint_label" msgid="6961788485681412417">"Koristite otisak prsta"</string>
<string name="use_face_label" msgid="6533512708069459542">"Koristite lice"</string>
<string name="use_biometric_label" msgid="6524145989441579428">"Koristite biometriju"</string>
- <string name="use_screen_lock_label" msgid="5459869335976243512">"Koristi zaključavanje ekrana"</string>
- <string name="use_fingerprint_or_screen_lock_label" msgid="7577690399303139443">"Koristite otisak prsta ili zaključavanje ekrana"</string>
- <string name="use_face_or_screen_lock_label" msgid="2116180187159450292">"Koristite zaključavanje licem ili zaključavanje ekrana"</string>
- <string name="use_biometric_or_screen_lock_label" msgid="5385448280139639016">"Koristite biometriju ili zaključavanje ekrana"</string>
+ <string name="use_screen_lock_label" msgid="5459869335976243512">"Koristi otključavanje ekrana"</string>
+ <string name="use_fingerprint_or_screen_lock_label" msgid="7577690399303139443">"Koristite otisak prsta ili otključavanje ekrana"</string>
+ <string name="use_face_or_screen_lock_label" msgid="2116180187159450292">"Koristite zaključavanje licem ili otključavanje ekrana"</string>
+ <string name="use_biometric_or_screen_lock_label" msgid="5385448280139639016">"Koristite biometriju ili otključavanje ekrana"</string>
<string name="fingerprint_prompt_message" msgid="7449360011861769080">"Nastavite pomoću otiska prsta"</string>
<string name="face_prompt_message" msgid="2282389249605674226">"Potvrdite identitet licem da biste nastavili"</string>
<string name="biometric_prompt_message" msgid="1160635338192065472">"Koristite biometrijski podatak da biste nastavili"</string>
- <string name="screen_lock_prompt_message" msgid="5659570757430909869">"Upotrebite zaključavanje ekrana da biste nastavili"</string>
- <string name="fingerprint_or_screen_lock_prompt_message" msgid="8382576858490514495">"Koristite otisak prsta ili zaključavanje ekrana da biste nastavili"</string>
- <string name="face_or_screen_lock_prompt_message" msgid="4562557128765735254">"Koristite lice ili zaključavanje ekrana da biste nastavili"</string>
- <string name="biometric_or_screen_lock_prompt_message" msgid="2102429900219199821">"Koristite biometrijski podatak ili zaključavanje ekrana da biste nastavili"</string>
+ <string name="screen_lock_prompt_message" msgid="5659570757430909869">"Upotrebite otključavanje ekrana da biste nastavili"</string>
+ <string name="fingerprint_or_screen_lock_prompt_message" msgid="8382576858490514495">"Koristite otisak prsta ili otključavanje ekrana da biste nastavili"</string>
+ <string name="face_or_screen_lock_prompt_message" msgid="4562557128765735254">"Koristite lice ili otključavanje ekrana da biste nastavili"</string>
+ <string name="biometric_or_screen_lock_prompt_message" msgid="2102429900219199821">"Koristite biometrijski podatak ili otključavanje ekrana da biste nastavili"</string>
</resources>
diff --git a/biometric/biometric/src/main/res/values-sr/strings.xml b/biometric/biometric/src/main/res/values-sr/strings.xml
index 3b8204b..bf059e9 100644
--- a/biometric/biometric/src/main/res/values-sr/strings.xml
+++ b/biometric/biometric/src/main/res/values-sr/strings.xml
@@ -33,15 +33,15 @@
<string name="use_fingerprint_label" msgid="6961788485681412417">"Користите отисак прста"</string>
<string name="use_face_label" msgid="6533512708069459542">"Користите лице"</string>
<string name="use_biometric_label" msgid="6524145989441579428">"Користите биометрију"</string>
- <string name="use_screen_lock_label" msgid="5459869335976243512">"Користи закључавање екрана"</string>
- <string name="use_fingerprint_or_screen_lock_label" msgid="7577690399303139443">"Користите отисак прста или закључавање екрана"</string>
- <string name="use_face_or_screen_lock_label" msgid="2116180187159450292">"Користите закључавање лицем или закључавање екрана"</string>
- <string name="use_biometric_or_screen_lock_label" msgid="5385448280139639016">"Користите биометрију или закључавање екрана"</string>
+ <string name="use_screen_lock_label" msgid="5459869335976243512">"Користи откључавање екрана"</string>
+ <string name="use_fingerprint_or_screen_lock_label" msgid="7577690399303139443">"Користите отисак прста или откључавање екрана"</string>
+ <string name="use_face_or_screen_lock_label" msgid="2116180187159450292">"Користите закључавање лицем или откључавање екрана"</string>
+ <string name="use_biometric_or_screen_lock_label" msgid="5385448280139639016">"Користите биометрију или откључавање екрана"</string>
<string name="fingerprint_prompt_message" msgid="7449360011861769080">"Наставите помоћу отиска прста"</string>
<string name="face_prompt_message" msgid="2282389249605674226">"Потврдите идентитет лицем да бисте наставили"</string>
<string name="biometric_prompt_message" msgid="1160635338192065472">"Користите биометријски податак да бисте наставили"</string>
- <string name="screen_lock_prompt_message" msgid="5659570757430909869">"Употребите закључавање екрана да бисте наставили"</string>
- <string name="fingerprint_or_screen_lock_prompt_message" msgid="8382576858490514495">"Користите отисак прста или закључавање екрана да бисте наставили"</string>
- <string name="face_or_screen_lock_prompt_message" msgid="4562557128765735254">"Користите лице или закључавање екрана да бисте наставили"</string>
- <string name="biometric_or_screen_lock_prompt_message" msgid="2102429900219199821">"Користите биометријски податак или закључавање екрана да бисте наставили"</string>
+ <string name="screen_lock_prompt_message" msgid="5659570757430909869">"Употребите откључавање екрана да бисте наставили"</string>
+ <string name="fingerprint_or_screen_lock_prompt_message" msgid="8382576858490514495">"Користите отисак прста или откључавање екрана да бисте наставили"</string>
+ <string name="face_or_screen_lock_prompt_message" msgid="4562557128765735254">"Користите лице или откључавање екрана да бисте наставили"</string>
+ <string name="biometric_or_screen_lock_prompt_message" msgid="2102429900219199821">"Користите биометријски податак или откључавање екрана да бисте наставили"</string>
</resources>
diff --git a/buildSrc-tests/lint-baseline.xml b/buildSrc-tests/lint-baseline.xml
index 935864f4..e3a6f65 100644
--- a/buildSrc-tests/lint-baseline.xml
+++ b/buildSrc-tests/lint-baseline.xml
@@ -57,7 +57,7 @@
<issue
id="EagerGradleConfiguration"
- message="Avoid using eager method get"
+ message="Avoid using method get"
errorLine1=" val allHostTests = project.tasks.register("allHostTests").get()"
errorLine2=" ~~~">
<location
@@ -147,7 +147,7 @@
<issue
id="EagerGradleConfiguration"
- message="Avoid using eager method findByName"
+ message="Avoid using method findByName"
errorLine1=" if (project.tasks.findByName("check") != null) {"
errorLine2=" ~~~~~~~~~~">
<location
@@ -183,7 +183,7 @@
<issue
id="EagerGradleConfiguration"
- message="Avoid using eager method get"
+ message="Avoid using method get"
errorLine1=" val jvmJarTask = jvmJarTaskProvider.get()"
errorLine2=" ~~~">
<location
@@ -192,7 +192,7 @@
<issue
id="EagerGradleConfiguration"
- message="Avoid using eager method get"
+ message="Avoid using method get"
errorLine1=" .get()"
errorLine2=" ~~~">
<location
@@ -210,7 +210,7 @@
<issue
id="EagerGradleConfiguration"
- message="Avoid using eager method findByName"
+ message="Avoid using method findByName"
errorLine1=" if (project.tasks.findByName("check") != null) {"
errorLine2=" ~~~~~~~~~~">
<location
@@ -237,7 +237,7 @@
<issue
id="EagerGradleConfiguration"
- message="Avoid using eager method findByName"
+ message="Avoid using method findByName"
errorLine1=" tasks.findByName(taskName) ?: throw GradleException("
errorLine2=" ~~~~~~~~~~">
<location
@@ -282,16 +282,7 @@
<issue
id="EagerGradleConfiguration"
- message="Use configureEach instead of all"
- errorLine1=" libraryExtension.libraryVariants.all { variant ->"
- errorLine2=" ~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/resources/PublicResourcesStubHelper.kt"/>
- </issue>
-
- <issue
- id="EagerGradleConfiguration"
- message="Avoid using eager method get"
+ message="Avoid using method get"
errorLine1=" val verifyOutputsTask = verifyOutputs.get()"
errorLine2=" ~~~">
<location
@@ -327,7 +318,7 @@
<issue
id="EagerGradleConfiguration"
- message="Avoid using eager method findByName"
+ message="Avoid using method findByName"
errorLine1=" .findByName(FINALIZE_TEST_CONFIGS_WITH_APKS_TASK)!!"
errorLine2=" ~~~~~~~~~~">
<location
@@ -336,7 +327,7 @@
<issue
id="EagerGradleConfiguration"
- message="Avoid using eager method findByName"
+ message="Avoid using method findByName"
errorLine1=" project.rootProject.tasks.findByName(FINALIZE_TEST_CONFIGS_WITH_APKS_TASK)!!.dependsOn(task)"
errorLine2=" ~~~~~~~~~~">
<location
@@ -344,6 +335,150 @@
</issue>
<issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" rootProject.findProperty(ENABLE_ARG) != "false""
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" rootProject.findProperty(ENABLE_ARG) != "false""
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" val baseCommitOverride: String? = rootProject.findProperty(BASE_COMMIT_ARG) as String?"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" val baseCommitOverride: String? = rootProject.findProperty(BASE_COMMIT_ARG) as String?"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" override val compileSdk: String by lazy { project.findProperty(COMPILE_SDK_VERSION).toString() }"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/AndroidXConfig.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" project.findProperty(TARGET_SDK_VERSION).toString().toInt()"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/AndroidXConfig.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" project.findProperty(ALTERNATIVE_PROJECT_URL) as? String"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXGradleProperties.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" return (project.findProperty(ENABLE_DOCUMENTATION) as? String)?.toBoolean() ?: true"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXGradleProperties.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1="fun Project.findBooleanProperty(propName: String) = (findProperty(propName) as? String)?.toBoolean()"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXGradleProperties.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" return checkNotNull(findProperty(name)) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXPlaygroundRootImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" parseTargetPlatformsFlag(project.findProperty(ENABLED_KMP_TARGET_PLATFORMS) as? String)"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/KmpPlatforms.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" findProperty(DISABLE_COMPILER_DAEMON_FLAG)?.toString()?.toBoolean() == true"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/KonanPrebuiltsSetup.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" val value = project.findProperty(STUDIO_TYPE)?.toString()"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/ProjectLayoutType.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" val value = project.findProperty(STUDIO_TYPE)?.toString()"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/ProjectLayoutType.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" val group = findProperty("group") as String"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/VersionFileWriterTask.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" val artifactId = findProperty("name") as String"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/VersionFileWriterTask.kt"/>
+ </issue>
+
+ <issue
id="InternalAgpApiUsage"
message="Avoid using internal Android Gradle Plugin APIs"
errorLine1="import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask"
@@ -371,33 +506,6 @@
</issue>
<issue
- id="InternalAgpApiUsage"
- message="Avoid using internal Android Gradle Plugin APIs"
- errorLine1="import com.android.build.gradle.internal.attributes.VariantAttr"
- errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
- </issue>
-
- <issue
- id="InternalAgpApiUsage"
- message="Avoid using internal Android Gradle Plugin APIs"
- errorLine1="import com.android.build.gradle.internal.publishing.AndroidArtifacts"
- errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
- </issue>
-
- <issue
- id="InternalAgpApiUsage"
- message="Avoid using internal Android Gradle Plugin APIs"
- errorLine1="import com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType"
- errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
- </issue>
-
- <issue
id="InternalGradleApiUsage"
message="Avoid using internal Gradle APIs"
errorLine1="import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency"
@@ -568,4 +676,22 @@
file="src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt"/>
</issue>
+ <issue
+ id="WithTypeWithoutConfigureEach"
+ message="Avoid passing a closure to withType, use withType().configureEach instead"
+ errorLine1=" project.tasks.withType(AbstractTestTask::class.java) { task ->"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+ </issue>
+
+ <issue
+ id="WithTypeWithoutConfigureEach"
+ message="Avoid passing a closure to withType, use withType().configureEach instead"
+ errorLine1=" project.tasks.withType(Test::class.java) { task -> configureJvmTestTask(project, task) }"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+ </issue>
+
</issues>
diff --git a/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt b/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
index 0964828..a691c88 100644
--- a/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
+++ b/buildSrc-tests/src/test/java/androidx/build/testConfiguration/AndroidTestConfigBuilderTest.kt
@@ -457,8 +457,6 @@
<option name="config-descriptor:metadata" key="applicationId" value="com.androidx.placeholder.Placeholder" />
<option name="wifi:disable" value="true" />
<option name="instrumentation-arg" key="notAnnotation" value="androidx.test.filters.FlakyTest" />
- <option name="instrumentation-arg" key="listener" value="androidx.benchmark.junit4.InstrumentationResultsRunListener" />
- <option name="instrumentation-arg" key="listener" value="androidx.benchmark.junit4.SideEffectRunListener" />
<include name="google/unbundled/common/setup" />
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
@@ -472,6 +470,8 @@
<test class="com.android.tradefed.testtype.AndroidJUnitTest">
<option name="runner" value="com.example.Runner"/>
<option name="package" value="com.androidx.placeholder.Placeholder" />
+ <option name="device-listeners" value="androidx.benchmark.junit4.InstrumentationResultsRunListener" />
+ <option name="device-listeners" value="androidx.benchmark.junit4.SideEffectRunListener" />
</test>
</configuration>
""".trimIndent()
@@ -498,8 +498,6 @@
<option name="instrumentation-arg" key="notAnnotation" value="androidx.test.filters.FlakyTest" />
<option name="instrumentation-arg" key="androidx.test.argument1" value="something1" />
<option name="instrumentation-arg" key="androidx.test.argument2" value="something2" />
- <option name="instrumentation-arg" key="listener" value="androidx.benchmark.junit4.InstrumentationResultsRunListener" />
- <option name="instrumentation-arg" key="listener" value="androidx.benchmark.macro.junit4.SideEffectRunListener" />
<include name="google/unbundled/common/setup" />
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
@@ -509,6 +507,8 @@
<test class="com.android.tradefed.testtype.AndroidJUnitTest">
<option name="runner" value="com.example.Runner"/>
<option name="package" value="com.androidx.placeholder.Placeholder" />
+ <option name="device-listeners" value="androidx.benchmark.macro.junit4.InstrumentationResultsRunListener" />
+ <option name="device-listeners" value="androidx.benchmark.macro.junit4.SideEffectRunListener" />
</test>
</configuration>
""".trimIndent()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 3463812..155cdcb 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -577,6 +577,10 @@
excludeVersionFiles(packagingOptions.resources)
}
+ project.extensions.getByType<ApplicationExtension>().apply {
+ configureAndroidApplicationOptions(project, androidXExtension)
+ }
+
project.extensions.getByType<ApplicationAndroidComponentsExtension>().apply {
beforeVariants(selector().withBuildType("release")) { variant ->
// Cast is needed because ApplicationAndroidComponentsExtension implements both
@@ -589,10 +593,6 @@
it.configureTests()
it.configureLocalAsbSigning(project.getKeystore())
}
- finalizeDsl { appExtension ->
- project.configureTestConfigGeneration(appExtension)
- appExtension.configureAndroidApplicationOptions(project, androidXExtension)
- }
}
project.buildOnServerDependsOnAssembleRelease()
@@ -646,8 +646,10 @@
}
project.configurePublicResourcesStub(project.multiplatformExtension!!)
- kotlinMultiplatformAndroidComponentsExtension.onVariant {
- project.configureMultiplatformSourcesForAndroid(it.name)
+ project.configureMultiplatformSourcesForAndroid { action ->
+ kotlinMultiplatformAndroidComponentsExtension.onVariant {
+ action(it.name)
+ }
}
project.configureVersionFileWriter(
project.multiplatformExtension!!,
@@ -808,8 +810,53 @@
}
}
+ val libraryAndroidComponentsExtension =
+ project.extensions.getByType<LibraryAndroidComponentsExtension>()
+
+ // Remove the android:targetSdkVersion element from the manifest used for AARs.
+ libraryAndroidComponentsExtension.onVariants { variant ->
+ project.createVariantAarManifestTransformerTask(variant.name, variant.artifacts)
+ }
+
+ project.extensions.getByType<com.android.build.api.dsl.LibraryExtension>().apply {
+ publishing { singleVariant(DEFAULT_PUBLISH_CONFIG) }
+ project.configureTestConfigGeneration(this)
+ project.addAppApkToTestConfigGeneration(androidXExtension)
+ }
+
+ libraryAndroidComponentsExtension.apply {
+ beforeVariants(selector().withBuildType("release")) { variant ->
+ variant.enableUnitTest = false
+ }
+ onVariants {
+ it.configureTests()
+ it.aotCompileMicrobenchmarks(project)
+ }
+ }
+
+ project.configureSourceJarForAndroid(libraryExtension)
+ project.configureVersionFileWriter(libraryAndroidComponentsExtension, androidXExtension)
+ project.configureJavaCompilationWarnings(androidXExtension)
+
+ project.configureDependencyVerification(androidXExtension) { taskProvider ->
+ libraryExtension.defaultPublishVariant { libraryVariant ->
+ taskProvider.configure { task ->
+ task.dependsOn(libraryVariant.javaCompileProvider)
+ }
+ }
+ }
+
val reportLibraryMetrics = project.configureReportLibraryMetricsTask()
project.addToBuildOnServer(reportLibraryMetrics)
+ libraryExtension.defaultPublishVariant { libraryVariant ->
+ reportLibraryMetrics.configure {
+ it.jarFiles.from(
+ libraryVariant.packageLibraryProvider.map { zip ->
+ zip.inputs.files
+ }
+ )
+ }
+ }
val prebuiltLibraries = listOf("libtracing_perfetto.so", "libc++_shared.so")
val copyPublicResourcesDirTask =
@@ -819,70 +866,33 @@
) { task ->
task.buildSrcResDir.set(File(project.getSupportRootFolder(), "buildSrc/res"))
}
-
- project.extensions.getByType<LibraryAndroidComponentsExtension>().apply {
- beforeVariants(selector().withBuildType("release")) { variant ->
- variant.enableUnitTest = false
+ libraryAndroidComponentsExtension.onVariants { variant ->
+ if (variant.buildType == DEFAULT_PUBLISH_CONFIG) {
+ // Standard docs, resource API, and Metalava configuration for AndroidX projects.
+ project.configureProjectForApiTasks(
+ LibraryApiTaskConfig(libraryExtension, variant),
+ androidXExtension
+ )
}
- onVariants { variant ->
- variant.configureTests()
- variant.aotCompileMicrobenchmarks(project)
- // Remove the android:targetSdkVersion element from the manifest used for AARs.
- project.createVariantAarManifestTransformerTask(variant.name, variant.artifacts)
-
- configurePublicResourcesStub(variant, copyPublicResourcesDirTask)
- if (variant.buildType == DEFAULT_PUBLISH_CONFIG) {
- // Standard docs, resource API, and Metalava configuration for AndroidX projects.
- project.configureProjectForApiTasks(
- LibraryApiTaskConfig(libraryExtension, variant),
- androidXExtension
- )
- }
- if (variant.name == DEFAULT_PUBLISH_CONFIG) {
- project.configureSourceJarForAndroid(variant)
- project.configureDependencyVerification(androidXExtension) { taskProvider ->
- taskProvider.configure { task ->
- task.dependsOn("compileReleaseJavaWithJavac")
+ configurePublicResourcesStub(variant, copyPublicResourcesDirTask)
+ val verifyELFRegionAlignmentTaskProvider = project.tasks.register(
+ variant.name + "VerifyELFRegionAlignment",
+ VerifyELFRegionAlignmentTask::class.java
+ ) { task ->
+ task.files.from(
+ variant.artifacts.get(SingleArtifact.MERGED_NATIVE_LIBS)
+ .map { dir ->
+ dir.asFileTree.files
+ .filter { it.extension == "so" }
+ .filter { it.path.contains("arm64-v8a") }
+ .filterNot { prebuiltLibraries.contains(it.name) }
}
- }
-
- reportLibraryMetrics.configure {
- it.jarFiles.from(
- project.tasks.named("bundleReleaseAar").map {
- zip -> zip.inputs.files
- }
- )
- }
- }
- val verifyELFRegionAlignmentTaskProvider = project.tasks.register(
- variant.name + "VerifyELFRegionAlignment",
- VerifyELFRegionAlignmentTask::class.java
- ) { task ->
- task.files.from(
- variant.artifacts.get(SingleArtifact.MERGED_NATIVE_LIBS)
- .map { dir ->
- dir.asFileTree.files
- .filter { it.extension == "so" }
- .filter { it.path.contains("arm64-v8a") }
- .filterNot { prebuiltLibraries.contains(it.name) }
- }
- )
- task.cacheEvenIfNoOutputs()
- }
- project.addToBuildOnServer(verifyELFRegionAlignmentTaskProvider)
+ )
+ task.cacheEvenIfNoOutputs()
}
- finalizeDsl { libraryExtension ->
- project.configureTestConfigGeneration(libraryExtension)
- project.addAppApkToTestConfigGeneration(androidXExtension)
- libraryExtension.apply {
- publishing { singleVariant(DEFAULT_PUBLISH_CONFIG) }
- }
- }
- project.configureVersionFileWriter(this, androidXExtension)
+ project.addToBuildOnServer(verifyELFRegionAlignmentTaskProvider)
}
- project.configureJavaCompilationWarnings(androidXExtension)
-
project.setUpCheckDocsTask(androidXExtension)
project.addToProjectMap(androidXExtension)
@@ -1261,6 +1271,7 @@
versionName = "1.0"
}
+ project.configureTestConfigGeneration(this)
project.addAppApkToTestConfigGeneration(androidXExtension)
project.addAppApkToFtlRunner()
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/Release.kt b/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
index 02c56fe..2d09d33 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
@@ -16,6 +16,7 @@
package androidx.build
import androidx.build.uptodatedness.cacheEvenIfNoOutputs
+import com.android.build.gradle.LibraryExtension
import java.io.File
import java.io.FileNotFoundException
import java.util.Locale
@@ -430,6 +431,18 @@
}
}
+/** Let you configure a library variant associated with [Release.DEFAULT_PUBLISH_CONFIG] */
+@Suppress("DEPRECATION") // LibraryVariant
+fun LibraryExtension.defaultPublishVariant(
+ config: (com.android.build.gradle.api.LibraryVariant) -> Unit
+) {
+ libraryVariants.all { variant ->
+ if (variant.name == Release.DEFAULT_PUBLISH_CONFIG) {
+ config(variant)
+ }
+ }
+}
+
val AndroidXExtension.publishedArtifacts: List<Artifact>
get() {
val groupString = mavenGroup?.group!!
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
index 762b9af..2841ad1 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
@@ -18,7 +18,7 @@
import androidx.build.dackka.DokkaAnalysisPlatform
import androidx.build.dackka.docsPlatform
-import com.android.build.api.variant.LibraryVariant
+import com.android.build.gradle.LibraryExtension
import com.google.gson.GsonBuilder
import java.util.Locale
import org.gradle.api.DefaultTask
@@ -45,34 +45,37 @@
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
/** Sets up a source jar task for an Android library project. */
-fun Project.configureSourceJarForAndroid(libraryVariant: LibraryVariant) {
- val sourceJar =
- tasks.register(
- "sourceJar${libraryVariant.name.capitalize()}",
- Jar::class.java
- ) { task ->
- task.archiveClassifier.set("sources")
- task.from(libraryVariant.sources.java!!.all)
- task.exclude { it.file.path.contains("generated") }
- // Do not allow source files with duplicate names, information would be lost
- // otherwise.
- task.duplicatesStrategy = DuplicatesStrategy.FAIL
- }
- registerSourcesVariant(sourceJar)
+fun Project.configureSourceJarForAndroid(libraryExtension: LibraryExtension) {
+ libraryExtension.defaultPublishVariant { variant ->
+ val sourceJar =
+ tasks.register(
+ "sourceJar${variant.name.capitalize()}",
+ Jar::class.java
+ ) {
+ it.archiveClassifier.set("sources")
+ it.from(libraryExtension.sourceSets.getByName("main").java.srcDirs)
+ // Do not allow source files with duplicate names, information would be lost
+ // otherwise.
+ it.duplicatesStrategy = DuplicatesStrategy.FAIL
+ }
+ registerSourcesVariant(sourceJar)
- // b/272214715
- configurations.whenObjectAdded {
- if (it.name == "debugSourcesElements" || it.name == "releaseSourcesElements") {
- it.artifacts.whenObjectAdded { _ ->
- it.attributes.attribute(
- DocsType.DOCS_TYPE_ATTRIBUTE,
- project.objects.named(DocsType::class.java, "fake-sources")
- )
+ // b/272214715
+ configurations.whenObjectAdded {
+ if (it.name == "debugSourcesElements" || it.name == "releaseSourcesElements") {
+ it.artifacts.whenObjectAdded { _ ->
+ it.attributes.attribute(
+ DocsType.DOCS_TYPE_ATTRIBUTE,
+ project.objects.named(DocsType::class.java, "fake-sources")
+ )
+ }
}
}
}
project.afterEvaluate {
- project.configureMultiplatformSourcesForAndroid(libraryVariant.name)
+ project.configureMultiplatformSourcesForAndroid { action ->
+ libraryExtension.defaultPublishVariant { action(it.name) }
+ }
}
val disableNames =
setOf(
@@ -81,19 +84,23 @@
disableUnusedSourceJarTasks(disableNames)
}
-fun Project.configureMultiplatformSourcesForAndroid(variantName: String) {
+fun Project.configureMultiplatformSourcesForAndroid(
+ withVariant: (action: (variantName: String) -> Unit) -> Unit
+) {
val mpExtension = multiplatformExtension
if (mpExtension != null && extra.has("publish")) {
- val sourceJar =
- project.tasks.named(
- "sourceJar${variantName.capitalize()}",
- Jar::class.java
- )
- // multiplatform projects use different source sets, so we need to modify the task
- sourceJar.configure { sourceJarTask ->
- // use an inclusion list of source sets, because that is the preferred policy
- sourceJarTask.from(mpExtension.sourceSets.getByName("commonMain").kotlin.srcDirs)
- sourceJarTask.from(mpExtension.sourceSets.getByName("androidMain").kotlin.srcDirs)
+ withVariant { variantName ->
+ val sourceJar =
+ project.tasks.named(
+ "sourceJar${variantName.capitalize()}",
+ Jar::class.java
+ )
+ // multiplatform projects use different source sets, so we need to modify the task
+ sourceJar.configure { sourceJarTask ->
+ // use an inclusion list of source sets, because that is the preferred policy
+ sourceJarTask.from(mpExtension.sourceSets.getByName("commonMain").kotlin.srcDirs)
+ sourceJarTask.from(mpExtension.sourceSets.getByName("androidMain").kotlin.srcDirs)
+ }
}
}
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index 2f04406..4cd160c 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -276,9 +276,8 @@
"mergeMultiplatformMetadata",
MergeMultiplatformMetadataTask::class.java
) {
- it.dependsOn(unzipMultiplatformSources)
it.mergedProjectMetadata.set(mergedProjectMetadata)
- it.inputDirectory.set(tempMultiplatformMetadataDirectory)
+ it.inputDirectory.set(unzipMultiplatformSources.flatMap { it.metadataOutput })
}
}
@@ -503,9 +502,11 @@
)
)
task.apply {
+ // Remove once there is property version of Copy#destinationDir
+ // Use samplesDir.set(unzipSamplesTask.flatMap { it.destinationDirectory })
+ // https://github.com/gradle/gradle/issues/25824
dependsOn(unzipJvmSourcesTask)
dependsOn(unzipSamplesTask)
- dependsOn(generateMetadataTask)
dependsOn(configureMultiplatformSourcesTask)
description =
@@ -532,7 +533,7 @@
excludedPackages.set(hiddenPackages.toSet())
excludedPackagesForJava.set(hiddenPackagesJava)
excludedPackagesForKotlin.set(emptySet())
- libraryMetadataFile.set(getMetadataRegularFile(project))
+ libraryMetadataFile.set(generateMetadataTask.flatMap { it.destinationFile })
projectStructureMetadataFile.set(mergedProjectMetadata)
// See go/dackka-source-link for details on these links.
baseSourceLink.set("https://cs.android.com/search?q=file:%s+class:%s")
@@ -563,8 +564,7 @@
val zipTask =
project.tasks.register("zipDocs", Zip::class.java) { task ->
task.apply {
- dependsOn(dackkaTask)
- from(generatedDocsDir)
+ from(dackkaTask.flatMap { it.destinationDir })
val baseName = "docs-$docsType"
val buildId = getBuildId()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
index 1bd89e1..3bd5ef8 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
@@ -63,6 +63,20 @@
"androidx.annotation.RequiresOptIn",
"--suppress-compatibility-meta-annotation",
"kotlin.RequiresOptIn",
+
+ // Skip reading comments in Metalava for two reasons:
+ // - We prefer for developers to specify api information via annotations instead
+ // of just javadoc comments (like @hide)
+ // - This allows us to improve cacheability of Metalava tasks
+ "--ignore-comments",
+ "--hide",
+ "DeprecationMismatch",
+ "--hide",
+ "DocumentExceptions",
+
+ // Don't track annotations that aren't needed for review or checking compat.
+ "--exclude-annotation",
+ "androidx.annotation.ReplaceWith",
)
val workQueue = workerExecutor.processIsolation()
workQueue.submit(MetalavaWorkAction::class.java) { parameters ->
@@ -394,8 +408,6 @@
args.addAll(
listOf(
"--error",
- "DeprecationMismatch", // Enforce deprecation mismatch
- "--error",
"ReferencesDeprecated",
"--error-message:api-lint",
"""
@@ -414,8 +426,6 @@
args.addAll(
listOf(
"--hide",
- "DeprecationMismatch",
- "--hide",
"UnhiddenSystemApi",
"--hide",
"ReferencesHidden",
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
index 17b65d8..01da246 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
@@ -110,12 +110,8 @@
sb.append(MODULE_METADATA_TAG_OPTION.replace("APPLICATION_ID", applicationId))
.append(WIFI_DISABLE_OPTION)
.append(FLAKY_TEST_OPTION)
- if (isMicrobenchmark) {
- if (isPostsubmit) {
- sb.append(MICROBENCHMARK_POSTSUBMIT_OPTIONS)
- } else {
- sb.append(MICROBENCHMARK_PRESUBMIT_OPTION)
- }
+ if (!isPostsubmit && (isMicrobenchmark || isMacrobenchmark)) {
+ sb.append(BENCHMARK_PRESUBMIT_INST_ARGS)
}
instrumentationArgsMap.forEach { (key, value) ->
sb.append("""
@@ -123,9 +119,6 @@
""".trimIndent())
}
- if (isMacrobenchmark) {
- sb.append(MACROBENCHMARK_POSTSUBMIT_OPTIONS)
- }
sb.append(SETUP_INCLUDE)
.append(TARGET_PREPARER_OPEN.replace("CLEANUP_APKS", "true"))
initialSetupApks.forEach { apk ->
@@ -148,6 +141,16 @@
sb.append(TEST_BLOCK_OPEN)
.append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
.append(PACKAGE_OPTION.replace("APPLICATION_ID", applicationId))
+ .apply {
+ if (isPostsubmit) {
+ // These listeners should be unified eventually (b/331974955)
+ if (isMicrobenchmark) {
+ sb.append(MICROBENCHMARK_POSTSUBMIT_LISTENERS)
+ } else if (isMacrobenchmark) {
+ sb.append(MACROBENCHMARK_POSTSUBMIT_LISTENERS)
+ }
+ }
+ }
.append(TEST_BLOCK_CLOSE)
sb.append(CONFIGURATION_CLOSE)
return sb.toString()
@@ -358,25 +361,27 @@
"""
.trimIndent()
-private val MICROBENCHMARK_PRESUBMIT_OPTION =
+private val BENCHMARK_PRESUBMIT_INST_ARGS =
"""
<option name="instrumentation-arg" key="androidx.benchmark.dryRunMode.enable" value="true" />
"""
.trimIndent()
-private val MICROBENCHMARK_POSTSUBMIT_OPTIONS =
+private val MICROBENCHMARK_POSTSUBMIT_LISTENERS =
"""
- <option name="instrumentation-arg" key="listener" value="androidx.benchmark.junit4.InstrumentationResultsRunListener" />
- <option name="instrumentation-arg" key="listener" value="androidx.benchmark.junit4.SideEffectRunListener" />
+ <option name="device-listeners" value="androidx.benchmark.junit4.InstrumentationResultsRunListener" />
+ <option name="device-listeners" value="androidx.benchmark.junit4.SideEffectRunListener" />
"""
.trimIndent()
-private val MACROBENCHMARK_POSTSUBMIT_OPTIONS =
+// NOTE: listeners are duplicated in macro package due to no common module w/ junit dependency
+// See b/331974955
+private val MACROBENCHMARK_POSTSUBMIT_LISTENERS =
"""
- <option name="instrumentation-arg" key="listener" value="androidx.benchmark.junit4.InstrumentationResultsRunListener" />
- <option name="instrumentation-arg" key="listener" value="androidx.benchmark.macro.junit4.SideEffectRunListener" />
+ <option name="device-listeners" value="androidx.benchmark.macro.junit4.InstrumentationResultsRunListener" />
+ <option name="device-listeners" value="androidx.benchmark.macro.junit4.SideEffectRunListener" />
"""
.trimIndent()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
index c890c2a..d673710 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
@@ -196,6 +196,11 @@
// macro benchmarks do not have a dryRunMode, so we don't run them in presubmit
configBuilder.isMacrobenchmark(true)
configBuilder.tag("macrobenchmarks")
+ if (additionalTags.get().contains("wear")) {
+ // Wear macrobenchmarks are tagged separately to enable running on wear in CI
+ // standard macrobenchmarks don't currently run well on wear (b/189952249)
+ configBuilder.tag("wear-macrobenchmarks")
+ }
} else {
configBuilder.tag("androidx_unit_tests")
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index fee0a04..c57116c 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -37,11 +37,10 @@
import com.android.build.api.dsl.TestExtension
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
-import com.android.build.api.variant.ApplicationVariant
+import com.android.build.api.variant.HasAndroidTest
import com.android.build.api.variant.HasDeviceTests
import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
-import com.android.build.api.variant.LibraryVariant
import com.android.build.api.variant.TestAndroidComponentsExtension
import com.android.build.api.variant.TestVariant
import com.android.build.api.variant.Variant
@@ -67,7 +66,8 @@
artifacts: Artifacts,
minSdk: Int,
testRunner: Provider<String>,
- instrumentationRunnerArgs: Provider<Map<String, String>>
+ instrumentationRunnerArgs: Provider<Map<String, String>>,
+ variant: Variant?
) {
val xmlName = "${path.asFilenamePrefix()}$variantName.xml"
val jsonName = "_${path.asFilenamePrefix()}$variantName.json"
@@ -131,7 +131,7 @@
task.testRunner.set(testRunner)
// Skip task if getTestSourceSetsForAndroid is empty, even if
// androidXExtension.deviceTests.enabled is set to true
- task.androidTestSourceCodeCollection.from(getTestSourceSetsForAndroid())
+ task.androidTestSourceCodeCollection.from(getTestSourceSetsForAndroid(variant))
task.enabled = androidXExtension.deviceTests.enabled
AffectedModuleDetector.configureTaskGuard(task)
}
@@ -209,7 +209,6 @@
it.attributes { container ->
container.attribute(
ARTIFACT_TYPE_ATTRIBUTE,
- // TODO: Replace with gradle-api attribute when made
"apk"
)
}
@@ -260,7 +259,6 @@
view.attributes {
it.attribute(
ARTIFACT_TYPE_ATTRIBUTE,
- // TODO: Replace with gradle-api attribute when made
"apk"
)
}
@@ -440,7 +438,8 @@
// replace minSdk after b/328495232 is fixed
commonExtension.defaultConfig.minSdk!!,
deviceTest.instrumentationRunner,
- deviceTest.instrumentationRunnerArguments
+ deviceTest.instrumentationRunnerArguments,
+ variant
)
}
}
@@ -455,7 +454,8 @@
provider { commonExtension.defaultConfig.testInstrumentationRunner!! },
provider {
commonExtension.defaultConfig.testInstrumentationRunnerArguments
- }
+ },
+ variant
)
}
}
@@ -463,12 +463,12 @@
}
}
+@Suppress("UnstableApiUsage")
fun Project.configureTestConfigGeneration(
kotlinMultiplatformAndroidTarget: KotlinMultiplatformAndroidTarget,
componentsExtension: KotlinMultiplatformAndroidComponentsExtension
) {
componentsExtension.onVariant { variant ->
- @Suppress("UnstableApiUsage") // usage of HasDeviceTests
variant.deviceTests.forEach { deviceTest ->
createTestConfigurationGenerationTask(
deviceTest.name,
@@ -477,45 +477,33 @@
kotlinMultiplatformAndroidTarget.minSdk!!,
deviceTest.instrumentationRunner,
deviceTest.instrumentationRunnerArguments,
+ null
)
}
}
}
-private fun Project.getTestSourceSetsForAndroid(): List<FileCollection> {
+private fun Project.getTestSourceSetsForAndroid(variant: Variant?): List<FileCollection> {
val testSourceFileCollections = mutableListOf<FileCollection>()
- // com.android.test modules keep test code in main sourceset
- extensions.findByType(TestVariant::class.java)?.let { variant ->
- testSourceFileCollections.addAll(variant.sources.java!!.all.get().map {
- directory -> project.files(directory.asFileTree)
- })
- // Add kotlin-android main source set
- extensions
- .findByType(KotlinAndroidProjectExtension::class.java)
- ?.sourceSets
- ?.find { it.name == "main" }
- ?.let { testSourceFileCollections.add(it.kotlin.sourceDirectories) }
- // Note, don't have to add kotlin-multiplatform as it is not compatible with
- // com.android.test modules
- }
-
- // Add Java androidTest source set
- extensions.findByType(ApplicationVariant::class.java)?.let { variant ->
- testSourceFileCollections.addAll(
- variant.sources.getByName("androidTest")
- .all.get().map {
- directory -> project.files(directory.asFileTree)
- }
- )
- }
-
- extensions.findByType(LibraryVariant::class.java)?.let { variant ->
- testSourceFileCollections.addAll(
- variant.sources.getByName("androidTest")
- .all.get().map {
- directory -> project.files(directory.asFileTree)
- }
- )
+ when (variant) {
+ is TestVariant -> {
+ // com.android.test modules keep test code in main sourceset
+ variant.sources.java?.all?.let { sourceSet ->
+ testSourceFileCollections.add(files(sourceSet))
+ }
+ // Add kotlin-android main source set
+ extensions
+ .findByType(KotlinAndroidProjectExtension::class.java)
+ ?.sourceSets
+ ?.find { it.name == "main" }
+ ?.let { testSourceFileCollections.add(it.kotlin.sourceDirectories) }
+ // Note, don't have to add kotlin-multiplatform as it is not compatible with
+ // com.android.test modules
+ }
+ is HasAndroidTest -> {
+ variant.androidTest?.sources?.java?.all
+ ?.let { testSourceFileCollections.add(files(it)) }
+ }
}
// Add kotlin-android androidTest source set
diff --git a/busytown/androidx-studio-integration-lint.sh b/busytown/androidx-studio-integration-lint.sh
index 80d7657..54081de 100755
--- a/busytown/androidx-studio-integration-lint.sh
+++ b/busytown/androidx-studio-integration-lint.sh
@@ -4,5 +4,5 @@
$SCRIPT_PATH/impl/build-studio-and-androidx.sh \
--ci \
-x verifyDependencyVersions \
- lintAnalyze \
- lintAnalyzeDebug
+ lintReportJvm \
+ lintReportDebug
diff --git a/busytown/impl/verify_no_caches_in_source_repo.sh b/busytown/impl/verify_no_caches_in_source_repo.sh
index 056f7e4..7936884 100755
--- a/busytown/impl/verify_no_caches_in_source_repo.sh
+++ b/busytown/impl/verify_no_caches_in_source_repo.sh
@@ -21,6 +21,15 @@
SCRIPT_DIR="$(cd $(dirname $0) && pwd)"
SOURCE_DIR="$(cd $SCRIPT_DIR/../.. && pwd)"
+# puts a copy of src at dest (even if dest's parent dir doesn't exist yet)
+function copy() {
+ src="$1"
+ dest="$2"
+
+ mkdir -p "$(dirname $dest)"
+ cp -r "$src" "$dest"
+}
+
# confirm that no files in the source repo were unexpectedly created (other than known exemptions)
function checkForGeneratedFilesInSourceRepo() {
@@ -67,10 +76,15 @@
# copy these new files into DIST_DIR in case anyone wants to inspect them
COPY_TO=$DIST_DIR/new_files
for f in $UNEXPECTED_GENERATED_FILES; do
- dest="$COPY_TO/$f"
- mkdir -p "$(dirname $dest)"
- cp "$SOURCE_DIR/$f" "$dest"
+ copy "$SOURCE_DIR/$f" "$COPY_TO/$f"
done
+
+ # b/331622149 temporarily also copy $OUT_DIR/androidx/room/integration-tests
+ if echo $UNEXPECTED_GENERATED_FILES | grep room.*core >/dev/null; then
+ ALSO_COPY=androidx/room/integration-tests/room-testapp-multiplatform/build
+ copy $OUT_DIR/$ALSO_COPY $COPY_TO/out/$ALSO_COPY
+ fi
+
echo >&2
echo Copied these generated files into $COPY_TO >&2
exit 1
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
index 43f9c2c..145da62 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
@@ -30,6 +30,7 @@
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter
+import androidx.camera.camera2.pipe.integration.adapter.ZslControlNoOpImpl
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.NoOpInactiveSurfaceCloser
@@ -95,8 +96,8 @@
val callbackMap = CameraCallbackMap()
val requestListener = ComboRequestListener()
val cameraGraphConfig = createCameraGraphConfig(
- sessionConfigAdapter, streamConfigMap,
- callbackMap, requestListener, cameraConfig, cameraQuirks, null
+ sessionConfigAdapter, streamConfigMap, callbackMap, requestListener, cameraConfig,
+ cameraQuirks, null, ZslControlNoOpImpl()
)
val cameraGraph = cameraPipe.create(cameraGraphConfig)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
index b5ee865..f7a6c0c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
@@ -39,6 +39,8 @@
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.FocusMeteringResult
import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCapture.FLASH_MODE_AUTO
+import androidx.camera.core.ImageCapture.FLASH_MODE_ON
import androidx.camera.core.impl.CameraControlInternal
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.Config
@@ -71,6 +73,7 @@
private val torchControl: TorchControl,
private val threads: UseCaseThreads,
private val zoomControl: ZoomControl,
+ private val zslControl: ZslControl,
val camera2cameraControl: Camera2CameraControl,
) : CameraControlInternal {
override fun getSensorRect(): Rect {
@@ -127,6 +130,10 @@
override fun setFlashMode(@ImageCapture.FlashMode flashMode: Int) {
flashControl.setFlashAsync(flashMode)
+ zslControl.setZslDisabledByFlashMode(
+ flashMode == FLASH_MODE_ON ||
+ flashMode == FLASH_MODE_AUTO
+ )
}
override fun setScreenFlash(screenFlash: ImageCapture.ScreenFlash?) {
@@ -139,16 +146,15 @@
)
override fun setZslDisabledByUserCaseConfig(disabled: Boolean) {
- // Override if Zero-Shutter Lag needs to be disabled by user case config.
+ zslControl.setZslDisabledByUserCaseConfig(disabled)
}
override fun isZslDisabledByByUserCaseConfig(): Boolean {
- // Override if Zero-Shutter Lag needs to be disabled by user case config.
- return false
+ return zslControl.isZslDisabledByUserCaseConfig()
}
override fun addZslConfig(sessionConfigBuilder: SessionConfig.Builder) {
- // Override if Zero-Shutter Lag needs to add config to session config.
+ zslControl.addZslConfig(sessionConfigBuilder)
}
override fun submitStillCaptureRequests(
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
index a22309f..66a0832 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
@@ -65,32 +65,32 @@
)
.build()
debug { "Created CameraFactoryAdapter in ${start.measureNow(timeSource).formatMs()}" }
- debug { "availableCamerasSelector: $availableCamerasSelector " }
Debug.traceStop()
result
}
- private var mAvailableCamerasSelector: CameraSelector? = availableCamerasSelector
- private var mAvailableCameraIds: List<String>
+ private val availableCameraIds: LinkedHashSet<String>
private val cameraCoordinator: CameraCoordinatorAdapter = CameraCoordinatorAdapter(
appComponent.getCameraPipe(),
appComponent.getCameraDevices(),
)
init {
- debug { "Created CameraFactoryAdapter" }
-
val optimizedCameraIds = CameraSelectionOptimizer.getSelectedAvailableCameraIds(
this,
- mAvailableCamerasSelector
+ availableCamerasSelector
)
- mAvailableCameraIds = CameraCompatibilityFilter.getBackwardCompatibleCameraIds(
- appComponent.getCameraDevices(),
- optimizedCameraIds
+
+ // Use a LinkedHashSet to preserve order
+ availableCameraIds = LinkedHashSet(
+ CameraCompatibilityFilter.getBackwardCompatibleCameraIds(
+ appComponent.getCameraDevices(),
+ optimizedCameraIds
+ )
)
}
/**
- * The [getCamera] method is responsible for providing CameraInternal object based on cameraID.
+ * The [getCamera] method is responsible for providing CameraInternal object based on cameraId.
* Use cameraId from set of cameraIds provided by [getAvailableCameraIds] method.
*/
override fun getCamera(cameraId: String): CameraInternal {
@@ -102,13 +102,12 @@
return cameraInternal
}
- override fun getAvailableCameraIds(): Set<String> =
- // Use a LinkedHashSet to preserve order
- LinkedHashSet(mAvailableCameraIds)
+ override fun getAvailableCameraIds(): Set<String> = availableCameraIds
override fun getCameraCoordinator(): CameraCoordinator {
return cameraCoordinator
}
+ /** This is an implementation specific object that is specific to the integration package */
override fun getCameraManager(): Any = appComponent
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryProvider.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryProvider.kt
index 08af33b..74b8b25 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryProvider.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryProvider.kt
@@ -34,7 +34,8 @@
/**
* The [CameraFactoryProvider] is responsible for creating the root dagger component that is used
- * to share resources across Camera instances.
+ * to share resources across Camera instances. There should generally be one
+ * [CameraFactoryProvider] instance per CameraX instance.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
class CameraFactoryProvider(
@@ -42,14 +43,12 @@
private val sharedAppContext: Context? = null,
private val sharedThreadConfig: CameraThreadConfig? = null
) : CameraFactory.Provider {
- private val cameraInteropStateCallbackRepository = CameraInteropStateCallbackRepository()
+ private val sharedInteropCallbacks = CameraInteropStateCallbackRepository()
private val lock = Any()
@GuardedBy("lock")
private var cachedCameraPipe: Pair<Context, Lazy<CameraPipe>>? = null
- private var cameraOpenRetryMaxTimeoutNs: DurationNs? = null
-
override fun newInstance(
context: Context,
threadConfig: CameraThreadConfig,
@@ -57,21 +56,24 @@
cameraOpenRetryMaxTimeoutInMs: Long
): CameraFactory {
- this.cameraOpenRetryMaxTimeoutNs = if (cameraOpenRetryMaxTimeoutInMs != -1L) null
+ val openRetryMaxTimeout = if (cameraOpenRetryMaxTimeoutInMs != -1L) null
else DurationNs(cameraOpenRetryMaxTimeoutInMs)
- val lazyCameraPipe = getOrCreateCameraPipe(context)
+ val lazyCameraPipe = getOrCreateCameraPipe(context, openRetryMaxTimeout)
return CameraFactoryAdapter(
lazyCameraPipe,
sharedAppContext ?: context,
sharedThreadConfig ?: threadConfig,
- cameraInteropStateCallbackRepository,
+ sharedInteropCallbacks,
availableCamerasLimiter
)
}
- private fun getOrCreateCameraPipe(context: Context): Lazy<CameraPipe> {
+ private fun getOrCreateCameraPipe(
+ context: Context,
+ openRetryMaxTimeout: DurationNs?,
+ ): Lazy<CameraPipe> {
if (sharedCameraPipe != null) {
return lazyOf(sharedCameraPipe)
}
@@ -79,19 +81,22 @@
synchronized(lock) {
val existing = cachedCameraPipe
if (existing == null) {
- val sharedCameraPipe = lazy { createCameraPipe(context) }
- cachedCameraPipe = context to sharedCameraPipe
- return sharedCameraPipe
+ val lazyCameraPipe = lazy {
+ createCameraPipe(context, openRetryMaxTimeout)
+ }
+ cachedCameraPipe = context to lazyCameraPipe
+ return lazyCameraPipe
} else {
check(context == existing.first) {
- "Mismatched context! Expected ${existing.first} but was $context"
+ "Failed to create CameraPipe, existing instance was created using " +
+ "${existing.first}, but received $context."
}
return existing.second
}
}
}
- private fun createCameraPipe(context: Context): CameraPipe {
+ private fun createCameraPipe(context: Context, openRetryMaxTimeout: DurationNs?): CameraPipe {
Debug.traceStart { "Create CameraPipe" }
val timeSource = SystemTimeSource()
val start = Timestamps.now(timeSource)
@@ -100,9 +105,9 @@
CameraPipe.Config(
appContext = context.applicationContext,
cameraInteropConfig = CameraPipe.CameraInteropConfig(
- cameraInteropStateCallbackRepository.deviceStateCallback,
- cameraInteropStateCallbackRepository.sessionStateCallback,
- cameraOpenRetryMaxTimeoutNs
+ sharedInteropCallbacks.deviceStateCallback,
+ sharedInteropCallbacks.sessionStateCallback,
+ openRetryMaxTimeout
)
)
)
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 b2a2064..dfcf6b0 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
@@ -24,20 +24,26 @@
import android.hardware.camera2.CameraCharacteristics.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION
import android.hardware.camera2.CameraMetadata
import android.hardware.camera2.params.DynamicRangeProfiles
+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.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata.Companion.supportsLogicalMultiCamera
+import androidx.camera.camera2.pipe.CameraMetadata.Companion.supportsPrivateReprocessing
import androidx.camera.camera2.pipe.CameraPipe
import androidx.camera.camera2.pipe.core.Log
import androidx.camera.camera2.pipe.integration.compat.DynamicRangeProfilesCompat
import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.ZslDisablerQuirk
import androidx.camera.camera2.pipe.integration.compat.workaround.isFlashAvailable
import androidx.camera.camera2.pipe.integration.config.CameraConfig
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.camera2.pipe.integration.impl.CameraCallbackMap
+import androidx.camera.camera2.pipe.integration.impl.CameraPipeCameraProperties
import androidx.camera.camera2.pipe.integration.impl.CameraProperties
import androidx.camera.camera2.pipe.integration.impl.DeviceInfoLogger
import androidx.camera.camera2.pipe.integration.impl.FocusMeteringControl
@@ -91,6 +97,17 @@
DeviceInfoLogger.logDeviceInfo(cameraProperties)
}
+ private val _physicalCameraInfos by lazy {
+ cameraProperties.metadata.physicalCameraIds.mapTo(mutableSetOf<CameraInfo>()) {
+ physicalCameraId ->
+ val cameraProperties = CameraPipeCameraProperties(
+ CameraConfig(physicalCameraId),
+ cameraProperties.metadata.awaitPhysicalMetadata(physicalCameraId)
+ )
+ PhysicalCameraInfoAdapter(cameraProperties)
+ }
+ }
+
private val isLegacyDevice by lazy {
cameraProperties.metadata[
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
@@ -102,6 +119,12 @@
Camera2CameraInfo.create(cameraProperties)
}
+ override fun isLogicalMultiCameraSupported(): Boolean {
+ return cameraProperties.metadata.supportsLogicalMultiCamera
+ }
+
+ override fun getPhysicalCameraInfos(): Set<CameraInfo> = _physicalCameraInfos
+
override fun getCameraId(): String = cameraConfig.cameraId.value
override fun getLensFacing(): Int =
getCameraSelectorLensFacing(cameraProperties.metadata[CameraCharacteristics.LENS_FACING]!!)
@@ -213,13 +236,12 @@
?: emptySet()
override fun isZslSupported(): Boolean {
- Log.warn { "TODO: isZslSupported are not yet supported." }
- return false
+ return Build.VERSION.SDK_INT >= 23 && isPrivateReprocessingSupported &&
+ DeviceQuirks[ZslDisablerQuirk::class.java] == null
}
override fun isPrivateReprocessingSupported(): Boolean {
- Log.warn { "TODO: isPrivateReprocessingSupported are not yet supported." }
- return false
+ return cameraProperties.metadata.supportsPrivateReprocessing
}
override fun getSupportedDynamicRanges(): Set<DynamicRange> {
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 a83161c..7bb9bc1 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
@@ -30,6 +30,8 @@
import androidx.camera.camera2.pipe.integration.impl.SESSION_PHYSICAL_CAMERA_ID_OPTION
import androidx.camera.camera2.pipe.integration.impl.STREAM_USE_CASE_OPTION
import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
+import androidx.camera.core.ExperimentalZeroShutterLag
+import androidx.camera.core.ImageCapture
import androidx.camera.core.impl.CameraCaptureCallback
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.Config
@@ -70,10 +72,11 @@
* Returns the configuration for the given capture type, or `null` if the
* configuration cannot be produced.
*/
+ @ExperimentalZeroShutterLag
override fun getConfig(
captureType: CaptureType,
captureMode: Int
- ): Config? {
+ ): Config {
debug { "Creating config for $captureType" }
val mutableConfig = MutableOptionsBundle.create()
@@ -102,7 +105,11 @@
val captureBuilder = CaptureConfig.Builder()
when (captureType) {
CaptureType.IMAGE_CAPTURE ->
- captureBuilder.templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
+ captureBuilder.templateType =
+ if (captureMode == ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG)
+ CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG
+ else
+ CameraDevice.TEMPLATE_STILL_CAPTURE
CaptureType.PREVIEW,
CaptureType.IMAGE_ANALYSIS,
@@ -165,7 +172,7 @@
implOptions = defaultCaptureConfig.implementationOptions
// Also copy these info to the CaptureConfig
- builder.setUseRepeatingSurface(defaultCaptureConfig.isUseRepeatingSurface)
+ builder.isUseRepeatingSurface = defaultCaptureConfig.isUseRepeatingSurface
builder.addAllTags(defaultCaptureConfig.tagBundle)
defaultCaptureConfig.surfaces.forEach { builder.addSurface(it) }
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapter.kt
index dd3bf72..5fb4eea 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapter.kt
@@ -19,7 +19,10 @@
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CaptureRequest
+import androidx.annotation.OptIn
import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.FrameInfo
+import androidx.camera.camera2.pipe.InputRequest
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraScope
@@ -30,6 +33,9 @@
import androidx.camera.camera2.pipe.integration.impl.CameraProperties
import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
import androidx.camera.camera2.pipe.integration.impl.toParameters
+import androidx.camera.camera2.pipe.media.AndroidImage
+import androidx.camera.core.ExperimentalGetImage
+import androidx.camera.core.impl.CameraCaptureResults
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.Config
import javax.inject.Inject
@@ -43,12 +49,14 @@
class CaptureConfigAdapter @Inject constructor(
cameraProperties: CameraProperties,
private val useCaseGraphConfig: UseCaseGraphConfig,
+ private val zslControl: ZslControl,
private val threads: UseCaseThreads,
) {
private val isLegacyDevice = cameraProperties.metadata[
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
] == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+ @OptIn(ExperimentalGetImage::class)
fun mapToRequest(
captureConfig: CaptureConfig,
requestTemplate: RequestTemplate,
@@ -95,12 +103,38 @@
)
}
+ var inputRequest: InputRequest? = null
+ var requestTemplateToSubmit = RequestTemplate(captureConfig.templateType)
+ if (captureConfig.templateType == CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG &&
+ !zslControl.isZslDisabledByUserCaseConfig() &&
+ !zslControl.isZslDisabledByFlashMode()
+ ) {
+ zslControl.dequeueImageFromBuffer()?.let { imageProxy ->
+ CameraCaptureResults.retrieveCameraCaptureResult(imageProxy.imageInfo)
+ ?.let { cameraCaptureResult ->
+ check(cameraCaptureResult is CaptureResultAdapter) {
+ "Unexpected capture result type: ${cameraCaptureResult.javaClass}"
+ }
+ val imageWrapper = AndroidImage(checkNotNull(imageProxy.image))
+ val frameInfo = checkNotNull(cameraCaptureResult.unwrapAs(FrameInfo::class))
+ inputRequest = InputRequest(imageWrapper, frameInfo)
+ }
+ }
+ }
+
+ // Apply still capture template type for regular still capture case
+ if (inputRequest == null) {
+ requestTemplateToSubmit =
+ captureConfig.getStillCaptureTemplate(requestTemplate, isLegacyDevice)
+ }
+
return Request(
streams = streamIdList,
listeners = listOf(callbacks) + additionalListeners,
parameters = optionBuilder.build().toParameters(),
extras = mapOf(CAMERAX_TAG_BUNDLE to captureConfig.tagBundle),
- template = captureConfig.getStillCaptureTemplate(requestTemplate, isLegacyDevice)
+ template = requestTemplateToSubmit,
+ inputRequest = inputRequest,
)
}
@@ -117,7 +151,9 @@
// repeating template is TEMPLATE_RECORD. Note:
// TEMPLATE_VIDEO_SNAPSHOT is not supported on legacy device.
templateToModify = CameraDevice.TEMPLATE_VIDEO_SNAPSHOT
- } else if (templateType == CaptureConfig.TEMPLATE_TYPE_NONE) {
+ } else if (templateType == CaptureConfig.TEMPLATE_TYPE_NONE ||
+ templateType == CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG
+ ) {
templateToModify = CameraDevice.TEMPLATE_STILL_CAPTURE
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt
index d37a3d9..85c6373 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt
@@ -79,7 +79,7 @@
class CaptureResultAdapter(
private val requestMetadata: RequestMetadata,
private val frameNumber: FrameNumber,
- private val result: FrameInfo
+ internal val result: FrameInfo
) : CameraCaptureResult, UnsafeWrapper {
override fun getAfMode(): AfMode = result.metadata.getAfMode()
override fun getAfState(): AfState = result.metadata.getAfState()
@@ -104,7 +104,13 @@
"Failed to unwrap $this as TotalCaptureResult"
}
- override fun <T : Any> unwrapAs(type: KClass<T>): T? = result.unwrapAs(type)
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : Any> unwrapAs(type: KClass<T>): T? {
+ return when (type) {
+ FrameInfo::class -> result as T
+ else -> result.unwrapAs(type)
+ }
+ }
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/PhysicalCameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/PhysicalCameraInfoAdapter.kt
new file mode 100644
index 0000000..da5bda2
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/PhysicalCameraInfoAdapter.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.adapter
+
+import android.annotation.SuppressLint
+import android.util.Range
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.UnsafeWrapper
+import androidx.camera.camera2.pipe.integration.impl.CameraProperties
+import androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo
+import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraState
+import androidx.camera.core.DynamicRange
+import androidx.camera.core.ExperimentalZeroShutterLag
+import androidx.camera.core.ExposureState
+import androidx.camera.core.FocusMeteringAction
+import androidx.camera.core.ZoomState
+import androidx.lifecycle.LiveData
+import kotlin.reflect.KClass
+
+/**
+ * Implementation of [CameraInfo] for physical camera. In comparison,
+ * [CameraInfoAdapter] is the version of logical camera.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class PhysicalCameraInfoAdapter(
+ private val cameraProperties: CameraProperties
+) : CameraInfo, UnsafeWrapper {
+
+ @OptIn(ExperimentalCamera2Interop::class)
+ internal val camera2CameraInfo: Camera2CameraInfo by lazy {
+ Camera2CameraInfo.create(cameraProperties)
+ }
+
+ override fun getSensorRotationDegrees(): Int {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun getSensorRotationDegrees(relativeRotation: Int): Int {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun hasFlashUnit(): Boolean {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun getTorchState(): LiveData<Int> {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun getZoomState(): LiveData<ZoomState> {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun getExposureState(): ExposureState {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun getCameraState(): LiveData<CameraState> {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun getImplementationType(): String {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun getCameraSelector(): CameraSelector {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun getLensFacing(): Int {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun getIntrinsicZoomRatio(): Float {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun isFocusMeteringSupported(action: FocusMeteringAction): Boolean {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ @SuppressLint("NullAnnotationGroup")
+ @ExperimentalZeroShutterLag
+ override fun isZslSupported(): Boolean {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun getSupportedFrameRateRanges(): Set<Range<Int>> {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun isLogicalMultiCameraSupported(): Boolean {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun isPrivateReprocessingSupported(): Boolean {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun querySupportedDynamicRanges(
+ candidateDynamicRanges: Set<DynamicRange>
+ ): Set<DynamicRange> {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ override fun getPhysicalCameraInfos(): Set<CameraInfo> {
+ throw UnsupportedOperationException("Physical camera doesn't support this function")
+ }
+
+ @OptIn(ExperimentalCamera2Interop::class)
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : Any> unwrapAs(type: KClass<T>): T? {
+ return when (type) {
+ Camera2CameraInfo::class -> camera2CameraInfo as T
+ else -> cameraProperties.metadata.unwrapAs(type)
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZslControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZslControl.kt
new file mode 100644
index 0000000..21b70b4
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZslControl.kt
@@ -0,0 +1,313 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.adapter
+
+import android.graphics.ImageFormat
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.params.InputConfiguration
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.os.Build
+import android.util.Size
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.camera.camera2.pipe.CameraMetadata.Companion.supportsPrivateReprocessing
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.pipe.integration.compat.quirk.ZslDisablerQuirk
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.camera2.pipe.integration.impl.CameraProperties
+import androidx.camera.camera2.pipe.integration.impl.area
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.MetadataImageReader
+import androidx.camera.core.SafeCloseImageReaderProxy
+import androidx.camera.core.impl.CameraCaptureCallback
+import androidx.camera.core.impl.DeferrableSurface
+import androidx.camera.core.impl.ImmediateSurface
+import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.internal.utils.ZslRingBuffer
+import javax.inject.Inject
+
+interface ZslControl {
+
+ /**
+ * Adds zero-shutter lag config to [SessionConfig].
+ *
+ * @param sessionConfigBuilder session config builder.
+ */
+ fun addZslConfig(sessionConfigBuilder: SessionConfig.Builder)
+
+ /**
+ * Determines whether the provided [DeferrableSurface] belongs to ZSL.
+ *
+ * @param surface The deferrable Surface to check.
+ * @param sessionConfig The session configuration where its input configuration will be used to
+ * determine whether the deferrable Surface belongs to ZSL.
+ */
+ fun isZslSurface(surface: DeferrableSurface, sessionConfig: SessionConfig): Boolean
+
+ /**
+ * Sets the flag if zero-shutter lag needs to be disabled by user case config.
+ *
+ *
+ * Zero-shutter lag will be disabled when any of the following conditions:
+ *
+ * * Extension is ON
+ * * VideoCapture is ON
+ *
+ *
+ * @param disabled True if zero-shutter lag should be disabled. Otherwise, should not be
+ * disabled. However, enabling zero-shutter lag needs other conditions e.g.
+ * flash mode OFF, so setting to false doesn't guarantee zero-shutter lag to
+ * be always ON.
+ */
+ fun setZslDisabledByUserCaseConfig(disabled: Boolean)
+
+ /**
+ * Checks if zero-shutter lag is disabled by user case config.
+ *
+ * @return True if zero-shutter lag should be disabled. Otherwise, returns false.
+ */
+ fun isZslDisabledByUserCaseConfig(): Boolean
+
+ /**
+ * Sets the flag if zero-shutter lag needs to be disabled by flash mode.
+ *
+ *
+ * Zero-shutter lag will be disabled when flash mode is not OFF.
+ *
+ * @param disabled True if zero-shutter lag should be disabled. Otherwise, should not be
+ * disabled. However, enabling zero-shutter lag needs other conditions e.g.
+ * Extension is OFF and VideoCapture is OFF, so setting to false doesn't
+ * guarantee zero-shutter lag to be always ON.
+ */
+ fun setZslDisabledByFlashMode(disabled: Boolean)
+
+ /**
+ * Checks if zero-shutter lag is disabled by flash mode.
+ *
+ * @return True if zero-shutter lag should be disabled. Otherwise, returns false.
+ */
+ fun isZslDisabledByFlashMode(): Boolean
+
+ /**
+ * Dequeues [ImageProxy] from ring buffer.
+ *
+ * @return [ImageProxy].
+ */
+ fun dequeueImageFromBuffer(): ImageProxy?
+}
+
+@RequiresApi(Build.VERSION_CODES.M)
+@CameraScope
+class ZslControlImpl @Inject constructor(
+ private val cameraProperties: CameraProperties
+) : ZslControl {
+ private val cameraMetadata = cameraProperties.metadata
+ private val streamConfigurationMap: StreamConfigurationMap by lazy {
+ checkNotNull(cameraMetadata[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP])
+ }
+
+ @VisibleForTesting
+ internal val zslRingBuffer =
+ ZslRingBuffer(RING_BUFFER_CAPACITY) { imageProxy -> imageProxy.close() }
+
+ private var isZslDisabledByUseCaseConfig = false
+ private var isZslDisabledByFlashMode = false
+ private var isZslDisabledByQuirks = DeviceQuirks[ZslDisablerQuirk::class.java] != null
+
+ @VisibleForTesting
+ internal var reprocessingImageReader: SafeCloseImageReaderProxy? = null
+ private var metadataMatchingCaptureCallback: CameraCaptureCallback? = null
+ private var reprocessingImageDeferrableSurface: DeferrableSurface? = null
+
+ override fun addZslConfig(sessionConfigBuilder: SessionConfig.Builder) {
+ reset()
+
+ // Early return only if use case config doesn't support zsl. If flash mode doesn't
+ // support zsl, we still create reprocessing capture session but will create a
+ // regular capture request when taking pictures. So when user switches flash mode, we
+ // could create reprocessing capture request if flash mode allows.
+ if (isZslDisabledByUseCaseConfig) {
+ return
+ }
+
+ if (isZslDisabledByQuirks) {
+ return
+ }
+
+ if (!cameraMetadata.supportsPrivateReprocessing) {
+ Log.info { "ZslControlImpl: Private reprocessing isn't supported" }
+ return
+ }
+
+ val size = streamConfigurationMap.getInputSizes(FORMAT).toList().maxBy { it.area() }
+ if (size == null) {
+ Log.warn { "ZslControlImpl: Unable to find a supported size for ZSL" }
+ return
+ }
+ Log.debug { "ZslControlImpl: Selected ZSL size: $size" }
+
+ val isJpegValidOutput =
+ streamConfigurationMap.getValidOutputFormatsForInput(FORMAT).contains(ImageFormat.JPEG)
+ if (!isJpegValidOutput) {
+ Log.warn { "ZslControlImpl: JPEG isn't valid output for ZSL format" }
+ return
+ }
+
+ val metadataImageReader = MetadataImageReader(
+ size.width,
+ size.height,
+ FORMAT,
+ MAX_IMAGES
+ )
+ val metadataCaptureCallback = metadataImageReader.cameraCaptureCallback
+ val reprocImageReader = SafeCloseImageReaderProxy(metadataImageReader)
+ metadataImageReader.setOnImageAvailableListener(
+ { reader ->
+ try {
+ val imageProxy = reader.acquireLatestImage()
+ if (imageProxy != null) {
+ zslRingBuffer.enqueue(imageProxy)
+ }
+ } catch (e: IllegalStateException) {
+ Log.error { "Failed to acquire latest image" }
+ }
+ }, CameraXExecutors.ioExecutor()
+ )
+
+ // Init the reprocessing image reader surface and add into the target surfaces of capture
+ val reprocDeferrableSurface = ImmediateSurface(
+ checkNotNull(reprocImageReader.surface),
+ Size(reprocImageReader.width, reprocImageReader.height),
+ FORMAT
+ )
+
+ reprocDeferrableSurface.terminationFuture.addListener(
+ { reprocImageReader.safeClose() },
+ CameraXExecutors.mainThreadExecutor()
+ )
+ sessionConfigBuilder.addSurface(reprocDeferrableSurface)
+
+ // Init capture and session state callback and enqueue the total capture result
+ sessionConfigBuilder.addCameraCaptureCallback(metadataCaptureCallback)
+
+ // Set input configuration for reprocessing capture request
+ sessionConfigBuilder.setInputConfiguration(
+ InputConfiguration(
+ reprocImageReader.width,
+ reprocImageReader.height,
+ reprocImageReader.imageFormat,
+ )
+ )
+
+ metadataMatchingCaptureCallback = metadataCaptureCallback
+ reprocessingImageReader = reprocImageReader
+ reprocessingImageDeferrableSurface = reprocDeferrableSurface
+ }
+
+ override fun isZslSurface(surface: DeferrableSurface, sessionConfig: SessionConfig): Boolean {
+ val inputConfig = sessionConfig.inputConfiguration
+ return surface.prescribedStreamFormat == inputConfig?.format &&
+ surface.prescribedSize.width == inputConfig.width &&
+ surface.prescribedSize.height == inputConfig.height
+ }
+
+ override fun setZslDisabledByUserCaseConfig(disabled: Boolean) {
+ isZslDisabledByUseCaseConfig = disabled
+ }
+
+ override fun isZslDisabledByUserCaseConfig(): Boolean {
+ return isZslDisabledByUseCaseConfig
+ }
+
+ override fun setZslDisabledByFlashMode(disabled: Boolean) {
+ isZslDisabledByFlashMode = disabled
+ }
+
+ override fun isZslDisabledByFlashMode(): Boolean {
+ return isZslDisabledByFlashMode
+ }
+
+ override fun dequeueImageFromBuffer(): ImageProxy? {
+ return try {
+ zslRingBuffer.dequeue()
+ } catch (e: NoSuchElementException) {
+ Log.warn { "ZslControlImpl#dequeueImageFromBuffer: No such element" }
+ null
+ }
+ }
+
+ private fun reset() {
+ val reprocImageDeferrableSurface = reprocessingImageDeferrableSurface
+ if (reprocImageDeferrableSurface != null) {
+ val reprocImageReaderProxy = reprocessingImageReader
+ if (reprocImageReaderProxy != null) {
+ reprocImageDeferrableSurface.terminationFuture.addListener(
+ { reprocImageReaderProxy.safeClose() },
+ CameraXExecutors.mainThreadExecutor()
+ )
+ // Clear the listener so that no more buffer is enqueued to |zslRingBuffer|.
+ reprocImageReaderProxy.clearOnImageAvailableListener()
+ reprocessingImageReader = null
+ }
+ reprocImageDeferrableSurface.close()
+ reprocessingImageDeferrableSurface = null
+ }
+
+ val ringBuffer = zslRingBuffer
+ while (!ringBuffer.isEmpty) {
+ ringBuffer.dequeue().close()
+ }
+ }
+
+ companion object {
+ // Due to b/232268355 and feedback from pixel team that private format will have better
+ // performance, we will use private only for zsl.
+ private const val FORMAT = ImageFormat.PRIVATE
+
+ @VisibleForTesting
+ internal const val RING_BUFFER_CAPACITY = 3
+
+ @VisibleForTesting
+ internal const val MAX_IMAGES = RING_BUFFER_CAPACITY * 3
+ }
+}
+
+/**
+ * No-Op implementation for [ZslControl].
+ */
+class ZslControlNoOpImpl @Inject constructor() : ZslControl {
+ override fun addZslConfig(sessionConfigBuilder: SessionConfig.Builder) {
+ }
+
+ override fun isZslSurface(surface: DeferrableSurface, sessionConfig: SessionConfig) = false
+
+ override fun setZslDisabledByUserCaseConfig(disabled: Boolean) {
+ }
+
+ override fun isZslDisabledByUserCaseConfig() = false
+
+ override fun setZslDisabledByFlashMode(disabled: Boolean) {
+ }
+
+ override fun isZslDisabledByFlashMode() = false
+
+ override fun dequeueImageFromBuffer(): ImageProxy? {
+ return null
+ }
+}
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 04d93f1..247b017 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
@@ -91,6 +91,9 @@
if (CaptureSessionOnClosedNotCalledQuirk.isEnabled()) {
quirks.add(CaptureSessionOnClosedNotCalledQuirk())
}
+ if (ZslDisablerQuirk.load()) {
+ quirks.add(ZslDisablerQuirk())
+ }
return quirks
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ZslDisablerQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ZslDisablerQuirk.kt
new file mode 100644
index 0000000..7121f7b
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/ZslDisablerQuirk.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat.quirk
+
+import android.annotation.SuppressLint
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.core.impl.Quirk
+
+/**
+ * QuirkSummary
+ * - Bug Id: 252818931, 261744070, 319913852
+ * - Description: On certain devices, the captured image has color issue for reprocessing. We need
+ * to disable zero-shutter lag and return false for [CameraInfo.isZslSupported].
+ * - Device(s): Samsung Fold4, Samsung s22, Xiaomi Mi 8
+ */
+@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 ZslDisablerQuirk : Quirk {
+
+ companion object {
+ private val AFFECTED_SAMSUNG_MODEL = listOf(
+ "SM-F936",
+ "SM-S901U",
+ "SM-S908U",
+ "SM-S908U1"
+ )
+
+ private val AFFECTED_XIAOMI_MODEL = listOf(
+ "MI 8"
+ )
+
+ fun load(): Boolean {
+ return isAffectedSamsungDevices() || isAffectedXiaoMiDevices()
+ }
+
+ private fun isAffectedSamsungDevices(): Boolean {
+ return ("samsung".equals(Build.BRAND, ignoreCase = true) &&
+ isAffectedModel(AFFECTED_SAMSUNG_MODEL))
+ }
+
+ private fun isAffectedXiaoMiDevices(): Boolean {
+ return ("xiaomi".equals(Build.BRAND, ignoreCase = true) &&
+ isAffectedModel(AFFECTED_XIAOMI_MODEL))
+ }
+
+ private fun isAffectedModel(modelList: List<String>): Boolean {
+ for (model in modelList) {
+ if (Build.MODEL.uppercase().startsWith(model)) {
+ return true
+ }
+ }
+ return false
+ }
+ }
+}
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 af0669e..9aaf1d4a9 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
@@ -20,6 +20,7 @@
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.params.StreamConfigurationMap
+import android.os.Build
import androidx.annotation.Nullable
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
@@ -32,6 +33,9 @@
import androidx.camera.camera2.pipe.integration.adapter.CameraControlAdapter
import androidx.camera.camera2.pipe.integration.adapter.CameraInfoAdapter
import androidx.camera.camera2.pipe.integration.adapter.CameraInternalAdapter
+import androidx.camera.camera2.pipe.integration.adapter.ZslControl
+import androidx.camera.camera2.pipe.integration.adapter.ZslControlImpl
+import androidx.camera.camera2.pipe.integration.adapter.ZslControlNoOpImpl
import androidx.camera.camera2.pipe.integration.compat.Camera2CameraControlCompat
import androidx.camera.camera2.pipe.integration.compat.CameraCompatModule
import androidx.camera.camera2.pipe.integration.compat.EvCompCompat
@@ -174,6 +178,18 @@
@Provides
@Named("cameraQuirksValues")
fun provideCameraQuirksValues(cameraQuirks: CameraQuirks): Quirks = cameraQuirks.quirks
+
+ @CameraScope
+ @Provides
+ fun provideZslControl(
+ cameraProperties: CameraProperties
+ ): ZslControl {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return ZslControlImpl(cameraProperties)
+ } else {
+ return ZslControlNoOpImpl()
+ }
+ }
}
@Binds
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 15bbea2..efbb0d7 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
@@ -30,6 +30,7 @@
import androidx.camera.camera2.pipe.CameraMetadata.Companion.supportsAutoFocusTrigger
import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Result3A
+import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
import androidx.camera.camera2.pipe.integration.adapter.propagateTo
import androidx.camera.camera2.pipe.integration.compat.ZoomCompat
@@ -50,8 +51,9 @@
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
/**
* Implementation of focus and metering controls exposed by [CameraControlInternal].
@@ -105,9 +107,12 @@
cameraProperties.metadata.getOrDefault(CameraCharacteristics.CONTROL_MAX_REGIONS_AE, 0)
private val maxAwbRegionCount =
cameraProperties.metadata.getOrDefault(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB, 0)
+
private var updateSignal: CompletableDeferred<FocusMeteringResult>? = null
private var cancelSignal: CompletableDeferred<Result3A?>? = null
+ private var autoCancelJob: Job? = null
+
fun startFocusAndMetering(
action: FocusMeteringAction,
autoFocusTimeoutMs: Long = AUTO_FOCUS_TIMEOUT_DURATION,
@@ -116,8 +121,10 @@
useCaseCamera?.let { useCaseCamera ->
threads.sequentialScope.launch {
+ autoCancelJob?.cancel()
cancelSignal?.setCancelException("Cancelled by another startFocusAndMetering()")
updateSignal?.setCancelException("Cancelled by another startFocusAndMetering()")
+
updateSignal = signal
val aeRectangles = meteringRegionsFromMeteringPoints(
@@ -167,14 +174,6 @@
awbRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
else null
- val (isCancelEnabled, timeout) = if (action.isAutoCancelEnabled &&
- action.autoCancelDurationInMillis < autoFocusTimeoutMs
- ) {
- (true to action.autoCancelDurationInMillis)
- } else {
- (false to autoFocusTimeoutMs)
- }
-
val deferredResult3A = if (
afRectangles.isEmpty() || !cameraProperties.metadata.supportsAutoFocusTrigger
) {
@@ -185,13 +184,24 @@
* instead of all cases because Controller3A.update3A() will invalidate
* the CameraGraph and thus may cause extra requests to the camera.
*/
+ debug { "startFocusAndMetering: updating 3A regions only" }
useCaseCamera.requestControl.update3aRegions(
aeRegions = aeRegions,
afRegions = afRegions,
awbRegions = awbRegions,
)
} else {
- /**
+ // No need to keep trying to focus if auto-cancel is already triggered
+ val finalFocusTimeout = if (action.isAutoCancelEnabled &&
+ action.autoCancelDurationInMillis < autoFocusTimeoutMs
+ ) {
+ action.autoCancelDurationInMillis
+ } else {
+ autoFocusTimeoutMs
+ }
+
+ debug { "startFocusAndMetering: updating 3A regions & triggering AF" }
+ /*
* 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.
@@ -205,7 +215,7 @@
else null,
afTriggerStartAeMode = cameraProperties.getSupportedAeMode(AeMode.ON),
timeLimitNs = TimeUnit.NANOSECONDS.convert(
- timeout,
+ finalFocusTimeout,
TimeUnit.MILLISECONDS
)
)
@@ -213,10 +223,12 @@
deferredResult3A.propagateToFocusMeteringResultDeferred(
resultDeferred = signal,
- isCancelEnabled = isCancelEnabled,
shouldTriggerAf = afRectangles.isNotEmpty(),
- useCaseCamera = useCaseCamera
)
+
+ if (action.isAutoCancelEnabled) {
+ triggerAutoCancel(action.autoCancelDurationInMillis, signal, useCaseCamera)
+ }
}
} ?: run {
signal.completeExceptionally(
@@ -227,31 +239,36 @@
return signal.asListenableFuture()
}
+ private fun triggerAutoCancel(
+ delayMillis: Long,
+ resultToCancel: CompletableDeferred<FocusMeteringResult>,
+ useCaseCamera: UseCaseCamera,
+ ) {
+ autoCancelJob?.cancel()
+
+ autoCancelJob = threads.scope.launch {
+ delay(delayMillis)
+ debug { "triggerAutoCancel: auto-canceling after $delayMillis ms" }
+ cancelFocusAndMeteringNowAsync(useCaseCamera, resultToCancel)
+ }
+ }
+
private fun Deferred<Result3A>.propagateToFocusMeteringResultDeferred(
resultDeferred: CompletableDeferred<FocusMeteringResult>,
- isCancelEnabled: Boolean,
shouldTriggerAf: Boolean,
- useCaseCamera: UseCaseCamera,
) {
invokeOnCompletion { throwable ->
if (throwable != null) {
resultDeferred.completeExceptionally(throwable)
} else {
val result3A = getCompleted()
+ debug { "propagateToFocusMeteringResultDeferred: result3A = $result3A" }
if (result3A.status == Result3A.Status.SUBMIT_FAILED) {
resultDeferred.completeExceptionally(
OperationCanceledException("Camera is not active.")
)
} else if (result3A.status == Result3A.Status.TIME_LIMIT_REACHED) {
- if (isCancelEnabled) {
- if (resultDeferred.isActive) {
- runBlocking {
- cancelFocusAndMeteringNowAsync(useCaseCamera, resultDeferred)
- }
- }
- } else {
- resultDeferred.complete(FocusMeteringResult.create(false))
- }
+ resultDeferred.complete(FocusMeteringResult.create(false))
} else {
resultDeferred.complete(
result3A.toFocusMeteringResult(
@@ -314,6 +331,7 @@
val signal = CompletableDeferred<Result3A?>()
useCaseCamera?.let { useCaseCamera ->
threads.sequentialScope.launch {
+ autoCancelJob?.cancel()
cancelSignal?.setCancelException("Cancelled by another cancelFocusAndMetering()")
cancelSignal = signal
cancelFocusAndMeteringNowAsync(useCaseCamera, updateSignal).propagateTo(signal)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 8aff64a8..8f5dd4e 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -32,6 +32,7 @@
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CameraPipe
import androidx.camera.camera2.pipe.CameraStream
+import androidx.camera.camera2.pipe.InputStream
import androidx.camera.camera2.pipe.OutputStream
import androidx.camera.camera2.pipe.StreamFormat
import androidx.camera.camera2.pipe.compat.CameraPipeKeys
@@ -40,6 +41,7 @@
import androidx.camera.camera2.pipe.integration.adapter.EncoderProfilesProviderAdapter
import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter
import androidx.camera.camera2.pipe.integration.adapter.SupportedSurfaceCombination
+import androidx.camera.camera2.pipe.integration.adapter.ZslControl
import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
import androidx.camera.camera2.pipe.integration.compat.quirk.CloseCameraDeviceOnCameraGraphCloseQuirk
import androidx.camera.camera2.pipe.integration.compat.quirk.CloseCaptureSessionOnDisconnectQuirk
@@ -55,6 +57,7 @@
import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
import androidx.camera.core.DynamicRange
import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraControlInternal
import androidx.camera.core.impl.CameraInfoInternal
import androidx.camera.core.impl.CameraInternal
import androidx.camera.core.impl.CameraMode
@@ -107,6 +110,8 @@
private val requestListener: ComboRequestListener,
private val cameraConfig: CameraConfig,
private val builder: UseCaseCameraComponent.Builder,
+ private val cameraControl: CameraControlInternal,
+ private val zslControl: ZslControl,
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") // Java version required for Dagger
private val controls: java.util.Set<UseCaseCameraControl>,
private val camera2CameraControl: Camera2CameraControl,
@@ -216,6 +221,7 @@
if (attachedUseCases.addAll(useCases)) {
if (!addOrRemoveRepeatingUseCase(getRunningUseCases())) {
+ updateZslDisabledByUseCaseConfigStatus()
refreshAttachedUseCases(attachedUseCases)
}
}
@@ -260,6 +266,12 @@
if (addOrRemoveRepeatingUseCase(getRunningUseCases())) {
return
}
+
+ if (attachedUseCases.isEmpty()) {
+ cameraControl.setZslDisabledByUserCaseConfig(false)
+ } else {
+ updateZslDisabledByUseCaseConfigStatus()
+ }
refreshAttachedUseCases(attachedUseCases)
}
pendingUseCasesToNotifyCameraControlReady.removeAll(useCases)
@@ -568,6 +580,7 @@
cameraConfig,
cameraQuirks,
cameraGraphFlags,
+ zslControl,
isExtensions,
)
}
@@ -647,6 +660,11 @@
return predicate(captureConfig.surfaces, sessionConfig.surfaces)
}
+ private fun updateZslDisabledByUseCaseConfigStatus() {
+ val disableZsl = attachedUseCases.any { it.currentConfig.isZslDisabled(false) }
+ cameraControl.setZslDisabledByUserCaseConfig(disableZsl)
+ }
+
companion object {
internal data class UseCaseManagerConfig(
val useCases: List<UseCase>,
@@ -667,11 +685,13 @@
cameraConfig: CameraConfig,
cameraQuirks: CameraQuirks,
cameraGraphFlags: CameraGraph.Flags?,
+ zslControl: ZslControl,
isExtensions: Boolean = false,
): CameraGraph.Config {
var containsVideo = false
var operatingMode = OperatingMode.NORMAL
val streamGroupMap = mutableMapOf<Int, MutableList<CameraStream.Config>>()
+ val inputStreams = mutableListOf<InputStream.Config>()
sessionConfigAdapter.getValidSessionConfigOrNull()?.let { sessionConfig ->
operatingMode = when (sessionConfig.sessionType) {
SESSION_REGULAR -> OperatingMode.NORMAL
@@ -681,6 +701,7 @@
val physicalCameraIdForAllStreams =
sessionConfig.toCamera2ImplConfig().getPhysicalCameraId(null)
+ var zslStream: CameraStream.Config? = null
for (outputConfig in sessionConfig.outputConfigs) {
val deferrableSurface = outputConfig.surface
val physicalCameraId =
@@ -718,6 +739,21 @@
if (surface.containerClass == MediaCodec::class.java) {
containsVideo = true
}
+ if (surface != deferrableSurface) continue
+ if (zslControl.isZslSurface(surface, sessionConfig)) {
+ zslStream = stream
+ }
+ }
+ }
+ if (sessionConfig.inputConfiguration != null) {
+ zslStream?.let {
+ inputStreams.add(
+ InputStream.Config(
+ stream = it,
+ format = it.outputs.single().format.value,
+ 1,
+ )
+ )
}
}
}
@@ -798,6 +834,7 @@
camera = cameraConfig.cameraId,
streams = streamConfigMap.keys.toList(),
exclusiveStreamGroups = streamGroupMap.values.toList(),
+ input = if (inputStreams.isEmpty()) null else inputStreams,
sessionMode = operatingMode,
defaultListeners = listOf(callbackMap, requestListener),
defaultParameters = defaultParameters,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfo.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfo.kt
index cf910d3..80539f3 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfo.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfo.kt
@@ -20,6 +20,7 @@
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.camera.camera2.pipe.integration.adapter.CameraInfoAdapter
+import androidx.camera.camera2.pipe.integration.adapter.PhysicalCameraInfoAdapter
import androidx.camera.camera2.pipe.integration.compat.workaround.getSafely
import androidx.camera.camera2.pipe.integration.impl.CameraProperties
import androidx.camera.core.CameraInfo
@@ -86,6 +87,12 @@
*/
@JvmStatic
fun from(@Suppress("UNUSED_PARAMETER") cameraInfo: CameraInfo): Camera2CameraInfo {
+ // Physical camera
+ if (cameraInfo is PhysicalCameraInfoAdapter) {
+ return cameraInfo.unwrapAs(Camera2CameraInfo::class)!!
+ }
+
+ // Logical camera
var cameraInfoImpl = (cameraInfo as CameraInfoInternal).implementation
Preconditions.checkArgument(
cameraInfoImpl is CameraInfoAdapter,
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
index ac3a615..226c5fc 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapterTest.kt
@@ -31,6 +31,8 @@
import androidx.camera.camera2.pipe.integration.impl.ZoomControl
import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_10B_UNCONSTRAINED
import androidx.camera.camera2.pipe.integration.internal.HLG10_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo
+import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
import androidx.camera.camera2.pipe.integration.testing.FakeCameraInfoAdapterCreator.createCameraInfoAdapter
import androidx.camera.camera2.pipe.integration.testing.FakeCameraInfoAdapterCreator.useCaseThreads
import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
@@ -437,6 +439,29 @@
}
@Test
+ fun cameraInfo_queryLogicalMultiCameraSupported() {
+ val cameraInfo: CameraInfo = createCameraInfoAdapter()
+
+ assertThat(cameraInfo.isLogicalMultiCameraSupported).isTrue()
+ }
+
+ @OptIn(ExperimentalCamera2Interop::class)
+ @Test
+ fun cameraInfo_getPhysicalCameraInfos() {
+ val physicalCameraIds = setOf(
+ CameraId.fromCamera2Id("5"),
+ CameraId.fromCamera2Id("6"))
+ val cameraInfo: CameraInfo = createCameraInfoAdapter()
+
+ assertThat(cameraInfo.physicalCameraInfos).isNotNull()
+ assertThat(cameraInfo.physicalCameraInfos.size).isEqualTo(2)
+ for (info in cameraInfo.physicalCameraInfos) {
+ assertThat(physicalCameraIds).contains(
+ CameraId(Camera2CameraInfo.from(info).getCameraId()))
+ }
+ }
+
+ @Test
fun intrinsicZoomRatioIsLessThan1_whenSensorHorizontalLengthWiderThanDefault() {
val cameraInfo: CameraInfoInternal = createCameraInfoAdapter(
cameraId = CameraId(ultraWideCameraId),
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt
index f83662e..70f0640 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CaptureConfigAdapterTest.kt
@@ -32,7 +32,6 @@
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.TagBundle
import androidx.testutils.assertThrows
-import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executors
import kotlinx.coroutines.CompletableDeferred
@@ -71,6 +70,7 @@
cameraStateAdapter = CameraStateAdapter(),
),
cameraProperties = fakeCameraProperties,
+ zslControl = ZslControlNoOpImpl(),
threads = fakeUseCaseThreads,
)
@@ -148,7 +148,7 @@
// Assert
runBlocking {
- Truth.assertThat(
+ assertThat(
withTimeoutOrNull(timeMillis = 5000) {
callbackAborted.await()
}
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 d5f89a3..230aa9f 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
@@ -86,6 +86,7 @@
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
@@ -1087,6 +1088,7 @@
// Arrange.
// Set an incomplete CompletableDeferred
fakeRequestControl.focusMeteringResult = CompletableDeferred()
+ fakeRequestControl.awaitFocusMetering = false // simulates async
val autoCancelDuration: Long = 500
val action = FocusMeteringAction.Builder(point1)
.setAutoCancelDuration(autoCancelDuration, TimeUnit.MILLISECONDS)
@@ -1100,6 +1102,8 @@
)
// simulate UseCaseCamera timing out during auto focus
+ advanceUntilIdle() // ensures all operations are triggered already
+ delay(autoFocusTimeoutDuration)
fakeRequestControl.focusMeteringResult.complete(
Result3A(status = Result3A.Status.TIME_LIMIT_REACHED)
)
@@ -1164,7 +1168,10 @@
@Test
fun startFocusMetering_afAutoModeIsSet() = runTest {
// Arrange.
- val action = FocusMeteringAction.Builder(point1, FocusMeteringAction.FLAG_AF).build()
+ val action = FocusMeteringAction
+ .Builder(point1, FocusMeteringAction.FLAG_AF)
+ .setAutoCancelDuration(8, TimeUnit.SECONDS)
+ .build()
val state3AControl = createState3AControl(CAMERA_ID_0)
focusMeteringControl = initFocusMeteringControl(
cameraId = CAMERA_ID_0,
@@ -1176,7 +1183,8 @@
// Act.
focusMeteringControl.startFocusAndMeteringAndAdvanceTestScope(
this,
- action
+ action,
+ testScopeAdvanceTimeMillis = 6000, // not cancelled yet
)[5, TimeUnit.SECONDS]
// Assert.
@@ -1186,6 +1194,32 @@
}
@Test
+ fun startFocusMetering_afModeResetAfterAutoCancel() = runTest {
+ // Arrange.
+ val action = FocusMeteringAction
+ .Builder(point1, FocusMeteringAction.FLAG_AF)
+ .build()
+ val state3AControl = createState3AControl(CAMERA_ID_0)
+ focusMeteringControl = initFocusMeteringControl(
+ cameraId = CAMERA_ID_0,
+ useCases = setOf(createPreview(Size(1920, 1080))),
+ useCaseThreads = fakeUseCaseThreads,
+ state3AControl = state3AControl,
+ )
+
+ // Act.
+ focusMeteringControl.startFocusAndMeteringAndAdvanceTestScope(
+ this,
+ action,
+ )[5, TimeUnit.SECONDS]
+
+ // Assert.
+ assertThat(
+ state3AControl.preferredFocusMode
+ ).isNull()
+ }
+
+ @Test
fun startFocusMetering_AfNotInvolved_afAutoModeNotSet() = runTest {
// Arrange.
val action = FocusMeteringAction.Builder(
@@ -1669,14 +1703,19 @@
private fun FocusMeteringControl.startFocusAndMeteringAndAdvanceTestScope(
testScope: TestScope,
action: FocusMeteringAction,
- autoFocusTimeoutMs: Long? = null
+ autoFocusTimeoutMs: Long? = null,
+ testScopeAdvanceTimeMillis: Long? = null,
): ListenableFuture<FocusMeteringResult> {
val future = autoFocusTimeoutMs?.let {
startFocusAndMetering(action, it)
} ?: run {
startFocusAndMetering(action)
}
- testScope.advanceUntilIdle()
+ if (testScopeAdvanceTimeMillis == null) {
+ testScope.advanceUntilIdle()
+ } else {
+ testScope.advanceTimeBy(testScopeAdvanceTimeMillis)
+ }
return future
}
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/ZslControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/ZslControlTest.kt
new file mode 100644
index 0000000..77f470e0
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/ZslControlTest.kt
@@ -0,0 +1,333 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.adapter
+
+import android.graphics.ImageFormat
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.os.Build
+import android.util.Size
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.integration.adapter.ZslControlImpl.Companion.MAX_IMAGES
+import androidx.camera.camera2.pipe.integration.adapter.ZslControlImpl.Companion.RING_BUFFER_CAPACITY
+import androidx.camera.camera2.pipe.integration.impl.CameraProperties
+import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.core.impl.SessionConfig
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+@RunWith(RobolectricCameraPipeTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.M)
+@DoNotInstrument
+class ZslControlImplTest {
+ private lateinit var zslControlImpl: ZslControlImpl
+ private lateinit var sessionConfigBuilder: SessionConfig.Builder
+
+ @Before
+ fun setUp() {
+ sessionConfigBuilder = SessionConfig.Builder().also { sessionConfigBuilder ->
+ sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG)
+ }
+ }
+
+ @Test
+ fun isPrivateReprocessingSupported_addZslConfig() {
+ zslControlImpl = ZslControlImpl(
+ createCameraProperties(
+ hasCapabilities = true,
+ isYuvReprocessingSupported = false,
+ isPrivateReprocessingSupported = true,
+ isJpegValidOutputFormat = true
+ )
+ )
+
+ zslControlImpl.addZslConfig(sessionConfigBuilder)
+
+ assertThat(zslControlImpl.reprocessingImageReader).isNotNull()
+ assertThat(zslControlImpl.reprocessingImageReader!!.imageFormat)
+ .isEqualTo(ImageFormat.PRIVATE)
+ assertThat(zslControlImpl.reprocessingImageReader!!.maxImages).isEqualTo(
+ MAX_IMAGES
+ )
+ assertThat(zslControlImpl.reprocessingImageReader!!.width).isEqualTo(
+ PRIVATE_REPROCESSING_MAXIMUM_SIZE.width
+ )
+ assertThat(zslControlImpl.reprocessingImageReader!!.height).isEqualTo(
+ PRIVATE_REPROCESSING_MAXIMUM_SIZE.height
+ )
+ assertThat(zslControlImpl.zslRingBuffer.maxCapacity).isEqualTo(
+ RING_BUFFER_CAPACITY
+ )
+ }
+
+ @Test
+ fun isYuvReprocessingSupported_notAddZslConfig() {
+ zslControlImpl = ZslControlImpl(
+ createCameraProperties(
+ hasCapabilities = true,
+ isYuvReprocessingSupported = true,
+ isPrivateReprocessingSupported = false,
+ isJpegValidOutputFormat = true
+ )
+ )
+
+ zslControlImpl.addZslConfig(sessionConfigBuilder)
+
+ assertThat(zslControlImpl.reprocessingImageReader).isNull()
+ }
+
+ @Test
+ fun isJpegNotValidOutputFormat_notAddZslConfig() {
+ zslControlImpl = ZslControlImpl(
+ createCameraProperties(
+ hasCapabilities = true,
+ isYuvReprocessingSupported = true,
+ isPrivateReprocessingSupported = false,
+ isJpegValidOutputFormat = false
+ )
+ )
+
+ zslControlImpl.addZslConfig(sessionConfigBuilder)
+
+ assertThat(zslControlImpl.reprocessingImageReader).isNull()
+ }
+
+ @Test
+ fun isReprocessingNotSupported_notAddZslConfig() {
+ zslControlImpl = ZslControlImpl(
+ createCameraProperties(
+ hasCapabilities = true,
+ isYuvReprocessingSupported = false,
+ isPrivateReprocessingSupported = false,
+ isJpegValidOutputFormat = false
+ )
+ )
+
+ zslControlImpl.addZslConfig(sessionConfigBuilder)
+
+ assertThat(zslControlImpl.reprocessingImageReader).isNull()
+ }
+
+ @Test
+ fun isZslDisabledByUserCaseConfig_notAddZslConfig() {
+ zslControlImpl = ZslControlImpl(
+ createCameraProperties(
+ hasCapabilities = true,
+ isYuvReprocessingSupported = false,
+ isPrivateReprocessingSupported = true,
+ isJpegValidOutputFormat = true
+ )
+ )
+ zslControlImpl.setZslDisabledByUserCaseConfig(true)
+
+ zslControlImpl.addZslConfig(sessionConfigBuilder)
+
+ assertThat(zslControlImpl.reprocessingImageReader).isNull()
+ }
+
+ @Test
+ fun isZslDisabledByFlashMode_addZslConfig() {
+ zslControlImpl = ZslControlImpl(
+ createCameraProperties(
+ hasCapabilities = true,
+ isYuvReprocessingSupported = false,
+ isPrivateReprocessingSupported = true,
+ isJpegValidOutputFormat = true
+ )
+ )
+ zslControlImpl.setZslDisabledByFlashMode(true)
+
+ zslControlImpl.addZslConfig(sessionConfigBuilder)
+
+ assertThat(zslControlImpl.reprocessingImageReader).isNotNull()
+ assertThat(zslControlImpl.reprocessingImageReader!!.imageFormat).isEqualTo(
+ ImageFormat.PRIVATE
+ )
+ assertThat(zslControlImpl.reprocessingImageReader!!.maxImages).isEqualTo(
+ MAX_IMAGES
+ )
+ assertThat(zslControlImpl.reprocessingImageReader!!.width).isEqualTo(
+ PRIVATE_REPROCESSING_MAXIMUM_SIZE.width
+ )
+ assertThat(zslControlImpl.reprocessingImageReader!!.height).isEqualTo(
+ PRIVATE_REPROCESSING_MAXIMUM_SIZE.height
+ )
+ assertThat(zslControlImpl.zslRingBuffer.maxCapacity).isEqualTo(
+ RING_BUFFER_CAPACITY
+ )
+ }
+
+ @Test
+ fun isZslDisabled_clearZslConfig() {
+ zslControlImpl = ZslControlImpl(
+ createCameraProperties(
+ hasCapabilities = true,
+ isYuvReprocessingSupported = false,
+ isPrivateReprocessingSupported = true,
+ isJpegValidOutputFormat = true
+ )
+ )
+
+ zslControlImpl.addZslConfig(sessionConfigBuilder)
+
+ zslControlImpl.setZslDisabledByUserCaseConfig(true)
+ zslControlImpl.addZslConfig(sessionConfigBuilder)
+
+ assertThat(zslControlImpl.reprocessingImageReader).isNull()
+ }
+
+ @Test
+ fun hasZslDisablerQuirk_notAddZslConfig() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-F936B")
+
+ zslControlImpl = ZslControlImpl(
+ createCameraProperties(
+ hasCapabilities = true,
+ isYuvReprocessingSupported = false,
+ isPrivateReprocessingSupported = true,
+ isJpegValidOutputFormat = true
+ )
+ )
+
+ zslControlImpl.addZslConfig(sessionConfigBuilder)
+
+ assertThat(zslControlImpl.reprocessingImageReader).isNull()
+ }
+
+ @Test
+ fun hasNoZslDisablerQuirk_addZslConfig() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-G973")
+
+ zslControlImpl = ZslControlImpl(
+ createCameraProperties(
+ hasCapabilities = true,
+ isYuvReprocessingSupported = false,
+ isPrivateReprocessingSupported = true,
+ isJpegValidOutputFormat = true
+ )
+ )
+
+ zslControlImpl.addZslConfig(sessionConfigBuilder)
+
+ assertThat(zslControlImpl.reprocessingImageReader).isNotNull()
+ assertThat(zslControlImpl.reprocessingImageReader!!.imageFormat).isEqualTo(
+ ImageFormat.PRIVATE
+ )
+ assertThat(zslControlImpl.reprocessingImageReader!!.maxImages).isEqualTo(
+ MAX_IMAGES
+ )
+ assertThat(zslControlImpl.reprocessingImageReader!!.width).isEqualTo(
+ PRIVATE_REPROCESSING_MAXIMUM_SIZE.width
+ )
+ assertThat(zslControlImpl.reprocessingImageReader!!.height).isEqualTo(
+ PRIVATE_REPROCESSING_MAXIMUM_SIZE.height
+ )
+ assertThat(zslControlImpl.zslRingBuffer.maxCapacity).isEqualTo(
+ RING_BUFFER_CAPACITY
+ )
+ }
+
+ private fun createCameraProperties(
+ hasCapabilities: Boolean,
+ isYuvReprocessingSupported: Boolean,
+ isPrivateReprocessingSupported: Boolean,
+ isJpegValidOutputFormat: Boolean
+ ): CameraProperties {
+ val characteristicsMap = mutableMapOf<CameraCharacteristics.Key<*>, Any?>()
+ val capabilities = arrayListOf<Int>()
+ if (isYuvReprocessingSupported) {
+ capabilities.add(
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING
+ )
+ }
+ if (isPrivateReprocessingSupported) {
+ capabilities.add(
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING
+ )
+ }
+
+ if (hasCapabilities) {
+ characteristicsMap[CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES] =
+ capabilities.toIntArray()
+
+ // Input formats
+ val streamConfigurationMap: StreamConfigurationMap = mock()
+
+ if (isYuvReprocessingSupported && isPrivateReprocessingSupported) {
+ whenever(streamConfigurationMap.inputFormats).thenReturn(
+ arrayOf(ImageFormat.YUV_420_888, ImageFormat.PRIVATE).toIntArray()
+ )
+ whenever(streamConfigurationMap.getInputSizes(ImageFormat.YUV_420_888)).thenReturn(
+ arrayOf(YUV_REPROCESSING_MAXIMUM_SIZE)
+ )
+ whenever(streamConfigurationMap.getInputSizes(ImageFormat.PRIVATE)).thenReturn(
+ arrayOf(PRIVATE_REPROCESSING_MAXIMUM_SIZE)
+ )
+ } else if (isYuvReprocessingSupported) {
+ whenever(streamConfigurationMap.inputFormats).thenReturn(
+ arrayOf(ImageFormat.YUV_420_888).toIntArray()
+ )
+ whenever(streamConfigurationMap.getInputSizes(ImageFormat.YUV_420_888)).thenReturn(
+ arrayOf(YUV_REPROCESSING_MAXIMUM_SIZE)
+ )
+ } else if (isPrivateReprocessingSupported) {
+ whenever(streamConfigurationMap.inputFormats).thenReturn(
+ arrayOf(ImageFormat.PRIVATE).toIntArray()
+ )
+ whenever(streamConfigurationMap.getInputSizes(ImageFormat.PRIVATE)).thenReturn(
+ arrayOf(PRIVATE_REPROCESSING_MAXIMUM_SIZE)
+ )
+ }
+
+ // Output formats for input
+ if (isJpegValidOutputFormat) {
+ whenever(streamConfigurationMap.getValidOutputFormatsForInput(ImageFormat.PRIVATE))
+ .thenReturn(arrayOf(ImageFormat.JPEG).toIntArray())
+ whenever(
+ streamConfigurationMap.getValidOutputFormatsForInput(ImageFormat.YUV_420_888)
+ ).thenReturn(arrayOf(ImageFormat.JPEG).toIntArray())
+ }
+
+ characteristicsMap[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP] =
+ streamConfigurationMap
+ }
+ val cameraMetadata = FakeCameraMetadata(
+ characteristics = characteristicsMap
+ )
+
+ return FakeCameraProperties(
+ cameraMetadata,
+ CameraId("0"),
+ )
+ }
+
+ companion object {
+ val YUV_REPROCESSING_MAXIMUM_SIZE = Size(4000, 3000)
+ val PRIVATE_REPROCESSING_MAXIMUM_SIZE = Size(3000, 2000)
+ }
+}
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 4ad90e72..191517a 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
@@ -24,6 +24,7 @@
import android.hardware.camera2.CaptureRequest.CONTROL_AE_MODE
import android.hardware.camera2.CaptureResult
import android.hardware.camera2.params.MeteringRectangle
+import android.media.Image
import android.os.Build
import android.os.Looper
import android.view.Surface
@@ -39,7 +40,9 @@
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
import androidx.camera.camera2.pipe.integration.adapter.CaptureConfigAdapter
+import androidx.camera.camera2.pipe.integration.adapter.CaptureResultAdapter
import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
+import androidx.camera.camera2.pipe.integration.adapter.ZslControl
import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
@@ -65,10 +68,14 @@
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.ImageProxy
import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.impl.DeferrableSurface
import androidx.camera.core.impl.ImmediateSurface
import androidx.camera.core.impl.MutableOptionsBundle
+import androidx.camera.core.impl.SessionConfig
import androidx.camera.core.impl.utils.futures.Futures
+import androidx.camera.core.internal.CameraCaptureResultImageInfo
import androidx.camera.testing.impl.mocks.MockScreenFlash
import androidx.testutils.MainDispatcherRule
import com.google.common.truth.Truth.assertThat
@@ -98,6 +105,8 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
import org.robolectric.shadows.StreamConfigurationMapBuilder
@@ -238,8 +247,45 @@
surfaceToStreamMap = mapOf(fakeDeferrableSurface to fakeStreamId),
cameraStateAdapter = CameraStateAdapter(),
)
- private val fakeCaptureConfigAdapter =
- CaptureConfigAdapter(fakeCameraProperties, fakeUseCaseGraphConfig, fakeUseCaseThreads)
+ private val fakeZslControl = object : ZslControl {
+ var _isZslDisabledByUseCaseConfig = false
+ var _isZslDisabledByFlashMode = false
+ var imageProxyToDequeue: ImageProxy? = null
+
+ override fun addZslConfig(sessionConfigBuilder: SessionConfig.Builder) {
+ // Do nothing
+ }
+
+ override fun isZslSurface(
+ surface: DeferrableSurface,
+ sessionConfig: SessionConfig
+ ): Boolean {
+ return false
+ }
+
+ override fun setZslDisabledByUserCaseConfig(disabled: Boolean) {
+ _isZslDisabledByUseCaseConfig = disabled
+ }
+
+ override fun isZslDisabledByUserCaseConfig(): Boolean {
+ return _isZslDisabledByUseCaseConfig
+ }
+
+ override fun setZslDisabledByFlashMode(disabled: Boolean) {
+ _isZslDisabledByFlashMode = disabled
+ }
+
+ override fun isZslDisabledByFlashMode(): Boolean {
+ return _isZslDisabledByFlashMode
+ }
+
+ override fun dequeueImageFromBuffer(): ImageProxy? {
+ return imageProxyToDequeue
+ }
+ }
+ private val fakeCaptureConfigAdapter = CaptureConfigAdapter(
+ fakeCameraProperties, fakeUseCaseGraphConfig, fakeZslControl, fakeUseCaseThreads
+ )
private var runningRepeatingJob: Job? = null
set(value) {
runningRepeatingJob?.cancel()
@@ -693,6 +739,158 @@
).isFalse()
}
+ @Config(minSdk = 23)
+ @Test
+ fun submitZslCaptureRequests_withZslTemplate_templateZeroShutterLagSent(): Unit = runTest {
+ // Arrange.
+ val requestList = mutableListOf<Request>()
+ fakeCameraGraphSession.requestHandler = { requests ->
+ requestList.addAll(requests)
+ requests.complete()
+ }
+ val imageCaptureConfig = CaptureConfig.Builder().let {
+ it.addSurface(fakeDeferrableSurface)
+ it.templateType = CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG
+ it.build()
+ }
+ configureZslControl()
+
+ // Act.
+ capturePipeline.submitStillCaptures(
+ listOf(imageCaptureConfig),
+ RequestTemplate(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG),
+ MutableOptionsBundle.create(),
+ captureMode = ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG,
+ flashMode = ImageCapture.FLASH_MODE_OFF,
+ flashType = ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ ).awaitAllWithTimeout()
+ advanceUntilIdle()
+
+ // Assert.
+ val request = requestList.single()
+ assertThat(request.streams.single()).isEqualTo(fakeStreamId)
+ assertThat(request.template).isEqualTo(
+ RequestTemplate(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG)
+ )
+ }
+
+ @Config(minSdk = 23)
+ @Test
+ fun submitZslCaptureRequests_withNoTemplate_templateStillPictureSent(): Unit = runTest {
+ // Arrange.
+ val requestList = mutableListOf<Request>()
+ fakeCameraGraphSession.requestHandler = { requests ->
+ requestList.addAll(requests)
+ requests.complete()
+ }
+ val imageCaptureConfig = CaptureConfig.Builder().let {
+ it.addSurface(fakeDeferrableSurface)
+ it.build()
+ }
+ configureZslControl()
+
+ // Act.
+ capturePipeline.submitStillCaptures(
+ listOf(imageCaptureConfig),
+ RequestTemplate(CameraDevice.TEMPLATE_PREVIEW),
+ MutableOptionsBundle.create(),
+ captureMode = ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG,
+ flashMode = ImageCapture.FLASH_MODE_OFF,
+ flashType = ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ ).awaitAllWithTimeout()
+
+ // Assert.
+ val request = requestList.single()
+ assertThat(request.streams.single()).isEqualTo(fakeStreamId)
+ assertThat(request.template).isEqualTo(
+ RequestTemplate(CameraDevice.TEMPLATE_STILL_CAPTURE)
+ )
+ }
+
+ @Config(minSdk = 23)
+ @Test
+ fun submitZslCaptureRequests_withZslDisabledByUseCaseConfig_templateStillPictureSent():
+ Unit = runTest {
+ // Arrange.
+ val requestList = mutableListOf<Request>()
+ fakeCameraGraphSession.requestHandler = { requests ->
+ requestList.addAll(requests)
+ requests.complete()
+ }
+ val imageCaptureConfig = CaptureConfig.Builder().let {
+ it.addSurface(fakeDeferrableSurface)
+ it.templateType = CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG
+ it.build()
+ }
+ configureZslControl()
+ fakeZslControl.setZslDisabledByUserCaseConfig(true)
+
+ // Act.
+ capturePipeline.submitStillCaptures(
+ listOf(imageCaptureConfig),
+ RequestTemplate(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG),
+ MutableOptionsBundle.create(),
+ captureMode = ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG,
+ flashMode = ImageCapture.FLASH_MODE_OFF,
+ flashType = ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ ).awaitAllWithTimeout()
+
+ // Assert.
+ val request = requestList.single()
+ assertThat(request.streams.single()).isEqualTo(fakeStreamId)
+ assertThat(request.template).isEqualTo(
+ RequestTemplate(CameraDevice.TEMPLATE_STILL_CAPTURE)
+ )
+ }
+
+ @Config(minSdk = 23)
+ @Test
+ fun submitZslCaptureRequests_withZslDisabledByFlashMode_templateStillPictureSent():
+ Unit = runTest {
+ // Arrange.
+ val requestList = mutableListOf<Request>()
+ fakeCameraGraphSession.requestHandler = { requests ->
+ requestList.addAll(requests)
+ requests.complete()
+ }
+ val imageCaptureConfig = CaptureConfig.Builder().let {
+ it.addSurface(fakeDeferrableSurface)
+ it.templateType = CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG
+ it.build()
+ }
+ configureZslControl()
+ fakeZslControl.setZslDisabledByFlashMode(true)
+
+ // Act.
+ capturePipeline.submitStillCaptures(
+ listOf(imageCaptureConfig),
+ RequestTemplate(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG),
+ MutableOptionsBundle.create(),
+ captureMode = ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG,
+ flashMode = ImageCapture.FLASH_MODE_OFF,
+ flashType = ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ ).awaitAllWithTimeout()
+
+ // Assert.
+ val request = requestList.single()
+ assertThat(request.streams.single()).isEqualTo(fakeStreamId)
+ assertThat(request.template).isEqualTo(
+ RequestTemplate(CameraDevice.TEMPLATE_STILL_CAPTURE)
+ )
+ }
+
+ private fun configureZslControl() {
+ val fakeImageProxy: ImageProxy = mock()
+ val fakeCaptureResult = CaptureResultAdapter(
+ FakeRequestMetadata(), FrameNumber(1), FakeFrameInfo()
+ )
+ val fakeImageInfo = CameraCaptureResultImageInfo(fakeCaptureResult)
+ val fakeImage: Image = mock()
+ whenever(fakeImageProxy.imageInfo).thenReturn(fakeImageInfo)
+ whenever(fakeImageProxy.image).thenReturn(fakeImage)
+ fakeZslControl.imageProxyToDequeue = fakeImageProxy
+ }
+
@Test
fun captureFailure_taskShouldFailure(): Unit = runTest {
// Arrange.
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
index e708509..397a83a 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestTest.kt
@@ -22,6 +22,7 @@
import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
import androidx.camera.camera2.pipe.integration.adapter.CaptureConfigAdapter
import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
+import androidx.camera.camera2.pipe.integration.adapter.ZslControlNoOpImpl
import androidx.camera.camera2.pipe.integration.compat.workaround.NotUseFlashModeTorchFor3aUpdate
import androidx.camera.camera2.pipe.integration.compat.workaround.NotUseTorchAsFlash
import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
@@ -441,6 +442,7 @@
fakeConfigAdapter = CaptureConfigAdapter(
useCaseGraphConfig = fakeUseCaseGraphConfig,
cameraProperties = fakeCameraProperties,
+ zslControl = ZslControlNoOpImpl(),
threads = fakeUseCaseThreads,
)
fakeUseCaseCameraState = UseCaseCameraState(
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt
index 9893b78..aef3719 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControlTest.kt
@@ -26,7 +26,6 @@
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
-import androidx.camera.camera2.pipe.integration.adapter.CaptureConfigAdapter
import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraph
@@ -78,11 +77,6 @@
surfaceToStreamMap = surfaceToStreamMap,
cameraStateAdapter = CameraStateAdapter(),
)
- private val fakeConfigAdapter = CaptureConfigAdapter(
- useCaseGraphConfig = fakeUseCaseGraphConfig,
- cameraProperties = fakeCameraProperties,
- threads = useCaseThreads,
- )
private val fakeUseCaseCameraState = UseCaseCameraState(
useCaseGraphConfig = fakeUseCaseGraphConfig,
threads = useCaseThreads,
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt
index 8095d71..f7f7978 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraTest.kt
@@ -22,7 +22,6 @@
import androidx.camera.camera2.pipe.CameraPipe
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.integration.adapter.CameraStateAdapter
-import androidx.camera.camera2.pipe.integration.adapter.CaptureConfigAdapter
import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter
import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpInactiveSurfaceCloser
@@ -75,11 +74,6 @@
surfaceToStreamMap = surfaceToStreamMap,
cameraStateAdapter = CameraStateAdapter(),
)
- private val fakeConfigAdapter = CaptureConfigAdapter(
- useCaseGraphConfig = fakeUseCaseGraphConfig,
- cameraProperties = fakeCameraProperties,
- threads = useCaseThreads,
- )
private val fakeUseCaseCameraState = UseCaseCameraState(
useCaseGraphConfig = fakeUseCaseGraphConfig,
threads = useCaseThreads,
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 127b304..f6ba121 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
@@ -30,6 +30,7 @@
import androidx.camera.camera2.pipe.integration.adapter.FakeTestUseCase
import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
import androidx.camera.camera2.pipe.integration.adapter.TestDeferrableSurface
+import androidx.camera.camera2.pipe.integration.adapter.ZslControlNoOpImpl
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.OutputSizesCorrector
@@ -520,6 +521,8 @@
callbackMap = CameraCallbackMap(),
requestListener = ComboRequestListener(),
builder = useCaseCameraComponentBuilder,
+ cameraControl = fakeCamera.cameraControlInternal,
+ zslControl = ZslControlNoOpImpl(),
controls = controls as java.util.Set<UseCaseCameraControl>,
cameraProperties = FakeCameraProperties(
metadata = fakeCameraMetadata,
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 427d14d..7ce86eb 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
@@ -18,6 +18,7 @@
import android.graphics.Rect
import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraMetadata
import android.hardware.camera2.params.StreamConfigurationMap
import android.util.Range
import android.util.Size
@@ -58,6 +59,8 @@
@RequiresApi(21)
object FakeCameraInfoAdapterCreator {
private val CAMERA_ID_0 = CameraId("0")
+ private val PHYSICAL_CAMERA_ID_5 = CameraId("5")
+ private val PHYSICAL_CAMERA_ID_6 = CameraId("6")
val useCaseThreads by lazy {
val executor = MoreExecutors.directExecutor()
@@ -85,6 +88,9 @@
Range(24, 24),
Range(30, 30),
Range(60, 60)
+ ),
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES to intArrayOf(
+ CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
)
)
@@ -95,7 +101,10 @@
cameraProperties: CameraProperties = FakeCameraProperties(
FakeCameraMetadata(
cameraId = cameraId,
- characteristics = cameraCharacteristics
+ characteristics = cameraCharacteristics,
+ physicalMetadata = mapOf(
+ PHYSICAL_CAMERA_ID_5 to FakeCameraMetadata(),
+ PHYSICAL_CAMERA_ID_6 to FakeCameraMetadata())
),
cameraId
),
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 2113912..53d949f 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
@@ -132,6 +132,8 @@
var cancelFocusMeteringCallCount = 0
var cancelFocusMeteringResult = CompletableDeferred(Result3A(status = Result3A.Status.OK))
+ var awaitFocusMetering = true
+
override suspend fun startFocusAndMeteringAsync(
aeRegions: List<MeteringRectangle>?,
afRegions: List<MeteringRectangle>?,
@@ -154,13 +156,19 @@
timeLimitNs
)
)
- withTimeoutOrNull(TimeUnit.MILLISECONDS.convert(timeLimitNs, TimeUnit.NANOSECONDS)) {
- focusMeteringResult.await()
- }.let { result3A ->
- if (result3A == null) {
- focusMeteringResult.complete(Result3A(status = Result3A.Status.TIME_LIMIT_REACHED))
+
+ if (awaitFocusMetering) {
+ withTimeoutOrNull(TimeUnit.MILLISECONDS.convert(timeLimitNs, TimeUnit.NANOSECONDS)) {
+ focusMeteringResult.await()
+ }.let { result3A ->
+ if (result3A == null) {
+ focusMeteringResult.complete(
+ Result3A(status = Result3A.Status.TIME_LIMIT_REACHED)
+ )
+ }
}
}
+
return focusMeteringResult
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
index 90c2c5d..12c29ee 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
@@ -25,6 +25,7 @@
import android.os.HandlerThread
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
+import androidx.camera.camera2.pipe.compat.AudioRestrictionController
import androidx.camera.camera2.pipe.config.CameraGraphConfigModule
import androidx.camera.camera2.pipe.config.CameraPipeComponent
import androidx.camera.camera2.pipe.config.CameraPipeConfigModule
@@ -116,6 +117,16 @@
}
/**
+ * This gets and sets the global [AudioRestrictionMode] tracked by [AudioRestrictionController].
+ */
+ var globalAudioRestrictionMode: AudioRestrictionMode
+ get(): AudioRestrictionMode =
+ component.cameraAudioRestrictionController().globalAudioRestrictionMode
+ set(value: AudioRestrictionMode) {
+ component.cameraAudioRestrictionController().globalAudioRestrictionMode = value
+ }
+
+ /**
* Application level configuration for [CameraPipe]. Nullable values are optional and reasonable
* defaults will be provided if values are not specified.
*/
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
index dfb9b4f..42c2066 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
@@ -331,9 +331,10 @@
) { it.awaitStarted() }
}
}
+ imageWriter?.close()
+ session.inputSurface?.release()
closed = true
}
- imageWriter?.close()
}
override fun toString(): String {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt
index 0d5c122..98dc196 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt
@@ -32,6 +32,8 @@
import androidx.camera.camera2.pipe.CameraPipe.CameraMetadataConfig
import androidx.camera.camera2.pipe.CameraSurfaceManager
import androidx.camera.camera2.pipe.compat.AndroidDevicePolicyManagerWrapper
+import androidx.camera.camera2.pipe.compat.AudioRestrictionController
+import androidx.camera.camera2.pipe.compat.AudioRestrictionControllerImpl
import androidx.camera.camera2.pipe.compat.DevicePolicyManagerWrapper
import androidx.camera.camera2.pipe.core.Debug
import androidx.camera.camera2.pipe.core.SystemTimeSource
@@ -71,6 +73,7 @@
fun cameraGraphComponentBuilder(): CameraGraphComponent.Builder
fun cameras(): CameraDevices
fun cameraSurfaceManager(): CameraSurfaceManager
+ fun cameraAudioRestrictionController(): AudioRestrictionController
}
@Module(includes = [ThreadConfigModule::class], subcomponents = [CameraGraphComponent::class])
@@ -165,5 +168,9 @@
@Singleton
@Provides
fun provideCameraSurfaceManager() = CameraSurfaceManager()
+
+ @Singleton
+ @Provides
+ fun provideAudioRestrictionController() = AudioRestrictionControllerImpl()
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index dc63a51..f9d8487 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -26,6 +26,7 @@
import static androidx.camera.camera2.internal.ZslUtil.isCapabilitySupported;
+import android.annotation.SuppressLint;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraMetadata;
import android.os.Build;
@@ -397,6 +398,7 @@
}
}
+ @SuppressLint("NullAnnotationGroup")
@OptIn(markerClass = androidx.camera.core.ExperimentalZeroShutterLag.class)
@Override
public boolean isZslSupported() {
@@ -645,7 +647,7 @@
mPhysicalCameraInfos = new HashSet<>();
for (String physicalCameraId : mCameraCharacteristicsCompat.getPhysicalCameraIds()) {
try {
- CameraInfo physicalCameraInfo = new Camera2CameraInfoImpl(
+ CameraInfo physicalCameraInfo = new Camera2PhysicalCameraInfoImpl(
physicalCameraId,
mCameraManager);
mPhysicalCameraInfos.add(physicalCameraInfo);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PhysicalCameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PhysicalCameraInfoImpl.java
new file mode 100644
index 0000000..a0050b6
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PhysicalCameraInfoImpl.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal;
+
+import android.annotation.SuppressLint;
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.CameraManagerCompat;
+import androidx.camera.camera2.interop.Camera2CameraInfo;
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.CameraState;
+import androidx.camera.core.DynamicRange;
+import androidx.camera.core.ExperimentalZeroShutterLag;
+import androidx.camera.core.ExposureState;
+import androidx.camera.core.FocusMeteringAction;
+import androidx.camera.core.ZoomState;
+import androidx.lifecycle.LiveData;
+
+import java.util.Set;
+
+@ExperimentalCamera2Interop
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class Camera2PhysicalCameraInfoImpl implements CameraInfo {
+
+ @NonNull private final String mCameraId;
+ @NonNull private final CameraCharacteristicsCompat mCameraCharacteristicsCompat;
+ @NonNull private final Camera2CameraInfo mCamera2CameraInfo;
+
+ public Camera2PhysicalCameraInfoImpl(@NonNull String cameraId,
+ @NonNull CameraManagerCompat cameraManager) throws CameraAccessExceptionCompat {
+ mCameraId = cameraId;
+ mCameraCharacteristicsCompat = cameraManager.getCameraCharacteristicsCompat(mCameraId);
+ mCamera2CameraInfo = new Camera2CameraInfo(this);
+ }
+
+ /**
+ * Gets the implementation of {@link Camera2CameraInfo}.
+ */
+ @NonNull
+ public Camera2CameraInfo getCamera2CameraInfo() {
+ return mCamera2CameraInfo;
+ }
+
+ @NonNull
+ public String getCameraId() {
+ return mCameraId;
+ }
+
+ @NonNull
+ public CameraCharacteristicsCompat getCameraCharacteristicsCompat() {
+ return mCameraCharacteristicsCompat;
+ }
+
+ @Override
+ public int getSensorRotationDegrees() {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @Override
+ public int getSensorRotationDegrees(int relativeRotation) {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @Override
+ public boolean hasFlashUnit() {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @NonNull
+ @Override
+ public LiveData<Integer> getTorchState() {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @NonNull
+ @Override
+ public LiveData<ZoomState> getZoomState() {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @NonNull
+ @Override
+ public ExposureState getExposureState() {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @NonNull
+ @Override
+ public LiveData<CameraState> getCameraState() {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @NonNull
+ @Override
+ public String getImplementationType() {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @NonNull
+ @Override
+ public CameraSelector getCameraSelector() {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @Override
+ public int getLensFacing() {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @Override
+ public float getIntrinsicZoomRatio() {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @Override
+ public boolean isFocusMeteringSupported(@NonNull FocusMeteringAction action) {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @SuppressLint("NullAnnotationGroup")
+ @ExperimentalZeroShutterLag
+ @Override
+ public boolean isZslSupported() {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @NonNull
+ @Override
+ public Set<Range<Integer>> getSupportedFrameRateRanges() {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @Override
+ public boolean isLogicalMultiCameraSupported() {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @Override
+ public boolean isPrivateReprocessingSupported() {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @NonNull
+ @Override
+ public Set<DynamicRange> querySupportedDynamicRanges(
+ @NonNull Set<DynamicRange> candidateDynamicRanges) {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+
+ @NonNull
+ @Override
+ public Set<CameraInfo> getPhysicalCameraInfos() {
+ throw new UnsupportedOperationException("Physical camera doesn't support this function");
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraInfo.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraInfo.java
index 33522d2..0782f7b 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraInfo.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraInfo.java
@@ -24,10 +24,12 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.camera.camera2.internal.Camera2CameraInfoImpl;
+import androidx.camera.camera2.internal.Camera2PhysicalCameraInfoImpl;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.core.util.Preconditions;
+import java.util.Collections;
import java.util.Map;
/**
@@ -37,10 +39,17 @@
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public final class Camera2CameraInfo {
private static final String TAG = "Camera2CameraInfo";
- private final Camera2CameraInfoImpl mCamera2CameraInfoImpl;
+
+ @Nullable
+ private Camera2CameraInfoImpl mCamera2CameraInfoImpl;
+
+ // TODO: clean up by passing in CameraId, CameraCharacteristicCompat and CameraManager
+ // instead of concrete implementation.
+ @Nullable
+ private Camera2PhysicalCameraInfoImpl mCamera2PhysicalCameraInfo;
/**
- * Creates a new camera information with Camera2 implementation.
+ * Creates a new logical camera information with Camera2 implementation.
*
*/
@RestrictTo(Scope.LIBRARY)
@@ -49,6 +58,14 @@
}
/**
+ * Creates a new physical camera information with Camera2 implementation.
+ */
+ @RestrictTo(Scope.LIBRARY)
+ public Camera2CameraInfo(@NonNull Camera2PhysicalCameraInfoImpl camera2PhysicalCameraInfo) {
+ mCamera2PhysicalCameraInfo = camera2PhysicalCameraInfo;
+ }
+
+ /**
* Gets the {@link Camera2CameraInfo} from a {@link CameraInfo}.
*
* @param cameraInfo The {@link CameraInfo} to get from.
@@ -59,6 +76,10 @@
*/
@NonNull
public static Camera2CameraInfo from(@NonNull CameraInfo cameraInfo) {
+ if (cameraInfo instanceof Camera2PhysicalCameraInfoImpl) {
+ return ((Camera2PhysicalCameraInfoImpl) cameraInfo).getCamera2CameraInfo();
+ }
+
CameraInfoInternal cameraInfoImpl =
((CameraInfoInternal) cameraInfo).getImplementation();
Preconditions.checkArgument(cameraInfoImpl instanceof Camera2CameraInfoImpl,
@@ -85,6 +106,9 @@
*/
@NonNull
public String getCameraId() {
+ if (mCamera2PhysicalCameraInfo != null) {
+ return mCamera2PhysicalCameraInfo.getCameraId();
+ }
return mCamera2CameraInfoImpl.getCameraId();
}
@@ -101,6 +125,9 @@
*/
@Nullable
public <T> T getCameraCharacteristic(@NonNull CameraCharacteristics.Key<T> key) {
+ if (mCamera2PhysicalCameraInfo != null) {
+ return mCamera2PhysicalCameraInfo.getCameraCharacteristicsCompat().get(key);
+ }
return mCamera2CameraInfoImpl.getCameraCharacteristicsCompat().get(key);
}
@@ -122,6 +149,12 @@
@NonNull
public static CameraCharacteristics extractCameraCharacteristics(
@NonNull CameraInfo cameraInfo) {
+ if (cameraInfo instanceof Camera2PhysicalCameraInfoImpl) {
+ return ((Camera2PhysicalCameraInfoImpl) cameraInfo)
+ .getCameraCharacteristicsCompat()
+ .toCameraCharacteristics();
+ }
+
CameraInfoInternal cameraInfoImpl = ((CameraInfoInternal) cameraInfo).getImplementation();
Preconditions.checkState(cameraInfoImpl instanceof Camera2CameraInfoImpl,
"CameraInfo does not contain any Camera2 information.");
@@ -140,6 +173,9 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public Map<String, CameraCharacteristics> getCameraCharacteristicsMap() {
+ if (mCamera2PhysicalCameraInfo != null) {
+ return Collections.emptyMap();
+ }
return mCamera2CameraInfoImpl.getCameraCharacteristicsMap();
}
}
diff --git a/camera/camera-core/api/current.txt b/camera/camera-core/api/current.txt
index abd42142..1eab25b 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -46,6 +46,7 @@
method public androidx.camera.core.ExposureState getExposureState();
method @FloatRange(from=0, fromInclusive=false) public default float getIntrinsicZoomRatio();
method public default int getLensFacing();
+ method public default java.util.Set<androidx.camera.core.CameraInfo!> getPhysicalCameraInfos();
method public int getSensorRotationDegrees();
method public int getSensorRotationDegrees(int);
method public default java.util.Set<android.util.Range<java.lang.Integer!>!> getSupportedFrameRateRanges();
@@ -53,6 +54,7 @@
method public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
method public boolean hasFlashUnit();
method public default boolean isFocusMeteringSupported(androidx.camera.core.FocusMeteringAction);
+ method public default boolean isLogicalMultiCameraSupported();
method @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public default boolean isZslSupported();
method public static boolean mustPlayShutterSound();
method public default java.util.Set<androidx.camera.core.DynamicRange!> querySupportedDynamicRanges(java.util.Set<androidx.camera.core.DynamicRange!>);
@@ -68,6 +70,7 @@
@RequiresApi(21) public final class CameraSelector {
method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
+ method public String? getPhysicalCameraId();
field public static final androidx.camera.core.CameraSelector DEFAULT_BACK_CAMERA;
field public static final androidx.camera.core.CameraSelector DEFAULT_FRONT_CAMERA;
field public static final int LENS_FACING_BACK = 1; // 0x1
@@ -81,6 +84,7 @@
method public androidx.camera.core.CameraSelector.Builder addCameraFilter(androidx.camera.core.CameraFilter);
method public androidx.camera.core.CameraSelector build();
method public androidx.camera.core.CameraSelector.Builder requireLensFacing(int);
+ method public androidx.camera.core.CameraSelector.Builder setPhysicalCameraId(String);
}
@RequiresApi(21) @com.google.auto.value.AutoValue public abstract class CameraState {
@@ -264,6 +268,7 @@
method public void setTargetRotation(int);
field public static final int COORDINATE_SYSTEM_ORIGINAL = 0; // 0x0
field public static final int COORDINATE_SYSTEM_SENSOR = 2; // 0x2
+ field public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
field public static final int OUTPUT_IMAGE_FORMAT_RGBA_8888 = 2; // 0x2
field public static final int OUTPUT_IMAGE_FORMAT_YUV_420_888 = 1; // 0x1
field public static final int STRATEGY_BLOCK_PRODUCER = 1; // 0x1
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index abd42142..1eab25b 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -46,6 +46,7 @@
method public androidx.camera.core.ExposureState getExposureState();
method @FloatRange(from=0, fromInclusive=false) public default float getIntrinsicZoomRatio();
method public default int getLensFacing();
+ method public default java.util.Set<androidx.camera.core.CameraInfo!> getPhysicalCameraInfos();
method public int getSensorRotationDegrees();
method public int getSensorRotationDegrees(int);
method public default java.util.Set<android.util.Range<java.lang.Integer!>!> getSupportedFrameRateRanges();
@@ -53,6 +54,7 @@
method public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
method public boolean hasFlashUnit();
method public default boolean isFocusMeteringSupported(androidx.camera.core.FocusMeteringAction);
+ method public default boolean isLogicalMultiCameraSupported();
method @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public default boolean isZslSupported();
method public static boolean mustPlayShutterSound();
method public default java.util.Set<androidx.camera.core.DynamicRange!> querySupportedDynamicRanges(java.util.Set<androidx.camera.core.DynamicRange!>);
@@ -68,6 +70,7 @@
@RequiresApi(21) public final class CameraSelector {
method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
+ method public String? getPhysicalCameraId();
field public static final androidx.camera.core.CameraSelector DEFAULT_BACK_CAMERA;
field public static final androidx.camera.core.CameraSelector DEFAULT_FRONT_CAMERA;
field public static final int LENS_FACING_BACK = 1; // 0x1
@@ -81,6 +84,7 @@
method public androidx.camera.core.CameraSelector.Builder addCameraFilter(androidx.camera.core.CameraFilter);
method public androidx.camera.core.CameraSelector build();
method public androidx.camera.core.CameraSelector.Builder requireLensFacing(int);
+ method public androidx.camera.core.CameraSelector.Builder setPhysicalCameraId(String);
}
@RequiresApi(21) @com.google.auto.value.AutoValue public abstract class CameraState {
@@ -264,6 +268,7 @@
method public void setTargetRotation(int);
field public static final int COORDINATE_SYSTEM_ORIGINAL = 0; // 0x0
field public static final int COORDINATE_SYSTEM_SENSOR = 2; // 0x2
+ field public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
field public static final int OUTPUT_IMAGE_FORMAT_RGBA_8888 = 2; // 0x2
field public static final int OUTPUT_IMAGE_FORMAT_YUV_420_888 = 1; // 0x1
field public static final int STRATEGY_BLOCK_PRODUCER = 1; // 0x1
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 ad649f9..65c1bcb 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
@@ -104,6 +104,15 @@
}
/**
+ * Options for how many outputs the effect handles.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @IntDef(value = {OUTPUT_OPTION_ONE_FOR_ALL_TARGETS, OUTPUT_OPTION_ONE_FOR_EACH_TARGET})
+ public @interface OutputOptions {
+ }
+
+ /**
* Bitmask options for the effect buffer formats.
*/
@Retention(RetentionPolicy.SOURCE)
@@ -173,8 +182,34 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final int TRANSFORMATION_PASSTHROUGH = 2;
+ /**
+ * Use this option to receive one output Surface for all the output targets.
+ *
+ * <p>When the effect targets multiple UseCases, e.g. Preview, VideoCapture, the effect will
+ * only receive one output Surface for all the outputs. CameraX is responsible for copying the
+ * processed frames to different output Surfaces.
+ *
+ * <p>Use this option if all UseCases receive the same content.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final int OUTPUT_OPTION_ONE_FOR_ALL_TARGETS = 0;
+
+ /**
+ * Use this option to receive one output Surface for each corresponding output target.
+ *
+ * <p>When the effect targets multiple UseCases, e.g. Preview, VideoCapture, the effect will
+ * receive two output Surfaces, one for Preview and one for VideoCapture. The effect is
+ * responsible for drawing the processed frames to the corresponding output Surfaces.
+ *
+ * <p>Use this option if each UseCase receives different content.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final int OUTPUT_OPTION_ONE_FOR_EACH_TARGET = 1;
+
@Targets
private final int mTargets;
+ @OutputOptions
+ private final int mOutputOption;
@Transformations
private final int mTransformation;
@NonNull
@@ -208,12 +243,24 @@
"Currently ImageProcessor can only target IMAGE_CAPTURE.");
mTargets = targets;
mTransformation = TRANSFORMATION_ARBITRARY;
+ mOutputOption = OUTPUT_OPTION_ONE_FOR_ALL_TARGETS;
mExecutor = executor;
mSurfaceProcessor = null;
mImageProcessor = imageProcessor;
mErrorListener = errorListener;
}
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ protected CameraEffect(
+ @Targets int targets,
+ @Transformations int transformation,
+ @NonNull Executor executor,
+ @NonNull SurfaceProcessor surfaceProcessor,
+ @NonNull Consumer<Throwable> errorListener) {
+ this(targets, OUTPUT_OPTION_ONE_FOR_ALL_TARGETS, transformation, executor, surfaceProcessor,
+ errorListener);
+ }
+
/**
* @param targets the target {@link UseCase} to which this effect should be applied.
* Currently {@link SurfaceProcessor} can target the following
@@ -226,6 +273,7 @@
* </ul>
* Targeting other {@link UseCase} combinations will throw
* {@link IllegalArgumentException}.
+ * @param outputOption the option to specify how many output Surface the effect will handle.
* @param transformation the transformation that the {@link SurfaceProcessor} will handle.
* @param executor the {@link Executor} on which the {@param imageProcessor} and
* {@param errorListener} will be invoked.
@@ -241,12 +289,14 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
protected CameraEffect(
@Targets int targets,
+ @OutputOptions int outputOption,
@Transformations int transformation,
@NonNull Executor executor,
@NonNull SurfaceProcessor surfaceProcessor,
@NonNull Consumer<Throwable> errorListener) {
checkSupportedTargets(SURFACE_PROCESSOR_TARGETS, targets);
mTargets = targets;
+ mOutputOption = outputOption;
mTransformation = transformation;
mExecutor = executor;
mSurfaceProcessor = surfaceProcessor;
@@ -282,13 +332,8 @@
@NonNull Executor executor,
@NonNull SurfaceProcessor surfaceProcessor,
@NonNull Consumer<Throwable> errorListener) {
- checkSupportedTargets(SURFACE_PROCESSOR_TARGETS, targets);
- mTargets = targets;
- mTransformation = TRANSFORMATION_ARBITRARY;
- mExecutor = executor;
- mSurfaceProcessor = surfaceProcessor;
- mImageProcessor = null;
- mErrorListener = errorListener;
+ this(targets, OUTPUT_OPTION_ONE_FOR_ALL_TARGETS, TRANSFORMATION_ARBITRARY, executor,
+ surfaceProcessor, errorListener);
}
/**
@@ -309,6 +354,15 @@
}
/**
+ * Gets the target option.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @OutputOptions
+ public int getOutputOption() {
+ return mOutputOption;
+ }
+
+ /**
* Gets the {@link Executor} associated with this effect.
*
* <p>This method returns the value set in the constructor.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
index 3ce702a..b1c39f8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -332,11 +332,13 @@
/**
* Returns if logical multi camera is supported on the device.
*
+ * <p>A logical camera is a grouping of two or more of those physical cameras.
+ * See <a href="https://developer.android.com/media/camera/camera2/multi-camera">Multi-camera API</a>
+ *
* @return true if supported, otherwise false.
* @see android.hardware.camera2.CameraMetadata
* #REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
*/
- @RestrictTo(Scope.LIBRARY_GROUP)
default boolean isLogicalMultiCameraSupported() {
return false;
}
@@ -420,9 +422,16 @@
/**
* Returns a set of physical camera {@link CameraInfo}s.
*
+ * <p>A logical camera is a grouping of two or more of those physical cameras.
+ * See <a href="https://developer.android.com/media/camera/camera2/multi-camera">Multi-camera API</a>
+ *
+ * <p> Check {@link #isLogicalMultiCameraSupported()} to see if the device is supporting
+ * physical camera or not. If the device doesn't support physical camera, empty set will
+ * be returned.
+ *
* @return Set of physical camera {@link CameraInfo}s.
+ * @see #isLogicalMultiCameraSupported()
*/
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
default Set<CameraInfo> getPhysicalCameraInfos() {
return Collections.emptySet();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
index ca80c82..bc66b44 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
@@ -15,6 +15,8 @@
*/
package androidx.camera.core;
+import android.hardware.camera2.params.SessionConfiguration;
+
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -213,9 +215,12 @@
/**
* Returns the physical camera id.
*
+ * <p>If physical camera id is not set via {@link Builder#setPhysicalCameraId(String)},
+ * it will return null.
+ *
* @return physical camera id.
+ * @see Builder#setPhysicalCameraId(String)
*/
- @RestrictTo(Scope.LIBRARY_GROUP)
@Nullable
public String getPhysicalCameraId() {
return mPhysicalCameraId;
@@ -294,10 +299,31 @@
/**
* Sets the physical camera id.
*
+ * <p>A logical camera is a grouping of two or more of those physical cameras.
+ * See <a href="https://developer.android.com/media/camera/camera2/multi-camera">Multi-camera API</a>
+ *
+ * <p> If we want to open one physical camera, for example ultra wide, we just need to set
+ * physical camera id in {@link CameraSelector} and bind to lifecycle. All CameraX features
+ * will work normally when only a single physical camera is used.
+ *
+ * <p>If we want to open multiple physical cameras, we need to have multiple
+ * {@link CameraSelector}s and set physical camera id on each, then bind to lifecycle with
+ * the {@link CameraSelector}s. Internally each physical camera id will be set on
+ * {@link UseCase}, for example, {@link Preview} and call
+ * {@link android.hardware.camera2.params.OutputConfiguration#setPhysicalCameraId(String)}.
+ *
+ * <p>Currently only two physical cameras for the same logical camera id are allowed
+ * and the device needs to support physical cameras by checking
+ * {@link CameraInfo#isLogicalMultiCameraSupported()}. In addition, there is no guarantee
+ * or API to query whether the device supports multiple physical camera opening or not.
+ * Internally the library checks
+ * {@link android.hardware.camera2.CameraDevice#isSessionConfigurationSupported(SessionConfiguration)},
+ * if the device does not support the multiple physical camera configuration,
+ * {@link IllegalArgumentException} will be thrown when binding to lifecycle.
+ *
* @param physicalCameraId physical camera id.
* @return this builder.
*/
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public Builder setPhysicalCameraId(@NonNull String physicalCameraId) {
mPhysicalCameraId = physicalCameraId;
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 aef1a6d..cdf3baf 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
@@ -54,6 +54,7 @@
import android.util.Size;
import android.view.Display;
import android.view.Surface;
+import android.view.View;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
@@ -969,6 +970,22 @@
public static final int COORDINATE_SYSTEM_ORIGINAL = 0;
/**
+ * {@link ImageAnalysis.Analyzer} option for returning UI coordinates.
+ *
+ * <p>When the {@link ImageAnalysis.Analyzer} is configured with this option, it will receive a
+ * {@link Matrix} that will receive a value that represents the transformation from camera
+ * sensor to the {@link View}, which can be used for highlighting detected result in UI. For
+ * example, laying over a bounding box on top of the detected face.
+ *
+ * <p>Note this option will only work with an artifact that displays the camera feed in UI.
+ * Generally, this is used by higher-level libraries such as the CameraController API that
+ * incorporates a viewfinder UI. It will not be effective when used with camera-core directly.
+ *
+ * @see ImageAnalysis.Analyzer
+ */
+ public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1;
+
+ /**
* {@link ImageAnalysis.Analyzer} option for returning the sensor coordinates.
*
* <p>Use this option if the app wishes to get the detected objects in camera sensor
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/ResolutionsMerger.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/ResolutionsMerger.java
index 9e55048..ae2491b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/ResolutionsMerger.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/ResolutionsMerger.java
@@ -53,6 +53,7 @@
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -619,6 +620,15 @@
Collections.sort(resolutions, new CompareSizesByArea(true));
}
+ /** Removes duplicate sizes and preserves the order. */
+ @NonNull
+ private static List<Size> removeDuplicates(@NonNull List<Size> resolutions) {
+ if (resolutions.isEmpty()) {
+ return resolutions;
+ }
+ return new ArrayList<>(new LinkedHashSet<>(resolutions));
+ }
+
/**
* Returns a list of resolution that all resolutions are with the input aspect-ratio.
*
@@ -690,6 +700,10 @@
if (childSizes.isEmpty() || parentSizes.isEmpty()) {
return new ArrayList<>();
}
+ // This method requires no duplicate parentSizes to correctly remove the last item.
+ // For example, if parentSizes = [1280x720, 1280x720] and 1280x720 can cover childSizes,
+ // removing the last item would cause 1280x720 to be mistakenly considered too large.
+ parentSizes = removeDuplicates(parentSizes);
List<Size> result = new ArrayList<>();
for (Size parentSize : parentSizes) {
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 9844499..d690404 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
@@ -253,8 +253,7 @@
isMirroringRequired(camera)); // Mirroring can be overridden by children.
mSharingInputEdge = getSharingInputEdge(mCameraEdge, camera);
- mSharingNode = new SurfaceProcessorNode(camera,
- DefaultSurfaceProcessor.Factory.newInstance(streamSpec.getDynamicRange()));
+ mSharingNode = getSharingNode(camera, streamSpec);
// Transform the input based on virtual camera configuration.
boolean isViewportSet = getViewPortCropRect() != null;
@@ -314,11 +313,19 @@
@NonNull
private SurfaceEdge getSharingInputEdge(@NonNull SurfaceEdge cameraEdge,
@NonNull CameraInternal camera) {
- if (getEffect() == null
- || getEffect().getTransformation() == CameraEffect.TRANSFORMATION_PASSTHROUGH) {
+ if (getEffect() == null) {
// No effect. The input edge is the camera edge.
return cameraEdge;
}
+ if (getEffect().getTransformation() == CameraEffect.TRANSFORMATION_PASSTHROUGH) {
+ // This is a passthrough effect for testing.
+ return cameraEdge;
+ }
+ if (getEffect().getOutputOption() == CameraEffect.OUTPUT_OPTION_ONE_FOR_EACH_TARGET) {
+ // When OUTPUT_OPTION_ONE_FOR_EACH_TARGET is used, we will apply the effect at the
+ // sharing stage.
+ return cameraEdge;
+ }
// Transform the camera edge to get the input edge.
mEffectNode = new SurfaceProcessorNode(camera,
getEffect().createSurfaceProcessorInternal());
@@ -338,6 +345,23 @@
return requireNonNull(out.get(outConfig));
}
+ @NonNull
+ private SurfaceProcessorNode getSharingNode(@NonNull CameraInternal camera,
+ @NonNull StreamSpec streamSpec) {
+ if (getEffect() != null
+ && getEffect().getOutputOption()
+ == CameraEffect.OUTPUT_OPTION_ONE_FOR_EACH_TARGET) {
+ // The effect wants to handle the sharing itself. Use the effect's node for sharing.
+ mEffectNode = new SurfaceProcessorNode(camera,
+ getEffect().createSurfaceProcessorInternal());
+ return mEffectNode;
+ } else {
+ // Create an internal node for sharing.
+ return new SurfaceProcessorNode(camera,
+ DefaultSurfaceProcessor.Factory.newInstance(streamSpec.getDynamicRange()));
+ }
+ }
+
private int getRotationAppliedByEffect() {
CameraEffect effect = checkNotNull(getEffect());
if (effect.getTransformation() == CameraEffect.TRANSFORMATION_CAMERA_AND_SURFACE_ROTATION) {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/ResolutionsMergerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/ResolutionsMergerTest.kt
index dde7b9e..f54e764 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/ResolutionsMergerTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/ResolutionsMergerTest.kt
@@ -629,6 +629,17 @@
}
@Test
+ fun getParentSizesThatAreTooLarge_containsDuplicateParentSize() {
+ val parentSizes = listOf(
+ SIZE_1920_1440,
+ SIZE_1920_1440, // duplicate
+ SIZE_1280_960,
+ )
+ val childSizes = setOf(SIZE_1920_1080)
+ assertThat(getParentSizesThatAreTooLarge(childSizes, parentSizes)).isEmpty()
+ }
+
+ @Test
fun hasUpscaling_return_false_whenTwoSizesAreEqualed() {
assertThat(hasUpscaling(SIZE_1280_960, SIZE_1280_960)).isFalse()
assertThat(hasUpscaling(SIZE_1920_1080, SIZE_1920_1080)).isFalse()
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 e427b91..71dc045 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
@@ -60,6 +60,7 @@
import androidx.camera.core.internal.TargetConfig.OPTION_TARGET_CLASS
import androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME
import androidx.camera.core.processing.DefaultSurfaceProcessor
+import androidx.camera.core.processing.SurfaceProcessorWithExecutor
import androidx.camera.testing.fakes.FakeCamera
import androidx.camera.testing.fakes.FakeCameraInfoInternal
import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult
@@ -147,6 +148,37 @@
}
@Test
+ fun effectHandleSharing_effectUsedAsSharingNode() {
+ // Arrange: create an effect that handles sharing.
+ effect = FakeSurfaceEffect(
+ PREVIEW or VIDEO_CAPTURE,
+ CameraEffect.TRANSFORMATION_CAMERA_AND_SURFACE_ROTATION,
+ CameraEffect.OUTPUT_OPTION_ONE_FOR_EACH_TARGET,
+ effectProcessor
+ )
+ val preview = Preview.Builder().build()
+ val videoCapture = VideoCapture.Builder(Recorder.Builder().build()).build()
+ streamSharing =
+ StreamSharing(frontCamera, setOf(preview, videoCapture), useCaseConfigFactory)
+ streamSharing.setViewPortCropRect(cropRect)
+ streamSharing.effect = effect
+
+ // Act: Bind effect and get sharing input edge.
+ streamSharing.bindToCamera(frontCamera, null, defaultConfig)
+ streamSharing.onSuggestedStreamSpecUpdated(StreamSpec.builder(size).build())
+
+ // Assert: the sharing node is built with the effect's processor
+ val sharingProcessor =
+ (streamSharing.sharingNode!!.surfaceProcessor as SurfaceProcessorWithExecutor).processor
+ assertThat(sharingProcessor).isEqualTo(effectProcessor)
+ assertThat(streamSharing.sharingInputEdge).isEqualTo(streamSharing.cameraEdge)
+ assertThat(streamSharing.virtualCameraAdapter.mChildrenEdges[preview]!!.targets)
+ .isEqualTo(PREVIEW)
+ assertThat(streamSharing.virtualCameraAdapter.mChildrenEdges[videoCapture]!!.targets)
+ .isEqualTo(VIDEO_CAPTURE)
+ }
+
+ @Test
fun effectHandleRotationAndMirroring_remainingTransformationIsEmpty() {
// Arrange: create an effect that handles rotation.
effect = FakeSurfaceEffect(
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureTest.kt
index 52c9a3d..c77e1e9 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureTest.kt
@@ -59,6 +59,7 @@
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.rules.TemporaryFolder
@@ -157,6 +158,7 @@
}
@Test
+ @Ignore("b/331617278")
fun canBindToLifeCycleAndTakePicture(): Unit = runBlocking {
val mockOnImageCapturedCallback = Mockito.mock(
ImageCapture.OnImageCapturedCallback::class.java
@@ -300,6 +302,7 @@
}
@Test
+ @Ignore("b/331617278")
fun canBindToLifeCycleAndTakePicture_diskIo(): Unit = runBlocking {
val mockOnImageSavedCallback = Mockito.mock(
ImageCapture.OnImageSavedCallback::class.java
@@ -485,6 +488,7 @@
}
@Test
+ @Ignore("b/331617278")
fun canBindToLifeCycleAndTakePictureWithCaptureProcessProgress(): Unit = runBlocking {
assumeTrue(isCaptureProcessProgressSupported())
@@ -521,6 +525,7 @@
}
@Test
+ @Ignore("b/331617278")
fun canBindToLifeCycleAndTakePictureWithCaptureProcessProgress_diskIo(): Unit = runBlocking {
assumeTrue(isCaptureProcessProgressSupported())
@@ -559,6 +564,7 @@
ExifRotationAvailability().isRotationOptionSupported
@Test
+ @Ignore("b/331617278")
fun canBindToLifeCycleAndTakePictureWithPostview(): Unit = runBlocking {
assumeTrue(isPostviewSupported())
@@ -612,6 +618,7 @@
}
@Test
+ @Ignore("b/331617278")
fun canBindToLifeCycleAndTakePictureWithPostview_diskIo(): Unit = runBlocking {
assumeTrue(isPostviewSupported())
@@ -658,6 +665,7 @@
}
@Test
+ @Ignore("b/331617278")
fun highResolutionDisabled_whenExtensionsEnabled(): Unit = runBlocking {
val imageCapture = ImageCapture.Builder().build()
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt
index cea52da..9341c78 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt
@@ -47,6 +47,7 @@
import org.junit.After
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
@@ -172,6 +173,7 @@
@UiThreadTest
@Test
+ @Ignore("b/331617278")
fun canBindToLifeCycleAndDisplayPreview(): Unit = runBlocking {
withContext(Dispatchers.Main) {
val preview = Preview.Builder().build()
@@ -195,6 +197,7 @@
}
@Test
+ @Ignore("b/331617278")
fun highResolutionDisabled_whenExtensionsEnabled(): Unit = runBlocking {
val preview = Preview.Builder().build()
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/VideoCaptureTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/VideoCaptureTest.kt
index 6661554..9acdf71 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/VideoCaptureTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/VideoCaptureTest.kt
@@ -59,6 +59,7 @@
import org.junit.After
import org.junit.Assume.assumeTrue
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
@@ -176,6 +177,7 @@
@UiThreadTest
@Test
+ @Ignore("b/331617278")
fun canBindToLifeCycleAndRecordVideo() {
// Arrange.
val file = createTempFile()
@@ -203,6 +205,7 @@
@UiThreadTest
@Test
+ @Ignore("b/331617278")
fun canBindToLifeCycleAndRecordVideoWithPreviewAndImageCaptureBound() {
// Arrange.
val file = createTempFile()
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
index 3114f4b..4cfc33f 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
@@ -90,6 +90,7 @@
import org.junit.Assume.assumeFalse
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
@@ -170,6 +171,7 @@
}
@Test
+ @Ignore("b/331617278")
fun useCasesCanWork_directlyUseOutputSurface() = runBlocking {
val fakeSessionProcessImpl = FakeSessionProcessImpl(
// Directly use output surface
@@ -199,6 +201,7 @@
}
@Test
+ @Ignore("b/331617278")
fun canInvokeStartTrigger() = runBlocking {
assumeTrue(ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_3))
val fakeSessionProcessImpl = FakeSessionProcessImpl()
@@ -224,6 +227,7 @@
}
@Test
+ @Ignore("b/331617278")
fun getRealtimeLatencyEstimate_advancedSessionProcessorInvokesSessionProcessorImpl() =
runBlocking {
assumeTrue(ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4))
@@ -244,6 +248,7 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Test
+ @Ignore("b/331617278")
fun isCurrentExtensionTypeAvailableReturnsCorrectFalseValue() =
runBlocking {
assumeTrue(ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4))
@@ -261,6 +266,7 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Suppress("UNCHECKED_CAST")
@Test
+ @Ignore("b/331617278")
fun isCurrentExtensionTypeAvailableReturnsCorrectTrueValue() =
runBlocking {
assumeTrue(ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4))
@@ -283,6 +289,7 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Test
+ @Ignore("b/331617278")
fun isExtensionStrengthAvailableReturnsCorrectFalseValue() =
runBlocking {
assumeTrue(ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4))
@@ -300,6 +307,7 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Suppress("UNCHECKED_CAST")
@Test
+ @Ignore("b/331617278")
fun isExtensionStrengthAvailableReturnsCorrectTrueValue() =
runBlocking {
assumeTrue(ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4))
@@ -364,6 +372,7 @@
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Ignore("b/331617278")
fun getCurrentExtensionType_advancedSessionProcessorMonitorSessionProcessorImplResults(): Unit =
runBlocking {
assumeTrue(ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4))
@@ -420,6 +429,7 @@
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Ignore("b/331617278")
fun setExtensionStrength_advancedSessionProcessorInvokesSessionProcessorImpl() =
runBlocking {
assumeTrue(ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4))
@@ -501,6 +511,7 @@
// Surface sharing of YUV format is supported after API 28.
@SdkSuppress(minSdkVersion = 28)
@Test
+ @Ignore("b/331617278")
fun useCasesCanWork_hasSharedSurfaceOutput() = runBlocking {
assumeAllowsSharedSurface()
var sharedConfigId = -1
@@ -557,6 +568,7 @@
// Test if physicalCameraId is set and returned in the image received in the image processor.
@SdkSuppress(minSdkVersion = 28) // physical camera id is supported in API28+
@Test
+ @Ignore("b/331617278")
fun useCasesCanWork_setPhysicalCameraId() = runBlocking {
assumeAllowsSharedSurface()
val physicalCameraIdList = getPhysicalCameraId(cameraSelector)
@@ -631,6 +643,7 @@
}
@Test
+ @Ignore("b/331617278")
fun canSetSessionTypeFromOemImpl() {
assumeTrue(ClientVersion.isMinimumCompatibleVersion(Version.VERSION_1_4) &&
ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4))
@@ -655,6 +668,7 @@
}
@Test
+ @Ignore("b/331617278")
fun defaultSessionType() {
// 1. Arrange.
val fakeSessionProcessImpl = FakeSessionProcessImpl()
@@ -676,6 +690,7 @@
}
@Test
+ @Ignore("b/331617278")
fun getSupportedPostviewSizeIsCorrect() {
// 1. Arrange
val postviewSizes = mutableMapOf(
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/ImageCaptureConfigProviderTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/ImageCaptureConfigProviderTest.kt
index 41907044..3cb4580 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/ImageCaptureConfigProviderTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/ImageCaptureConfigProviderTest.kt
@@ -44,6 +44,7 @@
import org.junit.After
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
@@ -88,6 +89,7 @@
@Test
@MediumTest
+ @Ignore("b/331617278")
fun canSetSupportedResolutionsToConfigTest(): Unit = runBlocking {
assumeTrue(CameraUtil.deviceHasCamera())
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/PreviewConfigProviderTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/PreviewConfigProviderTest.kt
index 995cf91..2169cbf 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/PreviewConfigProviderTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/PreviewConfigProviderTest.kt
@@ -44,6 +44,7 @@
import org.junit.After
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
@@ -88,6 +89,7 @@
@Test
@MediumTest
+ @Ignore("b/331617278")
fun canSetSupportedResolutionsToConfigTest(): Unit = runBlocking {
assumeTrue(CameraUtil.deviceHasCamera())
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
index ee5937f..301af04 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
@@ -101,6 +101,7 @@
import org.junit.Assume.assumeFalse
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
@@ -205,6 +206,7 @@
}
@Test
+ @Ignore("b/331617278")
fun canOutputCorrectly(): Unit = runBlocking {
val preview = Preview.Builder().build()
val imageCapture = ImageCapture.Builder().build()
@@ -226,6 +228,7 @@
}
@Test
+ @Ignore("b/331617278")
fun canSetSessionTypeFromOem() {
assumeTrue(ClientVersion.isMinimumCompatibleVersion(Version.VERSION_1_4) &&
ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4))
@@ -246,6 +249,7 @@
}
@Test
+ @Ignore("b/331617278")
fun setDifferentSessionTypes_throwException() {
assumeTrue(ClientVersion.isMinimumCompatibleVersion(Version.VERSION_1_4) &&
ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4))
@@ -265,6 +269,7 @@
}
@Test
+ @Ignore("b/331617278")
fun defaultSessionType() {
assumeTrue(ClientVersion.isMinimumCompatibleVersion(Version.VERSION_1_4) &&
ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4))
@@ -284,6 +289,7 @@
}
@Test
+ @Ignore("b/331617278")
fun imageCaptureError(): Unit = runBlocking {
assumeTrue(hasCaptureProcessor)
fakeCaptureExtenderImpl = FakeImageCaptureExtenderImpl(
@@ -303,6 +309,7 @@
}
@Test
+ @Ignore("b/331617278")
fun canOutputCorrectly_withoutAnalysis(): Unit = runBlocking {
val preview = Preview.Builder().build()
val imageCapture = ImageCapture.Builder().build()
@@ -322,6 +329,7 @@
}
@Test
+ @Ignore("b/331617278")
fun canOutputCorrectly_setTargetRotation(): Unit = runBlocking {
assumeTrue(hasCaptureProcessor)
val preview = Preview.Builder().build()
@@ -338,6 +346,7 @@
}
@Test
+ @Ignore("b/331617278")
fun canOutputCorrectlyAfterStopStart(): Unit = runBlocking {
val preview = Preview.Builder().build()
val imageCapture = ImageCapture.Builder().build()
@@ -369,6 +378,7 @@
verifyStillCapture(imageCapture)
}
+ @Ignore("b/331617278")
@Test
fun canInvokeEventsInOrder(): Unit = runBlocking {
val preview = Preview.Builder().build()
@@ -421,6 +431,7 @@
}
@Test
+ @Ignore("b/331617278")
fun getRealtimeCaptureLatencyEstimate_invokesCaptureExtenderImpl(): Unit = runBlocking {
assumeTrue(hasCaptureProcessor)
assumeTrue(ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4))
@@ -462,6 +473,7 @@
}
@Test
+ @Ignore("b/331617278")
fun repeatingRequest_containsPreviewCaptureStagesParameters(): Unit = runBlocking {
val previewBuilder = Preview.Builder()
val resultMonitor = ResultMonitor()
@@ -504,6 +516,7 @@
}
@Test
+ @Ignore("b/331617278")
fun processorRequestUpdateOnly_canUpdateRepeating(): Unit = runBlocking {
assumeTrue(previewProcessorType == PROCESSOR_TYPE_REQUEST_UPDATE_ONLY)
val previewBuilder = Preview.Builder()
@@ -549,6 +562,7 @@
}
@Test
+ @Ignore("b/331617278")
fun imageCapture_captureRequestParametersAreCorrect(): Unit = runBlocking {
initBasicExtenderSessionProcessor().use {
fakeCaptureExtenderImpl.captureStages = listOf(
@@ -602,6 +616,7 @@
}
@Test
+ @Ignore("b/331617278")
fun onEnableDisableRequestsAreSent(): Unit = runBlocking {
initBasicExtenderSessionProcessor().use {
// Verify onEnableSession
@@ -657,6 +672,7 @@
}
@Test
+ @Ignore("b/331617278")
fun getSupportedPostviewSizeIsCorrect() {
assumeTrue(ClientVersion.isMinimumCompatibleVersion(Version.VERSION_1_4) &&
ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4))
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
index 11038bb..ce62cae 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
@@ -29,6 +29,7 @@
import android.app.Application;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.hardware.camera2.params.SessionConfiguration;
import android.os.Handler;
import androidx.annotation.GuardedBy;
@@ -431,15 +432,44 @@
*
* <p>The concurrent camera is only supporting two cameras currently. If the input
* list of {@link SingleCameraConfig}s have less or more than two {@link SingleCameraConfig}s,
- * {@link IllegalArgumentException} will be thrown. If the device is not supporting
- * {@link PackageManager#FEATURE_CAMERA_CONCURRENT} or cameras are already used by other
+ * {@link IllegalArgumentException} will be thrown. If cameras are already used by other
* {@link UseCase}s, {@link UnsupportedOperationException} will be thrown.
*
- * <p>To set up concurrent camera, call {@link #getAvailableConcurrentCameraInfos()} to get
- * the list of available combinations of concurrent cameras. Each sub-list contains the
+ * <p>A logical camera is a grouping of two or more of those physical cameras.
+ * See <a href="https://developer.android.com/media/camera/camera2/multi-camera">Multi-camera API</a>
+ *
+ * <p>If we want to open concurrent logical cameras, which are one front camera and one
+ * back camera, the device needs to support {@link PackageManager#FEATURE_CAMERA_CONCURRENT}.
+ * To set up concurrent logical camera, call {@link #getAvailableConcurrentCameraInfos()} to
+ * get the list of available combinations of concurrent cameras. Each sub-list contains the
* {@link CameraInfo}s for a combination of cameras that can be operated concurrently.
- * Each camera can have its own {@link UseCase}s and {@link LifecycleOwner}. See
- * <a href="{@docRoot}training/camerax/architecture#lifecycles">CameraX lifecycles</a>
+ * Each logical camera can have its own {@link UseCase}s and {@link LifecycleOwner}.
+ * See <a href="{@docRoot}training/camerax/architecture#lifecycles">CameraX lifecycles</a>
+ *
+ * <p>If we want to open concurrent physical cameras, which are two front cameras or two back
+ * cameras, the device needs to support physical cameras and the capability could be checked via
+ * {@link CameraInfo#isLogicalMultiCameraSupported()}. Each physical cameras can have its own
+ * {@link UseCase}s but needs to have the same {@link LifecycleOwner}, otherwise
+ * {@link IllegalArgumentException} will be thrown.
+ *
+ * <p> If we want to open one physical camera, for example ultra wide, we just need to set
+ * physical camera id in {@link CameraSelector} and bind to lifecycle. All CameraX features
+ * will work normally when only a single physical camera is used.
+ *
+ * <p>If we want to open multiple physical cameras, we need to have multiple
+ * {@link CameraSelector}s, each in one {@link SingleCameraConfig} and set physical camera id,
+ * then bind to lifecycle with the {@link SingleCameraConfig}s. Internally each physical camera
+ * id will be set on {@link UseCase}, for example, {@link Preview} and call
+ * {@link android.hardware.camera2.params.OutputConfiguration#setPhysicalCameraId(String)}.
+ *
+ * <p>Currently only two physical cameras for the same logical camera id are allowed
+ * and the device needs to support physical cameras by checking
+ * {@link CameraInfo#isLogicalMultiCameraSupported()}. In addition, there is no guarantee
+ * or API to query whether the device supports multiple physical camera opening or not.
+ * Internally the library checks
+ * {@link android.hardware.camera2.CameraDevice#isSessionConfigurationSupported(SessionConfiguration)},
+ * if the device does not support the multiple physical camera configuration,
+ * {@link IllegalArgumentException} will be thrown.
*
* @param singleCameraConfigs input list of {@link SingleCameraConfig}s.
* @return output {@link ConcurrentCamera} instance.
@@ -450,6 +480,8 @@
*
* @see ConcurrentCamera
* @see #getAvailableConcurrentCameraInfos()
+ * @see CameraInfo#isLogicalMultiCameraSupported()
+ * @see CameraInfo#getPhysicalCameraInfos()
*/
@MainThread
@NonNull
diff --git a/camera/camera-mlkit-vision/src/test/java/androidx/camera/mlkit/vision/MlKitAnalyzerTest.kt b/camera/camera-mlkit-vision/src/test/java/androidx/camera/mlkit/vision/MlKitAnalyzerTest.kt
index c54a854..995880d 100644
--- a/camera/camera-mlkit-vision/src/test/java/androidx/camera/mlkit/vision/MlKitAnalyzerTest.kt
+++ b/camera/camera-mlkit-vision/src/test/java/androidx/camera/mlkit/vision/MlKitAnalyzerTest.kt
@@ -23,12 +23,12 @@
import android.util.Size
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageAnalysis.COORDINATE_SYSTEM_SENSOR
+import androidx.camera.core.ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED
import androidx.camera.core.ImageProxy
import androidx.camera.core.impl.utils.TransformUtils.getRectToRect
import androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor
import androidx.camera.testing.impl.fakes.FakeImageInfo
import androidx.camera.testing.impl.fakes.FakeImageProxy
-import androidx.camera.view.CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED
import com.google.common.truth.Truth.assertThat
import com.google.mlkit.vision.interfaces.Detector
import com.google.mlkit.vision.interfaces.Detector.TYPE_BARCODE_SCANNING
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/AndroidUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/AndroidUtil.java
index 99349a3..068267e 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/AndroidUtil.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/AndroidUtil.java
@@ -68,5 +68,15 @@
"Emulator API 21 has empty supported qualities. Unable to test.",
AndroidUtil.isEmulatorAndAPI21()
);
+ // Skip test for b/331618729
+ assumeFalse(
+ "Emulator API 28 crashes running this test.",
+ Build.VERSION.SDK_INT == 28 && isEmulator()
+ );
+ // Skip test for b/331618729
+ assumeFalse(
+ "Emulator API 30 crashes running this test.",
+ Build.VERSION.SDK_INT == 30 && isEmulator()
+ );
}
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeSurfaceEffect.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeSurfaceEffect.java
index 26925b4..9a5a3df 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeSurfaceEffect.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeSurfaceEffect.java
@@ -66,6 +66,14 @@
mSurfaceProcessorInternal = surfaceProcessorInternal;
}
+ public FakeSurfaceEffect(@Targets int targets, @Transformations int transformation,
+ @OutputOptions int targetOption,
+ @NonNull SurfaceProcessorInternal surfaceProcessorInternal) {
+ super(targets, transformation, targetOption, mainThreadExecutor(), surfaceProcessorInternal,
+ throwable -> {
+ });
+ }
+
/**
* Create a fake {@link CameraEffect} the {@link #createSurfaceProcessorInternal} value
* overridden.
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
index d780a8a..864bc494 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
@@ -199,6 +199,8 @@
DeviceQuirks.get(ExtraSupportedResolutionQuirk::class.java) != null
)
assumeTrue(AudioUtil.canStartAudioRecord(MediaRecorder.AudioSource.CAMCORDER))
+ // Skip for b/331618729
+ assumeNotBrokenEmulator()
CameraXUtil.initialize(
context,
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt
index 8e220bd..9b8528d 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt
@@ -70,6 +70,13 @@
)
}
+fun assumeNotBrokenEmulator() {
+ assumeFalse(
+ "Skip tests for Emulator API 30 crashing issue",
+ Build.MODEL.contains("gphone") && Build.VERSION.SDK_INT == 30
+ )
+}
+
@RequiresApi(21)
fun getRotationNeeded(
videoCapture: VideoCapture<Recorder>,
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/audio/AudioRecordCompatibilityTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/audio/AudioRecordCompatibilityTest.kt
index 55cc4e3..5121639 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/audio/AudioRecordCompatibilityTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/audio/AudioRecordCompatibilityTest.kt
@@ -25,7 +25,7 @@
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.camera.core.Logger
-import androidx.camera.testing.impl.LabTestRule
+import androidx.camera.testing.impl.RequiresDevice
import androidx.camera.video.internal.audio.AudioUtils.computeInterpolatedTimeNs
import androidx.camera.video.internal.audio.AudioUtils.getBytesPerFrame
import androidx.camera.video.internal.audio.AudioUtils.sizeToFrameCount
@@ -75,9 +75,6 @@
}
@get:Rule
- val labTest: LabTestRule = LabTestRule()
-
- @get:Rule
var audioPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.RECORD_AUDIO
)
@@ -103,7 +100,7 @@
}
// See b/301067226 for more information.
- @LabTestRule.LabTestOnly
+ @RequiresDevice
@SdkSuppress(minSdkVersion = 24)
@Test
fun read_withNoNegativeFramePositionIssue_whenRecordingMultipleTimes() {
@@ -130,7 +127,7 @@
}
// See b/301067226 for more information.
- @LabTestRule.LabTestOnly
+ @RequiresDevice
@SdkSuppress(minSdkVersion = 24)
@Test
fun read_withNoNegativeFramePositionIssue_whenRecordingAfterRecreatingMultipleTimes() {
@@ -158,7 +155,7 @@
}
// See b/301067226 for more information.
- @LabTestRule.LabTestOnly
+ @RequiresDevice
@SdkSuppress(minSdkVersion = 24)
@Test
fun read_withTimestampDiffToSystemInLimit_whenRecordingMultipleTimes() {
diff --git a/camera/camera-view/api/current.txt b/camera/camera-view/api/current.txt
index ea8f480..6b7f193 100644
--- a/camera/camera-view/api/current.txt
+++ b/camera/camera-view/api/current.txt
@@ -66,7 +66,7 @@
method @MainThread public androidx.camera.video.Recording startRecording(androidx.camera.video.MediaStoreOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
method @MainThread public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
method @MainThread public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
- field public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
+ field @Deprecated public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
field public static final int IMAGE_ANALYSIS = 2; // 0x2
field public static final int IMAGE_CAPTURE = 1; // 0x1
field public static final int TAP_TO_FOCUS_FAILED = 4; // 0x4
diff --git a/camera/camera-view/api/restricted_current.txt b/camera/camera-view/api/restricted_current.txt
index ea8f480..6b7f193 100644
--- a/camera/camera-view/api/restricted_current.txt
+++ b/camera/camera-view/api/restricted_current.txt
@@ -66,7 +66,7 @@
method @MainThread public androidx.camera.video.Recording startRecording(androidx.camera.video.MediaStoreOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
method @MainThread public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
method @MainThread public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
- field public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
+ field @Deprecated public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
field public static final int IMAGE_ANALYSIS = 2; // 0x2
field public static final int IMAGE_CAPTURE = 1; // 0x1
field public static final int TAP_TO_FOCUS_FAILED = 4; // 0x4
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
index 2890858..06a97b4 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
@@ -220,6 +220,7 @@
@Test
fun canRecordToMediaStore() {
+ if (Build.VERSION.SDK_INT == 28) return // b/264902324
assumeTrue(
"Ignore the test since the MediaStore.Video has compatibility issues.",
DeviceQuirks.get(MediaStoreVideoCannotWrite::class.java) == null
@@ -281,6 +282,7 @@
@Test
fun canRecordToFile_withoutAudio_whenAudioDisabled() {
+ if (Build.VERSION.SDK_INT == 28) return // b/264902324
// Arrange.
val file = createTempFile()
val outputOptions = FileOutputOptions.Builder(file).build()
@@ -299,6 +301,7 @@
@Test
fun canRecordToFile_whenLifecycleStops() {
+ if (Build.VERSION.SDK_INT == 28) return // b/264902324
assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
// Arrange.
@@ -374,6 +377,7 @@
@Test
fun canRecordToFile_rightAfterPreviousRecordingStopped() {
+ if (Build.VERSION.SDK_INT == 30) return // b/264902324
// Arrange.
val file1 = createTempFile()
val file2 = createTempFile()
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index 5a54f82..418aa5b 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -162,7 +162,9 @@
* camera-core directly.
*
* @see ImageAnalysis.Analyzer
+ * @deprecated Use {@link ImageAnalysis#COORDINATE_SYSTEM_VIEW_REFERENCED} instead.
*/
+ @Deprecated
public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1;
/**
@@ -1106,6 +1108,12 @@
* <p>Setting an analyzer function replaces any previous analyzer. Only one analyzer can be
* set at any time.
*
+ * <p>If the {@link ImageAnalysis.Analyzer#getTargetCoordinateSystem()} returns
+ * {@link ImageAnalysis#COORDINATE_SYSTEM_VIEW_REFERENCED}, the analyzer will receive a
+ * transformation via {@link ImageAnalysis.Analyzer#updateTransform} that converts
+ * coordinates from the {@link ImageAnalysis}'s coordinate system to the {@link PreviewView}'s
+ * coordinate system.
+ *
* <p> If the {@link ImageAnalysis.Analyzer#getDefaultTargetResolution()} returns a non-null
* value, calling this method will reconfigure the camera which might cause additional
* latency. To avoid this, set the value before controller is bound to the lifecycle.
@@ -1453,7 +1461,7 @@
return;
}
if (mAnalysisAnalyzer.getTargetCoordinateSystem()
- == COORDINATE_SYSTEM_VIEW_REFERENCED) {
+ == ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED) {
mAnalysisAnalyzer.updateTransform(matrix);
}
}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt b/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
index 19a2e7d..6a57950 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
+++ b/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
@@ -29,6 +29,7 @@
import androidx.camera.core.DynamicRange
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageAnalysis.COORDINATE_SYSTEM_ORIGINAL
+import androidx.camera.core.ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCapture.ScreenFlash
import androidx.camera.core.ImageProxy
@@ -49,7 +50,6 @@
import androidx.camera.testing.impl.fakes.FakeSurfaceProcessor
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
-import androidx.camera.view.CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED
import androidx.camera.view.internal.ScreenFlashUiInfo
import androidx.concurrent.futures.CallbackToFutureAdapter
import androidx.test.annotation.UiThreadTest
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
index 3a2368e..e64e886 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
@@ -38,8 +38,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.appcompat.app.AppCompatActivity;
-import androidx.camera.camera2.interop.Camera2CameraInfo;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraControl;
import androidx.camera.core.CameraInfo;
@@ -50,6 +50,7 @@
import androidx.camera.core.MeteringPoint;
import androidx.camera.core.Preview;
import androidx.camera.core.UseCaseGroup;
+import androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.app.ActivityCompat;
@@ -90,8 +91,8 @@
private boolean mIsConcurrentModeOn = false;
private boolean mIsLayoutPiP = true;
private boolean mIsFrontPrimary = true;
-
private boolean mIsDualSelfieEnabled = false;
+ private boolean mIsCameraPipeEnabled = false;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -129,6 +130,8 @@
mIsLayoutPiP = true;
bindPreviewForSingle(mCameraProvider);
mIsConcurrentModeOn = false;
+ mIsDualSelfieEnabled = false;
+ mDualSelfieButton.setChecked(false);
} else {
mIsLayoutPiP = true;
bindPreviewForPiP(mCameraProvider);
@@ -170,7 +173,13 @@
}
}
+ @SuppressLint("NullAnnotationGroup")
+ @OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
private void startCamera() {
+ if (mIsCameraPipeEnabled) {
+ ProcessCameraProvider.configureInstance(CameraPipeConfig.defaultConfig());
+ }
+
final ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
ProcessCameraProvider.getInstance(this);
cameraProviderFuture.addListener(() -> {
@@ -184,7 +193,6 @@
}, ContextCompat.getMainExecutor(this));
}
- @SuppressLint("RestrictedApiAndroidX")
void bindPreviewForSingle(@NonNull ProcessCameraProvider cameraProvider) {
cameraProvider.unbindAll();
mSideBySideLayout.setVisibility(GONE);
@@ -265,8 +273,9 @@
mBackPreviewView);
}
- @OptIn(markerClass = ExperimentalCamera2Interop.class)
- @SuppressLint({"RestrictedApiAndroidX", "NullAnnotationGroup"})
+ @SuppressLint("NullAnnotationGroup")
+ @OptIn(markerClass = {ExperimentalCamera2Interop.class,
+ androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class})
private void bindToLifecycleForConcurrentCamera(
@NonNull ProcessCameraProvider cameraProvider,
@NonNull LifecycleOwner lifecycleOwner,
@@ -288,13 +297,18 @@
String innerPhysicalCameraId = null;
String outerPhysicalCameraId = null;
for (CameraInfo info : cameraInfoPrimary.getPhysicalCameraInfos()) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- if (Camera2CameraInfo.from(info).getCameraCharacteristic(LENS_POSE_REFERENCE)
- == CameraCharacteristics.LENS_POSE_REFERENCE_PRIMARY_CAMERA) {
- innerPhysicalCameraId = Camera2CameraInfo.from(info).getCameraId();
- } else {
- outerPhysicalCameraId = Camera2CameraInfo.from(info).getCameraId();
- }
+ if (isPrimaryCamera(info)) {
+ innerPhysicalCameraId = mIsCameraPipeEnabled
+ ? androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo
+ .from(info).getCameraId()
+ : androidx.camera.camera2.interop.Camera2CameraInfo
+ .from(info).getCameraId();
+ } else {
+ outerPhysicalCameraId = mIsCameraPipeEnabled
+ ? androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo
+ .from(info).getCameraId()
+ : androidx.camera.camera2.interop.Camera2CameraInfo
+ .from(info).getCameraId();
}
}
@@ -380,6 +394,24 @@
}
}
+ @SuppressLint("NullAnnotationGroup")
+ @OptIn(markerClass = { ExperimentalCamera2Interop.class,
+ androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class })
+ private boolean isPrimaryCamera(@NonNull CameraInfo info) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ return true;
+ }
+ if (mIsCameraPipeEnabled) {
+ return androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo.from(info)
+ .getCameraCharacteristic(LENS_POSE_REFERENCE)
+ == CameraCharacteristics.LENS_POSE_REFERENCE_PRIMARY_CAMERA;
+ } else {
+ return androidx.camera.camera2.interop.Camera2CameraInfo.from(info)
+ .getCameraCharacteristic(LENS_POSE_REFERENCE)
+ == CameraCharacteristics.LENS_POSE_REFERENCE_PRIMARY_CAMERA;
+ }
+ }
+
private void setupZoomAndTapToFocus(Camera camera, PreviewView previewView) {
ScaleGestureDetector scaleDetector = new ScaleGestureDetector(this,
new ScaleGestureDetector.SimpleOnScaleGestureListener() {
diff --git a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
index dfd1d5a..899e02a 100644
--- a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
+++ b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
@@ -29,13 +29,13 @@
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
+import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.mlkit.vision.MlKitAnalyzer
import androidx.camera.video.MediaStoreOutputOptions
import androidx.camera.video.Recording
import androidx.camera.video.VideoRecordEvent
-import androidx.camera.view.CameraController
import androidx.camera.view.CameraController.IMAGE_ANALYSIS
import androidx.camera.view.CameraController.IMAGE_CAPTURE
import androidx.camera.view.CameraController.VIDEO_CAPTURE
@@ -324,7 +324,7 @@
analyzer = MlKitAnalyzer(
listOf(barcodeScanner),
- CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED,
+ ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED,
calibrationExecutor
) { result ->
// validating thread
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt
index 75ed2eb..39ad0ec 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt
@@ -41,6 +41,7 @@
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.google.common.truth.Truth.assertThat
@@ -156,6 +157,7 @@
}
// The test makes sure the TextureView surface texture keeps the same after switch.
+ @SdkSuppress(maxSdkVersion = 33) // b/331933633
@Test
fun testPreviewViewUpdateAfterSwitch() {
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt
index c3178fe..42171cb 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt
@@ -114,7 +114,7 @@
// We need to acquire OutputTransform from PreviewView for this to work
private val mlKitAnalyzer = MlKitAnalyzer(
listOf(barcodeScanner),
- COORDINATE_SYSTEM_VIEW_REFERENCED,
+ ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED,
Dispatchers.Main.asExecutor()
) { result ->
val barcodes = result.getValue(barcodeScanner)
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MlKitFragment.kt b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MlKitFragment.kt
index 17c2e9f..18ea43b 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MlKitFragment.kt
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MlKitFragment.kt
@@ -25,9 +25,9 @@
import android.widget.ToggleButton
import androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA
import androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA
+import androidx.camera.core.ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
import androidx.camera.mlkit.vision.MlKitAnalyzer
-import androidx.camera.view.CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.fragment.app.Fragment
diff --git a/car/app/app-automotive/src/main/res/values-nl/strings.xml b/car/app/app-automotive/src/main/res/values-nl/strings.xml
index d1c0cbd..c9bcda9 100644
--- a/car/app/app-automotive/src/main/res/values-nl/strings.xml
+++ b/car/app/app-automotive/src/main/res/values-nl/strings.xml
@@ -18,7 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="error_action_finish" msgid="7621130025103996211">"App sluiten"</string>
- <string name="error_action_update_host" msgid="4802951804749609593">"Controleren op updates"</string>
+ <string name="error_action_update_host" msgid="4802951804749609593">"Zoeken naar updates"</string>
<string name="error_action_retry" msgid="985347670495166517">"Opnieuw proberen"</string>
<string name="error_message_client_side_error" msgid="3323186720368387787">"App-fout. Meld deze fout aan de app-ontwikkelaar."</string>
<string name="error_message_host_error" msgid="5484419926049675696">"Systeemfout"</string>
diff --git a/car/app/app-automotive/src/main/res/values-te/strings.xml b/car/app/app-automotive/src/main/res/values-te/strings.xml
index f331f46..546f0bf 100644
--- a/car/app/app-automotive/src/main/res/values-te/strings.xml
+++ b/car/app/app-automotive/src/main/res/values-te/strings.xml
@@ -17,7 +17,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="error_action_finish" msgid="7621130025103996211">"యాప్ను మూసివేయి"</string>
+ <string name="error_action_finish" msgid="7621130025103996211">"యాప్ను మూసివేయండి"</string>
<string name="error_action_update_host" msgid="4802951804749609593">"అప్డేట్ల కోసం చెక్ చేయండి"</string>
<string name="error_action_retry" msgid="985347670495166517">"మళ్లీ ట్రై చేయండి"</string>
<string name="error_message_client_side_error" msgid="3323186720368387787">"యాప్ ఎర్రర్. దయచేసి ఈ ఎర్రర్ను యాప్ డెవలపర్కు రిపోర్ట్ చేయండి"</string>
diff --git a/car/app/app-samples/showcase/common/build.gradle b/car/app/app-samples/showcase/common/build.gradle
index fe9ac9f..05d3db5 100644
--- a/car/app/app-samples/showcase/common/build.gradle
+++ b/car/app/app-samples/showcase/common/build.gradle
@@ -26,6 +26,7 @@
plugins {
id("AndroidXPlugin")
id("com.android.library")
+ id("org.jetbrains.kotlin.android")
}
android {
@@ -39,6 +40,7 @@
implementation(project(":car:app:app"))
debugImplementation(libs.leakcanary)
+ implementation(libs.kotlinStdlibCommon)
implementation("androidx.core:core:1.7.0")
implementation project(":annotation:annotation-experimental")
}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java
index d9b083d..157077f 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SamplePlaces.java
@@ -23,7 +23,11 @@
import android.graphics.BitmapFactory;
import android.location.Location;
import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
import android.text.Spanned;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.LogPrinter;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
@@ -266,26 +270,25 @@
// Build a description string that includes the required distance span.
int distanceKm = getDistanceFromCurrentLocation(place.location) / 1000;
- SpannableString description = new SpannableString(" \u00b7 " + place.description);
- description.setSpan(
+ SpannableStringBuilder descriptionBuilder = new SpannableStringBuilder();
+
+ descriptionBuilder.append(
+ " ",
DistanceSpan.create(Distance.create(distanceKm, Distance.UNIT_KILOMETERS)),
- 0,
- 1,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- description.setSpan(
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+ descriptionBuilder.setSpan(
ForegroundCarColorSpan.create(CarColor.BLUE),
0,
1,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+ descriptionBuilder.append(" · ");
+ descriptionBuilder.append(place.description);
if (index == 4) {
- description.setSpan(
- CarIconSpan.create(
- createCarIconWithBitmap(carContext, R.drawable.ic_hi),
- CarIconSpan.ALIGN_CENTER
- ),
- 5,
- 6,
- Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ descriptionBuilder.append(" ",
+ CarIconSpan.create(createCarIconWithBitmap(carContext, R.drawable.ic_hi)),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
boolean isBrowsable = index > mPlaces.size() / 2;
@@ -294,7 +297,7 @@
listBuilder.addItem(
new Row.Builder()
.setTitle(place.title)
- .addText(description)
+ .addText(descriptionBuilder)
.setOnClickListener(() -> onClickPlace(place))
.setBrowsable(isBrowsable)
.setMetadata(
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SpannableStringBuilderAnnotationExtensions.kt b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SpannableStringBuilderAnnotationExtensions.kt
new file mode 100644
index 0000000..5f3cbcc
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/SpannableStringBuilderAnnotationExtensions.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.sample.showcase.common.common
+
+import android.content.Context
+import android.text.Annotation as AnnotationSpan
+import android.text.SpannableStringBuilder
+import android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE
+import android.util.Log
+import androidx.annotation.StringRes
+import androidx.car.app.model.CarColor
+import androidx.car.app.model.ClickableSpan
+import androidx.car.app.model.ForegroundCarColorSpan
+import androidx.car.app.utils.LogTags.TAG
+
+/**
+ * Provides [SpannableStringBuilder] extension methods, which can be used to find & replace
+ * [android.text.Annotation] tags.
+ */
+object SpannableStringBuilderAnnotationExtensions {
+ @JvmStatic
+ fun Context.getSpannableStringBuilder(@StringRes resId: Int): SpannableStringBuilder =
+ SpannableStringBuilder(getText(resId))
+
+ private fun SpannableStringBuilder.findAnnotationBounds(
+ key: String,
+ value: String
+ ): Pair<Int, Int>? = getSpans(0, length, AnnotationSpan::class.java)
+ .find { it.key == key && it.value == value }
+ ?.let { Pair(getSpanStart(it), getSpanEnd(it)) }
+
+ /**
+ * Nests the provided span within an existing [android.text.Annotation].
+ *
+ * The existing [android.text.Annotation] is found via the provided [key] and [value]. If no
+ * [android.text.Annotation] is found, then a new span will not be added.
+ */
+ @JvmStatic
+ fun SpannableStringBuilder.addSpanToAnnotatedPosition(
+ key: String,
+ value: String,
+ span: Any
+ ): SpannableStringBuilder {
+ findAnnotationBounds(key, value)
+ ?.let { setSpan(span, it.first, it.second, SPAN_INCLUSIVE_INCLUSIVE) }
+ ?: Log.e(TAG, "Unable to find annotation span for $key:$value")
+
+ return this
+ }
+
+ @JvmStatic
+ fun SpannableStringBuilder.addSpanToAnnotatedPosition(
+ key: String,
+ value: String,
+ color: CarColor
+ ): SpannableStringBuilder =
+ addSpanToAnnotatedPosition(key, value, ForegroundCarColorSpan.create(color))
+
+ @JvmStatic
+ fun SpannableStringBuilder.addSpanToAnnotatedPosition(
+ key: String,
+ value: String,
+ onClick: () -> Unit
+ ): SpannableStringBuilder =
+ addSpanToAnnotatedPosition(key, value, ClickableSpan.create(onClick))
+}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/Utils.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/Utils.java
deleted file mode 100644
index cf1760a..0000000
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/common/Utils.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 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.car.app.sample.showcase.common.common;
-
-import android.text.Spannable;
-import android.text.SpannableString;
-
-import androidx.annotation.NonNull;
-import androidx.car.app.model.CarColor;
-import androidx.car.app.model.ClickableSpan;
-import androidx.car.app.model.ForegroundCarColorSpan;
-
-/** Assorted utilities. */
-public abstract class Utils {
-
- /** Colorize the given string. */
- public static void colorize(@NonNull SpannableString s, @NonNull CarColor color, int index,
- int length) {
- s.setSpan(
- ForegroundCarColorSpan.create(color),
- index,
- index + length,
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
-
- /** Colorize the given string. */
- @NonNull
- public static CharSequence colorize(@NonNull String s, @NonNull CarColor color, int index,
- int length) {
- SpannableString ss = new SpannableString(s);
- ss.setSpan(
- ForegroundCarColorSpan.create(color),
- index,
- index + length,
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- return ss;
- }
-
- /** Make the given string clickable. */
- @NonNull
- public static CharSequence clickable(@NonNull String s, int index, int length,
- @NonNull Runnable action) {
- SpannableString ss = new SpannableString(s);
- ss.setSpan(
- ClickableSpan.create(action::run),
- index,
- index + length,
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- return ss;
- }
-
- private Utils() {
- }
-}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/templatelayouts/SignInTemplateDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/templatelayouts/SignInTemplateDemoScreen.java
index 970df24f..64d978c 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/templatelayouts/SignInTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/templatelayouts/SignInTemplateDemoScreen.java
@@ -16,10 +16,13 @@
package androidx.car.app.sample.showcase.common.screens.templatelayouts;
+import static android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE;
+
import static androidx.car.app.CarToast.LENGTH_LONG;
import android.graphics.Color;
import android.net.Uri;
+import android.text.SpannableStringBuilder;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
@@ -30,6 +33,7 @@
import androidx.car.app.model.Action;
import androidx.car.app.model.CarColor;
import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.ForegroundCarColorSpan;
import androidx.car.app.model.Header;
import androidx.car.app.model.InputCallback;
import androidx.car.app.model.MessageTemplate;
@@ -41,11 +45,13 @@
import androidx.car.app.model.signin.QRCodeSignInMethod;
import androidx.car.app.model.signin.SignInTemplate;
import androidx.car.app.sample.showcase.common.R;
-import androidx.car.app.sample.showcase.common.common.Utils;
+import androidx.car.app.sample.showcase.common.common.SpannableStringBuilderAnnotationExtensions;
import androidx.car.app.sample.showcase.common.screens.templatelayouts.messagetemplates.LongMessageTemplateDemoScreen;
import androidx.car.app.versioning.CarAppApiLevels;
import androidx.core.graphics.drawable.IconCompat;
+import kotlin.Unit;
+
/** A screen that demonstrates the sign-in template. */
public class SignInTemplateDemoScreen extends Screen {
private static final String EMAIL_REGEXP = "^(.+)@(.+)$";
@@ -84,9 +90,19 @@
};
carContext.getOnBackPressedDispatcher().addCallback(this, callback);
- mAdditionalText = Utils.clickable(getCarContext().getString(R.string.additional_text), 18,
- 16,
- () -> getScreenManager().push(new LongMessageTemplateDemoScreen(getCarContext())));
+ SpannableStringBuilder additionalText =
+ SpannableStringBuilderAnnotationExtensions.getSpannableStringBuilder(
+ getCarContext(), R.string.additional_text);
+ SpannableStringBuilderAnnotationExtensions.addSpanToAnnotatedPosition(
+ additionalText,
+ "link",
+ "terms_of_service",
+ () -> {
+ getScreenManager().push(new LongMessageTemplateDemoScreen(getCarContext()));
+ return Unit.INSTANCE;
+ }
+ );
+ mAdditionalText = additionalText;
mProviderSignInAction = new Action.Builder()
.setTitle(getCarContext().getString(R.string.google_sign_in))
@@ -120,7 +136,7 @@
return new MessageTemplate.Builder(
getCarContext().getString(R.string.sign_in_template_not_supported_text))
.setHeader(new Header.Builder().setTitle(getCarContext().getString(
- R.string.sign_in_template_not_supported_title))
+ R.string.sign_in_template_not_supported_title))
.setStartHeaderAction(Action.BACK).build())
.build();
}
@@ -287,11 +303,17 @@
R.drawable.ic_googleg);
CarColor noTint = CarColor.createCustom(Color.TRANSPARENT, Color.TRANSPARENT);
+ SpannableStringBuilder title = new SpannableStringBuilder()
+ .append(
+ getCarContext().getString(R.string.sign_in_with_google_title),
+ ForegroundCarColorSpan.create(
+ CarColor.createCustom(Color.BLACK, Color.BLACK)),
+ SPAN_INCLUSIVE_INCLUSIVE
+ );
+
ProviderSignInMethod providerSignInMethod = new ProviderSignInMethod(
new Action.Builder()
- .setTitle(Utils.colorize(
- getCarContext().getString(R.string.sign_in_with_google_title),
- CarColor.createCustom(Color.BLACK, Color.BLACK), 0, 19))
+ .setTitle(title)
.setBackgroundColor(CarColor.createCustom(Color.WHITE, Color.WHITE))
.setIcon(new CarIcon.Builder(providerIcon)
.setTint(noTint)
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/templatelayouts/listtemplates/TextAndIconsDemosScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/templatelayouts/listtemplates/TextAndIconsDemosScreen.java
index 49b098a..9153cd9 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/templatelayouts/listtemplates/TextAndIconsDemosScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/templatelayouts/listtemplates/TextAndIconsDemosScreen.java
@@ -16,33 +16,37 @@
package androidx.car.app.sample.showcase.common.screens.templatelayouts.listtemplates;
+import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
+
import static androidx.car.app.model.Action.BACK;
import static androidx.car.app.model.CarColor.GREEN;
import static androidx.car.app.model.CarColor.RED;
import static androidx.car.app.model.CarColor.YELLOW;
import android.graphics.BitmapFactory;
-import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
import androidx.car.app.CarContext;
import androidx.car.app.Screen;
import androidx.car.app.model.CarColor;
import androidx.car.app.model.CarIcon;
+import androidx.car.app.model.ForegroundCarColorSpan;
import androidx.car.app.model.Header;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.ListTemplate;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.sample.showcase.common.R;
-import androidx.car.app.sample.showcase.common.common.Utils;
+import androidx.car.app.sample.showcase.common.common.SpannableStringBuilderAnnotationExtensions;
import androidx.core.graphics.drawable.IconCompat;
/** Creates a screen that shows different types of texts and icons. */
public final class TextAndIconsDemosScreen extends Screen {
- private static final String FULL_STAR = "\u2605";
- private static final String HALF_STAR = "\u00BD";
+ private static final String FULL_STAR = "★";
+ private static final String HALF_STAR = "½";
public TextAndIconsDemosScreen(@NonNull CarContext carContext) {
super(carContext);
@@ -51,7 +55,6 @@
@NonNull
@Override
public Template onGetTemplate() {
-
ItemList.Builder listBuilder = new ItemList.Builder();
listBuilder.addItem(buildRowForTemplate(R.string.title_with_app_icon_row_title,
@@ -64,11 +67,11 @@
buildCarIconWithResource(R.drawable.banana)));
listBuilder.addItem(buildRowForTemplate(R.string.title_with_res_id_image_row_title,
- buildSecondaryText(R.string.example_1_text, RED, 16, 3),
+ buildSecondaryText(R.string.example_1_text),
buildCarIconWithResource(R.drawable.ic_fastfood_white_48dp, RED)));
listBuilder.addItem(buildRowForTemplate(R.string.title_with_svg_image_row_title,
- buildSecondaryText(R.string.example_2_text, GREEN, 16, 5),
+ buildSecondaryText(R.string.example_2_text),
buildCarIconWithResource(R.drawable.ic_emoji_food_beverage_white_48dp, GREEN)));
listBuilder.addItem(buildRowForTemplate(R.string.colored_secondary_row_title,
@@ -77,9 +80,9 @@
return new ListTemplate.Builder()
.setSingleList(listBuilder.build())
.setHeader(new Header.Builder()
- .setTitle(getCarContext().getString(R.string.text_icons_demo_title))
- .setStartHeaderAction(BACK)
- .build())
+ .setTitle(getCarContext().getString(R.string.text_icons_demo_title))
+ .setStartHeaderAction(BACK)
+ .build())
.build();
}
@@ -111,11 +114,21 @@
}
/**
- * build a colored line of secondary text using a specific CarColor and some custom text
- */
- private CharSequence buildSecondaryText(int textId, CarColor color, int index, int length) {
- return Utils.colorize(getCarContext().getString(textId),
- color, index, length);
+ * build a colored line of secondary text using a specific CarColor and some custom text
+ */
+ private CharSequence buildSecondaryText(@StringRes int textId) {
+ SpannableStringBuilder ssb =
+ SpannableStringBuilderAnnotationExtensions.getSpannableStringBuilder(
+ getCarContext(), textId);
+ SpannableStringBuilderAnnotationExtensions.addSpanToAnnotatedPosition(ssb, "color",
+ "red", CarColor.RED);
+ SpannableStringBuilderAnnotationExtensions.addSpanToAnnotatedPosition(ssb, "color",
+ "green", CarColor.GREEN);
+ SpannableStringBuilderAnnotationExtensions.addSpanToAnnotatedPosition(ssb, "color",
+ "blue", CarColor.BLUE);
+ SpannableStringBuilderAnnotationExtensions.addSpanToAnnotatedPosition(ssb, "color",
+ "yellow", CarColor.YELLOW);
+ return ssb;
}
private Row buildRowForTemplate(int title, CharSequence text) {
@@ -146,10 +159,9 @@
for (s = "", r = ratings; r > 0; --r) {
s += r < 1 ? HALF_STAR : FULL_STAR;
}
- SpannableString ss = new SpannableString(s + " ratings: " + ratings);
- if (!s.isEmpty()) {
- Utils.colorize(ss, YELLOW, 0, s.length());
- }
- return ss;
+ return new SpannableStringBuilder()
+ .append(s, ForegroundCarColorSpan.create(YELLOW), SPAN_EXCLUSIVE_EXCLUSIVE)
+ .append(" ratings: ")
+ .append(ratings.toString());
}
}
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ar/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ar/strings.xml
index 50819b5..31c6ba3 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ar/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ar/strings.xml
@@ -40,7 +40,7 @@
<string name="reject_action_title" msgid="6730366705938402668">"رفض"</string>
<string name="ok_action_title" msgid="7128494973966098611">"حسنًا"</string>
<string name="throw_action_title" msgid="7163710562670220163">"طرح"</string>
- <string name="commute_action_title" msgid="2585755255290185096">"تنقُّل"</string>
+ <string name="commute_action_title" msgid="2585755255290185096">"التنقُّل"</string>
<string name="sign_out_action_title" msgid="1653943000866713010">"تسجيل الخروج"</string>
<string name="try_anyway_action_title" msgid="7384500054249311718">"المحاولة على أيّ حال"</string>
<string name="yes_action_title" msgid="5507096013762092189">"نعم"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ca/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ca/strings.xml
index 62e4725..2668924 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ca/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ca/strings.xml
@@ -247,7 +247,7 @@
<string name="sign_in_instructions" msgid="9044850228284294762">"Introdueix les teves credencials"</string>
<string name="invalid_email_error_msg" msgid="5261362663718987167">"El nom d\'usuari ha de ser una adreça electrònica vàlida"</string>
<string name="invalid_length_error_msg" msgid="8238905276326976425">"El nom d\'usuari ha de tenir com a mínim %s caràcters"</string>
- <string name="invalid_password_error_msg" msgid="1090359893902674610">"La contrasenya no és vàlida"</string>
+ <string name="invalid_password_error_msg" msgid="1090359893902674610">"Contrasenya no vàlida"</string>
<string name="password_hint" msgid="2869107073860012864">"contrasenya"</string>
<string name="password_sign_in_instruction_prefix" msgid="9105788349198243508">"Nom d\'usuari"</string>
<string name="pin_sign_in_instruction" msgid="2288691296234360441">"Escriu aquest PIN al telèfon"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-hi/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-hi/strings.xml
index c9eaae3..c3058c4 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-hi/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-hi/strings.xml
@@ -27,7 +27,7 @@
<string name="cancel_action_title" msgid="1149738685397349236">"रद्द करें"</string>
<string name="stop_action_title" msgid="1187619482795416314">"बंद करें"</string>
<string name="more_action_title" msgid="1039516575011403837">"ज़्यादा देखें"</string>
- <string name="call_action_title" msgid="6218977436905001611">"कॉल करें"</string>
+ <string name="call_action_title" msgid="6218977436905001611">"कॉल"</string>
<string name="primary_action_title" msgid="7042003552215710683">"मुख्य"</string>
<string name="options_action_title" msgid="1168121856107932984">"विकल्प"</string>
<string name="search_action_title" msgid="3483459674263446335">"खोजें"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-in/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-in/strings.xml
index 38c0edf..abe09b9 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-in/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-in/strings.xml
@@ -40,7 +40,7 @@
<string name="reject_action_title" msgid="6730366705938402668">"Tolak"</string>
<string name="ok_action_title" msgid="7128494973966098611">"Oke"</string>
<string name="throw_action_title" msgid="7163710562670220163">"Lempar"</string>
- <string name="commute_action_title" msgid="2585755255290185096">"Perjalanan"</string>
+ <string name="commute_action_title" msgid="2585755255290185096">"Perjalanan sehari-hari"</string>
<string name="sign_out_action_title" msgid="1653943000866713010">"Logout"</string>
<string name="try_anyway_action_title" msgid="7384500054249311718">"Tetap Coba"</string>
<string name="yes_action_title" msgid="5507096013762092189">"Ya"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-zh-rTW/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-zh-rTW/strings.xml
index f4b2b1d..78779d1 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-zh-rTW/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-zh-rTW/strings.xml
@@ -94,7 +94,7 @@
<string name="no_energy_profile_permission" msgid="4662285713731308888">"沒有能源設定檔權限。"</string>
<string name="fuel_types" msgid="6811375173343218212">"燃料類型"</string>
<string name="unavailable" msgid="3636401138255192934">"無法取得"</string>
- <string name="ev_connector_types" msgid="735458637011996125">"電動車連接器類型"</string>
+ <string name="ev_connector_types" msgid="735458637011996125">"電動車充電插頭類型"</string>
<string name="example_title" msgid="530257630320010494">"範例 %d"</string>
<string name="example_1_text" msgid="8456567953748293512">"這是紅色的文字"</string>
<string name="example_2_text" msgid="718820705318661440">"這是綠色的文字"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
index 948d8fab6..f8c1adf3 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
@@ -109,10 +109,10 @@
<!-- ColorDemoScreen -->
<string name="example_title">Example %d</string>
- <string name="example_1_text">This text has a red color</string>
- <string name="example_2_text">This text has a green color</string>
- <string name="example_3_text">This text has a blue color</string>
- <string name="example_4_text">This text has a yellow color</string>
+ <string name="example_1_text">This text has a <annotation color="red">red</annotation> color</string>
+ <string name="example_2_text">This text has a <annotation color="green">green</annotation> color</string>
+ <string name="example_3_text">This text has a <annotation color="blue">blue</annotation> color</string>
+ <string name="example_4_text">This text has a <annotation color="yellow">yellow</annotation> color</string>
<string name="example_5_text">This text uses the primary color</string>
<string name="example_6_text">This text uses the secondary color</string>
<string name="color_demo">Color Demo</string>
@@ -305,7 +305,7 @@
<string name="search_hint">Search here</string>
<!-- SignInTemplateDemoScreen -->
- <string name="additional_text">Please review our terms of service</string>
+ <string name="additional_text">Please review our <annotation link="terms_of_service">terms of service</annotation></string>
<string name="google_sign_in">Google sign-in</string>
<string name="use_pin">Use PIN</string>
<string name="qr_code">QR Code</string>
diff --git a/compose/animation/animation-core/api/current.txt b/compose/animation/animation-core/api/current.txt
index b69132f..b4bf45a 100644
--- a/compose/animation/animation-core/api/current.txt
+++ b/compose/animation/animation-core/api/current.txt
@@ -125,7 +125,8 @@
method @Deprecated @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.InfiniteRepeatableSpec<T> infiniteRepeatable(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.InfiniteRepeatableSpec<T> infiniteRepeatable(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode, optional long initialStartOffset);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.KeyframesSpec<T> keyframes(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.KeyframesSpec.KeyframesSpecConfig<T>,kotlin.Unit> init);
- method @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.KeyframesWithSplineSpec<T> keyframesWithSpline(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T>,kotlin.Unit> init);
+ method @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public static <T> androidx.compose.animation.core.KeyframesWithSplineSpec<T> keyframesWithSpline(@FloatRange(from=0.0, to=1.0) float periodicBias, kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T>,kotlin.Unit> init);
+ method @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public static <T> androidx.compose.animation.core.KeyframesWithSplineSpec<T> keyframesWithSpline(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T>,kotlin.Unit> init);
method @Deprecated @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.RepeatableSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.RepeatableSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode, optional long initialStartOffset);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.SnapSpec<T> snap(optional int delayMillis);
@@ -207,35 +208,29 @@
}
@SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi @androidx.compose.runtime.Immutable public final class ArcAnimationSpec<T> implements androidx.compose.animation.core.DurationBasedAnimationSpec<T> {
- ctor public ArcAnimationSpec(optional androidx.compose.animation.core.ArcMode mode, optional int durationMillis, optional int delayMillis, optional androidx.compose.animation.core.Easing easing);
+ ctor public ArcAnimationSpec(optional int mode, optional int durationMillis, optional int delayMillis, optional androidx.compose.animation.core.Easing easing);
method public int getDelayMillis();
method public int getDurationMillis();
method public androidx.compose.animation.core.Easing getEasing();
- method public androidx.compose.animation.core.ArcMode getMode();
+ method public int getMode();
method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
property public final int delayMillis;
property public final int durationMillis;
property public final androidx.compose.animation.core.Easing easing;
- property public final androidx.compose.animation.core.ArcMode mode;
+ property public final int mode;
}
- @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public abstract sealed class ArcMode {
+ @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi @kotlin.jvm.JvmInline public final value class ArcMode {
field public static final androidx.compose.animation.core.ArcMode.Companion Companion;
}
public static final class ArcMode.Companion {
- }
-
- @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public static final class ArcMode.Companion.ArcAbove extends androidx.compose.animation.core.ArcMode {
- field public static final androidx.compose.animation.core.ArcMode.Companion.ArcAbove INSTANCE;
- }
-
- @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public static final class ArcMode.Companion.ArcBelow extends androidx.compose.animation.core.ArcMode {
- field public static final androidx.compose.animation.core.ArcMode.Companion.ArcBelow INSTANCE;
- }
-
- @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public static final class ArcMode.Companion.ArcLinear extends androidx.compose.animation.core.ArcMode {
- field public static final androidx.compose.animation.core.ArcMode.Companion.ArcLinear INSTANCE;
+ method public int getArcAbove();
+ method public int getArcBelow();
+ method public int getArcLinear();
+ property public final int ArcAbove;
+ property public final int ArcBelow;
+ property public final int ArcLinear;
}
@androidx.compose.runtime.Immutable public final class CubicBezierEasing implements androidx.compose.animation.core.Easing {
@@ -501,7 +496,7 @@
ctor public KeyframesSpec.KeyframesSpecConfig();
method public infix androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T> at(T, @IntRange(from=0L) int timeStamp);
method public infix androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T> atFraction(T, @FloatRange(from=0.0, to=1.0) float fraction);
- method @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public infix androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T> using(androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T>, androidx.compose.animation.core.ArcMode arcMode);
+ method @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public infix androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T> using(androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T>, int arcMode);
method @Deprecated public infix void with(androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T>, androidx.compose.animation.core.Easing easing);
}
@@ -519,6 +514,7 @@
@SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi @androidx.compose.runtime.Immutable public final class KeyframesWithSplineSpec<T> implements androidx.compose.animation.core.DurationBasedAnimationSpec<T> {
ctor public KeyframesWithSplineSpec(androidx.compose.animation.core.KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T> config);
+ ctor public KeyframesWithSplineSpec(androidx.compose.animation.core.KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T> config, @FloatRange(from=0.0, to=1.0) float periodicBias);
method public androidx.compose.animation.core.KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T> getConfig();
method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
property public final androidx.compose.animation.core.KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T> config;
diff --git a/compose/animation/animation-core/api/restricted_current.txt b/compose/animation/animation-core/api/restricted_current.txt
index a861c60..bea0bde 100644
--- a/compose/animation/animation-core/api/restricted_current.txt
+++ b/compose/animation/animation-core/api/restricted_current.txt
@@ -125,7 +125,8 @@
method @Deprecated @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.InfiniteRepeatableSpec<T> infiniteRepeatable(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.InfiniteRepeatableSpec<T> infiniteRepeatable(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode, optional long initialStartOffset);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.KeyframesSpec<T> keyframes(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.KeyframesSpec.KeyframesSpecConfig<T>,kotlin.Unit> init);
- method @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.KeyframesWithSplineSpec<T> keyframesWithSpline(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T>,kotlin.Unit> init);
+ method @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public static <T> androidx.compose.animation.core.KeyframesWithSplineSpec<T> keyframesWithSpline(@FloatRange(from=0.0, to=1.0) float periodicBias, kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T>,kotlin.Unit> init);
+ method @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public static <T> androidx.compose.animation.core.KeyframesWithSplineSpec<T> keyframesWithSpline(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T>,kotlin.Unit> init);
method @Deprecated @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.RepeatableSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.RepeatableSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode, optional long initialStartOffset);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.SnapSpec<T> snap(optional int delayMillis);
@@ -207,35 +208,29 @@
}
@SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi @androidx.compose.runtime.Immutable public final class ArcAnimationSpec<T> implements androidx.compose.animation.core.DurationBasedAnimationSpec<T> {
- ctor public ArcAnimationSpec(optional androidx.compose.animation.core.ArcMode mode, optional int durationMillis, optional int delayMillis, optional androidx.compose.animation.core.Easing easing);
+ ctor public ArcAnimationSpec(optional int mode, optional int durationMillis, optional int delayMillis, optional androidx.compose.animation.core.Easing easing);
method public int getDelayMillis();
method public int getDurationMillis();
method public androidx.compose.animation.core.Easing getEasing();
- method public androidx.compose.animation.core.ArcMode getMode();
+ method public int getMode();
method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
property public final int delayMillis;
property public final int durationMillis;
property public final androidx.compose.animation.core.Easing easing;
- property public final androidx.compose.animation.core.ArcMode mode;
+ property public final int mode;
}
- @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public abstract sealed class ArcMode {
+ @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi @kotlin.jvm.JvmInline public final value class ArcMode {
field public static final androidx.compose.animation.core.ArcMode.Companion Companion;
}
public static final class ArcMode.Companion {
- }
-
- @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public static final class ArcMode.Companion.ArcAbove extends androidx.compose.animation.core.ArcMode {
- field public static final androidx.compose.animation.core.ArcMode.Companion.ArcAbove INSTANCE;
- }
-
- @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public static final class ArcMode.Companion.ArcBelow extends androidx.compose.animation.core.ArcMode {
- field public static final androidx.compose.animation.core.ArcMode.Companion.ArcBelow INSTANCE;
- }
-
- @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public static final class ArcMode.Companion.ArcLinear extends androidx.compose.animation.core.ArcMode {
- field public static final androidx.compose.animation.core.ArcMode.Companion.ArcLinear INSTANCE;
+ method public int getArcAbove();
+ method public int getArcBelow();
+ method public int getArcLinear();
+ property public final int ArcAbove;
+ property public final int ArcBelow;
+ property public final int ArcLinear;
}
@androidx.compose.runtime.Immutable public final class CubicBezierEasing implements androidx.compose.animation.core.Easing {
@@ -501,7 +496,7 @@
ctor public KeyframesSpec.KeyframesSpecConfig();
method public infix androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T> at(T, @IntRange(from=0L) int timeStamp);
method public infix androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T> atFraction(T, @FloatRange(from=0.0, to=1.0) float fraction);
- method @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public infix androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T> using(androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T>, androidx.compose.animation.core.ArcMode arcMode);
+ method @SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi public infix androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T> using(androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T>, int arcMode);
method @Deprecated public infix void with(androidx.compose.animation.core.KeyframesSpec.KeyframeEntity<T>, androidx.compose.animation.core.Easing easing);
}
@@ -519,6 +514,7 @@
@SuppressCompatibility @androidx.compose.animation.core.ExperimentalAnimationSpecApi @androidx.compose.runtime.Immutable public final class KeyframesWithSplineSpec<T> implements androidx.compose.animation.core.DurationBasedAnimationSpec<T> {
ctor public KeyframesWithSplineSpec(androidx.compose.animation.core.KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T> config);
+ ctor public KeyframesWithSplineSpec(androidx.compose.animation.core.KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T> config, @FloatRange(from=0.0, to=1.0) float periodicBias);
method public androidx.compose.animation.core.KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T> getConfig();
method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
property public final androidx.compose.animation.core.KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T> config;
diff --git a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/AnimatableSamples.kt b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/AnimatableSamples.kt
index 0ddab6d..13e6acb 100644
--- a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/AnimatableSamples.kt
+++ b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/AnimatableSamples.kt
@@ -252,12 +252,12 @@
sizeAnimation: DeferredTargetAnimation<IntSize, AnimationVector2D>,
coroutineScope: CoroutineScope
) = this.approachLayout(
- isMeasurementApproachComplete = { lookaheadSize ->
+ isMeasurementApproachInProgress = { lookaheadSize ->
// Update the target of the size animation.
sizeAnimation.updateTarget(lookaheadSize, coroutineScope)
- // Return true if the size animation has no pending target change and has finished
+ // Return true if the size animation has pending target change or is currently
// running.
- sizeAnimation.isIdle
+ !sizeAnimation.isIdle
}
) { measurable, _ ->
// In the measurement approach, the goal is to gradually reach the destination size
diff --git a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/KeyframesWithSplineBuilderSample.kt b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/KeyframesWithSplineBuilderSample.kt
index 701201d..c69397e 100644
--- a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/KeyframesWithSplineBuilderSample.kt
+++ b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/KeyframesWithSplineBuilderSample.kt
@@ -18,8 +18,26 @@
import androidx.annotation.Sampled
import androidx.compose.animation.core.ExperimentalAnimationSpecApi
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animate
+import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframesWithSpline
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
@@ -56,3 +74,36 @@
DpOffset(400.dp, 50.dp) at 150
}
}
+
+@OptIn(ExperimentalAnimationSpecApi::class)
+@Composable
+@Sampled
+fun PeriodicKeyframesWithSplines() {
+ var alpha by remember { mutableFloatStateOf(0f) }
+ LaunchedEffect(Unit) {
+ animate(
+ initialValue = 0f,
+ targetValue = 0f,
+ animationSpec = infiniteRepeatable(
+ // With a periodicBias of 0.5f it creates a similar animation to a sinusoidal curve
+ // so the transition as the animation repeats is completely seamless
+ animation = keyframesWithSpline(periodicBias = 0.5f) {
+ durationMillis = 2000
+
+ 1f at 1000 using LinearEasing
+ },
+ repeatMode = RepeatMode.Restart
+ )
+ ) { value, _ ->
+ alpha = value
+ }
+ }
+ Image(
+ imageVector = Icons.Filled.Favorite,
+ contentDescription = null,
+ modifier = Modifier
+ .size(150.dp)
+ .graphicsLayer { this.alpha = alpha },
+ colorFilter = ColorFilter.tint(Color.Red)
+ )
+}
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt
index d6f6aea..64c1360 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt
+++ b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/KeyframeSplineAnimationTest.kt
@@ -61,6 +61,82 @@
)
}
+ // Tests the expected effect that different periodic bias have compared against a regular spline
+ @Test
+ fun interpolatedValues_periodic() {
+ // Testing a curve with points at [0, 1, 0], where the intermediate point is at half of the
+ // duration
+ // This means that on a linear interpolation, the initial velocity (past 0ms) would be
+ // positive and the final velocity would be negative with the same magnitude
+ val sharedConfig = KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<Float>().apply {
+ durationMillis = 1000
+
+ 1f at 500 using LinearEasing
+ }
+
+ // We'll compare different periodic bias against the regular spline
+ val splineAnimation = KeyframesWithSplineSpec(sharedConfig, Float.NaN)
+ .vectorize(Float.VectorConverter)
+ val splineAnimationBalancedBias = KeyframesWithSplineSpec(sharedConfig, 0.5f)
+ .vectorize(Float.VectorConverter)
+ val splineAnimationStartBias = KeyframesWithSplineSpec(sharedConfig, 0f)
+ .vectorize(Float.VectorConverter)
+ val splineAnimationEndBias = KeyframesWithSplineSpec(sharedConfig, 1f)
+ .vectorize(Float.VectorConverter)
+
+ // Periodic bias affect the velocity at the start and end of the animation.
+ // Note that we don't test at exactly 0 since that would always return the initial velocity
+ fun VectorizedDurationBasedAnimationSpec<AnimationVector1D>.startVelocity():
+ AnimationVector1D {
+ return getVelocityFromNanos(
+ playTimeNanos = 1L,
+ initialValue = AnimationVector1D(0f),
+ targetValue = AnimationVector1D(0f),
+ initialVelocity = AnimationVector1D(0f)
+ ).let { AnimationVector1D(it.value) } // Copy since vector is reused internally
+ }
+ fun VectorizedDurationBasedAnimationSpec<AnimationVector1D>.endVelocity():
+ AnimationVector1D {
+ return getVelocityFromNanos(
+ playTimeNanos = 1000 * MillisToNanos,
+ initialValue = AnimationVector1D(0f),
+ targetValue = AnimationVector1D(0f),
+ initialVelocity = AnimationVector1D(0f)
+ ).let { AnimationVector1D(it.value) } // Copy since vector is reused internally
+ }
+
+ val regularV0 = splineAnimation.startVelocity()
+ val regularV1 = splineAnimation.endVelocity()
+
+ val balancedV0 = splineAnimationBalancedBias.startVelocity()
+ val balancedV1 = splineAnimationBalancedBias.endVelocity()
+
+ val startBiasV0 =
+ splineAnimationStartBias.startVelocity()
+ val startBiasV1 =
+ splineAnimationStartBias.endVelocity()
+
+ val endBiasV0 = splineAnimationEndBias.startVelocity()
+ val endBiasV1 = splineAnimationEndBias.endVelocity()
+
+ // On splines with periodic bias, the start and end velocity should be the same
+ assertEquals(balancedV0.value, balancedV1.value, 0.0001f)
+ assertEquals(startBiasV0.value, startBiasV1.value, 0.0001f)
+ assertEquals(endBiasV0.value, endBiasV1.value, 0.001f)
+
+ // Velocities on balanced bias should be the average of the start/end velocities of the
+ // regular monotonic spline
+ val avg = (regularV0.value + regularV1.value) / 2f
+ assertEquals(avg, balancedV0.value)
+ assertEquals(avg, balancedV1.value)
+
+ // On fully biased at the start, the end velocity remains unchanged
+ assertEquals(startBiasV1.value, regularV1.value, 0.00001f)
+
+ // On fully biased at the end, the start velocity remains unchanged
+ assertEquals(endBiasV0.value, regularV0.value, 0.00001f)
+ }
+
@Test
fun testMultipleEasing() {
val animation = keyframesWithSpline {
@@ -215,8 +291,8 @@
}
)
- animationA = KeyframesWithSplineSpec(config)
- animationB = KeyframesWithSplineSpec(config)
+ animationA = KeyframesWithSplineSpec(config, Float.NaN)
+ animationB = KeyframesWithSplineSpec(config, Float.NaN)
// Test re-using config
assertNotEquals(animationA, animationB)
diff --git a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
index 3b1fd60..96a3ea0 100644
--- a/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
+++ b/compose/animation/animation-core/src/androidUnitTest/kotlin/androidx/compose/animation/core/MonoSplineTest.kt
@@ -31,7 +31,7 @@
val time = floatArrayOf(
0f, 5f, 10f
)
- val spline = MonoSpline(time, points)
+ val spline = MonoSpline(time, points, Float.NaN)
var value = spline.getPos(5f, 0).toDouble()
assertEquals(1.0, value, 0.001)
value = spline.getPos(7f, 0).toDouble()
@@ -53,7 +53,7 @@
val time = floatArrayOf(
0f, 1f, 2f, 3f, 4f, 5f
)
- val mspline = MonoSpline(time, points)
+ val mspline = MonoSpline(time, points, Float.NaN)
assertEquals(1.0f, mspline.getPos(1f, 0), 0.001f)
assertEquals(1.0f, mspline.getPos(1.1f, 0), 0.001f)
assertEquals(1.0f, mspline.getPos(1.3f, 0), 0.001f)
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt
index e0a9a50..8833cf2 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt
@@ -725,14 +725,44 @@
* [KeyframesWithSplineSpec] is best used with 2D values such as [Offset]. For example:
* @sample androidx.compose.animation.core.samples.KeyframesBuilderForOffsetWithSplines
*
+ * You may however, provide a [periodicBias] value (between 0f and 1f) to make a periodic spline.
+ * Periodic splines adjust the initial and final velocity to be the same. This is useful to
+ * create smooth repeatable animations. Such as an infinite pulsating animation:
+ *
+ * @sample androidx.compose.animation.core.samples.PeriodicKeyframesWithSplines
+ *
+ * The [periodicBias] value (from 0.0 to 1.0) indicates how much of the original starting and final
+ * velocity are modified to achieve periodicity:
+ * - 0f: Modifies only the starting velocity to match the final velocity
+ * - 1f: Modifies only the final velocity to match the starting velocity
+ * - 0.5f: Modifies both velocities equally, picking the average between the two
+ *
* @see keyframesWithSpline
* @sample androidx.compose.animation.core.samples.KeyframesBuilderForIntOffsetWithSplines
* @sample androidx.compose.animation.core.samples.KeyframesBuilderForDpOffsetWithSplines
*/
@ExperimentalAnimationSpecApi
@Immutable
-class KeyframesWithSplineSpec<T>(val config: KeyframesWithSplineSpecConfig<T>) :
- DurationBasedAnimationSpec<T> {
+class KeyframesWithSplineSpec<T>(
+ val config: KeyframesWithSplineSpecConfig<T>,
+) : DurationBasedAnimationSpec<T> {
+ // Periodic bias property, NaN by default. Only meant to be set by secondary constructor
+ private var periodicBias: Float = Float.NaN
+
+ /**
+ * Constructor that returns a periodic spline implementation.
+ *
+ * @param config Keyframe configuration of the spline, should contain the set of values,
+ * timestamps and easing curves to animate through.
+ * @param periodicBias A value from 0f to 1f, indicating how much the starting or ending
+ * velocities are modified respectively to achieve periodicity.
+ */
+ constructor(
+ config: KeyframesWithSplineSpecConfig<T>,
+ @FloatRange(0.0, 1.0) periodicBias: Float
+ ) : this(config) {
+ this.periodicBias = periodicBias
+ }
@ExperimentalAnimationSpecApi
class KeyframesWithSplineSpecConfig<T> :
@@ -763,7 +793,8 @@
timestamps = timestamps,
keyframes = timeToVectorMap,
durationMillis = config.durationMillis,
- delayMillis = config.delayMillis
+ delayMillis = config.delayMillis,
+ periodicBias = periodicBias
)
}
}
@@ -834,7 +865,6 @@
* @sample androidx.compose.animation.core.samples.KeyframesBuilderForDpOffsetWithSplines
*/
@ExperimentalAnimationSpecApi
-@Stable
fun <T> keyframesWithSpline(
init: KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T>.() -> Unit
): KeyframesWithSplineSpec<T> =
@@ -843,6 +873,37 @@
)
/**
+ * Creates a *periodic* [KeyframesWithSplineSpec] animation, initialized with [init].
+ *
+ * Use overload without [periodicBias] parameter for the non-periodic implementation.
+ *
+ * A periodic spline is one such that the starting and ending velocities are equal. This makes them
+ * useful to crete smooth repeatable animations. Such as an infinite pulsating animation:
+ *
+ * @sample androidx.compose.animation.core.samples.PeriodicKeyframesWithSplines
+ *
+ * The [periodicBias] value (from 0.0 to 1.0) indicates how much of the original starting and final
+ * velocity are modified to achieve periodicity:
+ * - 0f: Modifies only the starting velocity to match the final velocity
+ * - 1f: Modifies only the final velocity to match the starting velocity
+ * - 0.5f: Modifies both velocities equally, picking the average between the two
+ *
+ * @param periodicBias A value from 0f to 1f, indicating how much the starting or ending velocities
+ * are modified respectively to achieve periodicity.
+ * @param init Initialization function for the [KeyframesWithSplineSpec] animation
+ * @see KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig
+ */
+@ExperimentalAnimationSpecApi
+fun <T> keyframesWithSpline(
+ @FloatRange(0.0, 1.0) periodicBias: Float,
+ init: KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T>.() -> Unit
+): KeyframesWithSplineSpec<T> =
+ KeyframesWithSplineSpec(
+ config = KeyframesWithSplineSpec.KeyframesWithSplineSpecConfig<T>().apply(init),
+ periodicBias = periodicBias,
+ )
+
+/**
* Creates a [RepeatableSpec] that plays a [DurationBasedAnimationSpec] (e.g.
* [TweenSpec], [KeyframesSpec]) the amount of iterations specified by [iterations].
*
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonoSpline.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonoSpline.kt
index 850ad269..3dd88e4 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonoSpline.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonoSpline.kt
@@ -23,7 +23,7 @@
* time is an array of all positions and y is a list of arrays each with the values at each point
*/
@ExperimentalAnimationSpecApi
-internal class MonoSpline(time: FloatArray, y: Array<FloatArray>) {
+internal class MonoSpline(time: FloatArray, y: Array<FloatArray>, periodicBias: Float) {
private val timePoints: FloatArray
private val values: Array<FloatArray>
private val tangents: Array<FloatArray>
@@ -48,6 +48,17 @@
}
tangent[n - 1][j] = slope[n - 2][j]
}
+ if (!periodicBias.isNaN()) {
+ for (j in 0 until dim) {
+ // Slope indicated by bias, where 0.0f is the last slope and 1f is the initial slope
+ val adjustedSlope =
+ (slope[n - 2][j] * (1 - periodicBias)) + (slope[0][j] * periodicBias)
+ slope[0][j] = adjustedSlope
+ slope[n - 2][j] = adjustedSlope
+ tangent[n - 1][j] = adjustedSlope
+ tangent[0][j] = adjustedSlope
+ }
+ }
for (i in 0 until n - 1) {
for (j in 0 until dim) {
if (slope[i][j] == 0.0f) {
@@ -208,14 +219,24 @@
* You may provide [index] to simplify searching for the correct keyframe for the given [time].
*/
fun getSlope(time: Float, v: AnimationVector, index: Int = 0) {
- var t = time
+ val t = time
val n = timePoints.size
val dim = values[0].size
+
+ // If time is 0, max or out of range we directly return the corresponding slope value
if (t <= timePoints[0]) {
- t = timePoints[0]
+ for (j in 0 until dim) {
+ v[j] = tangents[0][j]
+ }
+ return
} else if (t >= timePoints[n - 1]) {
- t = timePoints[n - 1]
+ for (j in 0 until dim) {
+ v[j] = tangents[n - 1][j]
+ }
+ return
}
+
+ // Otherwise, calculate interpolated velocity
for (i in index until n - 1) {
if (t <= timePoints[i + 1]) {
val h = timePoints[i + 1] - timePoints[i]
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
index 9d09a90..07d9fb2 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
@@ -529,15 +529,6 @@
val index = timestamps.binarySearch(timeMillis)
return if (index < -1) -(index + 2) else index
}
-
- @Suppress("unused")
- private val ArcMode.value: Int
- get() = when (this) {
- ArcMode.Companion.ArcAbove -> ArcSpline.ArcAbove
- ArcMode.Companion.ArcBelow -> ArcSpline.ArcBelow
- ArcMode.Companion.ArcLinear -> ArcSpline.ArcStartLinear
- else -> ArcSpline.ArcStartLinear // Unknown mode, fallback to linear
- }
}
@OptIn(ExperimentalAnimationSpecApi::class)
@@ -557,35 +548,28 @@
* @see ArcAnimationSpec
*/
@ExperimentalAnimationSpecApi
-sealed class ArcMode {
+@JvmInline
+value class ArcMode internal constructor(internal val value: Int) {
+
companion object {
/**
* Interpolates using a quarter of an Ellipse where the curve is "above" the center of the
* Ellipse.
*/
- @ExperimentalAnimationSpecApi
- object ArcAbove : ArcMode()
+ val ArcAbove = ArcMode(ArcSpline.ArcAbove)
/**
* Interpolates using a quarter of an Ellipse where the curve is "below" the center of the
* Ellipse.
*/
- @ExperimentalAnimationSpecApi
- object ArcBelow : ArcMode()
+ val ArcBelow = ArcMode(ArcSpline.ArcBelow)
/**
* An [ArcMode] that forces linear interpolation.
*
* You'll likely only use this mode within a keyframe.
*/
- @ExperimentalAnimationSpecApi
- object ArcLinear : ArcMode()
-
- /**
- * Unused [ArcMode] to prevent exhaustive `when` usage.
- */
- @Suppress("unused")
- private object UnexpectedArc : ArcMode()
+ val ArcLinear = ArcMode(ArcSpline.ArcStartLinear)
}
}
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedMonoSplineKeyframesSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedMonoSplineKeyframesSpec.kt
index 0bd8e17..2673945 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedMonoSplineKeyframesSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedMonoSplineKeyframesSpec.kt
@@ -27,7 +27,8 @@
private val timestamps: IntList,
private val keyframes: IntObjectMap<Pair<V, Easing>>,
override val durationMillis: Int,
- override val delayMillis: Int
+ override val delayMillis: Int,
+ private val periodicBias: Float,
) : VectorizedDurationBasedAnimationSpec<V> {
// Objects initialized lazily once
private lateinit var valueVector: V
@@ -99,7 +100,7 @@
FloatArray(dimension, targetValue::get)
}
}
- monoSpline = MonoSpline(times, values)
+ monoSpline = MonoSpline(times, values, periodicBias)
}
}
diff --git a/compose/animation/animation/api/current.txt b/compose/animation/animation/api/current.txt
index 343fa22..c84af4c 100644
--- a/compose/animation/animation/api/current.txt
+++ b/compose/animation/animation/api/current.txt
@@ -56,9 +56,9 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface AnimatedVisibilityScope {
- method @SuppressCompatibility @androidx.compose.animation.ExperimentalAnimationApi public default androidx.compose.ui.Modifier animateEnterExit(androidx.compose.ui.Modifier, optional androidx.compose.animation.EnterTransition enter, optional androidx.compose.animation.ExitTransition exit, optional String label);
- method @SuppressCompatibility @androidx.compose.animation.ExperimentalAnimationApi public androidx.compose.animation.core.Transition<androidx.compose.animation.EnterExitState> getTransition();
- property @SuppressCompatibility @androidx.compose.animation.ExperimentalAnimationApi public abstract androidx.compose.animation.core.Transition<androidx.compose.animation.EnterExitState> transition;
+ method public default androidx.compose.ui.Modifier animateEnterExit(androidx.compose.ui.Modifier, optional androidx.compose.animation.EnterTransition enter, optional androidx.compose.animation.ExitTransition exit, optional String label);
+ method public androidx.compose.animation.core.Transition<androidx.compose.animation.EnterExitState> getTransition();
+ property public abstract androidx.compose.animation.core.Transition<androidx.compose.animation.EnterExitState> transition;
}
public final class AnimationModifierKt {
diff --git a/compose/animation/animation/api/restricted_current.txt b/compose/animation/animation/api/restricted_current.txt
index 343fa22..c84af4c 100644
--- a/compose/animation/animation/api/restricted_current.txt
+++ b/compose/animation/animation/api/restricted_current.txt
@@ -56,9 +56,9 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface AnimatedVisibilityScope {
- method @SuppressCompatibility @androidx.compose.animation.ExperimentalAnimationApi public default androidx.compose.ui.Modifier animateEnterExit(androidx.compose.ui.Modifier, optional androidx.compose.animation.EnterTransition enter, optional androidx.compose.animation.ExitTransition exit, optional String label);
- method @SuppressCompatibility @androidx.compose.animation.ExperimentalAnimationApi public androidx.compose.animation.core.Transition<androidx.compose.animation.EnterExitState> getTransition();
- property @SuppressCompatibility @androidx.compose.animation.ExperimentalAnimationApi public abstract androidx.compose.animation.core.Transition<androidx.compose.animation.EnterExitState> transition;
+ method public default androidx.compose.ui.Modifier animateEnterExit(androidx.compose.ui.Modifier, optional androidx.compose.animation.EnterTransition enter, optional androidx.compose.animation.ExitTransition exit, optional String label);
+ method public androidx.compose.animation.core.Transition<androidx.compose.animation.EnterExitState> getTransition();
+ property public abstract androidx.compose.animation.core.Transition<androidx.compose.animation.EnterExitState> transition;
}
public final class AnimationModifierKt {
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 40b35d5..6885430 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
@@ -64,6 +64,7 @@
import androidx.compose.animation.demos.suspendfun.InfiniteAnimationDemo
import androidx.compose.animation.demos.suspendfun.OffsetKeyframeSplinePlaygroundDemo
import androidx.compose.animation.demos.suspendfun.OffsetKeyframeWithSplineDemo
+import androidx.compose.animation.demos.suspendfun.PeriodicMonoSplineDemo
import androidx.compose.animation.demos.suspendfun.SuspendAnimationDemo
import androidx.compose.animation.demos.suspendfun.SuspendDoubleTapToLikeDemo
import androidx.compose.animation.demos.vectorgraphics.AnimatedVectorGraphicsDemo
@@ -168,6 +169,7 @@
OffsetKeyframeSplinePlaygroundDemo()
},
ComposableDemo("Arc Offset Demo") { ArcOffsetDemo() },
+ ComposableDemo("Periodic Spline Demo") { PeriodicMonoSplineDemo() },
)
),
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 62f03d5..ef1708d 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
@@ -81,18 +81,18 @@
}
}
.approachLayout(
- isMeasurementApproachComplete = {
+ isMeasurementApproachInProgress = {
outerSizeAnimation.updateTarget(it, coroutineScope, sizeAnimationSpec)
- outerSizeAnimation.isIdle
+ !outerSizeAnimation.isIdle
},
- isPlacementApproachComplete = {
+ isPlacementApproachInProgress = {
val target = lookaheadScopeCoordinates.localLookaheadPositionOf(it)
outerOffsetAnimation.updateTarget(
target.round(),
coroutineScope,
positionAnimationSpec
)
- outerOffsetAnimation.isIdle
+ !outerOffsetAnimation.isIdle
}
) { measurable, constraints ->
val (w, h) = outerSizeAnimation.updateTarget(
@@ -126,18 +126,18 @@
}
}
.approachLayout(
- isMeasurementApproachComplete = {
+ isMeasurementApproachInProgress = {
sizeAnimation.updateTarget(it, coroutineScope, sizeAnimationSpec)
- sizeAnimation.isIdle
+ !sizeAnimation.isIdle
},
- isPlacementApproachComplete = {
+ isPlacementApproachInProgress = {
val target = lookaheadScopeCoordinates.localLookaheadPositionOf(it)
offsetAnimation.updateTarget(
target.round(),
coroutineScope,
positionAnimationSpec
)
- offsetAnimation.isIdle
+ !offsetAnimation.isIdle
}
) { measurable, _ ->
// When layout changes, the lookahead pass will calculate a new final size for the
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 4f80c38..ea680e2 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
@@ -217,7 +217,7 @@
} ?: IntOffset(0, 0)
}
}
- this.approachLayout({ provider.progress == 1f }) { measurable, _ ->
+ this.approachLayout({ provider.progress != 1f }) { measurable, _ ->
val (width, height) = calculateSize(lookaheadSize)
val animatedConstraints = Constraints.fixed(width, height)
val placeable = measurable.measure(animatedConstraints)
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
index 56fed32..db9f9b9 100644
--- 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
@@ -157,8 +157,8 @@
DeferredTargetAnimation(IntOffset.VectorConverter)
}
val coroutineScope = rememberCoroutineScope()
- this.approachLayout(isMeasurementApproachComplete = { true },
- isPlacementApproachComplete = {
+ this.approachLayout(isMeasurementApproachInProgress = { false },
+ isPlacementApproachInProgress = {
offsetAnimation.updateTarget(
lookaheadScopeCoordinates.localLookaheadPositionOf(
it
@@ -166,7 +166,7 @@
coroutineScope,
spring(stiffness = Spring.StiffnessMediumLow)
)
- offsetAnimation.isIdle
+ !offsetAnimation.isIdle
}
) { measurable, constraints ->
measurable.measure(constraints).run {
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 d4e84bc..3a06425 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
@@ -163,14 +163,14 @@
val offsetAnim = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
val scope = rememberCoroutineScope()
this.approachLayout(
- isMeasurementApproachComplete = {
+ isMeasurementApproachInProgress = {
sizeAnim.updateTarget(it, scope)
- sizeAnim.isIdle
+ !sizeAnim.isIdle
},
- isPlacementApproachComplete = {
+ isPlacementApproachInProgress = {
val target = lookaheadScopeCoordinates.localLookaheadPositionOf(it)
offsetAnim.updateTarget(target.round(), scope, spring())
- offsetAnim.isIdle
+ !offsetAnim.isIdle
}
) { measurable, _ ->
val (animWidth, animHeight) = sizeAnim.updateTarget(
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 eeef14b..4ba36700 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
@@ -86,14 +86,14 @@
}
}
.approachLayout(
- isMeasurementApproachComplete = {
+ isMeasurementApproachInProgress = {
sizeAnimation.updateTarget(it, coroutineScope)
- sizeAnimation.isIdle
+ !sizeAnimation.isIdle
},
- isPlacementApproachComplete = {
+ isPlacementApproachInProgress = {
val target = lookaheadScopeCoordinates.localLookaheadPositionOf(it)
offsetAnimation.updateTarget(target.round(), coroutineScope, spring())
- offsetAnimation.isIdle
+ !offsetAnimation.isIdle
}
) { measurable, _ ->
with(coroutineScope) {
@@ -139,9 +139,9 @@
}
}
.approachLayout(
- isMeasurementApproachComplete = {
+ isMeasurementApproachInProgress = {
sizeAnimation.updateTarget(it, scope)
- sizeAnimation.isIdle
+ !sizeAnimation.isIdle
}
) { measurable, constraints ->
targetSize = lookaheadSize
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/suspendfun/OffsetKeyframeSplinePlaygroundDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/suspendfun/OffsetKeyframeSplinePlaygroundDemo.kt
index 4ec936a..6356b68 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/suspendfun/OffsetKeyframeSplinePlaygroundDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/suspendfun/OffsetKeyframeSplinePlaygroundDemo.kt
@@ -18,7 +18,10 @@
import android.util.Log
import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.ExperimentalAnimationSpecApi
+import androidx.compose.animation.core.InfiniteRepeatableSpec
+import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.keyframesWithSpline
import androidx.compose.foundation.gestures.detectDragGestures
@@ -35,15 +38,18 @@
import androidx.compose.material.Slider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateListOf
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.Modifier
import androidx.compose.ui.draw.drawBehind
@@ -52,6 +58,7 @@
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.PointMode
+import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.graphicsLayer
@@ -77,6 +84,7 @@
import kotlin.math.roundToLong
import kotlin.math.sin
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@Preview
@@ -148,6 +156,11 @@
private class SplineKeyframesPlaygroundModel(
private val scope: CoroutineScope
) {
+ private val zero2DVector = AnimationVector2D(0f, 0f)
+
+ // TODO: This is extremely hacky, find a way to improve
+ private var modificationIndicator by mutableLongStateOf(0L)
+
private val pointCount = 6
private val animatedOffset = Animatable(Offset.Zero, Offset.VectorConverter)
private val anchors = mutableStateListOf<Offset>()
@@ -158,6 +171,8 @@
private val durationPerAnchor = mutableFloatStateOf(600f)
private val pathPoints = mutableStateListOf<Offset>()
+ private val samplePoints = mutableListOf<Offset>()
+ private val sampleCount = 100
val totalDuration by derivedStateOf { anchors.size * durationPerAnchor.floatValue }
@@ -171,13 +186,14 @@
}
}
+ private val diamondPath = Path()
private val diamondColor = Color(0xFFFF9800)
private val diamondSize = 5.dp
private val textOffset = 6.dp
private val pathColor = diamondColor.copy(alpha = 0.7f)
private val pathWidth = 2.dp
- private val pathOn = 6.dp
- private val pathOff = 4.dp
+ private val pathOn = 3.dp
+ private val pathOff = 6.dp
@Composable
fun DrawContent(modifier: Modifier = Modifier) {
@@ -186,6 +202,18 @@
val fontFamilyResolver = LocalFontFamilyResolver.current
val textMeasurer = remember { TextMeasurer(fontFamilyResolver, density, layoutDirection) }
val textColor = MaterialTheme.colors.onSurface
+
+ remember(density.density) {
+ val diamondSizePx = with(density) { diamondSize.toPx() }
+ diamondPath.reset()
+ diamondPath.moveTo(0f, -diamondSizePx)
+ diamondPath.lineTo(diamondSizePx, 0f)
+ diamondPath.lineTo(0f, diamondSizePx)
+ diamondPath.lineTo(-diamondSizePx, 0f)
+ diamondPath.close()
+ false
+ }
+
init(density)
Box(
@@ -201,8 +229,9 @@
translate(center.x, center.y) {
drawPoints(
points = pathPoints,
- pointMode = PointMode.Lines,
+ pointMode = PointMode.Polygon,
color = pathColor,
+ cap = StrokeCap.Round,
strokeWidth = pathWidthPx,
pathEffect = pathEffect
)
@@ -212,24 +241,16 @@
if (anchors.isEmpty()) {
return@drawBehind
}
- val diamondSizePx = with(this) { diamondSize.toPx() }
val textOffsetPx = with(this) {
Offset(textOffset.toPx(), textOffset.toPx())
}
// Draw anchors
- val path = Path()
translate(center.x, center.y) {
anchors.forEachIndexed { index, anchorPosition ->
translate(anchorPosition.x, anchorPosition.y) {
- path.reset()
- path.moveTo(0f, -diamondSizePx)
- path.lineTo(diamondSizePx, 0f)
- path.lineTo(0f, diamondSizePx)
- path.lineTo(-diamondSizePx, 0f)
- path.close()
drawPath(
- path = path,
+ path = diamondPath,
color = diamondColor,
style = Fill
)
@@ -269,6 +290,37 @@
.graphicsLayer { rotationZ = angle.floatValue - 90f }
)
}
+
+ // TODO: This is extremely hacky, find a way to improve
+ LaunchedEffect(modificationIndicator) {
+ samplePoints.clear()
+ var i = 0
+ val vectorized = keyframesWithSpline(0.5f) {
+ durationMillis = totalDuration.roundToInt()
+
+ anchors.forEachIndexed { index, offset ->
+ val fraction = (index + 1f) / (anchorCount + 1)
+ offset atFraction fraction
+ }
+ }.vectorize(Offset.VectorConverter)
+
+ var timeMillis = 0f
+ val step = vectorized.durationMillis.toFloat() / sampleCount
+ while (isActive && i < sampleCount) {
+ val vectorValue = vectorized.getValueFromNanos(
+ playTimeNanos = timeMillis.roundToLong() * 1_000_000,
+ initialValue = zero2DVector,
+ targetValue = zero2DVector,
+ initialVelocity = zero2DVector
+ )
+ samplePoints.add(Offset(vectorValue.v1, vectorValue.v2))
+ timeMillis += step
+ i++
+ }
+ samplePoints.add(Offset.Zero)
+ pathPoints.clear()
+ pathPoints.addAll(samplePoints)
+ }
}
fun onNewDuration(newTotalDuration: Float) {
@@ -277,21 +329,22 @@
fun addAnchor(density: Density) {
scope.launch { animatedOffset.snapTo(Offset.Zero) }
- pathPoints.clear()
anchors.add(getNextPosition(density))
+ modificationIndicator++
}
fun removeAnchor() {
if (anchors.size > 1) {
scope.launch { animatedOffset.snapTo(Offset.Zero) }
- pathPoints.clear()
anchors.removeLast()
+ modificationIndicator++
}
}
private val minDuration = 600f
private val baseMaxDuration = 10000f
private val durationIncrement = minDuration
+
val range: ClosedFloatingPointRange<Float>
get() =
if (totalDuration < baseMaxDuration) {
@@ -307,21 +360,22 @@
private val angle = mutableFloatStateOf(0f)
fun onRun() {
- pathPoints.clear()
scope.launch {
animatedOffset.snapTo(Offset.Zero)
animatedOffset.animateTo(
targetValue = Offset.Zero,
- animationSpec = keyframesWithSpline {
- durationMillis = totalDuration.roundToInt()
+ animationSpec = InfiniteRepeatableSpec(
+ keyframesWithSpline(0.5f) {
+ durationMillis = totalDuration.roundToInt()
- anchors.forEachIndexed { index, offset ->
- val fraction = (index + 1f) / (anchorCount + 2)
- offset atFraction fraction
- }
- }
+ anchors.forEachIndexed { index, offset ->
+ val fraction = (index + 1f) / (anchorCount + 1)
+ offset atFraction fraction
+ }
+ },
+ RepeatMode.Restart
+ )
) {
- pathPoints.add(this.value)
angle.floatValue =
Math.toDegrees(atan2(y = velocity.y, x = velocity.x).toDouble()).toFloat() + 90f
}
@@ -383,7 +437,6 @@
// TODO: Consider using a threshold like this to find the anchor quicker
// private val diffThreshold = 10 * 10 * 2
private fun onDragStart(position: Offset, size: IntSize) {
- pathPoints.clear()
scope.launch { animatedOffset.snapTo(Offset.Zero) }
val relPosition = Offset(
position.x - (size.width * 0.5f),
@@ -413,6 +466,7 @@
val index = draggingIndex.intValue
if (index >= 0) {
anchors[index] = anchors[index] + dragAmount
+ modificationIndicator++
}
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/suspendfun/PeriodicMonoSplineDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/suspendfun/PeriodicMonoSplineDemo.kt
new file mode 100644
index 0000000..e7f44d3
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/suspendfun/PeriodicMonoSplineDemo.kt
@@ -0,0 +1,244 @@
+/*
+ * 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:OptIn(ExperimentalAnimationSpecApi::class)
+@file:Suppress("InfiniteTransitionLabel", "InfinitePropertiesLabel")
+
+package androidx.compose.animation.demos.suspendfun
+
+import androidx.compose.animation.core.AnimationVector2D
+import androidx.compose.animation.core.ExperimentalAnimationSpecApi
+import androidx.compose.animation.core.InfiniteRepeatableSpec
+import androidx.compose.animation.core.KeyframesWithSplineSpec
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.animateValue
+import androidx.compose.animation.core.keyframesWithSpline
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
+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.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.drawscope.Fill
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.drawscope.scale
+import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlin.math.roundToLong
+
+@Preview
+@Composable
+fun PeriodicMonoSplineDemo() {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState())
+ ) {
+ MonoSplineCurve(
+ periodicBias = Float.NaN,
+ modifier = Modifier
+ .height(300.dp)
+ .background(Color.LightGray)
+ )
+ MonoSplineCurve(
+ periodicBias = 0f,
+ modifier = Modifier
+ .height(300.dp)
+ .background(Color.LightGray)
+ )
+ MonoSplineCurve(
+ periodicBias = 0.5f,
+ modifier = Modifier
+ .height(300.dp)
+ .background(Color.LightGray)
+ )
+ MonoSplineCurve(
+ periodicBias = 1f,
+ modifier = Modifier
+ .height(300.dp)
+ .background(Color.LightGray)
+ )
+ }
+}
+
+private fun periodicKeyframes(periodicBias: Float): KeyframesWithSplineSpec<Offset> =
+ keyframesWithSpline(periodicBias) {
+ durationMillis = 2000
+
+ Offset(0.5f, 1f) atFraction 0.5f
+ }
+
+private val pathWidth = 2.dp
+private val pathColor = Color(0xFFFFC107)
+private val padding = 40.dp
+private val indicatorSize = 4.dp
+
+@Composable
+private fun MonoSplineCurve(
+ periodicBias: Float,
+ modifier: Modifier = Modifier
+) {
+ val sampleSize = 1_000
+ val density = LocalDensity.current
+ val keyframesSpec = remember(periodicBias) { periodicKeyframes(periodicBias) }
+ val points = remember { FloatArray(sampleSize) }
+ val pointsPath = remember { Path() }
+ var isDraw by remember { mutableStateOf(false) }
+
+ val infiniteAnimation = rememberInfiniteTransition()
+ val animatedValue = infiniteAnimation.animateValue(
+ initialValue = Offset.Zero,
+ targetValue = Offset(1f, 0f),
+ typeConverter = Offset.VectorConverter,
+ animationSpec = InfiniteRepeatableSpec(
+ animation = keyframesSpec,
+ repeatMode = RepeatMode.Restart
+ )
+ )
+
+ val text = remember(periodicBias) {
+ if (periodicBias.isNaN()) {
+ "Normal Monotonic Spline"
+ } else {
+ "Periodic with bias: $periodicBias"
+ }
+ }
+
+ val indicatorSizePx = with(density) { indicatorSize.toPx() }
+ val indicatorPath = remember(indicatorSizePx) {
+ Path().apply {
+ moveTo(0f, -indicatorSizePx)
+ lineTo(indicatorSizePx, 0f)
+ lineTo(0f, indicatorSizePx)
+ lineTo(-indicatorSizePx, 0f)
+ close()
+ }
+ }
+
+ Row(modifier) {
+ Box(
+ Modifier
+ .fillMaxHeight()
+ .weight(1f, true)
+ ) {
+ Text(text = text)
+ Box(
+ Modifier
+ .fillMaxSize()
+ .padding(padding)
+ .drawWithCache {
+ if (isDraw) {
+ val pathWidthPx = pathWidth.toPx()
+ pointsPath.reset()
+ val arraySize = points.size
+ val width = size.width
+ val height = size.height
+ points.forEachIndexed { index, yFactor ->
+ val xi = (index.toFloat() / arraySize) * width
+ pointsPath.lineTo(xi, yFactor * height)
+ }
+ onDrawBehind {
+ scale(1f, -1f) {
+ drawPath(
+ path = pointsPath,
+ color = pathColor,
+ style = Stroke(width = pathWidthPx)
+ )
+ }
+ }
+ } else {
+ onDrawBehind {}
+ }
+ }
+ .drawBehind {
+ scale(1f, -1f) {
+ val currValue = animatedValue.value
+ translate(size.width * currValue.x, size.height * currValue.y) {
+ drawPath(
+ path = indicatorPath,
+ color = Color.Red,
+ style = Fill
+ )
+ }
+ }
+ }
+ )
+ }
+ Box(
+ Modifier
+ .fillMaxHeight()
+ .width(40.dp)
+ .padding(vertical = padding)
+ .drawBehind {
+ scale(1f, -1f) {
+ drawCircle(
+ color = Color.Red,
+ radius = size.width * 0.5f,
+ center = Offset(
+ x = size.width * 0.5f,
+ y = size.height * animatedValue.value.y
+ )
+ )
+ }
+ }
+ )
+ }
+
+ LaunchedEffect(Unit) {
+ val zeroVector = AnimationVector2D(0f, 0f)
+ val vectorized = keyframesSpec.vectorize(Offset.VectorConverter)
+ var timeMillis = 0f
+ val step = vectorized.durationMillis.toFloat() / sampleSize
+ var count = 0
+ while (count < sampleSize) {
+ val vectorValue = vectorized.getValueFromNanos(
+ playTimeNanos = timeMillis.roundToLong() * 1_000_000,
+ initialValue = zeroVector,
+ targetValue = zeroVector,
+ initialVelocity = zeroVector
+ )
+ points[count] = vectorValue.v2
+ timeMillis += step
+ count++
+ }
+ isDraw = true
+ }
+}
diff --git a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
index 10ce8e0..4cae1c4 100644
--- a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
+++ b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
@@ -50,12 +50,14 @@
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameMillis
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalDensity
@@ -74,6 +76,7 @@
import kotlinx.coroutines.delay
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
@@ -927,6 +930,198 @@
assertTrue(box2EnterFinished)
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Test
+ fun AnimatedContentLookaheadTest() {
+ // Test that AnimatedContent's lookahead size is its target content's lookahead size.
+ // Also test that the lookahead placement for content is correct.
+ val size1 = 400
+ val size2 = 20
+ val transitionState = MutableTransitionState(true)
+ var playTimeMillis by mutableStateOf(0)
+ val testModifier = TestModifier()
+ var lookaheadPosition: Offset? = null
+ var approachPosition: Offset? = null
+ rule.mainClock.autoAdvance = false
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ LookaheadScope {
+ Box(testModifier) {
+ val transition = rememberTransition(transitionState)
+ playTimeMillis = (transition.playTimeNanos / 1_000_000L).toInt()
+ transition.AnimatedContent(
+ transitionSpec = {
+ if (true isTransitioningTo false) {
+ fadeIn() togetherWith fadeOut() using SizeTransform { _, _ ->
+ tween(durationMillis = 80, easing = LinearEasing)
+ }
+ } else {
+ fadeIn() togetherWith fadeOut() using SizeTransform { _, _ ->
+ tween(durationMillis = 80, easing = LinearEasing)
+ }
+ }
+ },
+ contentAlignment = Alignment.Center
+ ) {
+ if (it) {
+ Box(modifier = Modifier.size(size = size1.dp))
+ } else {
+ Box(modifier = Modifier
+ .layout { m, c ->
+ m
+ .measure(c)
+ .run {
+ layout(width, height) {
+ if (isLookingAhead) {
+ with(this@LookaheadScope) {
+ lookaheadPosition =
+ lookaheadScopeCoordinates
+ .localLookaheadPositionOf(
+ coordinates!!
+ )
+ }
+ } else {
+ approachPosition = lookaheadScopeCoordinates
+ .localPositionOf(
+ coordinates!!,
+ Offset.Zero
+ )
+ }
+ place(0, 0)
+ }
+ }
+ }
+ .size(size = size2.dp))
+ }
+ }
+ }
+ }
+ }
+ }
+ rule.runOnIdle {
+ assertTrue(transitionState.targetState)
+ assertEquals(IntSize(size1, size1), testModifier.lookaheadSize)
+ transitionState.targetState = false
+ }
+ rule.waitForIdle()
+ rule.mainClock.advanceTimeByFrame()
+
+ // Transition from item1 to item2 in 320ms, animating to full width in the first 160ms
+ // then full height in the next 160ms
+ while (transitionState.currentState != transitionState.targetState) {
+ rule.runOnIdle {
+ assertEquals(IntSize(size2, size2), testModifier.lookaheadSize)
+ assertNotNull(approachPosition)
+ assertNotNull(lookaheadPosition)
+ assertOffsetEquals(Offset(0f, 0f), lookaheadPosition!!)
+ }
+ rule.mainClock.advanceTimeByFrame()
+ }
+ rule.waitForIdle()
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Test
+ fun testTargetChangeLookaheadPlacement() {
+ var lookaheadPosition1: Offset? = null
+ var lookaheadPosition2: Offset? = null
+ val transitionState = MutableTransitionState(true)
+ var playTimeMillis by mutableStateOf(0)
+ rule.setContent {
+ LookaheadScope {
+ val transition = rememberTransition(transitionState)
+ playTimeMillis = (transition.playTimeNanos / 1_000_000L).toInt()
+ transition.AnimatedContent(
+ contentAlignment = Alignment.Center,
+ transitionSpec = { fadeIn() togetherWith fadeOut() using null }
+ ) {
+ if (it) {
+ Box(
+ Modifier
+ .layout { measurable, constraints ->
+ measurable
+ .measure(constraints)
+ .run {
+ layout(width, height) {
+ if (isLookingAhead) {
+ lookaheadPosition1 = lookaheadScopeCoordinates
+ .localLookaheadPositionOf(coordinates!!)
+ }
+ }
+ }
+ }
+ .fillMaxSize()
+ .background(Color.Blue)
+ )
+ } else {
+ Box(
+ Modifier
+ .layout { measurable, constraints ->
+ measurable
+ .measure(constraints)
+ .run {
+ layout(width, height) {
+ if (isLookingAhead) {
+ lookaheadPosition2 = lookaheadScopeCoordinates
+ .localLookaheadPositionOf(coordinates!!)
+ }
+ }
+ }
+ }
+ .size(100.dp)
+ .background(Color.Red)
+ )
+ }
+ }
+ }
+ }
+ rule.runOnIdle {
+ assertTrue(transitionState.targetState)
+ assertTrue(transitionState.currentState)
+ transitionState.targetState = false
+ }
+ rule.mainClock.autoAdvance = false
+ rule.runOnIdle {
+ assertNotNull(lookaheadPosition1)
+ assertOffsetEquals(Offset(0f, 0f), lookaheadPosition1!!)
+ transitionState.targetState = false
+ }
+ rule.waitForIdle()
+ rule.mainClock.advanceTimeByFrame()
+
+ // Transition from item1 to item2 in 320ms, animating to full width in the first 160ms
+ // then full height in the next 160ms
+ repeat(3) {
+ assertNotEquals(transitionState.currentState, transitionState.targetState)
+ rule.runOnIdle {
+ assertNotNull(lookaheadPosition2)
+ assertOffsetEquals(Offset(0f, 0f), lookaheadPosition2!!)
+ }
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle()
+ }
+
+ // Check that the lookahead position for the outgoing content changed
+ assertNotEquals(0f, lookaheadPosition1!!.x)
+ assertNotEquals(0f, lookaheadPosition1!!.y)
+ // Interruption during animation
+ transitionState.targetState = true
+ rule.mainClock.advanceTimeByFrame()
+ rule.waitForIdle()
+
+ rule.runOnIdle {
+ assertNotNull(lookaheadPosition1)
+ // Check that after the target state change, the new incoming content has
+ // a 0, 0 lookahead offset.
+ assertOffsetEquals(Offset(0f, 0f), lookaheadPosition1!!)
+ }
+ }
+
+ private fun assertOffsetEquals(expected: Offset, actual: Offset) {
+ assertEquals(expected.x, actual.x, 0.00001f)
+ assertEquals(expected.y, actual.y, 0.00001f)
+ }
+
@OptIn(InternalAnimationApi::class)
private val Transition<*>.playTimeMillis get() = (playTimeNanos / 1_000_000L).toInt()
}
diff --git a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt
index 52034ab..32c7532 100644
--- a/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt
+++ b/compose/animation/animation/src/androidInstrumentedTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt
@@ -384,11 +384,14 @@
internal class TestModifier : LayoutModifier {
var width: Int = 0
var height: Int = 0
+ var lookaheadSize: IntSize? = null
+ private set
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
+ if (isLookingAhead) lookaheadSize = IntSize(placeable.width, placeable.height)
width = placeable.width
height = placeable.height
return layout(width, height) {
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
index 644b02d..e4cfdf2 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
@@ -1,4 +1,3 @@
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -15,7 +14,9 @@
* limitations under the License.
*/
@file:OptIn(InternalAnimationApi::class)
+
package androidx.compose.animation
+
import androidx.collection.mutableScatterMapOf
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.Down
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.End
@@ -68,6 +69,7 @@
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastMaxOfOrNull
+
/**
* [AnimatedContent] is a container that automatically animates its content when [targetState]
* changes. Its [content] for different target states is defined in a mapping between a target
@@ -146,6 +148,7 @@
content = content
)
}
+
/**
* [ContentTransform] defines how the target content (i.e. content associated with target state)
* enters [AnimatedContent] and how the initial content disappears.
@@ -196,6 +199,7 @@
* content with the same index, the target content will be placed on top.
*/
var targetContentZIndex by mutableFloatStateOf(targetContentZIndex)
+
/**
* [sizeTransform] manages the expanding and shrinking of the container if there is any size
* change as new content enters the [AnimatedContent] and old content leaves.
@@ -206,6 +210,7 @@
var sizeTransform: SizeTransform? = sizeTransform
internal set
}
+
/**
* This creates a [SizeTransform] with the provided [clip] and [sizeAnimationSpec]. By default,
* [clip] will be true. This means during the size animation, the content will be clipped to the
@@ -223,6 +228,7 @@
)
}
): SizeTransform = SizeTransformImpl(clip, sizeAnimationSpec)
+
/**
* [SizeTransform] defines how to transform from one size to another when the size of the content
* changes. When [clip] is true, the content will be clipped to the animation size.
@@ -236,12 +242,14 @@
* Whether the content should be clipped using the animated size.
*/
val clip: Boolean
+
/**
* This allows [FiniteAnimationSpec] to be defined based on the [initialSize] before the size
* animation and the [targetSize] of the animation.
*/
fun createAnimationSpec(initialSize: IntSize, targetSize: IntSize): FiniteAnimationSpec<IntSize>
}
+
/**
* Private implementation of SizeTransform interface.
*/
@@ -255,6 +263,7 @@
targetSize: IntSize
): FiniteAnimationSpec<IntSize> = sizeAnimationSpec(initialSize, targetSize)
}
+
/**
* This creates a [ContentTransform] using the provided [EnterTransition] and [exit], where the
* enter and exit transition will be running simultaneously.
@@ -263,12 +272,14 @@
* @sample androidx.compose.animation.samples.AnimatedContentTransitionSpecSample
*/
infix fun EnterTransition.togetherWith(exit: ExitTransition) = ContentTransform(this, exit)
+
@ExperimentalAnimationApi
@Deprecated(
"Infix fun EnterTransition.with(ExitTransition) has been renamed to" +
" togetherWith", ReplaceWith("togetherWith(exit)")
)
infix fun EnterTransition.with(exit: ExitTransition) = ContentTransform(this, exit)
+
/**
* [AnimatedContentTransitionScope] provides functions that are convenient and only applicable in the
* context of [AnimatedContent], such as [slideIntoContainer] and [slideOutOfContainer].
@@ -280,6 +291,7 @@
* @sample androidx.compose.animation.samples.AnimatedContentTransitionSpecSample
*/
infix fun ContentTransform.using(sizeTransform: SizeTransform?): ContentTransform
+
/**
* [SlideDirection] defines the direction of the slide in/out for [slideIntoContainer] and
* [slideOutOfContainer]. The supported directions are: [Left], [Right], [Up] and [Down].
@@ -295,6 +307,7 @@
val Start = SlideDirection(4)
val End = SlideDirection(5)
}
+
override fun toString(): String {
return when (this) {
Left -> "Left"
@@ -307,6 +320,7 @@
}
}
}
+
/**
* This defines a horizontal/vertical slide-in that is specific to [AnimatedContent] from the
* edge of the container. The offset amount is dynamically calculated based on the current
@@ -336,6 +350,7 @@
),
initialOffset: (offsetForFullSlide: Int) -> Int = { it }
): EnterTransition
+
/**
* This defines a horizontal/vertical exit transition to completely slide out of the
* [AnimatedContent] container. The offset amount is dynamically calculated based on the current
@@ -364,6 +379,7 @@
),
targetOffset: (offsetForFullSlide: Int) -> Int = { it }
): ExitTransition
+
/**
* [KeepUntilTransitionsFinished] defers the disposal of the exiting content till both enter and
* exit transitions have finished. It can be combined with other [ExitTransition]s using
@@ -379,11 +395,13 @@
*/
val ExitTransition.Companion.KeepUntilTransitionsFinished: ExitTransition
get() = KeepUntilTransitionsFinished
+
/**
* This returns the [Alignment] specified on [AnimatedContent].
*/
val contentAlignment: Alignment
}
+
internal class AnimatedContentTransitionScopeImpl<S> internal constructor(
internal val transition: Transition<S>,
override var contentAlignment: Alignment,
@@ -395,12 +413,14 @@
override val initialState: S
@Suppress("UnknownNullness")
get() = transition.segment.initialState
+
/**
* Target state of a Transition Segment. This is the state that transition will end on.
*/
override val targetState: S
@Suppress("UnknownNullness")
get() = transition.segment.targetState
+
/**
* Customizes the [SizeTransform] of a given [ContentTransform]. For example:
*
@@ -409,6 +429,7 @@
override infix fun ContentTransform.using(sizeTransform: SizeTransform?) = this.apply {
this.sizeTransform = sizeTransform
}
+
/**
* This defines a horizontal/vertical slide-in that is specific to [AnimatedContent] from the
* edge of the container. The offset amount is dynamically calculated based on the current
@@ -445,19 +466,24 @@
currentSize.width - calculateOffset(IntSize(it, it), currentSize).x
)
}
+
towards.isRight -> slideInHorizontally(animationSpec) {
initialOffset.invoke(-calculateOffset(IntSize(it, it), currentSize).x - it)
}
+
towards == Up -> slideInVertically(animationSpec) {
initialOffset.invoke(
currentSize.height - calculateOffset(IntSize(it, it), currentSize).y
)
}
+
towards == Down -> slideInVertically(animationSpec) {
initialOffset.invoke(-calculateOffset(IntSize(it, it), currentSize).y - it)
}
+
else -> EnterTransition.None
}
+
private val AnimatedContentTransitionScope.SlideDirection.isLeft: Boolean
get() {
return this == Left || this == Start && layoutDirection == LayoutDirection.Ltr ||
@@ -468,9 +494,11 @@
return this == Right || this == Start && layoutDirection == LayoutDirection.Rtl ||
this == End && layoutDirection == LayoutDirection.Ltr
}
+
private fun calculateOffset(fullSize: IntSize, currentSize: IntSize): IntOffset {
return contentAlignment.align(fullSize, currentSize, LayoutDirection.Ltr)
}
+
/**
* This defines a horizontal/vertical exit transition to completely slide out of the
* [AnimatedContent] container. The offset amount is dynamically calculated based on the current
@@ -506,32 +534,39 @@
val targetSize = targetSizeMap[transition.targetState]?.value ?: IntSize.Zero
targetOffset.invoke(-calculateOffset(IntSize(it, it), targetSize).x - it)
}
+
towards.isRight -> slideOutHorizontally(animationSpec) {
val targetSize = targetSizeMap[transition.targetState]?.value ?: IntSize.Zero
targetOffset.invoke(
-calculateOffset(IntSize(it, it), targetSize).x + targetSize.width
)
}
+
towards == Up -> slideOutVertically(animationSpec) {
val targetSize = targetSizeMap[transition.targetState]?.value ?: IntSize.Zero
targetOffset.invoke(-calculateOffset(IntSize(it, it), targetSize).y - it)
}
+
towards == Down -> slideOutVertically(animationSpec) {
val targetSize = targetSizeMap[transition.targetState]?.value ?: IntSize.Zero
targetOffset.invoke(
-calculateOffset(IntSize(it, it), targetSize).y + targetSize.height
)
}
+
else -> ExitTransition.None
}
}
+
internal var measuredSize: IntSize by mutableStateOf(IntSize.Zero)
internal val targetSizeMap = mutableScatterMapOf<S, State<IntSize>>()
internal var animatedSize: State<IntSize>? = null
+
// Current size of the container. If there's any size animation, the current size will be
// read from the animation value, otherwise we'll use the current
private val currentSize: IntSize
get() = animatedSize?.value ?: measuredSize
+
@Suppress("ComposableModifierFactory", "ModifierFactoryExtensionFunction")
@Composable
internal fun createSizeAnimationModifier(
@@ -558,13 +593,18 @@
Modifier
}
}
+
// This helps track the target measurable without affecting the placement order. Target
// measurable needs to be measured first but placed last.
- internal data class ChildData(var isTarget: Boolean) : ParentDataModifier {
+ internal class ChildData(isTarget: Boolean) : ParentDataModifier {
+ // isTarget is read during measure. It is necessary to make this a MutableState
+ // such that when the target changes, measure is triggered
+ var isTarget by mutableStateOf(isTarget)
override fun Density.modifyParentData(parentData: Any?): Any {
return this@ChildData
}
}
+
private inner class SizeModifier(
val sizeAnimation: Transition<S>.DeferredAnimation<IntSize, AnimationVector2D>,
val sizeTransform: State<SizeTransform?>,
@@ -584,15 +624,22 @@
targetSizeMap[it]?.value ?: IntSize.Zero
}
animatedSize = size
- val offset = contentAlignment.align(
- IntSize(placeable.width, placeable.height), size.value, LayoutDirection.Ltr
- )
- return layout(size.value.width, size.value.height) {
+ val measuredSize: IntSize
+ if (isLookingAhead) {
+ measuredSize = IntSize(placeable.width, placeable.height)
+ } else {
+ measuredSize = size.value
+ }
+ return layout(measuredSize.width, measuredSize.height) {
+ val offset = contentAlignment.align(
+ IntSize(placeable.width, placeable.height), measuredSize, LayoutDirection.Ltr
+ )
placeable.place(offset)
}
}
}
}
+
/**
* Receiver scope for content lambda for AnimatedContent. In this scope,
* [transition][AnimatedVisibilityScope.transition] can be used to observe the state of the
@@ -602,6 +649,7 @@
private class AnimatedContentScopeImpl internal constructor(
animatedVisibilityScope: AnimatedVisibilityScope
) : AnimatedContentScope, AnimatedVisibilityScope by animatedVisibilityScope
+
/**
* [AnimatedContent] is a container that automatically animates its content when
* [Transition.targetState] changes. Its [content] for different target states is defined in a
@@ -772,6 +820,7 @@
measurePolicy = remember { AnimatedContentMeasurePolicy(rootScope) }
)
}
+
private class AnimatedContentMeasurePolicy(val rootScope: AnimatedContentTransitionScopeImpl<*>) :
MeasurePolicy {
override fun MeasureScope.measure(
@@ -779,12 +828,15 @@
constraints: Constraints
): MeasureResult {
val placeables = arrayOfNulls<Placeable>(measurables.size)
+ var targetSize = IntSize.Zero
// Measure the target composable first (but place it on top unless zIndex is specified)
measurables.fastForEachIndexed { index, measurable ->
if ((measurable.parentData as? AnimatedContentTransitionScopeImpl.ChildData)
?.isTarget == true
) {
- placeables[index] = measurable.measure(constraints)
+ placeables[index] = measurable.measure(constraints).also {
+ targetSize = IntSize(it.width, it.height)
+ }
}
}
// Measure the non-target composables after target, since these have no impact on
@@ -794,9 +846,21 @@
placeables[index] = measurable.measure(constraints)
}
}
- val maxWidth: Int = placeables.maxByOrNull { it?.width ?: 0 }?.width ?: 0
- val maxHeight = placeables.maxByOrNull { it?.height ?: 0 }?.height ?: 0
- rootScope.measuredSize = IntSize(maxWidth, maxHeight)
+ val maxWidth: Int = if (isLookingAhead) {
+ targetSize.width
+ } else {
+ placeables.maxByOrNull { it?.width ?: 0 }?.width ?: 0
+ }
+ val maxHeight = if (isLookingAhead) {
+ targetSize.height
+ } else {
+ placeables.maxByOrNull { it?.height ?: 0 }?.height ?: 0
+ }
+ if (!isLookingAhead) {
+ // update currently measured size only during approach
+ rootScope.measuredSize = IntSize(maxWidth, maxHeight)
+ }
+
// Position the children.
return layout(maxWidth, maxHeight) {
placeables.forEach { placeable ->
@@ -811,18 +875,22 @@
}
}
}
+
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
) = measurables.fastMaxOfOrNull { it.minIntrinsicWidth(height) } ?: 0
+
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
) = measurables.fastMaxOfOrNull { it.minIntrinsicHeight(width) } ?: 0
+
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
) = measurables.fastMaxOfOrNull { it.maxIntrinsicWidth(height) } ?: 0
+
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt
index 7c9e02f..970d9ca 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt
@@ -633,9 +633,6 @@
* [transition] allows custom enter/exit animations to be specified. It will run simultaneously
* with the built-in enter/exit transitions specified in [AnimatedVisibility].
*/
- @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
- @get:ExperimentalAnimationApi
- @ExperimentalAnimationApi
val transition: Transition<EnterExitState>
/**
@@ -651,11 +648,8 @@
* does not matter, as the transition animations will start simultaneously. See [EnterTransition]
* and [ExitTransition] for details on the three types of transition.
*
- * By default, the enter transition will be a combination of [fadeIn] and [expandIn] of the
- * content from the bottom end. And the exit transition will be shrinking the content towards
- * the bottom end while fading out (i.e. [fadeOut] + [shrinkOut]). The expanding and shrinking
- * will likely also animate the parent and siblings if they rely on the size of
- * appearing/disappearing content.
+ * By default, the enter transition will be a [fadeIn] of the content. And the exit transition
+ * will be fading out the content using [fadeOut].
*
* In some cases it may be desirable to have [AnimatedVisibility] apply no animation at all for
* enter and/or exit, such that children of [AnimatedVisibility] can each have their distinct
@@ -664,10 +658,9 @@
*
* @sample androidx.compose.animation.samples.AnimateEnterExitPartialContent
*/
- @ExperimentalAnimationApi
fun Modifier.animateEnterExit(
- enter: EnterTransition = fadeIn() + expandIn(),
- exit: ExitTransition = fadeOut() + shrinkOut(),
+ enter: EnterTransition = fadeIn(),
+ exit: ExitTransition = fadeOut(),
label: String = "animateEnterExit"
): Modifier = composed(
inspectorInfo = debugInspectorInfo {
@@ -818,17 +811,25 @@
Layout(
content = { scope.content() },
modifier = modifier
- .then(childTransition.createModifier(enter, exit, "Built-in")
+ .then(childTransition
+ .createModifier(enter, exit, "Built-in")
.then(if (onLookaheadMeasured != null) {
Modifier.layout { measurable, constraints ->
- measurable.measure(constraints).run {
- if (isLookingAhead) {
- onLookaheadMeasured.invoke(IntSize(width, height))
+ measurable
+ .measure(constraints)
+ .run {
+ if (isLookingAhead) {
+ onLookaheadMeasured.invoke(
+ IntSize(
+ width,
+ height
+ )
+ )
+ }
+ layout(width, height) {
+ place(0, 0)
+ }
}
- layout(width, height) {
- place(0, 0)
- }
- }
}
} else Modifier)
),
@@ -845,6 +846,7 @@
private class AnimatedEnterExitMeasurePolicy(
val scope: AnimatedVisibilityScopeImpl
) : MeasurePolicy {
+ var hasLookaheadOccurred = false
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
@@ -853,7 +855,13 @@
val maxWidth: Int = placeables.fastMaxBy { it.width }?.width ?: 0
val maxHeight = placeables.fastMaxBy { it.height }?.height ?: 0
// Position the children.
- scope.targetSize.value = IntSize(maxWidth, maxHeight)
+ if (isLookingAhead) {
+ hasLookaheadOccurred = true
+ scope.targetSize.value = IntSize(maxWidth, maxHeight)
+ } else if (!hasLookaheadOccurred) {
+ // Not in lookahead scope.
+ scope.targetSize.value = IntSize(maxWidth, maxHeight)
+ }
return layout(maxWidth, maxHeight) {
placeables.fastForEach {
it.place(0, 0)
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/androidUnitTest/kotlin/androidx/compose/compiler/plugins/kotlin/ComposeBytecodeCodegenTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/androidUnitTest/kotlin/androidx/compose/compiler/plugins/kotlin/ComposeBytecodeCodegenTest.kt
index eab846c..9f56179 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/androidUnitTest/kotlin/androidx/compose/compiler/plugins/kotlin/ComposeBytecodeCodegenTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/androidUnitTest/kotlin/androidx/compose/compiler/plugins/kotlin/ComposeBytecodeCodegenTest.kt
@@ -780,4 +780,27 @@
}
"""
)
+
+ @Test
+ fun composeValueClassDefaultParameter() =
+ validateBytecode(
+ """
+ import androidx.compose.runtime.*
+
+ @JvmInline
+ value class Data(val string: String)
+ @JvmInline
+ value class IntData(val value: Int)
+
+ @Composable fun Example(data: Data = Data(""), intData: IntData = IntData(0)) {}
+ """,
+ validate = {
+ // select Example function body
+ val match = Regex("public final static Example[\\s\\S]*?LOCALVARIABLE").find(it)!!
+ assertFalse(message = "Function body should not contain a not-null check.") {
+ match.value.contains("Intrinsics.checkNotNullParameter")
+ }
+ },
+ dumpClasses = true
+ )
}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
index 883ff3a..9457ace 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/jvmTest/kotlin/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
@@ -572,4 +572,20 @@
}
"""
)
+
+ @Test
+ fun composeValueClassDefaultParameter() =
+ verifyGoldenComposeIrTransform(
+ extra = """
+ @JvmInline
+ value class Data(val string: String)
+ @JvmInline
+ value class IntData(val value: Int)
+ """,
+ source = """
+ import androidx.compose.runtime.*
+
+ @Composable fun Example(data: Data = Data(""), intData: IntData = IntData(0)) {}
+ """
+ )
}
diff --git "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ComposerParamTransformTests/composeValueClassDefaultParameter\133useFir = false\135.txt" "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ComposerParamTransformTests/composeValueClassDefaultParameter\133useFir = false\135.txt"
new file mode 100644
index 0000000..f43c062
--- /dev/null
+++ "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ComposerParamTransformTests/composeValueClassDefaultParameter\133useFir = false\135.txt"
@@ -0,0 +1,36 @@
+//
+// Source
+// ------------------------------------------
+
+import androidx.compose.runtime.*
+
+@Composable fun Example(data: Data = Data(""), intData: IntData = IntData(0)) {}
+
+//
+// Transformed IR
+// ------------------------------------------
+
+@Composable
+fun Example(data: Data?, intData: IntData, %composer: Composer?, %changed: Int, %default: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Example)P(0:Data,1:IntData):Test.kt")
+ if (%changed and 0b0001 != 0 || !%composer.skipping) {
+ if (%default and 0b0001 != 0) {
+ data = Data("")
+ }
+ if (%default and 0b0010 != 0) {
+ intData = IntData(0)
+ }
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Example(data, intData, %composer, updateChangedFlags(%changed or 0b0001), %default)
+ }
+}
diff --git "a/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ComposerParamTransformTests/composeValueClassDefaultParameter\133useFir = true\135.txt" "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ComposerParamTransformTests/composeValueClassDefaultParameter\133useFir = true\135.txt"
new file mode 100644
index 0000000..f43c062
--- /dev/null
+++ "b/compose/compiler/compiler-hosted/integration-tests/src/test/resources/golden/androidx.compose.compiler.plugins.kotlin.ComposerParamTransformTests/composeValueClassDefaultParameter\133useFir = true\135.txt"
@@ -0,0 +1,36 @@
+//
+// Source
+// ------------------------------------------
+
+import androidx.compose.runtime.*
+
+@Composable fun Example(data: Data = Data(""), intData: IntData = IntData(0)) {}
+
+//
+// Transformed IR
+// ------------------------------------------
+
+@Composable
+fun Example(data: Data?, intData: IntData, %composer: Composer?, %changed: Int, %default: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Example)P(0:Data,1:IntData):Test.kt")
+ if (%changed and 0b0001 != 0 || !%composer.skipping) {
+ if (%default and 0b0001 != 0) {
+ data = Data("")
+ }
+ if (%default and 0b0010 != 0) {
+ intData = IntData(0)
+ }
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Example(data, intData, %composer, updateChangedFlags(%changed or 0b0001), %default)
+ }
+}
diff --git a/compose/compiler/compiler-hosted/runtime-tests/src/commonTest/kotlin/androidx/compose/compiler/test/CompositionTests.kt b/compose/compiler/compiler-hosted/runtime-tests/src/commonTest/kotlin/androidx/compose/compiler/test/CompositionTests.kt
index ff016ed..7a7952b 100644
--- a/compose/compiler/compiler-hosted/runtime-tests/src/commonTest/kotlin/androidx/compose/compiler/test/CompositionTests.kt
+++ b/compose/compiler/compiler-hosted/runtime-tests/src/commonTest/kotlin/androidx/compose/compiler/test/CompositionTests.kt
@@ -73,6 +73,13 @@
Text("1")
}
}
+
+ @Test
+ fun composeValueClassDefaultParameter() = compositionTest {
+ compose {
+ DefaultValueClass()
+ }
+ }
}
class CrossInlineState(content: @Composable () -> Unit = { }) {
@@ -86,3 +93,13 @@
@Composable
fun place() { content() }
}
+
+@JvmInline
+value class Data(val string: String)
+
+@Composable
+fun DefaultValueClass(
+ data: Data = Data("Hello")
+) {
+ println(data)
+}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
index 8644e51..41f6447 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
@@ -623,7 +623,11 @@
return when {
type.isPrimitiveType() -> type
type.isInlineClassType() -> if (context.platform.isJvm() || constructorAccessible) {
- type
+ if (type.unboxInlineClass().isPrimitiveType()) {
+ type
+ } else {
+ type.makeNullable()
+ }
} else {
// k/js and k/native: private constructors of value classes can be not accessible.
// Therefore it won't be possible to create a "fake" default argument for calls.
diff --git a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsControllerTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsControllerTest.kt
index f9791e05..3540fb5 100644
--- a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsControllerTest.kt
+++ b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsControllerTest.kt
@@ -183,7 +183,7 @@
val consumed = connection.onPostScroll(
consumed = Offset.Zero,
available = Offset(3f, directionMultiplier),
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
)
assertThat(consumed).isEqualTo(Offset(0f, directionMultiplier))
}
@@ -196,7 +196,7 @@
connection.onPostScroll(
consumed = Offset.Zero,
available = Offset(3f, directionMultiplier * 5f),
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
)
coordinates.size
}
@@ -236,7 +236,7 @@
// The first scroll triggers the animation controller to be requested
val consumed = connection.onPreScroll(
available = Offset(3f, -directionMultiplier),
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
)
assertThat(consumed).isEqualTo(Offset(0f, -directionMultiplier))
}
@@ -248,7 +248,7 @@
val size = rule.runOnUiThread {
connection.onPreScroll(
available = Offset(3f, directionMultiplier * -5f),
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
)
coordinates.size
}
@@ -447,7 +447,7 @@
connection.onPostScroll(
consumed = Offset.Zero,
available = Offset(0f, directionMultiplier),
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
)
}
} while (!isVisible)
@@ -458,7 +458,7 @@
connection.onPostScroll(
consumed = Offset.Zero,
available = Offset(0f, directionMultiplier * sizeDifference),
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
)
}
@@ -509,7 +509,7 @@
rule.runOnIdle {
connection.onPreScroll(
available = Offset(0f, directionMultiplier * -1f),
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
)
}
} while (insetsSize != shownSize)
@@ -519,7 +519,7 @@
val sizeDifference = shownSize / 2f + 1f - insetsSize
connection.onPreScroll(
available = Offset(0f, directionMultiplier * sizeDifference),
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
)
}
diff --git a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt
index e99233e..f5c86b7 100644
--- a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt
+++ b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt
@@ -46,6 +46,8 @@
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.children
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
@@ -64,10 +66,22 @@
class WindowInsetsDeviceTest {
@get:Rule
val rule = createAndroidComposeRule<WindowInsetsActivity>()
+ private lateinit var finishLatch: CountDownLatch
+ private val finishLatchGetter
+ get() = finishLatch
+ private val observer = object : DefaultLifecycleObserver {
+ override fun onDestroy(owner: LifecycleOwner) {
+ finishLatchGetter.countDown()
+ }
+ }
@Before
fun setup() {
rule.activity.createdLatch.await(1, TimeUnit.SECONDS)
+ finishLatch = CountDownLatch(1)
+ rule.runOnUiThread {
+ rule.activity.lifecycle.addObserver(observer)
+ }
}
@After
@@ -75,6 +89,7 @@
rule.runOnUiThread {
rule.activity.finish()
}
+ assertThat(finishLatch.await(1, TimeUnit.SECONDS)).isTrue()
}
@OptIn(ExperimentalLayoutApi::class)
@@ -91,8 +106,12 @@
val innerComposable: @Composable () -> Unit = {
imeInset2 = WindowInsets.ime.getBottom(LocalDensity.current)
Box(
- Modifier.fillMaxSize().imePadding().imeNestedScroll()
- .nestedScroll(connection, dispatcher).background(
+ Modifier
+ .fillMaxSize()
+ .imePadding()
+ .imeNestedScroll()
+ .nestedScroll(connection, dispatcher)
+ .background(
Color.Cyan
)
)
@@ -140,7 +159,7 @@
dispatcher.dispatchPostScroll(
Offset.Zero,
Offset(0f, -10f),
- NestedScrollSource.Drag
+ NestedScrollSource.UserInput
)
Snapshot.sendApplyNotifications()
iteration++
diff --git a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt
index 7e408e5..c72c7d7 100644
--- a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt
+++ b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt
@@ -53,11 +53,15 @@
import androidx.core.view.DisplayCutoutCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.forEach
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import org.junit.After
import org.junit.Before
@@ -73,9 +77,22 @@
private lateinit var insetsView: InsetsView
+ private lateinit var finishLatch: CountDownLatch
+ private val finishLatchGetter
+ get() = finishLatch
+ private val observer = object : DefaultLifecycleObserver {
+ override fun onDestroy(owner: LifecycleOwner) {
+ finishLatchGetter.countDown()
+ }
+ }
+
@Before
fun setup() {
WindowInsetsHolder.setUseTestInsets(true)
+ finishLatch = CountDownLatch(1)
+ rule.runOnUiThread {
+ rule.activity.lifecycle.addObserver(observer)
+ }
}
@After
@@ -84,6 +101,7 @@
rule.runOnUiThread {
rule.activity.finish()
}
+ assertThat(finishLatch.await(1, TimeUnit.SECONDS)).isTrue()
}
@Test
diff --git a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsSizeTest.kt b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsSizeTest.kt
index 7c113ab..7fdd264 100644
--- a/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsSizeTest.kt
+++ b/compose/foundation/foundation-layout/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/layout/WindowInsetsSizeTest.kt
@@ -36,10 +36,14 @@
import androidx.core.graphics.Insets as AndroidXInsets
import androidx.core.view.DisplayCutoutCompat
import androidx.core.view.WindowInsetsCompat
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -54,9 +58,22 @@
private lateinit var insetsView: InsetsView
+ private lateinit var finishLatch: CountDownLatch
+ private val finishLatchGetter
+ get() = finishLatch
+ private val observer = object : DefaultLifecycleObserver {
+ override fun onDestroy(owner: LifecycleOwner) {
+ finishLatchGetter.countDown()
+ }
+ }
+
@Before
fun setup() {
WindowInsetsHolder.setUseTestInsets(true)
+ finishLatch = CountDownLatch(1)
+ rule.runOnUiThread {
+ rule.activity.lifecycle.addObserver(observer)
+ }
}
@After
@@ -65,6 +82,7 @@
rule.runOnUiThread {
rule.activity.finish()
}
+ assertThat(finishLatch.await(1, TimeUnit.SECONDS)).isTrue()
}
@OptIn(ExperimentalLayoutApi::class)
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Padding.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Padding.kt
index 338f910..1ff4160 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Padding.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Padding.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.layout
+import androidx.compose.foundation.layout.PaddingValues.Absolute
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
@@ -212,6 +213,14 @@
@Stable
private val bottom: Dp = 0.dp
) : PaddingValues {
+
+ init {
+ require(left.value >= 0) { "Left padding must be non-negative" }
+ require(top.value >= 0) { "Top padding must be non-negative" }
+ require(right.value >= 0) { "Right padding must be non-negative" }
+ require(bottom.value >= 0) { "Bottom padding must be non-negative" }
+ }
+
override fun calculateLeftPadding(layoutDirection: LayoutDirection) = left
override fun calculateTopPadding() = top
@@ -299,6 +308,14 @@
@Stable
val bottom: Dp = 0.dp
) : PaddingValues {
+
+ init {
+ require(start.value >= 0) { "Start padding must be non-negative" }
+ require(top.value >= 0) { "Top padding must be non-negative" }
+ require(end.value >= 0) { "End padding must be non-negative" }
+ require(bottom.value >= 0) { "Bottom padding must be non-negative" }
+ }
+
override fun calculateLeftPadding(layoutDirection: LayoutDirection) =
if (layoutDirection == LayoutDirection.Ltr) start else end
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.kt
index 2e3bcb8..af3d877 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurePolicy.kt
@@ -168,8 +168,11 @@
remainder -= weightedSize.fastRoundToInt()
} catch (e: IllegalArgumentException) {
throw IllegalArgumentException(
- e.message + " Tracked at " +
- "https://issuetracker.google.com/issues/297974033" +
+ "This log indicates a hard-to-reproduce Compose issue, " +
+ "modified with additional debugging details. " +
+ "Please help us by adding your experiences to the bug link provided. " +
+ "Thank you for helping us improve Compose. " +
+ "https://issuetracker.google.com/issues/297974033 " +
"mainAxisMax " + mainAxisMax +
"mainAxisMin " + mainAxisMin +
"targetSpace " + targetSpace +
@@ -224,8 +227,11 @@
)
} catch (e: IllegalArgumentException) {
throw IllegalArgumentException(
- e.message + " Tracked at " +
- "https://issuetracker.google.com/issues/300280216" +
+ "This log indicates a hard-to-reproduce Compose issue, " +
+ "modified with additional debugging details. " +
+ "Please help us by adding your experiences to the bug link provided. " +
+ "Thank you for helping us improve Compose. " +
+ "https://issuetracker.google.com/issues/300280216 " +
"mainAxisMax " + mainAxisMax +
"mainAxisMin " + mainAxisMin +
"targetSpace " + targetSpace +
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 81ac637..6a5ced6 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -435,7 +435,7 @@
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface BringIntoViewSpec {
- method public float calculateScrollDistance(float offset, float size, float containerSize);
+ method public default float calculateScrollDistance(float offset, float size, float containerSize);
method public default androidx.compose.animation.core.AnimationSpec<java.lang.Float> getScrollAnimationSpec();
property public default androidx.compose.animation.core.AnimationSpec<java.lang.Float> scrollAnimationSpec;
field public static final androidx.compose.foundation.gestures.BringIntoViewSpec.Companion Companion;
@@ -446,6 +446,11 @@
property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> DefaultScrollAnimationSpec;
}
+ public final class BringIntoViewSpec_androidKt {
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.foundation.gestures.BringIntoViewSpec> getLocalBringIntoViewSpec();
+ property @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.foundation.gestures.BringIntoViewSpec> LocalBringIntoViewSpec;
+ }
+
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface Drag2DScope {
method public void dragBy(long pixels);
}
@@ -544,7 +549,7 @@
}
public final class ScrollableDefaults {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec();
+ method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public androidx.compose.foundation.OverscrollEffect overscrollEffect();
method public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
@@ -552,7 +557,7 @@
}
public final class ScrollableKt {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec? bringIntoViewSpec);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
@@ -1358,7 +1363,7 @@
method public final float getCurrentPageOffsetFraction();
method public final androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
method public final androidx.compose.foundation.pager.PagerLayoutInfo getLayoutInfo();
- method public final float getOffsetFractionForPage(int page);
+ method public final float getOffsetDistanceInPages(int page);
method public abstract int getPageCount();
method public final int getSettledPage();
method public final int getTargetPage();
@@ -1537,7 +1542,7 @@
package androidx.compose.foundation.text {
public final class BasicSecureTextFieldKt {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(androidx.compose.foundation.text.input.TextFieldState state, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.text.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.input.KeyboardActionHandler? onKeyboardAction, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult?>,kotlin.Unit>? onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional androidx.compose.foundation.text.input.TextFieldDecorator? decorator, optional int textObfuscationMode);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(androidx.compose.foundation.text.input.TextFieldState state, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.text.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.input.KeyboardActionHandler? onKeyboardAction, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult?>,kotlin.Unit>? onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional androidx.compose.foundation.text.input.TextFieldDecorator? decorator, optional int textObfuscationMode, optional char textObfuscationCharacter);
}
public final class BasicTextFieldKt {
@@ -1695,7 +1700,8 @@
method public char charAt(int index);
method public androidx.compose.foundation.text.input.TextFieldBuffer.ChangeList getChanges();
method public int getLength();
- method public androidx.compose.foundation.text.input.TextFieldCharSequence getOriginalValue();
+ method public long getOriginalSelection();
+ method public CharSequence getOriginalText();
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public long getSelection();
method public boolean hasSelection();
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public void placeCursorAfterCharAt(int index);
@@ -1706,7 +1712,8 @@
property @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final androidx.compose.foundation.text.input.TextFieldBuffer.ChangeList changes;
property public final boolean hasSelection;
property public final int length;
- property public final androidx.compose.foundation.text.input.TextFieldCharSequence originalValue;
+ property public final long originalSelection;
+ property public final CharSequence originalText;
property @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final long selection;
}
@@ -1726,20 +1733,6 @@
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void selectAll(androidx.compose.foundation.text.input.TextFieldBuffer);
}
- public sealed interface TextFieldCharSequence extends java.lang.CharSequence {
- method public boolean contentEquals(CharSequence other);
- method public boolean equals(Object? other);
- method public androidx.compose.ui.text.TextRange? getComposition();
- method public long getSelection();
- method public int hashCode();
- property public abstract androidx.compose.ui.text.TextRange? composition;
- property public abstract long selection;
- }
-
- public final class TextFieldCharSequenceKt {
- method public static androidx.compose.foundation.text.input.TextFieldCharSequence TextFieldCharSequence(optional String text, optional long selection);
- }
-
public fun interface TextFieldDecorator {
method @androidx.compose.runtime.Composable public void Decoration(kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField);
}
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index c07846c..711926c 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -437,7 +437,7 @@
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface BringIntoViewSpec {
- method public float calculateScrollDistance(float offset, float size, float containerSize);
+ method public default float calculateScrollDistance(float offset, float size, float containerSize);
method public default androidx.compose.animation.core.AnimationSpec<java.lang.Float> getScrollAnimationSpec();
property public default androidx.compose.animation.core.AnimationSpec<java.lang.Float> scrollAnimationSpec;
field public static final androidx.compose.foundation.gestures.BringIntoViewSpec.Companion Companion;
@@ -448,6 +448,11 @@
property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> DefaultScrollAnimationSpec;
}
+ public final class BringIntoViewSpec_androidKt {
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.foundation.gestures.BringIntoViewSpec> getLocalBringIntoViewSpec();
+ property @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.foundation.gestures.BringIntoViewSpec> LocalBringIntoViewSpec;
+ }
+
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface Drag2DScope {
method public void dragBy(long pixels);
}
@@ -546,7 +551,7 @@
}
public final class ScrollableDefaults {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec();
+ method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public androidx.compose.foundation.OverscrollEffect overscrollEffect();
method public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
@@ -554,7 +559,7 @@
}
public final class ScrollableKt {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec? bringIntoViewSpec);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
@@ -1360,7 +1365,7 @@
method public final float getCurrentPageOffsetFraction();
method public final androidx.compose.foundation.interaction.InteractionSource getInteractionSource();
method public final androidx.compose.foundation.pager.PagerLayoutInfo getLayoutInfo();
- method public final float getOffsetFractionForPage(int page);
+ method public final float getOffsetDistanceInPages(int page);
method public abstract int getPageCount();
method public final int getSettledPage();
method public final int getTargetPage();
@@ -1539,7 +1544,7 @@
package androidx.compose.foundation.text {
public final class BasicSecureTextFieldKt {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(androidx.compose.foundation.text.input.TextFieldState state, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.text.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.input.KeyboardActionHandler? onKeyboardAction, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult?>,kotlin.Unit>? onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional androidx.compose.foundation.text.input.TextFieldDecorator? decorator, optional int textObfuscationMode);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(androidx.compose.foundation.text.input.TextFieldState state, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.text.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.input.KeyboardActionHandler? onKeyboardAction, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult?>,kotlin.Unit>? onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional androidx.compose.foundation.text.input.TextFieldDecorator? decorator, optional int textObfuscationMode, optional char textObfuscationCharacter);
}
public final class BasicTextFieldKt {
@@ -1697,7 +1702,8 @@
method public char charAt(int index);
method public androidx.compose.foundation.text.input.TextFieldBuffer.ChangeList getChanges();
method public int getLength();
- method public androidx.compose.foundation.text.input.TextFieldCharSequence getOriginalValue();
+ method public long getOriginalSelection();
+ method public CharSequence getOriginalText();
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public long getSelection();
method public boolean hasSelection();
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public void placeCursorAfterCharAt(int index);
@@ -1708,7 +1714,8 @@
property @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final androidx.compose.foundation.text.input.TextFieldBuffer.ChangeList changes;
property public final boolean hasSelection;
property public final int length;
- property public final androidx.compose.foundation.text.input.TextFieldCharSequence originalValue;
+ property public final long originalSelection;
+ property public final CharSequence originalText;
property @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final long selection;
}
@@ -1728,20 +1735,6 @@
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void selectAll(androidx.compose.foundation.text.input.TextFieldBuffer);
}
- public sealed interface TextFieldCharSequence extends java.lang.CharSequence {
- method public boolean contentEquals(CharSequence other);
- method public boolean equals(Object? other);
- method public androidx.compose.ui.text.TextRange? getComposition();
- method public long getSelection();
- method public int hashCode();
- property public abstract androidx.compose.ui.text.TextRange? composition;
- property public abstract long selection;
- }
-
- public final class TextFieldCharSequenceKt {
- method public static androidx.compose.foundation.text.input.TextFieldCharSequence TextFieldCharSequence(optional String text, optional long selection);
- }
-
public fun interface TextFieldDecorator {
method @androidx.compose.runtime.Composable public void Decoration(kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField);
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
index 2c7ae2c..245b962 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
@@ -18,6 +18,7 @@
package androidx.compose.foundation.demos
import android.annotation.SuppressLint
+import android.content.res.Configuration
import androidx.compose.animation.core.AnimationConstants
import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.animateTo
@@ -27,6 +28,7 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.gestures.animateScrollBy
@@ -46,8 +48,10 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
@@ -90,7 +94,10 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Color.Companion.Red
+import androidx.compose.ui.graphics.Color.Companion.White
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.Preview
@@ -130,6 +137,7 @@
ComposableDemo("Grid drag and drop") { LazyGridDragAndDropDemo() },
ComposableDemo("Staggered grid") { LazyStaggeredGridDemo() },
ComposableDemo("Animate item placement") { AnimateItemPlacementDemo() },
+ ComposableDemo("Focus Scrolling") { BringIntoViewDemo() },
PagingDemos
)
@@ -349,7 +357,8 @@
Spacer(
Modifier
.fillParentMaxSize()
- .background(it))
+ .background(it)
+ )
}
}
}
@@ -555,6 +564,7 @@
}
}
}
+
val lazyContent: LazyListScope.() -> Unit = {
items(count) {
item1(it)
@@ -1037,9 +1047,11 @@
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun AnimateItemPlacementDemo() {
- val items = remember { mutableStateListOf<Int>().apply {
- repeat(20) { add(it) }
- } }
+ val items = remember {
+ mutableStateListOf<Int>().apply {
+ repeat(20) { add(it) }
+ }
+ }
val selectedIndexes = remember { mutableStateMapOf<Int, Boolean>() }
var reverse by remember { mutableStateOf(false) }
Column {
@@ -1068,7 +1080,8 @@
LazyColumn(
Modifier
.fillMaxWidth()
- .weight(1f), reverseLayout = reverse) {
+ .weight(1f), reverseLayout = reverse
+ ) {
items(items, key = { it }) { item ->
val selected = selectedIndexes.getOrDefault(item, false)
val modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
@@ -1089,7 +1102,8 @@
Spacer(
Modifier
.width(16.dp)
- .height(height))
+ .height(height)
+ )
Text("Item $item")
}
}
@@ -1105,3 +1119,31 @@
})
}
}
+
+@Preview(uiMode = Configuration.UI_MODE_TYPE_TELEVISION)
+@Composable
+private fun BringIntoViewDemo() {
+ LazyRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ ) {
+ items(100) {
+ var color by remember { mutableStateOf(Color.White) }
+ Box(
+ modifier = Modifier
+ .size(100.dp)
+ .padding(4.dp)
+ .background(Color.Gray)
+ .onFocusChanged {
+ color = if (it.isFocused) Red else White
+ }
+ .border(5.dp, color)
+ .focusable(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(text = it.toString())
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicSecureTextFieldDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicSecureTextFieldDemos.kt
index 3c3fd72..134f8a0 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicSecureTextFieldDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicSecureTextFieldDemos.kt
@@ -27,9 +27,12 @@
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicSecureTextField
+import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.TextObfuscationMode
+import androidx.compose.foundation.text.input.insert
+import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.Text
@@ -45,6 +48,7 @@
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.substring
import androidx.compose.ui.unit.dp
import androidx.core.text.isDigitsOnly
@@ -70,6 +74,9 @@
TagLine(tag = "Hidden")
BasicSecureTextFieldDemo(TextObfuscationMode.Hidden)
+ TagLine(tag = "Changing Mask")
+ ChangingMaskDemo(TextObfuscationMode.RevealLastTyped)
+
TagLine(tag = "Number Password")
NumberPasswordDemo()
@@ -91,6 +98,46 @@
@OptIn(ExperimentalFoundationApi::class)
@Composable
+fun ChangingMaskDemo(textObfuscationMode: TextObfuscationMode) {
+ val maskState = rememberTextFieldState("\u2022")
+ val passwordState = rememberTextFieldState("hunter2")
+ Column {
+ // single character TextField
+ BasicTextField(
+ state = maskState,
+ modifier = demoTextFieldModifiers,
+ inputTransformation = {
+ // only handle single character insertion, reject everything else
+ val isSingleCharacterInsertion = changes.changeCount == 1 &&
+ changes.getRange(0).length == 1 &&
+ changes.getOriginalRange(0).length == 0
+
+ if (!isSingleCharacterInsertion) {
+ revertAllChanges()
+ } else {
+ replace(
+ start = 0,
+ end = length,
+ text = asCharSequence()
+ .substring(changes.getRange(0))
+ )
+ }
+ },
+ outputTransformation = {
+ insert(0, "Enter mask character: ")
+ }
+ )
+ }
+ BasicSecureTextField(
+ state = passwordState,
+ textObfuscationMode = textObfuscationMode,
+ textObfuscationCharacter = maskState.text[0],
+ modifier = demoTextFieldModifiers
+ )
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
fun NumberPasswordDemo() {
val state = remember { TextFieldState() }
BasicSecureTextField(
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
index 1a1e3f6..69c8e0c 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/OverscrollSample.kt
@@ -94,7 +94,7 @@
val consumedByScroll = performScroll(leftForScroll)
val overscrollDelta = leftForScroll - consumedByScroll
// if it is a drag, not a fling, add the delta left to our over scroll value
- if (abs(overscrollDelta.y) > 0.5 && source == NestedScrollSource.Drag) {
+ if (abs(overscrollDelta.y) > 0.5 && source == NestedScrollSource.UserInput) {
scope.launch {
// multiply by 0.1 for the sake of parallax effect
overscrollOffset.snapTo(overscrollOffset.value + overscrollDelta.y * 0.1f)
@@ -197,7 +197,10 @@
// Horizontal, so convert the delta to a horizontal offset
val deltaAsOffset = Offset(delta, 0f)
// Wrap the original logic inside applyToScroll
- overscrollEffect.applyToScroll(deltaAsOffset, NestedScrollSource.Drag) { remainingOffset ->
+ overscrollEffect.applyToScroll(
+ deltaAsOffset,
+ NestedScrollSource.UserInput
+ ) { remainingOffset ->
val remainingDelta = remainingOffset.x
val newPosition = (dragPosition + remainingDelta).coerceIn(minPosition, maxPosition)
// Calculate how much delta we have consumed
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt
index 1645cb76..ae7c7ec 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt
@@ -17,15 +17,26 @@
package androidx.compose.foundation.samples
import androidx.annotation.Sampled
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.BringIntoViewSpec
+import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Icon
@@ -34,15 +45,20 @@
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import kotlin.math.abs
import kotlin.math.roundToInt
@Sampled
@@ -113,3 +129,68 @@
)
}
}
+
+@ExperimentalFoundationApi
+@Sampled
+@Composable
+fun FocusScrollingInLazyRowSample() {
+ // a bring into view spec that pivots around the center of the scrollable container
+ val customBringIntoViewSpec = object : BringIntoViewSpec {
+ val customAnimationSpec = tween<Float>(easing = LinearEasing)
+ override val scrollAnimationSpec: AnimationSpec<Float>
+ get() = customAnimationSpec
+
+ override fun calculateScrollDistance(
+ offset: Float,
+ size: Float,
+ containerSize: Float
+ ): Float {
+ val trailingEdgeOfItemRequestingFocus = offset + size
+
+ val sizeOfItemRequestingFocus =
+ abs(trailingEdgeOfItemRequestingFocus - offset)
+ val childSmallerThanParent = sizeOfItemRequestingFocus <= containerSize
+ val initialTargetForLeadingEdge =
+ containerSize / 2f - (sizeOfItemRequestingFocus / 2f)
+ val spaceAvailableToShowItem = containerSize - initialTargetForLeadingEdge
+
+ val targetForLeadingEdge =
+ if (childSmallerThanParent &&
+ spaceAvailableToShowItem < sizeOfItemRequestingFocus
+ ) {
+ containerSize - sizeOfItemRequestingFocus
+ } else {
+ initialTargetForLeadingEdge
+ }
+
+ return offset - targetForLeadingEdge
+ }
+ }
+
+ // LocalBringIntoViewSpec will apply to all scrollables in the hierarchy.
+ CompositionLocalProvider(LocalBringIntoViewSpec provides customBringIntoViewSpec) {
+ LazyRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ ) {
+ items(100) {
+ var color by remember { mutableStateOf(Color.White) }
+ Box(
+ modifier = Modifier
+ .size(100.dp)
+ .padding(4.dp)
+ .background(Color.Gray)
+ .onFocusChanged {
+ color = if (it.isFocused) Color.Red else Color.White
+ }
+ .border(5.dp, color)
+ .focusable(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(text = it.toString())
+ }
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
index 4ed0d6d..90f9d1e 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
@@ -52,6 +52,7 @@
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.input.InputMode
import androidx.compose.ui.input.InputMode.Companion.Keyboard
@@ -143,7 +144,9 @@
Box {
BasicText(
"ClickableText",
- modifier = Modifier.testTag("myClickable").clickable {}
+ modifier = Modifier
+ .testTag("myClickable")
+ .clickable {}
)
}
}
@@ -160,7 +163,9 @@
Box {
BasicText(
"ClickableText",
- modifier = Modifier.testTag("myClickable").clickable(enabled = false) {}
+ modifier = Modifier
+ .testTag("myClickable")
+ .clickable(enabled = false) {}
)
}
}
@@ -179,7 +184,8 @@
Box {
BasicText(
"ClickableText",
- modifier = Modifier.testTag("myClickable")
+ modifier = Modifier
+ .testTag("myClickable")
.clickable(enabled = enabled, role = role) {}
)
}
@@ -1324,7 +1330,9 @@
private fun Modifier.dynamicPointerInputModifier(
enabled: Boolean,
key: Any? = Unit,
- onPress: () -> Unit
+ onPress: () -> Unit = { },
+ onMove: () -> Unit = { },
+ onRelease: () -> Unit = { },
) = if (enabled) {
pointerInput(key) {
awaitPointerEventScope {
@@ -1332,6 +1340,10 @@
val event = awaitPointerEvent()
if (event.type == PointerEventType.Press) {
onPress()
+ } else if (event.type == PointerEventType.Move) {
+ onMove()
+ } else if (event.type == PointerEventType.Release) {
+ onRelease()
}
}
}
@@ -1557,9 +1569,13 @@
Box(Modifier
.size(200.dp)
.testTag("myClickable")
- .dynamicPointerInputModifier(activateDynamicPointerInput, "unique_key_123") {
- dynamicPressCounter++
- }
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ key = "unique_key_123",
+ onPress = {
+ dynamicPressCounter++
+ }
+ )
.clickable {
clickableClickCounter++
activateDynamicPointerInput = true
@@ -1593,7 +1609,12 @@
Box(Modifier
.size(200.dp)
.testTag("myClickable")
- .dynamicPointerInputModifier(activateDynamicPointerInput) { dynamicPressCounter++ }
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onPress = {
+ dynamicPressCounter++
+ }
+ )
.clickable {
clickableClickCounter++
activateDynamicPointerInput = true
@@ -1616,6 +1637,132 @@
}
}
+ // Tests a dynamic pointer input AND a dynamic clickable{} above an existing pointer input.
+ @Test
+ fun dynamicInputModifiersInTouchStream_addsAboveClickableWithUnitKey_triggersAllModifiers() {
+ var activeDynamicClickable by mutableStateOf(false)
+ var dynamicClickableCounter by mutableStateOf(0)
+
+ var activeDynamicPointerInput by mutableStateOf(false)
+ var dynamicPointerInputPressCounter by mutableStateOf(0)
+ var dynamicPointerInputMoveCounter by mutableStateOf(0)
+ var dynamicPointerInputReleaseCounter by mutableStateOf(0)
+
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEventCounter by mutableStateOf(0)
+
+ rule.setContent {
+ Box(Modifier
+ .size(200.dp)
+ .testTag("myClickable")
+ .dynamicPointerInputModifier(
+ enabled = activeDynamicPointerInput,
+ onPress = {
+ dynamicPointerInputPressCounter++
+ },
+ onMove = {
+ dynamicPointerInputMoveCounter++
+ },
+ onRelease = {
+ dynamicPointerInputReleaseCounter++
+ activeDynamicClickable = true
+ }
+ )
+ .dynamicClickableModifier(activeDynamicClickable) {
+ dynamicClickableCounter++
+ }
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ awaitPointerEvent()
+ originalPointerInputEventCounter++
+ activeDynamicPointerInput = true
+ }
+ }
+ }
+ )
+ }
+
+ // Even though we are enabling the dynamic pointer input, it will NOT receive events until
+ // the next event stream (after the click is over) which is why you see zeros below.
+ // Only two events are triggered for click (down/up)
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ // With these events, we enable the dynamic pointer input
+ assertEquals(2, originalPointerInputEventCounter)
+
+ assertEquals(0, dynamicPointerInputPressCounter)
+ assertEquals(0, dynamicPointerInputMoveCounter)
+ assertEquals(0, dynamicPointerInputReleaseCounter)
+
+ assertEquals(0, dynamicClickableCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performTouchInput {
+ down(Offset(0f, 0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(3, originalPointerInputEventCounter)
+
+ assertEquals(1, dynamicPointerInputPressCounter)
+ assertEquals(0, dynamicPointerInputMoveCounter)
+ assertEquals(0, dynamicPointerInputReleaseCounter)
+
+ assertEquals(0, dynamicClickableCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performTouchInput {
+ moveTo(Offset(1f, 1f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(4, originalPointerInputEventCounter)
+
+ assertEquals(1, dynamicPointerInputPressCounter)
+ assertEquals(1, dynamicPointerInputMoveCounter)
+ assertEquals(0, dynamicPointerInputReleaseCounter)
+
+ assertEquals(0, dynamicClickableCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performTouchInput {
+ up()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(5, originalPointerInputEventCounter)
+
+ assertEquals(1, dynamicPointerInputPressCounter)
+ assertEquals(1, dynamicPointerInputMoveCounter)
+ // With this release counter, we enable the dynamic clickable{}
+ assertEquals(1, dynamicPointerInputReleaseCounter)
+
+ assertEquals(0, dynamicClickableCounter)
+ }
+
+ // Only two events are triggered for click (down/up)
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(7, originalPointerInputEventCounter)
+
+ assertEquals(2, dynamicPointerInputPressCounter)
+ assertEquals(1, dynamicPointerInputMoveCounter)
+ assertEquals(2, dynamicPointerInputReleaseCounter)
+
+ assertEquals(1, dynamicClickableCounter)
+ }
+ }
+
/*
* Tests adding dynamic modifier with COMPLETE mouse events, that is, the expected events from
* using a hardware device with an Android device.
@@ -1632,9 +1779,13 @@
Box(Modifier
.size(200.dp)
.testTag("myClickable")
- .dynamicPointerInputModifier(activateDynamicPointerInput, "unique_key_123") {
- dynamicPressCounter++
- }
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ key = "unique_key_123",
+ onPress = {
+ dynamicPressCounter++
+ }
+ )
.clickable {
clickableClickCounter++
activateDynamicPointerInput = true
@@ -1680,7 +1831,12 @@
Box(Modifier
.size(200.dp)
.testTag("myClickable")
- .dynamicPointerInputModifier(activateDynamicPointerInput) { dynamicPressCounter++ }
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onPress = {
+ dynamicPressCounter++
+ }
+ )
.clickable {
clickableClickCounter++
activateDynamicPointerInput = true
@@ -1710,6 +1866,153 @@
}
}
+ /* Tests dynamically adding a pointer input DURING an event stream (specifically, Hover).
+ * Hover is the only scenario where you can add a new pointer input modifier during the event
+ * stream AND receive events in the same active stream from that new pointer input modifier.
+ * It isn't possible in the down/up scenario because you add the new modifier during the down
+ * but you don't get another down until the next event stream.
+ */
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputModifierHoverMouse_addsAbovePointerInputWithUnitKey_triggersBothModifiers() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEventCounter by mutableStateOf(0)
+
+ var dynamicPressCounter by mutableStateOf(0)
+ var dynamicReleaseCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier
+ .size(200.dp)
+ .testTag("myClickable")
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onPress = {
+ dynamicPressCounter++
+ },
+ onRelease = {
+ dynamicReleaseCounter++
+ }
+ )
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ awaitPointerEvent()
+ originalPointerInputEventCounter++
+ activateDynamicPointerInput = true
+ }
+ }
+ }
+ )
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEventCounter)
+ assertEquals(0, dynamicPressCounter)
+ assertEquals(0, dynamicReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ press()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputEventCounter)
+ assertEquals(1, dynamicPressCounter)
+ assertEquals(0, dynamicReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ release()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(3, originalPointerInputEventCounter)
+ assertEquals(1, dynamicPressCounter)
+ assertEquals(1, dynamicReleaseCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(4, originalPointerInputEventCounter)
+ assertEquals(1, dynamicPressCounter)
+ assertEquals(1, dynamicReleaseCounter)
+ }
+ }
+
+ /* This is the same as the test above, but
+ * 1. Using clickable{}
+ * 2. It enables the dynamic pointer input and starts the hover event stream in a more
+ * hacky way (using mouse click without hover which triggers hover enter on release).
+ */
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputModifierIncompleteMouse_addsAboveClickableHackyEvents_triggersBothModifiers() {
+ var clickableClickCounter by mutableStateOf(0)
+ // Note: I'm tracking press instead of release because clickable{} consumes release
+ var dynamicPressCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier
+ .size(200.dp)
+ .testTag("myClickable")
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onPress = {
+ dynamicPressCounter++
+ }
+ )
+ .clickable {
+ clickableClickCounter++
+ activateDynamicPointerInput = true
+ }
+ )
+ }
+
+ // Usually, a proper event stream from hardware for mouse input would be:
+ // - enter() (hover enter)
+ // - click()
+ // - exit()
+ // However, in this case, I'm just calling click() which triggers actions:
+ // - press
+ // - release
+ // - hover enter
+ // This starts a hover event stream (in a more hacky way) and also enables the dynamic
+ // pointer input to start recording events.
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ click()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, clickableClickCounter)
+ assertEquals(0, dynamicPressCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ click()
+ }
+
+ rule.runOnIdle {
+ assertEquals(2, clickableClickCounter)
+ assertEquals(1, dynamicPressCounter)
+ }
+ }
+
@OptIn(ExperimentalTestApi::class)
@Test
@LargeTest
@@ -2614,7 +2917,10 @@
inputModeManager = LocalInputModeManager.current
// Add focusable to the top so that when initial focus is dispatched, the clickable
// doesn't become focused
- Box(Modifier.padding(10.dp).focusable()) {
+ Box(
+ Modifier
+ .padding(10.dp)
+ .focusable()) {
Box(
modifier = Modifier
.testTag("clickable")
@@ -2623,7 +2929,10 @@
indication = indication
) {}
) {
- Box(Modifier.focusRequester(focusRequester).focusable())
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .focusable())
}
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/IndicationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/IndicationTest.kt
index a75ac01..2eeb8d1 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/IndicationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/IndicationTest.kt
@@ -341,6 +341,9 @@
)
}
+ // Due to b/302303969 there are no guarantees runOnIdle() will wait for drawing to happen
+ rule.waitUntil { drawnNode == 1 }
+
rule.runOnIdle {
assertThat(createCalls).isEqualTo(1)
assertThat(drawnNode).isEqualTo(1)
@@ -357,6 +360,9 @@
)
}
+ // Due to b/302303969 there are no guarantees runOnIdle() will wait for drawing to happen
+ rule.waitUntil { drawnNode == 2 }
+
rule.runOnIdle {
// New instance that doesn't compare equal, so we should create again
assertThat(createCalls).isEqualTo(2)
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/OverscrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
index 5a42438..7276c9f 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/OverscrollTest.kt
@@ -136,7 +136,7 @@
rule.runOnIdle {
assertThat(controller.lastVelocity.x).isGreaterThan(0f)
- assertThat(controller.lastNestedScrollSource).isEqualTo(NestedScrollSource.Fling)
+ assertThat(controller.lastNestedScrollSource).isEqualTo(NestedScrollSource.SideEffect)
}
}
@@ -166,7 +166,7 @@
assertThat(abs(acummulatedScroll - 1000f * 9 / 10)).isWithin(0.1f)
assertThat(controller.lastPreScrollDelta).isEqualTo(Offset(1000f - slop, 0f))
- assertThat(controller.lastNestedScrollSource).isEqualTo(NestedScrollSource.Drag)
+ assertThat(controller.lastNestedScrollSource).isEqualTo(NestedScrollSource.UserInput)
}
rule.onNodeWithTag(boxTag).performTouchInput {
@@ -208,7 +208,7 @@
assertThat(abs(acummulatedScroll - 1000f * 9 / 10)).isWithin(0.1f)
assertThat(controller.lastPreScrollDelta).isEqualTo(Offset(1000f - slop, 0f))
- assertThat(controller.lastNestedScrollSource).isEqualTo(NestedScrollSource.Drag)
+ assertThat(controller.lastNestedScrollSource).isEqualTo(NestedScrollSource.UserInput)
controller.lastPreScrollDelta = Offset.Zero
}
@@ -381,7 +381,7 @@
val offset = Offset(x = 0f, y = 50f)
controller.applyToScroll(
offset,
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
) { Offset.Zero }
// we have to disable further invalidation requests as otherwise while the overscroll
// effect is considered active (as it is in a pulled state) this will infinitely
@@ -455,7 +455,7 @@
val offset = Offset(x = 0f, y = 50f)
controller.applyToScroll(
offset,
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
) { Offset.Zero }
// we have to disable further invalidation requests as otherwise while the overscroll
// effect is considered active (as it is in a pulled state) this will infinitely
@@ -586,7 +586,7 @@
val offset = Offset(x = 50f, y = 0f)
controller.applyToScroll(
offset,
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
) { Offset.Zero }
// we have to disable further invalidation requests as otherwise while the overscroll
// effect is considered active (as it is in a pulled state) this will infinitely
@@ -717,7 +717,7 @@
val offset = Offset(x = 50f, y = 50f)
controller.applyToScroll(
offset,
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
) { Offset.Zero }
// we have to disable further invalidation requests as otherwise while the overscroll
// effect is considered active (as it is in a pulled state) this will infinitely
@@ -842,7 +842,7 @@
val offset = Offset(x = 0f, y = 50f)
controller.applyToScroll(
offset,
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
) { Offset.Zero }
// we have to disable further invalidation requests as otherwise while the overscroll
// effect is considered active (as it is in a pulled state) this will infinitely
@@ -904,7 +904,7 @@
val offset = Offset(x = 50f, y = 0f)
controller.applyToScroll(
offset,
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
) { Offset.Zero }
// we have to disable further invalidation requests as otherwise while the overscroll
// effect is considered active (as it is in a pulled state) this will infinitely
@@ -966,7 +966,7 @@
val offset = Offset(x = 50f, y = 50f)
controller.applyToScroll(
offset,
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
) { Offset.Zero }
// we have to disable further invalidation requests as otherwise while the overscroll
// effect is considered active (as it is in a pulled state) this will infinitely
@@ -1004,7 +1004,7 @@
val offset = Offset(-10f, -10f)
var offsetConsumed: Offset? = null
- effect.applyToScroll(offset, NestedScrollSource.Drag) {
+ effect.applyToScroll(offset, NestedScrollSource.UserInput) {
offsetConsumed = offset - it
Offset.Zero
}
@@ -1037,7 +1037,7 @@
val offset = Offset(0f, 10f)
var offsetConsumed: Offset? = null
- effect.applyToScroll(offset, NestedScrollSource.Drag) {
+ effect.applyToScroll(offset, NestedScrollSource.UserInput) {
offsetConsumed = offset - it
Offset.Zero
}
@@ -1340,7 +1340,7 @@
assertThat(controller.lastInitialDragDelta.y).isZero()
assertThat(controller.lastOverscrollDelta.x)
.isEqualTo(controller.lastInitialDragDelta.x / 2)
- assertThat(controller.lastNestedScrollSource).isEqualTo(NestedScrollSource.Drag)
+ assertThat(controller.lastNestedScrollSource).isEqualTo(NestedScrollSource.UserInput)
}
rule.onNodeWithTag(boxTag).performTouchInput {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
index 523307e..1603c65 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
@@ -1305,7 +1305,7 @@
): Offset {
// we should get in post scroll as much as left in controller callback
assertThat(available.x).isEqualTo(expectedLeft)
- return if (source == NestedScrollSource.Fling) Offset.Zero else available
+ return if (source == NestedScrollSource.SideEffect) Offset.Zero else available
}
override suspend fun onPostFling(
@@ -1374,7 +1374,7 @@
): Offset {
// we should get in post scroll as much as left in controller callback
assertThat(available.x).isEqualTo(-expectedLeft)
- return if (source == NestedScrollSource.Fling) Offset.Zero else available
+ return if (source == NestedScrollSource.SideEffect) Offset.Zero else available
}
override suspend fun onPostFling(
@@ -1459,14 +1459,14 @@
val lastValueBeforeFling = rule.runOnIdle {
val preScrollConsumed = dispatcher
- .dispatchPreScroll(Offset(20f, 20f), NestedScrollSource.Drag)
+ .dispatchPreScroll(Offset(20f, 20f), NestedScrollSource.UserInput)
// scrollable is not interested in pre scroll
assertThat(preScrollConsumed).isEqualTo(Offset.Zero)
val consumed = dispatcher.dispatchPostScroll(
Offset(20f, 20f),
Offset(50f, 50f),
- NestedScrollSource.Drag
+ NestedScrollSource.UserInput
)
assertThat(consumed.x - expectedConsumed).isWithin(0.001f)
value
@@ -1640,7 +1640,7 @@
available: Offset,
source: NestedScrollSource
): Offset {
- if (source == NestedScrollSource.Fling && available != Offset.Zero) {
+ if (source == NestedScrollSource.SideEffect && available != Offset.Zero) {
throw CancellationException()
}
return Offset.Zero
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
index dc9ae1e..d06e95361 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
@@ -557,176 +557,6 @@
}
@Test
- fun anchoredDraggable_positionalThresholds_fixed_targetState() {
- val positionalThreshold = 56.dp
- val positionalThresholdPx = with(rule.density) { positionalThreshold.toPx() }
- val absThreshold = abs(positionalThresholdPx)
- val state = AnchoredDraggableState(
- initialValue = A,
- positionalThreshold = { positionalThresholdPx },
- velocityThreshold = DefaultVelocityThreshold,
- snapAnimationSpec = tween(),
- decayAnimationSpec = DefaultDecayAnimationSpec
- )
- rule.setContent {
- Box(Modifier.fillMaxSize()) {
- Box(
- Modifier
- .requiredSize(AnchoredDraggableBoxSize)
- .testTag(AnchoredDraggableTestTag)
- .anchoredDraggable(
- state = state,
- orientation = Orientation.Horizontal
- )
- .onSizeChanged { layoutSize ->
- val anchors = DraggableAnchors {
- A at 0f
- B at layoutSize.width / 2f
- C at layoutSize.width.toFloat()
- }
- state.updateAnchors(anchors)
- }
- .offset {
- IntOffset(
- state
- .requireOffset()
- .roundToInt(), 0
- )
- }
- .background(Color.Red)
- )
- }
- }
-
- val initialOffset = state.requireOffset()
-
- // Swipe towards B, close before threshold
- state.dispatchRawDelta(initialOffset + (absThreshold * 0.9f))
- rule.waitForIdle()
-
- assertThat(state.currentValue).isEqualTo(A)
- assertThat(state.targetValue).isEqualTo(A)
-
- // Swipe towards B, close after threshold
- state.dispatchRawDelta(absThreshold * 0.2f)
- rule.waitForIdle()
-
- assertThat(state.currentValue).isEqualTo(A)
- assertThat(state.targetValue).isEqualTo(B)
-
- runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
- rule.waitForIdle()
-
- assertThat(state.currentValue).isEqualTo(B)
- assertThat(state.targetValue).isEqualTo(B)
-
- // Swipe towards A, close before threshold
- state.dispatchRawDelta(-(absThreshold * 0.9f))
- rule.waitForIdle()
-
- assertThat(state.currentValue).isEqualTo(B)
- assertThat(state.targetValue).isEqualTo(B)
-
- // Swipe towards A, close after threshold
- state.dispatchRawDelta(-(absThreshold * 0.2f))
- rule.waitForIdle()
-
- assertThat(state.currentValue).isEqualTo(B)
- assertThat(state.targetValue).isEqualTo(A)
-
- runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
- rule.waitForIdle()
-
- assertThat(state.currentValue).isEqualTo(A)
- assertThat(state.targetValue).isEqualTo(A)
- }
-
- @Test
- fun anchoredDraggable_positionalThresholds_fixed_negativeThreshold_targetState() {
- val positionalThreshold = (-56).dp
- val positionalThresholdPx = with(rule.density) { positionalThreshold.toPx() }
- val absThreshold = abs(positionalThresholdPx)
- val state = AnchoredDraggableState(
- initialValue = A,
- positionalThreshold = { positionalThresholdPx },
- velocityThreshold = DefaultVelocityThreshold,
- snapAnimationSpec = tween(),
- decayAnimationSpec = DefaultDecayAnimationSpec
- )
- rule.setContent {
- Box(Modifier.fillMaxSize()) {
- Box(
- Modifier
- .requiredSize(AnchoredDraggableBoxSize)
- .testTag(AnchoredDraggableTestTag)
- .anchoredDraggable(
- state = state,
- orientation = Orientation.Horizontal
- )
- .onSizeChanged { layoutSize ->
- val anchors = DraggableAnchors {
- A at 0f
- B at layoutSize.width / 2f
- C at layoutSize.width.toFloat()
- }
- state.updateAnchors(anchors)
- }
- .offset {
- IntOffset(
- state
- .requireOffset()
- .roundToInt(), 0
- )
- }
- .background(Color.Red)
- )
- }
- }
-
- val initialOffset = state.requireOffset()
-
- // Swipe towards B, close before threshold
- state.dispatchRawDelta(initialOffset + (absThreshold * 0.9f))
- rule.waitForIdle()
-
- assertThat(state.currentValue).isEqualTo(A)
- assertThat(state.targetValue).isEqualTo(A)
-
- // Swipe towards B, close after threshold
- state.dispatchRawDelta(absThreshold * 0.2f)
- rule.waitForIdle()
-
- assertThat(state.currentValue).isEqualTo(A)
- assertThat(state.targetValue).isEqualTo(B)
-
- runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
- rule.waitForIdle()
-
- assertThat(state.currentValue).isEqualTo(B)
- assertThat(state.targetValue).isEqualTo(B)
-
- // Swipe towards A, close before threshold
- state.dispatchRawDelta(-(absThreshold * 0.9f))
- rule.waitForIdle()
-
- assertThat(state.currentValue).isEqualTo(B)
- assertThat(state.targetValue).isEqualTo(B)
-
- // Swipe towards A, close after threshold
- state.dispatchRawDelta(-(absThreshold * 0.2f))
- rule.waitForIdle()
-
- assertThat(state.currentValue).isEqualTo(B)
- assertThat(state.targetValue).isEqualTo(A)
-
- runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
- rule.waitForIdle()
-
- assertThat(state.currentValue).isEqualTo(A)
- assertThat(state.targetValue).isEqualTo(A)
- }
-
- @Test
fun anchoredDraggable_velocityThreshold_settle_velocityHigherThanThreshold_advances() =
runBlocking(AutoTestFrameClock()) {
val velocity = 100.dp
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt
index 970617f..6a9ecc5 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableStateTest.kt
@@ -172,8 +172,8 @@
state.updateAnchors(
DraggableAnchors {
A at 0f
- B at layoutSize.width / 2f
- C at layoutSize.width.toFloat()
+ B at layoutSize.height / 2f
+ C at layoutSize.height.toFloat()
}
)
}
@@ -286,6 +286,59 @@
}
@Test
+ fun anchoredDraggable_targetState_updatedWithDeltaDispatch() {
+ val state = AnchoredDraggableState(
+ initialValue = A,
+ positionalThreshold = { it / 2f },
+ velocityThreshold = defaultVelocityThreshold,
+ snapAnimationSpec = tween(),
+ decayAnimationSpec = defaultDecayAnimationSpec,
+ anchors = DraggableAnchors {
+ A at 0f
+ B at 200f
+ C at 400f
+ }
+ )
+
+ val initialOffset = state.requireOffset()
+
+ // Swipe towards B, close before threshold
+ val aToBThreshold = abs(state.anchors.positionOf(A) - state.anchors.positionOf(B)) / 2f
+ state.dispatchRawDelta(initialOffset + (aToBThreshold * 0.9f))
+
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(A)
+
+ // Swipe towards B, close after threshold
+ state.dispatchRawDelta(aToBThreshold * 0.2f)
+
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(B)
+
+ runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(B)
+
+ // Swipe towards A, close before threshold
+ state.dispatchRawDelta(-(aToBThreshold * 0.9f))
+
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(B)
+
+ // Swipe towards A, close after threshold
+ state.dispatchRawDelta(-(aToBThreshold * 0.2f))
+
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(A)
+
+ runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
+
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(A)
+ }
+
+ @Test
fun anchoredDraggable_progress() {
rule.mainClock.autoAdvance = false
val animationDuration = 320
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/contextmenu/ContextMenuUiTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/contextmenu/ContextMenuUiTest.kt
new file mode 100644
index 0000000..4130d79
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/contextmenu/ContextMenuUiTest.kt
@@ -0,0 +1,579 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.contextmenu
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.ceilToIntPx
+import androidx.compose.foundation.text.selection.fetchTextLayoutResult
+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.testutils.assertIsEqualTo
+import androidx.compose.testutils.assertPixelColor
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toPixelMap
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.click
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performScrollToNode
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.center
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.roundToIntRect
+import androidx.compose.ui.window.PopupPositionProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlin.test.fail
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private fun IntOffset.dx(dx: Int): IntOffset = copy(x = x + dx)
+private fun IntOffset.dy(dy: Int): IntOffset = copy(y = y + dy)
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class ContextMenuUiTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val tag = "testTag"
+ private val longText = "M ".repeat(200).trimEnd()
+
+ private fun ContextMenuScope.testItem(
+ label: String = "Item",
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit = {},
+ ) {
+ item(
+ label = label,
+ modifier = modifier,
+ onClick = onClick,
+ )
+ }
+
+ // region ContextMenuItem Tests
+ @Composable
+ private fun TestItem(
+ label: String = "Item",
+ enabled: Boolean = true,
+ modifier: Modifier = Modifier.testTag(tag),
+ leadingIcon: @Composable ((iconColor: Color) -> Unit)? = null,
+ onClick: () -> Unit = {},
+ ) {
+ ContextMenuItem(
+ label = label,
+ enabled = enabled,
+ modifier = modifier,
+ leadingIcon = leadingIcon,
+ onClick = onClick
+ )
+ }
+
+ @Test
+ fun whenContextMenuItem_enabled_isClicked_onClickTriggers() {
+ var onClickCount = 0
+ rule.setContent {
+ TestItem(label = "Test") { onClickCount++ }
+ }
+
+ rule.onNodeWithTag(tag).performClick()
+ assertThat(onClickCount).isEqualTo(1)
+ }
+
+ @Test
+ fun whenContextMenuItem_disabled_isClicked_noActions() {
+ var onClickCount = 0
+ rule.setContent {
+ TestItem(label = "Test", enabled = false) { onClickCount++ }
+ }
+
+ rule.onNodeWithTag(tag).performClick()
+ assertThat(onClickCount).isEqualTo(0)
+ }
+
+ @Test
+ fun whenContextMenuItem_withMinSize_sizeIsAsExpected() {
+ rule.setContent {
+ // emulate the context menu column asking for max intrinsic width
+ Box(Modifier.width(IntrinsicSize.Max)) {
+ TestItem(label = "M")
+ }
+ }
+
+ rule.onNodeWithTag(tag).run {
+ assertHeightIsEqualTo(ContextMenuSpec.ListItemHeight)
+ assertWidthIsEqualTo(ContextMenuSpec.ContainerWidthMin)
+ }
+ }
+
+ @Test
+ fun whenContextMenuItem_withMaxSize_sizeIsAsExpected() {
+ rule.setContent {
+ // emulate the context menu column asking for max intrinsic width
+ Box(Modifier.width(IntrinsicSize.Max)) {
+ TestItem(
+ label = "M".repeat(200),
+ leadingIcon = { color ->
+ Box(
+ modifier = Modifier
+ .background(color)
+ .fillMaxSize()
+ )
+ },
+ )
+ }
+ }
+
+ rule.onNodeWithTag(tag).run {
+ assertHeightIsEqualTo(ContextMenuSpec.ListItemHeight)
+ assertWidthIsEqualTo(ContextMenuSpec.ContainerWidthMax)
+ }
+ }
+
+ @Test
+ fun whenContextMenuItem_withLeadingIconMaxSpace_iconSizeIsAsExpected() {
+ rule.setContent {
+ // emulate the context menu column asking for max intrinsic width
+ Box(Modifier.width(IntrinsicSize.Max)) {
+ TestItem(
+ modifier = Modifier,
+ leadingIcon = { color ->
+ Box(
+ modifier = Modifier
+ .testTag(tag)
+ .background(color)
+ .fillMaxSize()
+ )
+ },
+ )
+ }
+ }
+
+ val interaction = rule.onNodeWithTag(tag, useUnmergedTree = true)
+ interaction.assertWidthIsEqualTo(ContextMenuSpec.IconSize)
+ interaction.assertHeightIsEqualTo(ContextMenuSpec.IconSize)
+
+ // Assumption: The immediate parent of the tagged composable is the box with requiredSizeIn.
+ val parentSize = interaction.fetchSemanticsNode().layoutInfo.parentInfo?.run {
+ with(density) { DpSize(width.toDp(), height.toDp()) }
+ } ?: fail("Parent layout of empty box not found.")
+
+ parentSize.width.assertIsEqualTo(ContextMenuSpec.IconSize, subject = "parent.width")
+ parentSize.height.assertIsEqualTo(ContextMenuSpec.IconSize, subject = "parent.height")
+ }
+
+ @Test
+ fun whenContextMenuItem_withLeadingIconMinSize_iconSizeIsAsExpected() {
+ rule.setContent {
+ // emulate the context menu column asking for max intrinsic width
+ Box(Modifier.width(IntrinsicSize.Max)) {
+ TestItem(modifier = Modifier, leadingIcon = { Spacer(Modifier.testTag(tag)) })
+ }
+ }
+
+ val interaction = rule.onNodeWithTag(tag, useUnmergedTree = true)
+ interaction.assertWidthIsEqualTo(0.dp)
+ interaction.assertHeightIsEqualTo(0.dp)
+
+ // Assumption: The immediate parent of the tagged composable is the box with requiredSizeIn.
+ val parentSize = interaction.fetchSemanticsNode().layoutInfo.parentInfo?.run {
+ with(density) { DpSize(width.toDp(), height.toDp()) }
+ } ?: fail("Parent layout of empty box not found.")
+
+ parentSize.width.assertIsEqualTo(ContextMenuSpec.IconSize, subject = "parent.width")
+ parentSize.height.assertIsEqualTo(0.dp, subject = "parent.height")
+ }
+
+ @Test
+ fun whenContextMenuItem_enabled_semanticsIsCorrect() {
+ var onClickCount = 0
+ val label = "Test"
+ rule.setContent {
+ TestItem(label = label) { onClickCount++ }
+ }
+
+ rule.onNodeWithTag(tag).apply {
+ assertHasClickAction()
+ val node = fetchSemanticsNode("No Semantics Node found for ContextMenuItem")
+
+ val accessibilityAction = node.config[SemanticsActions.OnClick]
+ assertThat(accessibilityAction.label).isEqualTo(label)
+
+ val action = accessibilityAction.action
+ assertThat(action).isNotNull()
+ assertThat(onClickCount).isEqualTo(0)
+ action!!.invoke()
+ assertThat(onClickCount).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun whenContextMenuItem_disabled_semanticsIsCorrect() {
+ var onClickCount = 0
+ val label = "Test"
+ rule.setContent {
+ TestItem(label = label, enabled = false) { onClickCount++ }
+ }
+
+ rule.onNodeWithTag(tag).apply {
+ assertHasClickAction()
+ val node = fetchSemanticsNode("No Semantics Node found for ContextMenuItem")
+
+ val accessibilityAction = node.config[SemanticsActions.OnClick]
+ assertThat(accessibilityAction.label).isEqualTo(label)
+
+ val action = accessibilityAction.action
+ assertThat(action).isNotNull()
+ assertThat(onClickCount).isEqualTo(0)
+ action!!.invoke()
+ assertThat(onClickCount).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun whenContextMenuItem_withLongLabel_doesNotWrap() {
+ rule.setContent { TestItem(label = longText) }
+
+ val textLayoutResult = rule.onNode(hasText(longText), useUnmergedTree = true)
+ .fetchTextLayoutResult()
+
+ assertThat(textLayoutResult.lineCount).isEqualTo(1)
+ }
+
+ @Test
+ fun whenContextMenuItem_enabled_correctTextStyling() {
+ rule.setContent { TestItem(label = longText) }
+
+ val textNode = rule.onNode(hasText(longText), useUnmergedTree = true)
+ textNode.assertExists("Text does not exist.")
+
+ val textStyle = textNode.fetchTextLayoutResult().layoutInput.style
+ assertThat(textStyle.color).isEqualTo(ContextMenuSpec.TextColor)
+ assertThat(textStyle.textAlign).isEqualTo(ContextMenuSpec.LabelHorizontalTextAlignment)
+ }
+
+ @Test
+ fun whenContextMenuItem_disabled_correctTextStyling() {
+ rule.setContent { TestItem(label = longText, enabled = false) }
+
+ val textNode = rule.onNode(hasText(longText), useUnmergedTree = true)
+ textNode.assertExists("Text does not exist.")
+
+ val textStyle = textNode.fetchTextLayoutResult().layoutInput.style
+ assertThat(textStyle.color).isEqualTo(ContextMenuSpec.DisabledColor)
+ assertThat(textStyle.textAlign).isEqualTo(ContextMenuSpec.LabelHorizontalTextAlignment)
+ }
+
+ @Test
+ fun whenContextMenuItem_enabled_correctIconColor() {
+ var iconColor: Color? = null
+ rule.setContent { TestItem(label = longText, leadingIcon = { iconColor = it }) }
+ assertThat(iconColor).isEqualTo(ContextMenuSpec.IconColor)
+ }
+
+ @Test
+ fun whenContextMenuItem_disabled_correctIconColor() {
+ var iconColor: Color? = null
+ rule.setContent {
+ TestItem(label = longText, enabled = false, leadingIcon = { iconColor = it })
+ }
+ assertThat(iconColor).isEqualTo(ContextMenuSpec.DisabledColor)
+ }
+ // endregion ContextMenuItem Tests
+
+ // region ContextMenuColumn Tests
+ @Composable
+ private fun TestColumn(
+ contextMenuBuilderBlock: ContextMenuScope.() -> Unit,
+ ) {
+ ContextMenuColumn(Modifier.testTag(tag)) {
+ val scope = remember { ContextMenuScope() }
+ with(scope) {
+ clear()
+ contextMenuBuilderBlock()
+ Content()
+ }
+ }
+ }
+
+ @Test
+ fun whenContextMenuColumn_usedNormally_allInputItemsAreRendered() {
+ val labelFunction: (Int) -> String = { "Item $it" }
+ rule.setContent {
+ TestColumn {
+ repeat(5) { testItem(label = labelFunction(it)) }
+ }
+ }
+
+ rule.onNodeWithTag(tag).assertIsDisplayed()
+ repeat(5) { rule.onNode(hasText(labelFunction(it))).assertExists() }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun whenContextMenuColumn_usedNormally_backgroundColorIsAsExpected() {
+ rule.setContent {
+ TestColumn {
+ // small label to keep text out of the way
+ testItem(
+ label = "Item",
+ modifier = Modifier.drawWithContent {
+ // Don't draw content, only the column will draw.
+ // Layout will still fill space.
+ }
+ )
+ }
+ }
+
+ val node = rule.onNodeWithTag(tag)
+ val bitmap = node.captureToImage()
+ val density = node.fetchSemanticsNode().layoutInfo.density
+
+ // Ignore some padding around the edges where the shadow/rounded corners are.
+ val padding = with(density) { ContextMenuSpec.CornerRadius.toPx().ceilToIntPx() }
+ val pixelMap = bitmap.toPixelMap(
+ startX = padding,
+ startY = padding,
+ width = bitmap.width - padding * 2,
+ height = bitmap.height - padding * 2,
+ )
+
+ for (x in 0 until pixelMap.width) {
+ for (y in 0 until pixelMap.height) {
+ pixelMap.assertPixelColor(ContextMenuSpec.BackgroundColor, x, y)
+ }
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun whenContextMenuColumn_usedNormally_hasExpectedShadow() {
+ val containerTag = "containerTag"
+ rule.setContent {
+ Box(
+ modifier = Modifier
+ .testTag(containerTag)
+ .padding(ContextMenuSpec.MenuContainerElevation)
+ ) {
+ TestColumn {
+ // small label to keep text out of the way
+ testItem(label = ".")
+ }
+ }
+ }
+
+ val outerNode = rule.onNodeWithTag(containerTag)
+ val pixelMap = outerNode.captureToImage().toPixelMap()
+ val outerRect = outerNode.fetchSemanticsNode().boundsInRoot
+
+ val innerRect = rule.onNodeWithTag(tag).fetchSemanticsNode().boundsInRoot
+
+ val columnBoundsInParent = innerRect.translate(-outerRect.topLeft).roundToIntRect()
+
+ // Verify that the center of each side in the shadow is not the background color.
+ // Check one pixel outwards from the column's bounds.
+ fun assertShadowAt(offset: IntOffset) {
+ val (x, y) = offset
+ val pixelColor = pixelMap[x, y]
+ val message = "Expected pixel at [$x, $y] to not be ${ContextMenuSpec.BackgroundColor}."
+ assertWithMessage(message)
+ .that(pixelColor).isNotEqualTo(ContextMenuSpec.BackgroundColor)
+ }
+
+ assertShadowAt(columnBoundsInParent.centerLeft.dx(-1))
+ assertShadowAt(columnBoundsInParent.topCenter.dy(-1))
+ assertShadowAt(columnBoundsInParent.centerRight.dx(1))
+ assertShadowAt(columnBoundsInParent.bottomCenter.dy(1))
+ }
+
+ @Test
+ fun whenContextMenuColumn_allInputItemsAreRendered() {
+ val labelFunction: (Int) -> String = { "Item $it" }
+ rule.setContent {
+ TestColumn {
+ repeat(5) {
+ testItem(label = labelFunction(it))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(tag).assertIsDisplayed()
+ repeat(5) { rule.onNode(hasText(labelFunction(it))).assertExists() }
+ }
+
+ @Test
+ fun whenContextMenuColumn_5ItemsMaxWidth_sizeIsAsExpected() {
+ rule.setContent {
+ TestColumn {
+ repeat(5) {
+ testItem(label = "Item ${it.toString().repeat(100)}")
+ }
+ }
+ }
+
+ rule.onNodeWithTag(tag).run {
+ // 5 items and the vertical padding.
+ assertHeightIsEqualTo(
+ ContextMenuSpec.ListItemHeight * 5 + ContextMenuSpec.VerticalPadding * 2
+ )
+ assertWidthIsEqualTo(ContextMenuSpec.ContainerWidthMax)
+ }
+ }
+
+ @Test
+ fun whenContextMenuColumn_1ItemMinWidth_sizeIsAsExpected() {
+ rule.setContent {
+ TestColumn { testItem() }
+ }
+
+ rule.onNodeWithTag(tag).run {
+ // 1 item and the vertical padding.
+ assertHeightIsEqualTo(
+ ContextMenuSpec.ListItemHeight + ContextMenuSpec.VerticalPadding * 2
+ )
+ assertWidthIsEqualTo(ContextMenuSpec.ContainerWidthMin)
+ }
+ }
+
+ @Test
+ fun whenContextMenuColumn_with100Items_scrolls() {
+ rule.setContent {
+ TestColumn {
+ repeat(100) {
+ testItem(label = "Item ${it + 1}")
+ }
+ }
+ }
+
+ val firstItemMatcher = hasText("Item 1")
+ val lastItemMatcher = hasText("Item 100")
+
+ rule.onNode(firstItemMatcher).assertIsDisplayed()
+ rule.onNode(lastItemMatcher).assertIsNotDisplayed()
+
+ rule.onNodeWithTag(tag).performScrollToNode(lastItemMatcher)
+
+ rule.onNode(firstItemMatcher).assertIsNotDisplayed()
+ rule.onNode(lastItemMatcher).assertIsDisplayed()
+ }
+
+ // Items may have varying text sizes. We want to ensure the clickable portion of
+ // the Row extends all the way to the end of the Column horizontally.
+ @Test
+ fun whenContextMenuColumn_withItemsOfDifferentWidths_clickBoxCoversEndOfItems() {
+ val size = 5
+ val clickCounts = IntArray(size)
+ rule.setContent {
+ TestColumn {
+ repeat(5) {
+ val suffix = "Item ".repeat(it).trimEnd()
+ testItem(label = "Item $suffix") { clickCounts[it]++ }
+ }
+ }
+ }
+
+ val listItemCenterBaselineOffset = ContextMenuSpec.ListItemHeight * 0.5f
+ rule.onNodeWithTag(tag).performTouchInput {
+ val x = right - 1f
+ with(rule.density) {
+ repeat(size) {
+ val y = ContextMenuSpec.ListItemHeight * it + listItemCenterBaselineOffset
+ click(Offset(x, y.toPx()))
+ }
+ }
+ }
+
+ assertWithMessage("Each item should have been clicked once.")
+ .that(clickCounts)
+ .asList()
+ .containsExactly(1, 1, 1, 1, 1)
+ }
+ // endregion ContextMenuColumn Tests
+
+ // region ContextMenuPopup Tests
+ private val centeringPopupPositionProvider = object : PopupPositionProvider {
+ override fun calculatePosition(
+ anchorBounds: IntRect,
+ windowSize: IntSize,
+ layoutDirection: LayoutDirection,
+ popupContentSize: IntSize
+ ): IntOffset = windowSize.center - popupContentSize.center
+ }
+
+ @Test
+ fun whenContextMenuPopup_removedAndReAdded_appearsAsExpected() {
+ var show by mutableStateOf(false)
+ val itemTag = "itemTag"
+ rule.setContent {
+ if (show) {
+ ContextMenuPopup(
+ modifier = Modifier.testTag(tag),
+ popupPositionProvider = centeringPopupPositionProvider,
+ onDismiss = {},
+ ) {
+ testItem(label = "Item", modifier = Modifier.testTag(itemTag))
+ }
+ }
+ }
+
+ repeat(2) {
+ show = false
+ rule.waitForIdle()
+
+ rule.onNodeWithTag(tag).assertDoesNotExist()
+ rule.onNodeWithTag(itemTag).assertDoesNotExist()
+
+ show = true
+ rule.waitForIdle()
+
+ rule.onNodeWithTag(tag).assertExists()
+ rule.onNodeWithTag(itemTag).assertExists()
+ }
+ }
+ // endregion ContextMenuPopup Tests
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerStateNonGestureScrollingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerStateNonGestureScrollingTest.kt
index 6782093..b39ac82 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerStateNonGestureScrollingTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerStateNonGestureScrollingTest.kt
@@ -240,17 +240,17 @@
}
@Test
- fun calculatePageCountOffset_shouldBeBasedOnCurrentPage() {
+ fun getOffsetDistanceInPages_shouldBeBasedOnCurrentPage() {
val pageToOffsetCalculations = mutableMapOf<Int, Float>()
createPager(modifier = Modifier.fillMaxSize(), pageSize = { PageSize.Fixed(20.dp) }) {
- pageToOffsetCalculations[it] = pagerState.getOffsetFractionForPage(it)
+ pageToOffsetCalculations[it] = pagerState.getOffsetDistanceInPages(it)
Page(index = it)
}
for ((page, offset) in pageToOffsetCalculations) {
val currentPage = pagerState.currentPage
val currentPageOffset = pagerState.currentPageOffsetFraction
- Truth.assertThat(offset).isEqualTo((currentPage - page) + currentPageOffset)
+ Truth.assertThat(offset).isEqualTo((page - currentPage) - currentPageOffset)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicSecureTextFieldTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicSecureTextFieldTest.kt
index 22bb8a7..eb0228a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicSecureTextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicSecureTextFieldTest.kt
@@ -37,7 +37,6 @@
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
@@ -55,7 +54,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -305,6 +303,102 @@
}
}
+ @Test
+ fun obfuscationMethodHidden_usesCustomObfuscationCharacter() {
+ inputMethodInterceptor.setContent {
+ BasicSecureTextField(
+ state = rememberTextFieldState(),
+ textObfuscationMode = TextObfuscationMode.Hidden,
+ textObfuscationCharacter = '&',
+ modifier = Modifier.testTag(Tag)
+ )
+ }
+
+ with(rule.onNodeWithTag(Tag)) {
+ performTextInput("abc")
+ rule.mainClock.advanceTimeByFrame()
+ assertThat(fetchTextLayoutResult().layoutInput.text.text)
+ .isEqualTo("&&&")
+ performTextInput("d")
+ rule.mainClock.advanceTimeByFrame()
+ assertThat(fetchTextLayoutResult().layoutInput.text.text)
+ .isEqualTo("&&&&")
+ }
+ }
+
+ @Test
+ fun obfuscationMethodRevealLastTyped_usesCustomObfuscationCharacter() {
+ inputMethodInterceptor.setContent {
+ BasicSecureTextField(
+ state = rememberTextFieldState(),
+ textObfuscationMode = TextObfuscationMode.RevealLastTyped,
+ textObfuscationCharacter = '&',
+ modifier = Modifier.testTag(Tag)
+ )
+ }
+
+ with(rule.onNodeWithTag(Tag)) {
+ performTextInput("a")
+ rule.mainClock.advanceTimeBy(200)
+ assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("a")
+ rule.mainClock.advanceTimeBy(1500)
+ assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("&")
+ }
+ }
+
+ @Test
+ fun obfuscationMethodHidden_toggleObfuscationCharacter() {
+ var character by mutableStateOf('*')
+ inputMethodInterceptor.setContent {
+ BasicSecureTextField(
+ state = rememberTextFieldState(),
+ textObfuscationMode = TextObfuscationMode.Hidden,
+ textObfuscationCharacter = character,
+ modifier = Modifier.testTag(Tag)
+ )
+ }
+
+ with(rule.onNodeWithTag(Tag)) {
+ performTextInput("abc")
+ rule.mainClock.advanceTimeByFrame()
+ assertThat(fetchTextLayoutResult().layoutInput.text.text)
+ .isEqualTo("***")
+ character = '&'
+ rule.mainClock.advanceTimeByFrame()
+ assertThat(fetchTextLayoutResult().layoutInput.text.text)
+ .isEqualTo("&&&")
+ }
+ }
+
+ @Test
+ fun obfuscationMethodRevealLastTyped_toggleObfuscationCharacter() {
+ var character by mutableStateOf('*')
+ inputMethodInterceptor.setContent {
+ BasicSecureTextField(
+ state = rememberTextFieldState(),
+ textObfuscationMode = TextObfuscationMode.RevealLastTyped,
+ textObfuscationCharacter = character,
+ modifier = Modifier.testTag(Tag)
+ )
+ }
+
+ with(rule.onNodeWithTag(Tag)) {
+ performTextInput("a")
+ rule.mainClock.advanceTimeBy(200)
+ assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("a")
+ rule.mainClock.advanceTimeBy(1500)
+ assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("*")
+
+ character = '&'
+
+ performTextInput("b")
+ rule.mainClock.advanceTimeBy(200)
+ assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("&b")
+ rule.mainClock.advanceTimeBy(1500)
+ assertThat(fetchTextLayoutResult().layoutInput.text.text).isEqualTo("&&")
+ }
+ }
+
@OptIn(ExperimentalTestApi::class)
@Test
fun semantics_copy() {
@@ -411,14 +505,4 @@
imm.expectNoMoreCalls()
}
}
-
- private fun requestFocus(tag: String) =
- rule.onNodeWithTag(tag).requestFocus()
-
- private fun assertTextSelection(expected: TextRange) {
- val selection = rule.onNodeWithTag(Tag).fetchSemanticsNode()
- .config.getOrNull(SemanticsProperties.TextSelectionRange)
- assertWithMessage("Expected selection to be $expected")
- .that(selection).isEqualTo(expected)
- }
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.android.kt
index 8347e5c..c1cc0fc 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/AndroidOverscroll.android.kt
@@ -495,7 +495,7 @@
val leftForOverscroll = leftForDelta - consumedByDelta
var needsInvalidation = false
- if (source == NestedScrollSource.Drag) {
+ if (source == NestedScrollSource.UserInput) {
// Ignore small deltas (< 0.5) as this usually comes from floating point rounding issues
// and can cause scrolling to lock up (b/265363356)
val appliedHorizontalOverscroll = if (leftForOverscroll.x > 0.5f) {
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/contextmenu/ContextMenuUi.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/contextmenu/ContextMenuUi.android.kt
new file mode 100644
index 0000000..18896826
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/contextmenu/ContextMenuUi.android.kt
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.contextmenu
+
+import android.annotation.SuppressLint
+import androidx.annotation.VisibleForTesting
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSizeIn
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupPositionProvider
+import androidx.compose.ui.window.PopupProperties
+
+private const val DisabledAlpha = 0.38f
+private const val IconAlpha = 0.6f
+
+@VisibleForTesting
+internal object ContextMenuSpec {
+ // TODO(b/331955999) Determine colors/theming
+ private val PrimaryColor = Color.Black
+ val BackgroundColor = Color.White
+
+ val TextColor = PrimaryColor
+ val IconColor = PrimaryColor.copy(alpha = IconAlpha)
+ val DisabledColor = PrimaryColor.copy(DisabledAlpha)
+
+ // Layout constants from https://m3.material.io/components/menus/specs
+ val ContainerWidthMin = 112.dp
+ val ContainerWidthMax = 280.dp
+ val ListItemHeight = 48.dp
+ val MenuContainerElevation = 3.dp
+ val CornerRadius = 4.dp
+ val LabelVerticalTextAlignment = Alignment.CenterVertically
+ val LabelHorizontalTextAlignment = TextAlign.Start
+ val HorizontalPadding = 12.dp // left/right of column and between elements in rows
+ val VerticalPadding = 8.dp // top/bottom of column and around dividers
+ val IconSize = 24.dp
+}
+
+private val DefaultPopupProperties = PopupProperties(focusable = true)
+
+@Composable
+internal fun ContextMenuPopup(
+ popupPositionProvider: PopupPositionProvider,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+ contextMenuBuilderBlock: ContextMenuScope.() -> Unit,
+) {
+ Popup(
+ popupPositionProvider = popupPositionProvider,
+ onDismissRequest = onDismiss,
+ properties = DefaultPopupProperties,
+ ) {
+ ContextMenuColumn(modifier) {
+ val scope = remember { ContextMenuScope() }
+ with(scope) {
+ clear()
+ contextMenuBuilderBlock()
+ Content()
+ }
+ }
+ }
+}
+
+@VisibleForTesting
+@Composable
+internal fun ContextMenuColumn(
+ modifier: Modifier = Modifier,
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ Column(
+ modifier = modifier
+ .shadow(
+ ContextMenuSpec.MenuContainerElevation,
+ RoundedCornerShape(ContextMenuSpec.CornerRadius)
+ )
+ .background(ContextMenuSpec.BackgroundColor)
+ .width(IntrinsicSize.Max)
+ .padding(vertical = ContextMenuSpec.VerticalPadding)
+ .verticalScroll(rememberScrollState()),
+ content = content,
+ )
+}
+
+// Very similar to M3 DropdownMenuItemContent
+@SuppressLint("ComposableLambdaParameterPosition")
+@VisibleForTesting
+@Composable
+internal fun ContextMenuItem(
+ label: String,
+ enabled: Boolean,
+ modifier: Modifier = Modifier,
+ /**
+ * Icon to place in front of the label. If null, the icon will not be rendered
+ * and the text will instead be further towards the start. The `iconColor` will
+ * change based on whether the item is disabled or not.
+ */
+ leadingIcon: @Composable ((iconColor: Color) -> Unit)? = null,
+ /**
+ * Lambda called when this item is clicked.
+ *
+ * Note: If you want the context menu to close when this item is clicked,
+ * you will have to do it in this lambda.
+ */
+ // TODO(b/331690843) add how to do this to the kdoc above.
+ onClick: () -> Unit,
+) {
+ Row(
+ verticalAlignment = ContextMenuSpec.LabelVerticalTextAlignment,
+ horizontalArrangement = Arrangement.spacedBy(ContextMenuSpec.HorizontalPadding),
+ modifier = modifier
+ .clickable(
+ enabled = enabled,
+ onClickLabel = label,
+ ) {
+ // Semantics can call this even if it is disabled (at least in tests),
+ // so check enabled status again before invoking any callbacks.
+ if (enabled) onClick()
+ }
+ .fillMaxWidth()
+ .sizeIn(
+ minWidth = ContextMenuSpec.ContainerWidthMin,
+ maxWidth = ContextMenuSpec.ContainerWidthMax,
+ minHeight = ContextMenuSpec.ListItemHeight,
+ maxHeight = ContextMenuSpec.ListItemHeight,
+ )
+ .padding(horizontal = ContextMenuSpec.HorizontalPadding)
+ ) {
+ leadingIcon?.let { icon ->
+ Box(
+ modifier = Modifier.requiredSizeIn(
+ minWidth = ContextMenuSpec.IconSize,
+ maxWidth = ContextMenuSpec.IconSize,
+ maxHeight = ContextMenuSpec.IconSize,
+ )
+ ) { icon(if (enabled) ContextMenuSpec.IconColor else ContextMenuSpec.DisabledColor) }
+ }
+ BasicText(
+ text = label,
+ style = TextStyle(
+ color = if (enabled) ContextMenuSpec.TextColor else ContextMenuSpec.DisabledColor,
+ textAlign = ContextMenuSpec.LabelHorizontalTextAlignment,
+ ),
+ maxLines = 1,
+ modifier = Modifier.weight(1f, fill = true)
+ )
+ }
+}
+
+/**
+ * Scope used to add components to a context menu.
+ */
+// We cannot expose a @Composable in the context menu API because we don't want folks adding
+// arbitrary composables into a context menu. Instead, we expose this API which then maps to
+// context menu composables.
+internal class ContextMenuScope internal constructor() {
+ private val composables = mutableListOf<@Composable () -> Unit>()
+
+ @Composable
+ internal fun Content() {
+ composables.fastForEach { composable -> composable() }
+ }
+
+ internal fun clear() {
+ composables.clear()
+ }
+
+ /**
+ * Adds an item to the context menu list.
+ *
+ * @param label string to display in the text of the item.
+ * @param modifier [Modifier] to apply to the item.
+ * @param enabled whether or not the item should be enabled.
+ * This affects whether the item is clickable, has a hover indication, and the text styling.
+ * @param leadingIcon Composable to put in the leading icon position.
+ * The color is the color to draw with and will change based on if the item is enabled or not.
+ * This will measured with a required width of `24.dp` and a required maxHeight of `24.dp`.
+ * The result will be centered vertically in the row.
+ * @param onClick Action to perform on the item being clicked.
+ * Returns whether or not the context menu should be dismissed.
+ */
+ fun item(
+ label: String,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ /**
+ * Icon to place in front of the label. If null, the icon will not be rendered
+ * and the text will instead be further towards the start. The `iconColor` will
+ * change based on whether the item is disabled or not.
+ */
+ leadingIcon: @Composable ((iconColor: Color) -> Unit)? = null,
+ /**
+ * Lambda called when this item is clicked.
+ *
+ * Note: If you want the context menu to close when this item is clicked,
+ * you will have to do it in this lambda.
+ */
+ // TODO(b/331690843) add how to do this to the kdoc above.
+ onClick: () -> Unit,
+ ) {
+ check(label.isNotBlank()) { "Label must not be blank" }
+ composables += {
+ ContextMenuItem(
+ modifier = modifier,
+ label = label,
+ enabled = enabled,
+ leadingIcon = leadingIcon,
+ onClick = onClick
+ )
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.android.kt
new file mode 100644
index 0000000..482a642
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.android.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.gestures
+
+import android.content.pm.PackageManager.FEATURE_LEANBACK
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.BringIntoViewSpec.Companion.DefaultBringIntoViewSpec
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.compositionLocalWithComputedDefaultOf
+import androidx.compose.ui.platform.LocalContext
+import kotlin.math.abs
+
+/**
+ * A composition local to customize the focus scrolling behavior used by some scrollable containers.
+ * [LocalBringIntoViewSpec] has a platform defined behavior. If the App is running on a TV device,
+ * the scroll behavior will pivot around 30% of the container size. For other platforms, the scroll
+ * behavior will move the least to bring the requested region into view.
+ */
+@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:ExperimentalFoundationApi
+@ExperimentalFoundationApi
+actual val LocalBringIntoViewSpec: ProvidableCompositionLocal<BringIntoViewSpec> =
+ compositionLocalWithComputedDefaultOf {
+ val hasTvFeature =
+ LocalContext.currentValue.packageManager.hasSystemFeature(FEATURE_LEANBACK)
+ if (!hasTvFeature) {
+ DefaultBringIntoViewSpec
+ } else {
+ PivotBringIntoViewSpec
+ }
+ }
+
+@OptIn(ExperimentalFoundationApi::class)
+internal val PivotBringIntoViewSpec = object : BringIntoViewSpec {
+ val parentFraction = 0.3f
+ val childFraction = 0f
+ override val scrollAnimationSpec: AnimationSpec<Float> = tween<Float>(
+ durationMillis = 125,
+ easing = CubicBezierEasing(0.25f, 0.1f, .25f, 1f)
+ )
+
+ override fun calculateScrollDistance(
+ offset: Float,
+ size: Float,
+ containerSize: Float
+ ): Float {
+ val leadingEdgeOfItemRequestingFocus = offset
+ val trailingEdgeOfItemRequestingFocus = offset + size
+
+ val sizeOfItemRequestingFocus =
+ abs(trailingEdgeOfItemRequestingFocus - leadingEdgeOfItemRequestingFocus)
+ val childSmallerThanParent = sizeOfItemRequestingFocus <= containerSize
+ val initialTargetForLeadingEdge =
+ parentFraction * containerSize -
+ (childFraction * sizeOfItemRequestingFocus)
+ val spaceAvailableToShowItem = containerSize - initialTargetForLeadingEdge
+
+ val targetForLeadingEdge =
+ if (childSmallerThanParent &&
+ spaceAvailableToShowItem < sizeOfItemRequestingFocus
+ ) {
+ containerSize - sizeOfItemRequestingFocus
+ } else {
+ initialTargetForLeadingEdge
+ }
+
+ return leadingEdgeOfItemRequestingFocus - targetForLeadingEdge
+ }
+}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.android.kt
index 700bbca..2a44b69 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.android.kt
@@ -16,11 +16,8 @@
package androidx.compose.foundation.text.input.internal
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.text.input.TextFieldCharSequence
-import androidx.compose.foundation.text.input.toCharArray
-@OptIn(ExperimentalFoundationApi::class)
internal actual fun CharSequence.toCharArray(
destination: CharArray,
destinationOffset: Int,
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/InputTransformationTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/InputTransformationTest.kt
index 551712a..d517272 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/InputTransformationTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/InputTransformationTest.kt
@@ -22,6 +22,9 @@
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.maxTextLength
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -146,6 +149,40 @@
}
@Test
+ fun chainedFilters_mergeKeyboardOptions_withPrecedenceToNext() {
+ val options1 = KeyboardOptions(
+ keyboardType = KeyboardType.Password,
+ capitalization = KeyboardCapitalization.Sentences
+ )
+ val options2 = KeyboardOptions(
+ keyboardType = KeyboardType.Email,
+ imeAction = ImeAction.Search
+ )
+ val filter1 = object : InputTransformation {
+ override val keyboardOptions = options1
+
+ override fun TextFieldBuffer.transformInput() {
+ }
+ }
+ val filter2 = object : InputTransformation {
+ override val keyboardOptions = options2
+
+ override fun TextFieldBuffer.transformInput() {
+ }
+ }
+
+ val chain = filter1.then(filter2)
+
+ assertThat(chain.keyboardOptions).isEqualTo(
+ KeyboardOptions(
+ keyboardType = KeyboardType.Email,
+ capitalization = KeyboardCapitalization.Sentences,
+ imeAction = ImeAction.Search
+ )
+ )
+ }
+
+ @Test
fun chainedFilters_applySecondSemantics_afterFirstSemantics() {
val filter1 = object : InputTransformation {
override fun SemanticsPropertyReceiver.applySemantics() {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ScrollingContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ScrollingContainer.kt
index 0504ecc..244dc7a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ScrollingContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ScrollingContainer.kt
@@ -37,9 +37,10 @@
reverseScrolling: Boolean,
flingBehavior: FlingBehavior?,
interactionSource: MutableInteractionSource?,
- bringIntoViewSpec: BringIntoViewSpec = ScrollableDefaults.bringIntoViewSpec()
+ bringIntoViewSpec: BringIntoViewSpec? = null
): Modifier {
val overscrollEffect = ScrollableDefaults.overscrollEffect()
+
return clipScrollableContainer(orientation)
.overscroll(overscrollEffect)
.scrollable(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index 9adcd08..d31eb71 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -205,7 +205,7 @@
} else {
overscrollEffect!!.applyToScroll(
delta = dragDelta.delta.reverseIfNeeded(),
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
) { deltaForDrag ->
val dragOffset = state.newOffsetForDelta(deltaForDrag.toFloat())
val consumedDelta = (dragOffset - state.requireOffset()).toOffset()
@@ -670,15 +670,14 @@
private set
/**
- * The target value. This is the closest value to the current offset, taking into account
- * positional thresholds. If no interactions like animations or drags are in progress, this
- * will be the current value.
+ * The target value. This is the closest value to the current offset. If no interactions like
+ * animations or drags are in progress, this will be the current value.
*/
val targetValue: T by derivedStateOf {
dragTarget ?: run {
val currentOffset = offset
if (!currentOffset.isNaN()) {
- computeTarget(currentOffset, currentValue, velocity = 0f)
+ anchors.closestAnchor(offset) ?: currentValue
} else currentValue
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.kt
new file mode 100644
index 0000000..f030e6a
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.gestures
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Modifier
+import kotlin.math.abs
+
+/**
+ * A composition local to customize the focus scrolling behavior used by some scrollable containers.
+ * [LocalBringIntoViewSpec] has a platform defined default behavior.
+ */
+@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:ExperimentalFoundationApi
+@ExperimentalFoundationApi
+expect val LocalBringIntoViewSpec: ProvidableCompositionLocal<BringIntoViewSpec>
+
+/**
+ * The configuration of how a scrollable reacts to bring into view requests.
+ *
+ * Note: API shape and naming are still being refined, therefore API is marked as experimental.
+ *
+ * Check the following sample for a use case usage of this API:
+ * @sample androidx.compose.foundation.samples.FocusScrollingInLazyRowSample
+ */
+@ExperimentalFoundationApi
+@Stable
+interface BringIntoViewSpec {
+
+ /**
+ * An Animation Spec to be used as the animation to run to fulfill the BringIntoView requests.
+ */
+ val scrollAnimationSpec: AnimationSpec<Float> get() = DefaultScrollAnimationSpec
+
+ /**
+ * Calculate the offset needed to bring one of the scrollable container's child into view.
+ * This will be called for every frame of the scrolling animation. This means that, as the
+ * animation progresses, the offset will naturally change to fulfill the scroll request.
+ *
+ * All distances below are represented in pixels.
+ * @param offset from the side closest to the start of the container.
+ * @param size is the child size.
+ * @param containerSize Is the main axis size of the scrollable container.
+ *
+ * @return The necessary amount to scroll to satisfy the bring into view request.
+ * Returning zero from here means that the request was satisfied and the scrolling animation
+ * should stop.
+ */
+ fun calculateScrollDistance(
+ offset: Float,
+ size: Float,
+ containerSize: Float
+ ): Float = defaultCalculateScrollDistance(offset, size, containerSize)
+
+ companion object {
+
+ /**
+ * The default animation spec used by [Modifier.scrollable] to run Bring Into View requests.
+ */
+ val DefaultScrollAnimationSpec: AnimationSpec<Float> = spring()
+
+ internal val DefaultBringIntoViewSpec = object : BringIntoViewSpec {}
+
+ internal fun defaultCalculateScrollDistance(
+ offset: Float,
+ size: Float,
+ containerSize: Float
+ ): Float {
+ val trailingEdge = offset + size
+ @Suppress("UnnecessaryVariable") val leadingEdge = offset
+ return when {
+
+ // If the item is already visible, no need to scroll.
+ leadingEdge >= 0 && trailingEdge <= containerSize -> 0f
+
+ // If the item is visible but larger than the parent, we don't scroll.
+ leadingEdge < 0 && trailingEdge > containerSize -> 0f
+
+ // Find the minimum scroll needed to make one of the edges coincide with the parent's
+ // edge.
+ abs(leadingEdge) < abs(trailingEdge - containerSize) -> leadingEdge
+ else -> trailingEdge - containerSize
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt
index 9976dc1..18ce61c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt
@@ -26,7 +26,9 @@
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.LayoutAwareModifierNode
+import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.node.requireLayoutCoordinates
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toSize
@@ -65,8 +67,9 @@
private var orientation: Orientation,
private var scrollState: ScrollableState,
private var reverseDirection: Boolean,
- private var bringIntoViewSpec: BringIntoViewSpec
-) : Modifier.Node(), BringIntoViewResponder, LayoutAwareModifierNode {
+ private var bringIntoViewSpec: BringIntoViewSpec?
+) : Modifier.Node(), BringIntoViewResponder, LayoutAwareModifierNode,
+ CompositionLocalConsumerModifierNode {
/**
* Ongoing requests from [bringChildIntoView], with the invariant that it is always sorted by
@@ -111,6 +114,10 @@
return computeDestination(localRect, viewportSize)
}
+ private fun requireBringIntoViewSpec(): BringIntoViewSpec {
+ return bringIntoViewSpec ?: currentValueOf(LocalBringIntoViewSpec)
+ }
+
override suspend fun bringChildIntoView(localRect: () -> Rect?) {
// Avoid creating no-op requests and no-op animations if the request does not require
// scrolling or returns null.
@@ -171,6 +178,7 @@
}
private fun launchAnimation() {
+ val bringIntoViewSpec = requireBringIntoViewSpec()
check(!isAnimationRunning) { "launchAnimation called when previous animation was running" }
if (DEBUG) println("[$TAG] launchAnimation")
@@ -182,7 +190,7 @@
try {
isAnimationRunning = true
scrollState.scroll {
- animationState.value = calculateScrollDelta()
+ animationState.value = calculateScrollDelta(bringIntoViewSpec)
if (DEBUG) println(
"[$TAG] Starting scroll animation down from ${animationState.value}…"
)
@@ -247,7 +255,7 @@
// Compute a new scroll target taking into account any resizes,
// replacements, or added/removed requests since the last frame.
- animationState.value = calculateScrollDelta()
+ animationState.value = calculateScrollDelta(bringIntoViewSpec)
if (DEBUG) println(
"[$TAG] scroll target after frame: ${animationState.value}"
)
@@ -286,7 +294,7 @@
* Calculates how far we need to scroll to satisfy all existing BringIntoView requests and the
* focused child tracking.
*/
- private fun calculateScrollDelta(): Float {
+ private fun calculateScrollDelta(bringIntoViewSpec: BringIntoViewSpec): Float {
if (viewportSize == IntSize.Zero) return 0f
val rectangleToMakeVisible: Rect = findBringIntoViewRequest()
@@ -358,7 +366,7 @@
return when (orientation) {
Vertical -> Offset(
x = 0f,
- y = bringIntoViewSpec.calculateScrollDistance(
+ y = requireBringIntoViewSpec().calculateScrollDistance(
childBounds.top,
childBounds.bottom - childBounds.top,
size.height
@@ -366,7 +374,7 @@
)
Horizontal -> Offset(
- x = bringIntoViewSpec.calculateScrollDistance(
+ x = requireBringIntoViewSpec().calculateScrollDistance(
childBounds.left,
childBounds.right - childBounds.left,
size.width
@@ -390,7 +398,7 @@
orientation: Orientation,
state: ScrollableState,
reverseDirection: Boolean,
- bringIntoViewSpec: BringIntoViewSpec
+ bringIntoViewSpec: BringIntoViewSpec?
) {
this.orientation = orientation
this.scrollState = state
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index cf92947..14d7aee 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -16,11 +16,9 @@
package androidx.compose.foundation.gestures
-import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.DecayAnimationSpec
import androidx.compose.animation.core.animateDecay
-import androidx.compose.animation.core.spring
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.animation.splineBasedDecay
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -52,9 +50,8 @@
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.NestedScrollSource.Companion.Drag
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.Fling
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.Wheel
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.SideEffect
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput
import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
@@ -155,7 +152,9 @@
* @param interactionSource [MutableInteractionSource] that will be used to emit
* drag events when this scrollable is being dragged.
* @param bringIntoViewSpec The configuration that this scrollable should use to perform
- * scrolling when scroll requests are received from the focus system.
+ * scrolling when scroll requests are received from the focus system. If null is provided the
+ * system will use the behavior provided by [LocalBringIntoViewSpec] which by default has a
+ * platform dependent implementation.
*
* Note: This API is experimental as it brings support for some experimental features:
* [overscrollEffect] and [bringIntoViewSpec].
@@ -170,7 +169,7 @@
reverseDirection: Boolean = false,
flingBehavior: FlingBehavior? = null,
interactionSource: MutableInteractionSource? = null,
- bringIntoViewSpec: BringIntoViewSpec = ScrollableDefaults.bringIntoViewSpec()
+ bringIntoViewSpec: BringIntoViewSpec? = null
) = this then ScrollableElement(
state,
orientation,
@@ -191,7 +190,7 @@
val reverseDirection: Boolean,
val flingBehavior: FlingBehavior?,
val interactionSource: MutableInteractionSource?,
- val bringIntoViewSpec: BringIntoViewSpec
+ val bringIntoViewSpec: BringIntoViewSpec?
) : ModifierNodeElement<ScrollableNode>() {
override fun create(): ScrollableNode {
return ScrollableNode(
@@ -270,7 +269,7 @@
enabled: Boolean,
reverseDirection: Boolean,
interactionSource: MutableInteractionSource?,
- bringIntoViewSpec: BringIntoViewSpec
+ private val bringIntoViewSpec: BringIntoViewSpec?
) : DragGestureNode(
canDrag = CanDragCalculation,
enabled = enabled,
@@ -355,7 +354,7 @@
reverseDirection: Boolean,
flingBehavior: FlingBehavior?,
interactionSource: MutableInteractionSource?,
- bringIntoViewSpec: BringIntoViewSpec
+ bringIntoViewSpec: BringIntoViewSpec?
) {
if (this.enabled != enabled) { // enabled changed
@@ -447,7 +446,7 @@
// lazily launch one coroutine (with the first event) and use a Channel
// to communicate the scroll amount to the UI thread.
coroutineScope.launch {
- scrollingLogic.dispatchUserInputDelta(scrollAmount, Wheel)
+ scrollingLogic.dispatchUserInputDelta(scrollAmount, UserInput)
}
true
} else {
@@ -482,7 +481,7 @@
// lazily launch one coroutine (with the first event) and use a Channel
// to communicate the scroll amount to the UI thread.
coroutineScope.launch {
- scrollingLogic.dispatchUserInputDelta(scrollAmount, Wheel)
+ scrollingLogic.dispatchUserInputDelta(scrollAmount, UserInput)
}
event.changes.fastForEach { it.consume() }
}
@@ -491,80 +490,6 @@
}
/**
- * The configuration of how a scrollable reacts to bring into view requests.
- *
- * Note: API shape and naming are still being refined, therefore API is marked as experimental.
- */
-@ExperimentalFoundationApi
-@Stable
-interface BringIntoViewSpec {
-
- /**
- * A retargetable Animation Spec to be used as the animation to run to fulfill the
- * BringIntoView requests.
- */
- val scrollAnimationSpec: AnimationSpec<Float> get() = DefaultScrollAnimationSpec
-
- /**
- * Calculate the offset needed to bring one of the scrollable container's child into view.
- *
- * @param offset from the side closest to the origin (For the x-axis this is 'left',
- * for the y-axis this is 'top').
- * @param size is the child size.
- * @param containerSize Is the main axis size of the scrollable container.
- *
- * All distances above are represented in pixels.
- *
- * @return The necessary amount to scroll to satisfy the bring into view request.
- * Returning zero from here means that the request was satisfied and the scrolling animation
- * should stop.
- *
- * This will be called for every frame of the scrolling animation. This means that, as the
- * animation progresses, the offset will naturally change to fulfill the scroll request.
- */
- fun calculateScrollDistance(
- offset: Float,
- size: Float,
- containerSize: Float
- ): Float
-
- companion object {
-
- /**
- * The default animation spec used by [Modifier.scrollable] to run Bring Into View requests.
- */
- val DefaultScrollAnimationSpec: AnimationSpec<Float> = spring()
-
- internal val DefaultBringIntoViewSpec = object : BringIntoViewSpec {
-
- override val scrollAnimationSpec: AnimationSpec<Float> = DefaultScrollAnimationSpec
-
- override fun calculateScrollDistance(
- offset: Float,
- size: Float,
- containerSize: Float
- ): Float {
- val trailingEdge = offset + size
- @Suppress("UnnecessaryVariable") val leadingEdge = offset
- return when {
-
- // If the item is already visible, no need to scroll.
- leadingEdge >= 0 && trailingEdge <= containerSize -> 0f
-
- // If the item is visible but larger than the parent, we don't scroll.
- leadingEdge < 0 && trailingEdge > containerSize -> 0f
-
- // Find the minimum scroll needed to make one of the edges coincide with the parent's
- // edge.
- abs(leadingEdge) < abs(trailingEdge - containerSize) -> leadingEdge
- else -> trailingEdge - containerSize
- }
- }
- }
- }
-}
-
-/**
* Contains the default values used by [scrollable]
*/
object ScrollableDefaults {
@@ -620,6 +545,13 @@
* A default implementation for [BringIntoViewSpec] that brings a child into view
* using the least amount of effort.
*/
+ @Deprecated(
+ "This has been replaced by composition locals LocalBringIntoViewSpec",
+ replaceWith = ReplaceWith(
+ "LocalBringIntoView.current",
+ "androidx.compose.foundation.gestures.LocalBringIntoViewSpec"
+ )
+ )
@ExperimentalFoundationApi
fun bringIntoViewSpec(): BringIntoViewSpec = DefaultBringIntoViewSpec
}
@@ -678,7 +610,7 @@
)
private var latestScrollScope: ScrollScope = NoOpScrollScope
- private var latestScrollSource: NestedScrollSource = Drag
+ private var latestScrollSource: NestedScrollSource = UserInput
private val performScroll: (delta: Offset) -> Offset = { delta ->
val consumedByPreScroll =
@@ -709,14 +641,13 @@
*/
private fun ScrollScope.dispatchScroll(
initialAvailableDelta: Offset,
- source: NestedScrollSource
+ source: NestedScrollSource,
+ overscrollEnabledForSource: Boolean
): Offset {
latestScrollSource = source
latestScrollScope = this
val overscroll = overscrollEffect
- return if (source == Wheel) {
- performScroll(initialAvailableDelta)
- } else if (overscroll != null && shouldDispatchOverscroll) {
+ return if (overscroll != null && shouldDispatchOverscroll && overscrollEnabledForSource) {
overscroll.applyToScroll(initialAvailableDelta, source, performScroll)
} else {
performScroll(initialAvailableDelta)
@@ -766,7 +697,11 @@
var result: Velocity = available
scrollableState.scroll {
val outerScopeScroll: (Offset) -> Offset = { delta ->
- dispatchScroll(delta.reverseIfNeeded(), Fling).reverseIfNeeded()
+ dispatchScroll(
+ delta.reverseIfNeeded(),
+ SideEffect,
+ overscrollEnabledForSource = true
+ ).reverseIfNeeded()
}
val scope = object : ScrollScope {
override fun scrollBy(pixels: Float): Float {
@@ -792,7 +727,7 @@
suspend fun dispatchUserInputDelta(delta: Offset, source: NestedScrollSource) {
scrollableState.scroll(MutatePriority.UserInput) {
- dispatchScroll(delta, source)
+ dispatchScroll(delta, source, overscrollEnabledForSource = false)
}
}
@@ -801,7 +736,11 @@
) {
scrollableState.scroll(MutatePriority.UserInput) {
forEachDelta {
- dispatchScroll(it.delta.singleAxisOffset(), Drag)
+ dispatchScroll(
+ it.delta.singleAxisOffset(),
+ UserInput,
+ overscrollEnabledForSource = true
+ )
}
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
index 0de86a8..8771eed 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
@@ -22,9 +22,9 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.BringIntoViewSpec
import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.TargetedFlingBehavior
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
@@ -134,7 +134,7 @@
PagerWrapperFlingBehavior(flingBehavior, state)
}
- val defaultBringIntoViewSpec = ScrollableDefaults.bringIntoViewSpec()
+ val defaultBringIntoViewSpec = LocalBringIntoViewSpec.current
val pagerBringIntoViewSpec = remember(state, defaultBringIntoViewSpec) {
PagerBringIntoViewSpec(
state,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
index 23ccc24..316e643 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
@@ -397,7 +397,7 @@
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
return if (
// rounding error and drag only
- source == NestedScrollSource.Drag && abs(state.currentPageOffsetFraction) > 0e-6
+ source == NestedScrollSource.UserInput && abs(state.currentPageOffsetFraction) > 0e-6
) {
// find the current and next page (in the direction of dragging)
val currentPageOffset = state.currentPageOffsetFraction * state.pageSize
@@ -434,7 +434,7 @@
available: Offset,
source: NestedScrollSource
): Offset {
- if (source == NestedScrollSource.Fling && available.mainAxis() != 0f) {
+ if (source == NestedScrollSource.SideEffect && available.mainAxis() != 0f) {
throw CancellationException()
}
return Offset.Zero
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
index 8374731..9b9ca90 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
@@ -707,21 +707,33 @@
}
/**
- * An utility function to help to calculate a given page's offset. Since this is based off
- * [currentPageOffsetFraction] the same concept applies: a fraction offset that represents
- * how far [page] is from the settled position (represented by [currentPage] offset). The
- * difference here is that [currentPageOffsetFraction] is a value between -0.5 and 0.5 and
- * the value calculate by this function can be larger than these numbers if [page] is different
- * than [currentPage].
+ * An utility function to help to calculate a given page's offset. This is an offset that
+ * represents how far [page] is from the settled position (represented by [currentPage]
+ * offset). The difference here is that [currentPageOffsetFraction] is a value between -0.5 and
+ * 0.5 and the value calculated by this function can be larger than these numbers if [page] is
+ * different than [currentPage].
+ *
+ * For instance, if currentPage=0 and we call [getOffsetDistanceInPages] for page 3, the result
+ * will be 3, meaning the given page is 3 pages away from the current page (the sign represent
+ * the direction of the offset, positive is forward, negative is backwards). Another example is
+ * if currentPage=3 and we call [getOffsetDistanceInPages] for page 1, the result would be -2,
+ * meaning we're 2 pages away (moving backwards) to the current page.
+ *
+ * This offset also works in conjunction with [currentPageOffsetFraction], so if [currentPage]
+ * is out of its snapped position (i.e. currentPageOffsetFraction!=0) then the calculated value
+ * will still represent the offset in number of pages (in this case, not whole pages).
+ * For instance, if currentPage=1 and we're slightly offset, currentPageOffsetFraction=0.2,
+ * if we call this to page 2, the result would be 0.8, that is 0.8 page away from current page
+ * (moving forward).
*
* @param page The page to calculate the offset from. This should be between 0 and [pageCount].
* @return The offset of [page] with respect to [currentPage].
*/
- fun getOffsetFractionForPage(page: Int): Float {
+ fun getOffsetDistanceInPages(page: Int): Float {
require(page in 0..pageCount) {
"page $page is not within the range 0 to $pageCount"
}
- return (currentPage - page) + currentPageOffsetFraction
+ return page - currentPage - currentPageOffsetFraction
}
/**
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicSecureTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicSecureTextField.kt
index 4b17dc9..28dff29 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicSecureTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicSecureTextField.kt
@@ -28,14 +28,15 @@
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.TextObfuscationMode
import androidx.compose.foundation.text.input.internal.CodepointTransformation
-import androidx.compose.foundation.text.input.internal.mask
import androidx.compose.foundation.text.input.then
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
@@ -105,6 +106,8 @@
* @param decorator Allows to add decorations around text field, such as icon, placeholder, helper
* messages or similar, and automatically increase the hit target area of the text field.
* @param textObfuscationMode Determines the method used to obscure the input text.
+ * @param textObfuscationCharacter Which character to use while obfuscating the text. It doesn't
+ * have an effect when [textObfuscationMode] is set to [TextObfuscationMode.Visible].
*/
@ExperimentalFoundationApi
// This takes a composable lambda, but it is not primarily a container.
@@ -125,8 +128,10 @@
// Last parameter must not be a function unless it's intended to be commonly used as a trailing
// lambda.
textObfuscationMode: TextObfuscationMode = TextObfuscationMode.RevealLastTyped,
+ textObfuscationCharacter: Char = DefaultObfuscationCharacter,
) {
- val secureTextFieldController = remember { SecureTextFieldController() }
+ val obfuscationMaskState = rememberUpdatedState(textObfuscationCharacter)
+ val secureTextFieldController = remember { SecureTextFieldController(obfuscationMaskState) }
LaunchedEffect(secureTextFieldController) {
// start a coroutine that listens for scheduled hide events.
secureTextFieldController.observeHideEvents()
@@ -138,20 +143,22 @@
val revealLastTypedEnabled = textObfuscationMode == TextObfuscationMode.RevealLastTyped
// while toggling between obfuscation methods if the revealing gets disabled, reset the reveal.
- if (!revealLastTypedEnabled) {
- secureTextFieldController.passwordInputTransformation.hide()
+ LaunchedEffect(revealLastTypedEnabled) {
+ if (!revealLastTypedEnabled) {
+ secureTextFieldController.passwordInputTransformation.hide()
+ }
}
- val codepointTransformation = when {
- revealLastTypedEnabled -> {
- secureTextFieldController.codepointTransformation
+ val codepointTransformation = remember(textObfuscationMode) {
+ when (textObfuscationMode) {
+ TextObfuscationMode.RevealLastTyped -> {
+ secureTextFieldController.codepointTransformation
+ }
+ TextObfuscationMode.Hidden -> {
+ CodepointTransformation { _, _ -> obfuscationMaskState.value.code }
+ }
+ else -> null
}
-
- textObfuscationMode == TextObfuscationMode.Hidden -> {
- PasswordCodepointTransformation
- }
-
- else -> null
}
val secureTextFieldModifier = modifier
@@ -190,7 +197,9 @@
}
}
-internal class SecureTextFieldController {
+internal class SecureTextFieldController(
+ private val obfuscationMaskState: State<Char>
+) {
/**
* A special [InputTransformation] that tracks changes to the content to identify the last typed
* character to reveal. `scheduleHide` lambda is delegated to a member function to be able to
@@ -206,7 +215,7 @@
// reveal the last typed character by not obscuring it
codepoint
} else {
- DEFAULT_OBFUSCATION_MASK.code
+ obfuscationMaskState.value.code
}
}
@@ -279,10 +288,7 @@
// adopted from PasswordTransformationMethod from Android platform.
private const val LAST_TYPED_CHARACTER_REVEAL_DURATION_MILLIS = 1500L
-private const val DEFAULT_OBFUSCATION_MASK = '\u2022'
-
-@OptIn(ExperimentalFoundationApi::class)
-private val PasswordCodepointTransformation = CodepointTransformation.mask(DEFAULT_OBFUSCATION_MASK)
+private const val DefaultObfuscationCharacter: Char = '\u2022'
/**
* Overrides the TextToolbar and keyboard shortcuts to never allow copy or cut options by the
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardOptions.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardOptions.kt
index f7b24c3..64a3e60 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardOptions.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardOptions.kt
@@ -406,7 +406,7 @@
* [other]s null or `Unspecified` properties are replaced with the non-null properties of
* this object.
*
- * If the [other] is null, returns this.
+ * If the either this or [other] is null, returns the non-null one.
*/
// TODO(b/331222000) Rename to be more clear about precedence.
fun merge(other: KeyboardOptions?): KeyboardOptions =
@@ -420,7 +420,7 @@
* [other]. This differs from the behavior of [copy], which always takes the
* passed value over the current one, even if an unspecified value is passed.
*
- * If the [other] is null, returns this.
+ * If the either this or [other] is null, returns the non-null one.
*/
@Stable
internal fun fillUnspecifiedValuesWith(other: KeyboardOptions?): KeyboardOptions {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/InputTransformation.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/InputTransformation.kt
index 91f0fc8..b6e4c7b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/InputTransformation.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/InputTransformation.kt
@@ -109,8 +109,9 @@
* Creates a filter chain that will run [next] after this. Filters are applied sequentially, so any
* changes made by this filter will be visible to [next].
*
- * The returned filter will use the [KeyboardOptions] from [next] if non-null, otherwise it will
- * use the options from this transformation.
+ * The returned filter will [merge][KeyboardOptions.merge] this transformation's [KeyboardOptions]
+ * with those from [next], preferring options from [next] where both transformations specify the
+ * same option.
*
* @sample androidx.compose.foundation.samples.BasicTextFieldInputTransformationChainingSample
*
@@ -173,8 +174,8 @@
) : InputTransformation {
override val keyboardOptions: KeyboardOptions?
- // TODO(b/295951492) Do proper merging.
- get() = second.keyboardOptions ?: first.keyboardOptions
+ get() = second.keyboardOptions?.fillUnspecifiedValuesWith(first.keyboardOptions)
+ ?: first.keyboardOptions
override fun SemanticsPropertyReceiver.applySemantics() {
with(first) { applySemantics() }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldBuffer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldBuffer.kt
index d0b9f83..8ff9788 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldBuffer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldBuffer.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.text.input
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.text.input.InputTransformation.Companion.transformInput
import androidx.compose.foundation.text.input.TextFieldBuffer.ChangeList
import androidx.compose.foundation.text.input.internal.ChangeTracker
import androidx.compose.foundation.text.input.internal.OffsetMappingCalculator
@@ -42,15 +43,12 @@
*
* To get one of these, and for usage samples, see [TextFieldState.edit]. Every change to the buffer
* is tracked in a [ChangeList] which you can access via the [changes] property.
- *
- * @property originalValue The value reverted to when [revertAllChanges] is called. This is not
- * necessarily `initialValue`.
*/
@OptIn(ExperimentalFoundationApi::class)
class TextFieldBuffer internal constructor(
initialValue: TextFieldCharSequence,
initialChanges: ChangeTracker? = null,
- val originalValue: TextFieldCharSequence = initialValue,
+ internal val originalValue: TextFieldCharSequence = initialValue,
private val offsetMappingCalculator: OffsetMappingCalculator? = null,
) : Appendable {
@@ -72,6 +70,20 @@
val length: Int get() = buffer.length
/**
+ * Original text content of the buffer before any changes were applied. Calling
+ * [revertAllChanges] will set the contents of this buffer to this value.
+ */
+ val originalText: CharSequence
+ get() = originalValue.text
+
+ /**
+ * Original selection before the changes. Calling [revertAllChanges] will set the selection
+ * to this value.
+ */
+ val originalSelection: TextRange
+ get() = originalValue.selection
+
+ /**
* The [ChangeList] represents the changes made to this value and is inherently mutable. This
* means that the returned [ChangeList] always reflects the complete list of changes made to
* this value at any given time, even those made after reading this property.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldCharSequence.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldCharSequence.kt
index 14daca4..573b7cf 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldCharSequence.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldCharSequence.kt
@@ -16,7 +16,6 @@
package androidx.compose.foundation.text.input
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.text.input.internal.toCharArray
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.coerceIn
@@ -31,15 +30,28 @@
* This class also may contain the range being composed by the IME, if any, although this is not
* exposed.
*
+ * @param text If this TextFieldCharSequence is actually a copy of another, make sure to use the
+ * backing CharSequence object to stop unnecessary nesting and logic that depends on exact equality
+ * of CharSequence comparison that's using [CharSequence.equals].
+ *
* @see TextFieldBuffer
*/
-sealed interface TextFieldCharSequence : CharSequence {
+internal class TextFieldCharSequence(
+ text: CharSequence = "",
+ selection: TextRange = TextRange.Zero,
+ composition: TextRange? = null
+) : CharSequence {
+
+ override val length: Int
+ get() = text.length
+
+ val text: CharSequence = if (text is TextFieldCharSequence) text.text else text
+
/**
* The selection range. If the selection is collapsed, it represents cursor
* location. When selection range is out of bounds, it is constrained with the text length.
*/
- @ExperimentalFoundationApi
- val selection: TextRange
+ val selection: TextRange = selection.coerceIn(0, text.length)
/**
* Composition range created by IME. If null, there is no composition range.
@@ -51,81 +63,7 @@
*
* Composition can only be set by the system.
*/
- @ExperimentalFoundationApi
- val composition: TextRange?
-
- /**
- * Returns true if the text in this object is equal to the text in [other], disregarding any
- * other properties of this (such as selection) or [other].
- */
- fun contentEquals(other: CharSequence): Boolean
-
- abstract override fun toString(): String
- abstract override fun equals(other: Any?): Boolean
- abstract override fun hashCode(): Int
-}
-
-fun TextFieldCharSequence(
- text: String = "",
- selection: TextRange = TextRange.Zero
-): TextFieldCharSequence = TextFieldCharSequenceWrapper(text, selection, composition = null)
-
-internal fun TextFieldCharSequence(
- text: CharSequence,
- selection: TextRange,
- composition: TextRange? = null
-): TextFieldCharSequence = TextFieldCharSequenceWrapper(text, selection, composition)
-
-/**
- * Returns the backing CharSequence object that this TextFieldCharSequence is wrapping. This is
- * useful for external equality comparisons that cannot use [TextFieldCharSequence.contentEquals].
- */
-internal fun TextFieldCharSequence.getBackingCharSequence(): CharSequence {
- return when (this) {
- is TextFieldCharSequenceWrapper -> this.text
- }
-}
-
-/**
- * Copies the contents of this sequence from [[sourceStartIndex], [sourceEndIndex]) into
- * [destination] starting at [destinationOffset].
- */
-internal fun TextFieldCharSequence.toCharArray(
- destination: CharArray,
- destinationOffset: Int,
- sourceStartIndex: Int,
- sourceEndIndex: Int
-) = (this as TextFieldCharSequenceWrapper).toCharArray(
- destination,
- destinationOffset,
- sourceStartIndex,
- sourceEndIndex
-)
-
-@OptIn(ExperimentalFoundationApi::class)
-private class TextFieldCharSequenceWrapper(
- text: CharSequence,
- selection: TextRange,
- composition: TextRange?
-) : TextFieldCharSequence {
-
- /**
- * If this TextFieldCharSequence is actually a copy of another, make sure to use the backing
- * CharSequence object to stop unnecessary nesting and logic that depends on exact equality of
- * CharSequence comparison that's using [CharSequence.equals].
- */
- val text: CharSequence = if (text is TextFieldCharSequenceWrapper) {
- text.text
- } else {
- text
- }
-
- override val length: Int
- get() = text.length
-
- override val selection: TextRange = selection.coerceIn(0, text.length)
-
- override val composition: TextRange? = composition?.coerceIn(0, text.length)
+ val composition: TextRange? = composition?.coerceIn(0, text.length)
override operator fun get(index: Int): Char = text[index]
@@ -134,8 +72,12 @@
override fun toString(): String = text.toString()
- override fun contentEquals(other: CharSequence): Boolean = text.contentEquals(other)
+ fun contentEquals(other: CharSequence): Boolean = text.contentEquals(other)
+ /**
+ * Copies the contents of this sequence from [[sourceStartIndex], [sourceEndIndex]) into
+ * [destination] starting at [destinationOffset].
+ */
fun toCharArray(
destination: CharArray,
destinationOffset: Int,
@@ -154,7 +96,7 @@
if (other === null) return false
if (this::class != other::class) return false
- other as TextFieldCharSequenceWrapper
+ other as TextFieldCharSequence
if (selection != other.selection) return false
if (composition != other.composition) return false
@@ -179,7 +121,6 @@
*
* @see TextRange.min
*/
-@OptIn(ExperimentalFoundationApi::class)
internal fun TextFieldCharSequence.getTextBeforeSelection(maxChars: Int): CharSequence =
subSequence(kotlin.math.max(0, selection.min - maxChars), selection.min)
@@ -191,13 +132,11 @@
*
* @see TextRange.max
*/
-@OptIn(ExperimentalFoundationApi::class)
internal fun TextFieldCharSequence.getTextAfterSelection(maxChars: Int): CharSequence =
subSequence(selection.max, kotlin.math.min(selection.max + maxChars, length))
/**
* Returns the currently selected text.
*/
-@OptIn(ExperimentalFoundationApi::class)
internal fun TextFieldCharSequence.getSelectedText(): CharSequence =
subSequence(selection.min, selection.max)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
index 29b0b5e..3fb23af 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
@@ -126,7 +126,7 @@
* @see edit
* @see snapshotFlow
*/
- val text: CharSequence get() = value.getBackingCharSequence()
+ val text: CharSequence get() = value.text
/**
* The current selection range. If the selection is collapsed, it represents cursor location.
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.desktop.kt
new file mode 100644
index 0000000..061d648
--- /dev/null
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.desktop.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.gestures
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.staticCompositionLocalOf
+
+/*
+* A composition local to customize the focus scrolling behavior used by some scrollable containers.
+* [LocalBringIntoViewSpec] has a platform defined behavior. The scroll default behavior will move
+* the least to bring the requested region into view.
+*/
+@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:ExperimentalFoundationApi
+@ExperimentalFoundationApi
+actual val LocalBringIntoViewSpec: ProvidableCompositionLocal<BringIntoViewSpec> =
+ staticCompositionLocalOf {
+ BringIntoViewSpec.DefaultBringIntoViewSpec
+ }
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.desktop.kt
index 543f44b..e6245d4 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.desktop.kt
@@ -16,11 +16,8 @@
package androidx.compose.foundation.text.input.internal
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.text.input.TextFieldCharSequence
-import androidx.compose.foundation.text.input.toCharArray
-@OptIn(ExperimentalFoundationApi::class)
internal actual fun CharSequence.toCharArray(
destination: CharArray,
destinationOffset: Int,
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt
index 445b390..2769c3f 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt
@@ -431,7 +431,7 @@
val offsetBeforeScroll = bottomSheetState.requireOffset()
scrollDispatcher.dispatchPreScroll(
Offset(x = 0f, y = -sheetHeightPx),
- NestedScrollSource.Drag
+ NestedScrollSource.UserInput
)
rule.waitForIdle()
Truth.assertWithMessage("Offset after scroll is equal to offset before scroll")
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
index 8b0d427..3b87fa0 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
@@ -971,7 +971,7 @@
val offsetBeforeScroll = sheetState.requireOffset()
scrollDispatcher.dispatchPreScroll(
Offset(x = 0f, y = -sheetHeightPx),
- NestedScrollSource.Drag,
+ NestedScrollSource.UserInput,
)
rule.waitForIdle()
assertWithMessage("Offset after scroll is equal to offset before scroll")
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
index d9c5610..01decee 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
@@ -72,7 +72,10 @@
refreshThreshold = with(LocalDensity.current) { threshold.toDp() }
)
- Box(Modifier.pullRefresh(state).testTag(PullRefreshTag)) {
+ Box(
+ Modifier
+ .pullRefresh(state)
+ .testTag(PullRefreshTag)) {
LazyColumn {
items(100) {
Text("item $it")
@@ -102,7 +105,10 @@
refreshThreshold = with(LocalDensity.current) { threshold.toDp() }
)
- Box(Modifier.pullRefresh(state).testTag(PullRefreshTag)) {
+ Box(
+ Modifier
+ .pullRefresh(state)
+ .testTag(PullRefreshTag)) {
LazyColumn {
items(100) {
Text("item $it")
@@ -146,7 +152,10 @@
refreshThreshold = with(LocalDensity.current) { threshold.toDp() }
)
- Box(Modifier.pullRefresh(state).testTag(PullRefreshTag)) {
+ Box(
+ Modifier
+ .pullRefresh(state)
+ .testTag(PullRefreshTag)) {
LazyColumn {
items(100) {
Text("item $it")
@@ -197,7 +206,10 @@
refreshThreshold = with(LocalDensity.current) { threshold.toDp() }
)
- Box(Modifier.pullRefresh(state).testTag(PullRefreshTag)) {
+ Box(
+ Modifier
+ .pullRefresh(state)
+ .testTag(PullRefreshTag)) {
LazyColumn {
items(100) {
Text("item $it")
@@ -246,7 +258,10 @@
refreshThreshold = with(LocalDensity.current) { threshold.toDp() }
)
- Box(Modifier.pullRefresh(state).testTag(PullRefreshTag)) {
+ Box(
+ Modifier
+ .pullRefresh(state)
+ .testTag(PullRefreshTag)) {
LazyColumn {
items(100) {
Text("item $it")
@@ -287,7 +302,10 @@
refreshingOffset = with(LocalDensity.current) { refreshingOffset.toDp() }
)
- Box(Modifier.pullRefresh(state).testTag(PullRefreshTag)) {
+ Box(
+ Modifier
+ .pullRefresh(state)
+ .testTag(PullRefreshTag)) {
LazyColumn {
items(100) {
Text("item $it")
@@ -340,7 +358,10 @@
refreshThreshold = with(LocalDensity.current) { threshold.toDp() }
)
- Box(Modifier.pullRefresh(state).testTag(PullRefreshTag)) {
+ Box(
+ Modifier
+ .pullRefresh(state)
+ .testTag(PullRefreshTag)) {
LazyColumn {
items(100) {
Text("item $it")
@@ -408,8 +429,14 @@
onRefresh = { },
refreshThreshold = with(LocalDensity.current) { refreshThreshold.toDp() }
)
- Box(Modifier.size(200.dp).pullRefresh(state)) {
- Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+ Box(
+ Modifier
+ .size(200.dp)
+ .pullRefresh(state)) {
+ Box(
+ Modifier
+ .size(100.dp)
+ .nestedScroll(connection, dispatcher))
}
}
@@ -417,7 +444,8 @@
val dragUpOffset = Offset(0f, -100f)
rule.runOnIdle {
- val preConsumed = dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.Drag)
+ val preConsumed =
+ dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.UserInput)
// Pull refresh is not showing, so we should consume nothing
assertThat(preConsumed).isEqualTo(Offset.Zero)
assertThat(state.position).isEqualTo(0f)
@@ -428,7 +456,8 @@
rule.runOnIdle {
assertThat(state.position).isEqualTo(100f /* 200 / 2 for drag multiplier */)
- val preConsumed = dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.Drag)
+ val preConsumed =
+ dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.UserInput)
// Pull refresh is currently showing, so we should consume all the delta
assertThat(preConsumed).isEqualTo(dragUpOffset)
assertThat(state.position).isEqualTo(50f /* (200 - 100) / 2 for drag multiplier */)
@@ -449,8 +478,14 @@
onRefresh = { },
refreshingOffset = with(LocalDensity.current) { refreshingOffset.toDp() }
)
- Box(Modifier.size(200.dp).pullRefresh(state)) {
- Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+ Box(
+ Modifier
+ .size(200.dp)
+ .pullRefresh(state)) {
+ Box(
+ Modifier
+ .size(100.dp)
+ .nestedScroll(connection, dispatcher))
}
}
@@ -458,7 +493,8 @@
val dragUpOffset = Offset(0f, -100f)
rule.runOnIdle {
- val preConsumed = dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.Drag)
+ val preConsumed =
+ dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.UserInput)
// Pull refresh is refreshing, so we should consume nothing
assertThat(preConsumed).isEqualTo(Offset.Zero)
assertThat(state.position).isEqualTo(refreshingOffset)
@@ -479,8 +515,14 @@
onRefresh = { },
refreshThreshold = with(LocalDensity.current) { refreshThreshold.toDp() }
)
- Box(Modifier.size(200.dp).pullRefresh(state)) {
- Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+ Box(
+ Modifier
+ .size(200.dp)
+ .pullRefresh(state)) {
+ Box(
+ Modifier
+ .size(100.dp)
+ .nestedScroll(connection, dispatcher))
}
}
@@ -488,7 +530,8 @@
val dragUpOffset = Offset(0f, 100f)
rule.runOnIdle {
- val preConsumed = dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.Drag)
+ val preConsumed =
+ dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.UserInput)
// We should ignore positive delta in prescroll, so we should consume nothing
assertThat(preConsumed).isEqualTo(Offset.Zero)
assertThat(state.position).isEqualTo(0f)
@@ -499,7 +542,8 @@
rule.runOnIdle {
assertThat(state.position).isEqualTo(100f /* 200 / 2 for drag multiplier */)
- val preConsumed = dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.Drag)
+ val preConsumed =
+ dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.UserInput)
// We should ignore positive delta in prescroll, so we should consume nothing
assertThat(preConsumed).isEqualTo(Offset.Zero)
assertThat(state.position).isEqualTo(100f /* 200 / 2 for drag multiplier */)
@@ -520,8 +564,14 @@
onRefresh = { },
refreshingOffset = with(LocalDensity.current) { refreshingOffset.toDp() }
)
- Box(Modifier.size(200.dp).pullRefresh(state)) {
- Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+ Box(
+ Modifier
+ .size(200.dp)
+ .pullRefresh(state)) {
+ Box(
+ Modifier
+ .size(100.dp)
+ .nestedScroll(connection, dispatcher))
}
}
@@ -529,7 +579,8 @@
val dragUpOffset = Offset(0f, 100f)
rule.runOnIdle {
- val preConsumed = dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.Drag)
+ val preConsumed =
+ dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.UserInput)
// Pull refresh is refreshing, so we should consume nothing
assertThat(preConsumed).isEqualTo(Offset.Zero)
assertThat(state.position).isEqualTo(refreshingOffset)
@@ -550,8 +601,14 @@
onRefresh = { },
refreshThreshold = with(LocalDensity.current) { refreshThreshold.toDp() }
)
- Box(Modifier.size(200.dp).pullRefresh(state)) {
- Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+ Box(
+ Modifier
+ .size(200.dp)
+ .pullRefresh(state)) {
+ Box(
+ Modifier
+ .size(100.dp)
+ .nestedScroll(connection, dispatcher))
}
}
@@ -562,7 +619,7 @@
val postConsumed = dispatcher.dispatchPostScroll(
Offset.Zero,
dragUpOffset,
- NestedScrollSource.Drag
+ NestedScrollSource.UserInput
)
// We should ignore negative delta in postscroll, so we should consume nothing
assertThat(postConsumed).isEqualTo(Offset.Zero)
@@ -577,7 +634,7 @@
val postConsumed = dispatcher.dispatchPostScroll(
Offset.Zero,
dragUpOffset,
- NestedScrollSource.Drag
+ NestedScrollSource.UserInput
)
// We should ignore negative delta in postscroll, so we should consume nothing
assertThat(postConsumed).isEqualTo(Offset.Zero)
@@ -599,8 +656,14 @@
onRefresh = { },
refreshingOffset = with(LocalDensity.current) { refreshingOffset.toDp() }
)
- Box(Modifier.size(200.dp).pullRefresh(state)) {
- Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+ Box(
+ Modifier
+ .size(200.dp)
+ .pullRefresh(state)) {
+ Box(
+ Modifier
+ .size(100.dp)
+ .nestedScroll(connection, dispatcher))
}
}
@@ -611,7 +674,7 @@
val postConsumed = dispatcher.dispatchPostScroll(
Offset.Zero,
dragUpOffset,
- NestedScrollSource.Drag
+ NestedScrollSource.UserInput
)
// Pull refresh is refreshing, so we should consume nothing
assertThat(postConsumed).isEqualTo(Offset.Zero)
@@ -633,8 +696,14 @@
onRefresh = { },
refreshThreshold = with(LocalDensity.current) { refreshThreshold.toDp() }
)
- Box(Modifier.size(200.dp).pullRefresh(state)) {
- Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+ Box(
+ Modifier
+ .size(200.dp)
+ .pullRefresh(state)) {
+ Box(
+ Modifier
+ .size(100.dp)
+ .nestedScroll(connection, dispatcher))
}
}
@@ -645,7 +714,7 @@
val postConsumed = dispatcher.dispatchPostScroll(
Offset.Zero,
dragUpOffset,
- NestedScrollSource.Drag
+ NestedScrollSource.UserInput
)
// We should consume all the delta
assertThat(postConsumed).isEqualTo(dragUpOffset)
@@ -660,7 +729,7 @@
val postConsumed = dispatcher.dispatchPostScroll(
Offset.Zero,
dragUpOffset,
- NestedScrollSource.Drag
+ NestedScrollSource.UserInput
)
// We should consume all the delta again
assertThat(postConsumed).isEqualTo(dragUpOffset)
@@ -683,8 +752,14 @@
onRefresh = { },
refreshingOffset = with(LocalDensity.current) { refreshingOffset.toDp() }
)
- Box(Modifier.size(200.dp).pullRefresh(state)) {
- Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+ Box(
+ Modifier
+ .size(200.dp)
+ .pullRefresh(state)) {
+ Box(
+ Modifier
+ .size(100.dp)
+ .nestedScroll(connection, dispatcher))
}
}
@@ -695,7 +770,7 @@
val postConsumed = dispatcher.dispatchPostScroll(
Offset.Zero,
dragUpOffset,
- NestedScrollSource.Drag
+ NestedScrollSource.UserInput
)
// Pull refresh is refreshing, so we should consume nothing
assertThat(postConsumed).isEqualTo(Offset.Zero)
@@ -718,8 +793,14 @@
onRefresh = { onRefreshCalled = true },
refreshThreshold = with(LocalDensity.current) { refreshThreshold.toDp() }
)
- Box(Modifier.size(200.dp).pullRefresh(state)) {
- Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+ Box(
+ Modifier
+ .size(200.dp)
+ .pullRefresh(state)) {
+ Box(
+ Modifier
+ .size(100.dp)
+ .nestedScroll(connection, dispatcher))
}
}
@@ -761,10 +842,12 @@
rule.runOnIdle {
assertThat(state.position)
- .isEqualTo(calculateIndicatorPosition(
- refreshThreshold * (3 / 2f) /* account for drag multiplier */,
- refreshThreshold
- ))
+ .isEqualTo(
+ calculateIndicatorPosition(
+ refreshThreshold * (3 / 2f) /* account for drag multiplier */,
+ refreshThreshold
+ )
+ )
val preConsumed = runBlocking { dispatcher.dispatchPreFling(flingUp) }
// Upwards fling, so we should consume nothing
assertThat(preConsumed).isEqualTo(Velocity.Zero)
@@ -792,8 +875,14 @@
onRefresh = {},
refreshingOffset = with(LocalDensity.current) { refreshingOffset.toDp() }
)
- Box(Modifier.size(200.dp).pullRefresh(state)) {
- Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+ Box(
+ Modifier
+ .size(200.dp)
+ .pullRefresh(state)) {
+ Box(
+ Modifier
+ .size(100.dp)
+ .nestedScroll(connection, dispatcher))
}
}
@@ -828,8 +917,14 @@
onRefresh = { onRefreshCalled = true },
refreshThreshold = with(LocalDensity.current) { refreshThreshold.toDp() }
)
- Box(Modifier.size(200.dp).pullRefresh(state)) {
- Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+ Box(
+ Modifier
+ .size(200.dp)
+ .pullRefresh(state)) {
+ Box(
+ Modifier
+ .size(100.dp)
+ .nestedScroll(connection, dispatcher))
}
}
@@ -871,10 +966,12 @@
rule.runOnIdle {
assertThat(state.position)
- .isEqualTo(calculateIndicatorPosition(
- refreshThreshold * (3 / 2f) /* account for drag multiplier */,
- refreshThreshold
- ))
+ .isEqualTo(
+ calculateIndicatorPosition(
+ refreshThreshold * (3 / 2f) /* account for drag multiplier */,
+ refreshThreshold
+ )
+ )
val preConsumed = runBlocking { dispatcher.dispatchPreFling(flingDown) }
// Downwards fling, and we are currently showing, so we should consume all
assertThat(preConsumed).isEqualTo(flingDown)
@@ -902,8 +999,14 @@
onRefresh = {},
refreshingOffset = with(LocalDensity.current) { refreshingOffset.toDp() }
)
- Box(Modifier.size(200.dp).pullRefresh(state)) {
- Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+ Box(
+ Modifier
+ .size(200.dp)
+ .pullRefresh(state)) {
+ Box(
+ Modifier
+ .size(100.dp)
+ .nestedScroll(connection, dispatcher))
}
}
@@ -938,8 +1041,14 @@
onRefresh = { onRefreshCalled = true },
refreshThreshold = with(LocalDensity.current) { refreshThreshold.toDp() }
)
- Box(Modifier.size(200.dp).pullRefresh(state)) {
- Box(Modifier.size(100.dp).nestedScroll(connection, dispatcher))
+ Box(
+ Modifier
+ .size(200.dp)
+ .pullRefresh(state)) {
+ Box(
+ Modifier
+ .size(100.dp)
+ .nestedScroll(connection, dispatcher))
}
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt
index fc760ab..00636a4 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt
@@ -682,7 +682,7 @@
): NestedScrollConnection = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
- return if (delta < 0 && source == NestedScrollSource.Drag) {
+ return if (delta < 0 && source == NestedScrollSource.UserInput) {
state.dispatchRawDelta(delta).toOffset()
} else {
Offset.Zero
@@ -694,7 +694,7 @@
available: Offset,
source: NestedScrollSource
): Offset {
- return if (source == NestedScrollSource.Drag) {
+ return if (source == NestedScrollSource.UserInput) {
state.dispatchRawDelta(available.toFloat()).toOffset()
} else {
Offset.Zero
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
index 8e434dd..223a9db 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
@@ -571,7 +571,7 @@
): NestedScrollConnection = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
- return if (delta < 0 && source == NestedScrollSource.Drag) {
+ return if (delta < 0 && source == NestedScrollSource.UserInput) {
state.dispatchRawDelta(delta).toOffset()
} else {
Offset.Zero
@@ -583,7 +583,7 @@
available: Offset,
source: NestedScrollSource
): Offset {
- return if (source == NestedScrollSource.Drag) {
+ return if (source == NestedScrollSource.UserInput) {
state.dispatchRawDelta(available.toFloat()).toOffset()
} else {
Offset.Zero
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
index d2f9de7..c7e699b 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
@@ -866,7 +866,7 @@
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
- return if (delta < 0 && source == NestedScrollSource.Drag) {
+ return if (delta < 0 && source == NestedScrollSource.UserInput) {
state.dispatchRawDelta(delta).toOffset()
} else {
Offset.Zero
@@ -878,7 +878,7 @@
available: Offset,
source: NestedScrollSource
): Offset {
- return if (source == NestedScrollSource.Drag) {
+ return if (source == NestedScrollSource.UserInput) {
state.dispatchRawDelta(available.toFloat()).toOffset()
} else {
Offset.Zero
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 86e9f43..65c3069 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
@@ -554,7 +554,7 @@
): NestedScrollConnection = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
- return if (delta < 0 && source == NestedScrollSource.Drag) {
+ return if (delta < 0 && source == NestedScrollSource.UserInput) {
state.dispatchRawDelta(delta).toOffset()
} else {
Offset.Zero
@@ -566,7 +566,7 @@
available: Offset,
source: NestedScrollSource
): Offset {
- return if (source == NestedScrollSource.Drag) {
+ return if (source == NestedScrollSource.UserInput) {
state.dispatchRawDelta(available.toFloat()).toOffset()
} else {
Offset.Zero
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
index fc39fae..c98d89b 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
@@ -863,7 +863,7 @@
get() = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
- return if (delta < 0 && source == NestedScrollSource.Drag) {
+ return if (delta < 0 && source == NestedScrollSource.UserInput) {
performDrag(delta).toOffset()
} else {
Offset.Zero
@@ -875,7 +875,7 @@
available: Offset,
source: NestedScrollSource
): Offset {
- return if (source == NestedScrollSource.Drag) {
+ return if (source == NestedScrollSource.UserInput) {
performDrag(available.toFloat()).toOffset()
} else {
Offset.Zero
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefresh.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefresh.kt
index 1ef9312..baab13f 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefresh.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefresh.kt
@@ -21,7 +21,6 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.Drag
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.Velocity
@@ -84,7 +83,10 @@
source: NestedScrollSource
): Offset = when {
!enabled -> Offset.Zero
- source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up
+ source == NestedScrollSource.UserInput && available.y < 0 -> Offset(
+ 0f,
+ onPull(available.y)
+ ) // Swiping up
else -> Offset.Zero
}
@@ -94,7 +96,10 @@
source: NestedScrollSource
): Offset = when {
!enabled -> Offset.Zero
- source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down
+ source == NestedScrollSource.UserInput && available.y > 0 -> Offset(
+ 0f,
+ onPull(available.y)
+ ) // Pulling down
else -> Offset.Zero
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AnimateBoundsModifier.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AnimateBoundsModifier.kt
index 6ce252b..be35102 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AnimateBoundsModifier.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AnimateBoundsModifier.kt
@@ -50,11 +50,11 @@
return@composed this
}
this.approachLayout(
- isMeasurementApproachComplete = {
- animateFraction == 1f
+ isMeasurementApproachInProgress = {
+ animateFraction != 1f
},
- isPlacementApproachComplete = {
- animateFraction == 1f
+ isPlacementApproachInProgress = {
+ animateFraction != 1f
},
) { measurable, _ ->
// When layout changes, the lookahead pass will calculate a new final size for the
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/SearchBarBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/SearchBarBenchmark.kt
index 407bcfa..dc4c13f 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/SearchBarBenchmark.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/SearchBarBenchmark.kt
@@ -20,6 +20,7 @@
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
+import androidx.compose.material3.SearchBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
@@ -54,7 +55,7 @@
}
@Test
- fun changeActiveState() {
+ fun changeExpandedState() {
benchmarkRule.toggleStateBenchmarkComposeMeasureLayout(
caseFactory = testCaseFactory,
assertOneRecomposition = false,
@@ -71,24 +72,29 @@
@Composable
override fun MeasuredContent() {
state = remember { mutableStateOf(true) }
+ val inputField: @Composable () -> Unit = {
+ SearchBarDefaults.InputField(
+ query = "",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = state.value,
+ onExpandedChange = { state.value = it },
+ )
+ }
when (type) {
SearchBarType.FullScreen ->
SearchBar(
- query = "",
- onQueryChange = {},
- onSearch = {},
- active = state.value,
- onActiveChange = { state.value = it },
+ inputField = inputField,
+ expanded = state.value,
+ onExpandedChange = { state.value = it },
content = {},
)
SearchBarType.Docked ->
DockedSearchBar(
- query = "",
- onQueryChange = {},
- onSearch = {},
- active = state.value,
- onActiveChange = { state.value = it },
+ inputField = inputField,
+ expanded = state.value,
+ onExpandedChange = { state.value = it },
content = {},
)
}
diff --git a/compose/material3/material3-common/api/current.txt b/compose/material3/material3-common/api/current.txt
index 55ca903..48edfd6 100644
--- a/compose/material3/material3-common/api/current.txt
+++ b/compose/material3/material3-common/api/current.txt
@@ -17,7 +17,7 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> LocalMinimumInteractiveComponentSize;
}
- @SuppressCompatibility @androidx.compose.material3.common.ExperimentalMaterial3CommonApi public final class TonalPalette {
+ public final class TonalPalette {
ctor public TonalPalette(long neutral100, long neutral99, long neutral98, long neutral96, long neutral95, long neutral94, long neutral92, long neutral90, long neutral87, long neutral80, long neutral70, long neutral60, long neutral50, long neutral40, long neutral30, long neutral24, long neutral22, long neutral20, long neutral17, long neutral12, long neutral10, long neutral6, long neutral4, long neutral0, long neutralVariant100, long neutralVariant99, long neutralVariant98, long neutralVariant96, long neutralVariant95, long neutralVariant94, long neutralVariant92, long neutralVariant90, long neutralVariant87, long neutralVariant80, long neutralVariant70, long neutralVariant60, long neutralVariant50, long neutralVariant40, long neutralVariant30, long neutralVariant24, long neutralVariant22, long neutralVariant20, long neutralVariant17, long neutralVariant12, long neutralVariant10, long neutralVariant6, long neutralVariant4, long neutralVariant0, long primary100, long primary99, long primary95, long primary90, long primary80, long primary70, long primary60, long primary50, long primary40, long primary30, long primary20, long primary10, long primary0, long secondary100, long secondary99, long secondary95, long secondary90, long secondary80, long secondary70, long secondary60, long secondary50, long secondary40, long secondary30, long secondary20, long secondary10, long secondary0, long tertiary100, long tertiary99, long tertiary95, long tertiary90, long tertiary80, long tertiary70, long tertiary60, long tertiary50, long tertiary40, long tertiary30, long tertiary20, long tertiary10, long tertiary0);
method public long getNeutral0();
method public long getNeutral10();
@@ -195,7 +195,7 @@
property public final long tertiary99;
}
- @SuppressCompatibility @androidx.compose.material3.common.ExperimentalMaterial3CommonApi public final class TonalPaletteDefaults {
+ public final class TonalPaletteDefaults {
method public androidx.compose.material3.common.TonalPalette getBaselineTonalPalette();
property public final androidx.compose.material3.common.TonalPalette BaselineTonalPalette;
field public static final androidx.compose.material3.common.TonalPaletteDefaults INSTANCE;
diff --git a/compose/material3/material3-common/api/restricted_current.txt b/compose/material3/material3-common/api/restricted_current.txt
index 55ca903..48edfd6 100644
--- a/compose/material3/material3-common/api/restricted_current.txt
+++ b/compose/material3/material3-common/api/restricted_current.txt
@@ -17,7 +17,7 @@
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> LocalMinimumInteractiveComponentSize;
}
- @SuppressCompatibility @androidx.compose.material3.common.ExperimentalMaterial3CommonApi public final class TonalPalette {
+ public final class TonalPalette {
ctor public TonalPalette(long neutral100, long neutral99, long neutral98, long neutral96, long neutral95, long neutral94, long neutral92, long neutral90, long neutral87, long neutral80, long neutral70, long neutral60, long neutral50, long neutral40, long neutral30, long neutral24, long neutral22, long neutral20, long neutral17, long neutral12, long neutral10, long neutral6, long neutral4, long neutral0, long neutralVariant100, long neutralVariant99, long neutralVariant98, long neutralVariant96, long neutralVariant95, long neutralVariant94, long neutralVariant92, long neutralVariant90, long neutralVariant87, long neutralVariant80, long neutralVariant70, long neutralVariant60, long neutralVariant50, long neutralVariant40, long neutralVariant30, long neutralVariant24, long neutralVariant22, long neutralVariant20, long neutralVariant17, long neutralVariant12, long neutralVariant10, long neutralVariant6, long neutralVariant4, long neutralVariant0, long primary100, long primary99, long primary95, long primary90, long primary80, long primary70, long primary60, long primary50, long primary40, long primary30, long primary20, long primary10, long primary0, long secondary100, long secondary99, long secondary95, long secondary90, long secondary80, long secondary70, long secondary60, long secondary50, long secondary40, long secondary30, long secondary20, long secondary10, long secondary0, long tertiary100, long tertiary99, long tertiary95, long tertiary90, long tertiary80, long tertiary70, long tertiary60, long tertiary50, long tertiary40, long tertiary30, long tertiary20, long tertiary10, long tertiary0);
method public long getNeutral0();
method public long getNeutral10();
@@ -195,7 +195,7 @@
property public final long tertiary99;
}
- @SuppressCompatibility @androidx.compose.material3.common.ExperimentalMaterial3CommonApi public final class TonalPaletteDefaults {
+ public final class TonalPaletteDefaults {
method public androidx.compose.material3.common.TonalPalette getBaselineTonalPalette();
property public final androidx.compose.material3.common.TonalPalette BaselineTonalPalette;
field public static final androidx.compose.material3.common.TonalPaletteDefaults INSTANCE;
diff --git a/compose/material3/material3-common/build.gradle b/compose/material3/material3-common/build.gradle
index cb14811..32cbf10 100644
--- a/compose/material3/material3-common/build.gradle
+++ b/compose/material3/material3-common/build.gradle
@@ -117,4 +117,5 @@
description = "Compose Material 3 Common Library. This library contains foundational, themeless " +
"components that can be shared between different Material libraries or used by app" +
" developers. It builds upon the Jetpack Compose libraries."
+ samples(project(":compose:material3:material3-common:material3-common-samples"))
}
diff --git a/compose/material3/material3-common/samples/build.gradle b/compose/material3/material3-common/samples/build.gradle
new file mode 100644
index 0000000..057acd9
--- /dev/null
+++ b/compose/material3/material3-common/samples/build.gradle
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+
+ implementation(libs.kotlinStdlib)
+
+ compileOnly(project(":annotation:annotation-sampled"))
+
+ implementation("androidx.compose.material:material-icons-extended:1.6.0")
+ implementation(project(":compose:material3:material3-common"))
+ implementation("androidx.compose.runtime:runtime:1.2.1")
+ implementation("androidx.compose.ui:ui:1.2.1")
+ implementation("androidx.compose.ui:ui-tooling-preview:1.4.1")
+
+ debugImplementation("androidx.compose.ui:ui-tooling:1.4.1")
+}
+
+androidx {
+ name = "Compose Material3 Common Samples"
+ type = LibraryType.SAMPLES
+ inceptionYear = "2024"
+ description = "Contains the sample code for the AndroidX Compose Material3-common components."
+}
+
+android {
+ namespace "androidx.compose.material3.common.samples"
+}
diff --git a/compose/material3/material3-common/samples/src/main/java/androidx/compose/material3/common/samples/IconSamples.kt b/compose/material3/material3-common/samples/src/main/java/androidx/compose/material3/common/samples/IconSamples.kt
new file mode 100644
index 0000000..80f62fa
--- /dev/null
+++ b/compose/material3/material3-common/samples/src/main/java/androidx/compose/material3/common/samples/IconSamples.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.common.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Lock
+import androidx.compose.material3.common.Icon
+import androidx.compose.material3.common.TonalPaletteDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+
+@Preview
+@Sampled
+@Composable
+fun IconSample() {
+ Icon(
+ Icons.Outlined.Lock,
+ contentDescription = "Localized description",
+ tint = TonalPaletteDefaults.BaselineTonalPalette.primary100
+ )
+}
diff --git a/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/Icon.kt b/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/Icon.kt
index ec79327..f219c21 100644
--- a/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/Icon.kt
+++ b/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/Icon.kt
@@ -60,6 +60,8 @@
* @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.
+ *
+ * @sample androidx.compose.material3.common.samples.IconSample
*/
@Composable
fun Icon(
diff --git a/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/TonalPalette.kt b/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/TonalPalette.kt
index 32a2e46..4326a8d 100644
--- a/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/TonalPalette.kt
+++ b/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/TonalPalette.kt
@@ -33,7 +33,6 @@
* - Secondary (S)
* - Tertiary (T)
*/
-@ExperimentalMaterial3CommonApi
class TonalPalette(
// The neutral tonal range from the generated dynamic color palette.
// Ordered from the lightest shade [neutral100] to the darkest shade [neutral0].
@@ -139,7 +138,6 @@
val tertiary0: Color
)
-@ExperimentalMaterial3CommonApi
object TonalPaletteDefaults {
/**
* Baseline colors in Material.
diff --git a/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/androidx-compose-material3-material3-common-documentation.md b/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/androidx-compose-material3-material3-common-documentation.md
deleted file mode 100644
index e837788..0000000
--- a/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/androidx-compose-material3-material3-common-documentation.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# Module root
-
-Compose Material 3 Common
-
-# Package androidx.compose.material3.common
-
-## Overview
-
diff --git a/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/compose-material3-material3-common-documentation.md b/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/compose-material3-material3-common-documentation.md
new file mode 100644
index 0000000..6f5f807
--- /dev/null
+++ b/compose/material3/material3-common/src/commonMain/kotlin/androidx/compose/material3/common/compose-material3-material3-common-documentation.md
@@ -0,0 +1,24 @@
+# Module root
+
+Compose Material 3 Common
+
+# Package androidx.compose.material3.common
+This library contains themeless components that can be shared between different Compose Material libraries to build Material Design components. It builds upon the Jetpack Compose libraries.
+
+In this page, you'll find documentation for types, properties, and functions available in the `androidx.compose.material3.common` package.
+
+## Overview
+
+### Color Palette
+
+| | **APIs** | **Description** |
+|-------------------|----------------|-------------------------------------------------|
+| **Tonal Palette** | [TonalPalette] | Tonal palette used to build the M3 color scheme |
+
+### Icons
+
+| | **APIs** | **Description** |
+| ---- | -------- | --------------- |
+| **Icon** | [Icon] | M3 icon |
+
+Also check out the <a href="https://developer.android.com/reference/kotlin/androidx/compose/material/icons/package-summary" class="external" target="_blank">androidx.compose.material.icons package</a>.
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index e0a473f..c6e24d83 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -394,7 +394,7 @@
}
@androidx.compose.runtime.Immutable public final class ColorScheme {
- ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline, long outlineVariant, long scrim);
+ ctor @Deprecated public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline, long outlineVariant, long scrim);
ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline, long outlineVariant, long scrim, long surfaceBright, long surfaceDim, long surfaceContainer, long surfaceContainerHigh, long surfaceContainerHighest, long surfaceContainerLow, long surfaceContainerLowest);
method @Deprecated public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
method public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim, optional long surfaceBright, optional long surfaceDim, optional long surfaceContainer, optional long surfaceContainerHigh, optional long surfaceContainerHighest, optional long surfaceContainerLow, optional long surfaceContainerLowest);
@@ -1121,7 +1121,8 @@
}
@androidx.compose.runtime.Immutable public final class OutlinedTextFieldDefaults {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void ContainerBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape, optional float focusedBorderThickness, optional float unfocusedBorderThickness);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Container(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape, optional float focusedBorderThickness, optional float unfocusedBorderThickness);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void ContainerBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape, optional float focusedBorderThickness, optional float unfocusedBorderThickness);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void DecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> container);
method @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors colors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors colors(optional long focusedTextColor, optional long unfocusedTextColor, optional long disabledTextColor, optional long errorTextColor, optional long focusedContainerColor, optional long unfocusedContainerColor, optional long disabledContainerColor, optional long errorContainerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors? selectionColors, optional long focusedBorderColor, optional long unfocusedBorderColor, optional long disabledBorderColor, optional long errorBorderColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long focusedPlaceholderColor, optional long unfocusedPlaceholderColor, optional long disabledPlaceholderColor, optional long errorPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
@@ -1282,17 +1283,20 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class SearchBarColors {
- ctor public SearchBarColors(long containerColor, long dividerColor, androidx.compose.material3.TextFieldColors inputFieldColors);
+ ctor public SearchBarColors(long containerColor, long dividerColor);
+ ctor @Deprecated public SearchBarColors(long containerColor, long dividerColor, @Deprecated androidx.compose.material3.TextFieldColors inputFieldColors);
method public long getContainerColor();
method public long getDividerColor();
- method public androidx.compose.material3.TextFieldColors getInputFieldColors();
+ method @Deprecated public androidx.compose.material3.TextFieldColors getInputFieldColors();
property public final long containerColor;
property public final long dividerColor;
- property public final androidx.compose.material3.TextFieldColors inputFieldColors;
+ property @Deprecated public final androidx.compose.material3.TextFieldColors inputFieldColors;
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class SearchBarDefaults {
- method @androidx.compose.runtime.Composable public androidx.compose.material3.SearchBarColors colors(optional long containerColor, optional long dividerColor, optional androidx.compose.material3.TextFieldColors inputFieldColors);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void InputField(String query, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onQueryChange, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onSearch, boolean expanded, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onExpandedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.SearchBarColors colors(optional long containerColor, optional long dividerColor);
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material3.SearchBarColors colors(optional long containerColor, optional long dividerColor, optional androidx.compose.material3.TextFieldColors inputFieldColors);
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getDockedShape();
method @Deprecated public float getElevation();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFullScreenShape();
@@ -1315,8 +1319,10 @@
}
public final class SearchBar_androidKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void DockedSearchBar(String query, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onQueryChange, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onSearch, boolean active, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onActiveChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SearchBar(String query, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onQueryChange, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onSearch, boolean active, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onActiveChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void DockedSearchBar(String query, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onQueryChange, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onSearch, boolean active, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onActiveChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void DockedSearchBar(kotlin.jvm.functions.Function0<kotlin.Unit> inputField, boolean expanded, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onExpandedChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SearchBar(String query, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onQueryChange, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onSearch, boolean active, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onActiveChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SearchBar(kotlin.jvm.functions.Function0<kotlin.Unit> inputField, boolean expanded, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onExpandedChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
}
@androidx.compose.runtime.Immutable public final class SegmentedButtonColors {
@@ -1854,7 +1860,8 @@
}
@androidx.compose.runtime.Immutable public final class TextFieldDefaults {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void ContainerBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Container(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape, optional float focusedIndicatorLineThickness, optional float unfocusedIndicatorLineThickness);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void ContainerBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void DecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> container);
method @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors colors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors colors(optional long focusedTextColor, optional long unfocusedTextColor, optional long disabledTextColor, optional long errorTextColor, optional long focusedContainerColor, optional long unfocusedContainerColor, optional long disabledContainerColor, optional long errorContainerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors? selectionColors, optional long focusedIndicatorColor, optional long unfocusedIndicatorColor, optional long disabledIndicatorColor, optional long errorIndicatorColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long focusedPlaceholderColor, optional long unfocusedPlaceholderColor, optional long disabledPlaceholderColor, optional long errorPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
@@ -2131,12 +2138,23 @@
field public static final androidx.compose.material3.carousel.CarouselDefaults INSTANCE;
}
- public final class CarouselKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalMultiBrowseCarousel(androidx.compose.material3.carousel.CarouselState state, float preferredItemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional float minSmallItemWidth, optional float maxSmallItemWidth, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalUncontainedCarousel(androidx.compose.material3.carousel.CarouselState state, float itemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselItemInfo {
+ method public float getMaxSize();
+ method public float getMinSize();
+ method public float getSize();
+ property public abstract float maxSize;
+ property public abstract float minSize;
+ property public abstract float size;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselScope {
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselItemScope {
+ method public androidx.compose.material3.carousel.CarouselItemInfo getCarouselItemInfo();
+ property public abstract androidx.compose.material3.carousel.CarouselItemInfo carouselItemInfo;
+ }
+
+ public final class CarouselKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalMultiBrowseCarousel(androidx.compose.material3.carousel.CarouselState state, float preferredItemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional float minSmallItemWidth, optional float maxSmallItemWidth, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselItemScope,? super java.lang.Integer,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalUncontainedCarousel(androidx.compose.material3.carousel.CarouselState state, float itemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselItemScope,? super java.lang.Integer,kotlin.Unit> content);
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class CarouselState implements androidx.compose.foundation.gestures.ScrollableState {
@@ -2157,7 +2175,9 @@
}
public final class CarouselStateKt {
+ method public static float endOffset(androidx.compose.material3.carousel.CarouselItemInfo);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.carousel.CarouselState rememberCarouselState(optional int initialItem, kotlin.jvm.functions.Function0<java.lang.Integer> itemCount);
+ method public static float startOffset(androidx.compose.material3.carousel.CarouselItemInfo);
}
}
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index e0a473f..c6e24d83 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -394,7 +394,7 @@
}
@androidx.compose.runtime.Immutable public final class ColorScheme {
- ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline, long outlineVariant, long scrim);
+ ctor @Deprecated public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline, long outlineVariant, long scrim);
ctor public ColorScheme(long primary, long onPrimary, long primaryContainer, long onPrimaryContainer, long inversePrimary, long secondary, long onSecondary, long secondaryContainer, long onSecondaryContainer, long tertiary, long onTertiary, long tertiaryContainer, long onTertiaryContainer, long background, long onBackground, long surface, long onSurface, long surfaceVariant, long onSurfaceVariant, long surfaceTint, long inverseSurface, long inverseOnSurface, long error, long onError, long errorContainer, long onErrorContainer, long outline, long outlineVariant, long scrim, long surfaceBright, long surfaceDim, long surfaceContainer, long surfaceContainerHigh, long surfaceContainerHighest, long surfaceContainerLow, long surfaceContainerLowest);
method @Deprecated public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim);
method public androidx.compose.material3.ColorScheme copy(optional long primary, optional long onPrimary, optional long primaryContainer, optional long onPrimaryContainer, optional long inversePrimary, optional long secondary, optional long onSecondary, optional long secondaryContainer, optional long onSecondaryContainer, optional long tertiary, optional long onTertiary, optional long tertiaryContainer, optional long onTertiaryContainer, optional long background, optional long onBackground, optional long surface, optional long onSurface, optional long surfaceVariant, optional long onSurfaceVariant, optional long surfaceTint, optional long inverseSurface, optional long inverseOnSurface, optional long error, optional long onError, optional long errorContainer, optional long onErrorContainer, optional long outline, optional long outlineVariant, optional long scrim, optional long surfaceBright, optional long surfaceDim, optional long surfaceContainer, optional long surfaceContainerHigh, optional long surfaceContainerHighest, optional long surfaceContainerLow, optional long surfaceContainerLowest);
@@ -1121,7 +1121,8 @@
}
@androidx.compose.runtime.Immutable public final class OutlinedTextFieldDefaults {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void ContainerBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape, optional float focusedBorderThickness, optional float unfocusedBorderThickness);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Container(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape, optional float focusedBorderThickness, optional float unfocusedBorderThickness);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void ContainerBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape, optional float focusedBorderThickness, optional float unfocusedBorderThickness);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void DecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> container);
method @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors colors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors colors(optional long focusedTextColor, optional long unfocusedTextColor, optional long disabledTextColor, optional long errorTextColor, optional long focusedContainerColor, optional long unfocusedContainerColor, optional long disabledContainerColor, optional long errorContainerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors? selectionColors, optional long focusedBorderColor, optional long unfocusedBorderColor, optional long disabledBorderColor, optional long errorBorderColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long focusedPlaceholderColor, optional long unfocusedPlaceholderColor, optional long disabledPlaceholderColor, optional long errorPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
@@ -1282,17 +1283,20 @@
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class SearchBarColors {
- ctor public SearchBarColors(long containerColor, long dividerColor, androidx.compose.material3.TextFieldColors inputFieldColors);
+ ctor public SearchBarColors(long containerColor, long dividerColor);
+ ctor @Deprecated public SearchBarColors(long containerColor, long dividerColor, @Deprecated androidx.compose.material3.TextFieldColors inputFieldColors);
method public long getContainerColor();
method public long getDividerColor();
- method public androidx.compose.material3.TextFieldColors getInputFieldColors();
+ method @Deprecated public androidx.compose.material3.TextFieldColors getInputFieldColors();
property public final long containerColor;
property public final long dividerColor;
- property public final androidx.compose.material3.TextFieldColors inputFieldColors;
+ property @Deprecated public final androidx.compose.material3.TextFieldColors inputFieldColors;
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class SearchBarDefaults {
- method @androidx.compose.runtime.Composable public androidx.compose.material3.SearchBarColors colors(optional long containerColor, optional long dividerColor, optional androidx.compose.material3.TextFieldColors inputFieldColors);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void InputField(String query, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onQueryChange, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onSearch, boolean expanded, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onExpandedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.SearchBarColors colors(optional long containerColor, optional long dividerColor);
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material3.SearchBarColors colors(optional long containerColor, optional long dividerColor, optional androidx.compose.material3.TextFieldColors inputFieldColors);
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getDockedShape();
method @Deprecated public float getElevation();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFullScreenShape();
@@ -1315,8 +1319,10 @@
}
public final class SearchBar_androidKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void DockedSearchBar(String query, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onQueryChange, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onSearch, boolean active, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onActiveChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SearchBar(String query, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onQueryChange, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onSearch, boolean active, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onActiveChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void DockedSearchBar(String query, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onQueryChange, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onSearch, boolean active, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onActiveChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void DockedSearchBar(kotlin.jvm.functions.Function0<kotlin.Unit> inputField, boolean expanded, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onExpandedChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional float tonalElevation, optional float shadowElevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SearchBar(String query, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onQueryChange, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onSearch, boolean active, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onActiveChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SearchBar(kotlin.jvm.functions.Function0<kotlin.Unit> inputField, boolean expanded, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onExpandedChange, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SearchBarColors colors, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
}
@androidx.compose.runtime.Immutable public final class SegmentedButtonColors {
@@ -1854,7 +1860,8 @@
}
@androidx.compose.runtime.Immutable public final class TextFieldDefaults {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void ContainerBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void Container(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape, optional float focusedIndicatorLineThickness, optional float unfocusedIndicatorLineThickness);
+ method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void ContainerBox(boolean enabled, boolean isError, androidx.compose.foundation.interaction.InteractionSource interactionSource, androidx.compose.material3.TextFieldColors colors, optional androidx.compose.ui.graphics.Shape shape);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public void DecorationBox(String value, kotlin.jvm.functions.Function0<kotlin.Unit> innerTextField, boolean enabled, boolean singleLine, androidx.compose.ui.text.input.VisualTransformation visualTransformation, androidx.compose.foundation.interaction.InteractionSource interactionSource, optional boolean isError, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? prefix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? suffix, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function0<kotlin.Unit> container);
method @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors colors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.TextFieldColors colors(optional long focusedTextColor, optional long unfocusedTextColor, optional long disabledTextColor, optional long errorTextColor, optional long focusedContainerColor, optional long unfocusedContainerColor, optional long disabledContainerColor, optional long errorContainerColor, optional long cursorColor, optional long errorCursorColor, optional androidx.compose.foundation.text.selection.TextSelectionColors? selectionColors, optional long focusedIndicatorColor, optional long unfocusedIndicatorColor, optional long disabledIndicatorColor, optional long errorIndicatorColor, optional long focusedLeadingIconColor, optional long unfocusedLeadingIconColor, optional long disabledLeadingIconColor, optional long errorLeadingIconColor, optional long focusedTrailingIconColor, optional long unfocusedTrailingIconColor, optional long disabledTrailingIconColor, optional long errorTrailingIconColor, optional long focusedLabelColor, optional long unfocusedLabelColor, optional long disabledLabelColor, optional long errorLabelColor, optional long focusedPlaceholderColor, optional long unfocusedPlaceholderColor, optional long disabledPlaceholderColor, optional long errorPlaceholderColor, optional long focusedSupportingTextColor, optional long unfocusedSupportingTextColor, optional long disabledSupportingTextColor, optional long errorSupportingTextColor, optional long focusedPrefixColor, optional long unfocusedPrefixColor, optional long disabledPrefixColor, optional long errorPrefixColor, optional long focusedSuffixColor, optional long unfocusedSuffixColor, optional long disabledSuffixColor, optional long errorSuffixColor);
@@ -2131,12 +2138,23 @@
field public static final androidx.compose.material3.carousel.CarouselDefaults INSTANCE;
}
- public final class CarouselKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalMultiBrowseCarousel(androidx.compose.material3.carousel.CarouselState state, float preferredItemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional float minSmallItemWidth, optional float maxSmallItemWidth, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalUncontainedCarousel(androidx.compose.material3.carousel.CarouselState state, float itemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselItemInfo {
+ method public float getMaxSize();
+ method public float getMinSize();
+ method public float getSize();
+ property public abstract float maxSize;
+ property public abstract float minSize;
+ property public abstract float size;
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselScope {
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselItemScope {
+ method public androidx.compose.material3.carousel.CarouselItemInfo getCarouselItemInfo();
+ property public abstract androidx.compose.material3.carousel.CarouselItemInfo carouselItemInfo;
+ }
+
+ public final class CarouselKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalMultiBrowseCarousel(androidx.compose.material3.carousel.CarouselState state, float preferredItemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional float minSmallItemWidth, optional float maxSmallItemWidth, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselItemScope,? super java.lang.Integer,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalUncontainedCarousel(androidx.compose.material3.carousel.CarouselState state, float itemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselItemScope,? super java.lang.Integer,kotlin.Unit> content);
}
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class CarouselState implements androidx.compose.foundation.gestures.ScrollableState {
@@ -2157,7 +2175,9 @@
}
public final class CarouselStateKt {
+ method public static float endOffset(androidx.compose.material3.carousel.CarouselItemInfo);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.carousel.CarouselState rememberCarouselState(optional int initialItem, kotlin.jvm.functions.Function0<java.lang.Integer> itemCount);
+ method public static float startOffset(androidx.compose.material3.carousel.CarouselItemInfo);
}
}
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 71aa822..242201a 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -67,6 +67,7 @@
import androidx.compose.material3.samples.ExposedDropdownMenuSample
import androidx.compose.material3.samples.ExtendedFloatingActionButtonSample
import androidx.compose.material3.samples.ExtendedFloatingActionButtonTextSample
+import androidx.compose.material3.samples.FadingHorizontalMultiBrowseCarouselSample
import androidx.compose.material3.samples.FancyIndicatorContainerTabs
import androidx.compose.material3.samples.FancyIndicatorTabs
import androidx.compose.material3.samples.FancyTabs
@@ -340,6 +341,13 @@
sourceUrl = CarouselExampleSourceUrl
) {
HorizontalUncontainedCarouselSample()
+ },
+ Example(
+ name = ::FadingHorizontalMultiBrowseCarouselSample.name,
+ description = CarouselExampleDescription,
+ sourceUrl = CarouselExampleSourceUrl
+ ) {
+ FadingHorizontalMultiBrowseCarouselSample()
}
)
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
index 5410945..90522ff 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
@@ -20,17 +20,21 @@
import androidx.annotation.Sampled
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Text
import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel
import androidx.compose.material3.carousel.HorizontalUncontainedCarousel
import androidx.compose.material3.carousel.rememberCarouselState
+import androidx.compose.material3.carousel.startOffset
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -123,3 +127,60 @@
}
}
}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Sampled
+@Composable
+fun FadingHorizontalMultiBrowseCarouselSample() {
+
+ data class CarouselItem(
+ val id: Int,
+ @DrawableRes val imageResId: Int,
+ @StringRes val contentDescriptionResId: Int
+ )
+
+ val items = listOf(
+ CarouselItem(0, R.drawable.carousel_image_1, R.string.carousel_image_1_description),
+ CarouselItem(1, R.drawable.carousel_image_2, R.string.carousel_image_2_description),
+ CarouselItem(2, R.drawable.carousel_image_3, R.string.carousel_image_3_description),
+ CarouselItem(3, R.drawable.carousel_image_4, R.string.carousel_image_4_description),
+ CarouselItem(4, R.drawable.carousel_image_5, R.string.carousel_image_5_description),
+ )
+ val state = rememberCarouselState { items.count() }
+ HorizontalMultiBrowseCarousel(
+ state = state,
+ modifier = Modifier
+ .width(412.dp)
+ .height(221.dp),
+ preferredItemWidth = 130.dp,
+ itemSpacing = 8.dp,
+ contentPadding = PaddingValues(horizontal = 16.dp)
+ ) { i ->
+ val item = items[i]
+ Card(
+ modifier = Modifier
+ .height(205.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .graphicsLayer {
+ alpha = carouselItemInfo.size / carouselItemInfo.maxSize
+ }
+ ) {
+ Image(
+ painter = painterResource(id = item.imageResId),
+ contentDescription = stringResource(item.contentDescriptionResId),
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
+ Text(
+ text = "sample text",
+ modifier = Modifier.graphicsLayer {
+ translationX = carouselItemInfo.startOffset()
+ }
+ )
+ }
+ }
+ }
+}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/IconButtonSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/IconButtonSamples.kt
index 5eebb1e..38ce76c 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/IconButtonSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/IconButtonSamples.kt
@@ -75,7 +75,6 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Sampled
@Composable
@@ -85,7 +84,6 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Sampled
@Composable
@@ -100,7 +98,6 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Sampled
@Composable
@@ -110,7 +107,6 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Sampled
@Composable
@@ -125,7 +121,6 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Sampled
@Composable
@@ -135,7 +130,6 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Sampled
@Composable
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt
index 6555891..36f9db7 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt
@@ -21,11 +21,18 @@
import androidx.compose.animation.core.animate
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.ListItem
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
@@ -62,18 +69,33 @@
state.endRefresh()
}
}
- Box(Modifier.nestedScroll(state.nestedScrollConnection)) {
- LazyColumn(Modifier.fillMaxSize()) {
- if (!state.isRefreshing) {
- items(itemCount) {
- ListItem({ Text(text = "Item ${itemCount - it}") })
+ Scaffold(
+ modifier = Modifier.nestedScroll(state.nestedScrollConnection),
+ topBar = {
+ TopAppBar(
+ title = { Text("TopAppBar") },
+ // Provide an accessible alternative to trigger refresh.
+ actions = {
+ IconButton(onClick = { state.startRefresh() }) {
+ Icon(Icons.Filled.Refresh, "Trigger Refresh")
+ }
+ }
+ )
+ }
+ ) {
+ Box(Modifier.padding(it)) {
+ LazyColumn(Modifier.fillMaxSize()) {
+ if (!state.isRefreshing) {
+ items(itemCount) {
+ ListItem({ Text(text = "Item ${itemCount - it}") })
+ }
}
}
+ PullToRefreshContainer(
+ modifier = Modifier.align(Alignment.TopCenter),
+ state = state,
+ )
}
- PullToRefreshContainer(
- modifier = Modifier.align(Alignment.TopCenter),
- state = state,
- )
}
}
@@ -95,20 +117,35 @@
val scaleFraction = if (state.isRefreshing) 1f else
LinearOutSlowInEasing.transform(state.progress).coerceIn(0f, 1f)
- Box(Modifier.nestedScroll(state.nestedScrollConnection)) {
- LazyColumn(Modifier.fillMaxSize()) {
- if (!state.isRefreshing) {
- items(itemCount) {
- ListItem({ Text(text = "Item ${itemCount - it}") })
+ Scaffold(
+ modifier = Modifier.nestedScroll(state.nestedScrollConnection),
+ topBar = {
+ TopAppBar(
+ title = { Text("TopAppBar") },
+ // Provide an accessible alternative to trigger refresh.
+ actions = {
+ IconButton(onClick = { state.startRefresh() }) {
+ Icon(Icons.Filled.Refresh, "Trigger Refresh")
+ }
+ }
+ )
+ }
+ ) {
+ Box(Modifier.padding(it)) {
+ LazyColumn(Modifier.fillMaxSize()) {
+ if (!state.isRefreshing) {
+ items(itemCount) {
+ ListItem({ Text(text = "Item ${itemCount - it}") })
+ }
}
}
+ PullToRefreshContainer(
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .graphicsLayer(scaleX = scaleFraction, scaleY = scaleFraction),
+ state = state,
+ )
}
- PullToRefreshContainer(
- modifier = Modifier
- .align(Alignment.TopCenter)
- .graphicsLayer(scaleX = scaleFraction, scaleY = scaleFraction),
- state = state,
- )
}
}
@@ -127,18 +164,33 @@
state.endRefresh()
}
}
- Box(Modifier.nestedScroll(state.nestedScrollConnection)) {
- LazyColumn(Modifier.fillMaxSize()) {
- if (!state.isRefreshing) {
- items(itemCount) {
- ListItem({ Text(text = "Item ${itemCount - it}") })
+ Scaffold(
+ modifier = Modifier.nestedScroll(state.nestedScrollConnection),
+ topBar = {
+ TopAppBar(
+ title = { Text("TopAppBar") },
+ // Provide an accessible alternative to trigger refresh.
+ actions = {
+ IconButton(onClick = { state.startRefresh() }) {
+ Icon(Icons.Filled.Refresh, "Trigger Refresh")
+ }
+ }
+ )
+ }
+ ) {
+ Box(Modifier.padding(it)) {
+ LazyColumn(Modifier.fillMaxSize()) {
+ if (!state.isRefreshing) {
+ items(itemCount) {
+ ListItem({ Text(text = "Item ${itemCount - it}") })
+ }
}
}
- }
- if (state.isRefreshing) {
- LinearProgressIndicator()
- } else {
- LinearProgressIndicator(progress = { state.progress })
+ if (state.isRefreshing) {
+ LinearProgressIndicator()
+ } else {
+ LinearProgressIndicator(progress = { state.progress })
+ }
}
}
}
@@ -171,7 +223,7 @@
available: Offset,
source: NestedScrollSource,
): Offset = when {
- source == NestedScrollSource.Drag && available.y < 0 -> {
+ source == NestedScrollSource.UserInput && available.y < 0 -> {
// Swiping up
val y = if (isRefreshing) 0f else {
val newOffset = (verticalOffset + available.y).coerceAtLeast(0f)
@@ -190,7 +242,7 @@
available: Offset,
source: NestedScrollSource
): Offset = when {
- source == NestedScrollSource.Drag && available.y > 0 -> {
+ source == NestedScrollSource.UserInput && available.y > 0 -> {
// Swiping Down
val y = if (isRefreshing) 0f else {
val newOffset = (verticalOffset + available.y).coerceAtLeast(0f)
@@ -226,18 +278,32 @@
}
}
}
-
- Box(Modifier.nestedScroll(state.nestedScrollConnection)) {
- LazyColumn(Modifier.fillMaxSize()) {
- if (!state.isRefreshing) {
- items(itemCount) {
- ListItem({ Text(text = "Item ${itemCount - it}") })
+ Scaffold(
+ modifier = Modifier.nestedScroll(state.nestedScrollConnection),
+ topBar = {
+ TopAppBar(
+ title = { Text("TopAppBar") },
+ // Provide an accessible alternative to trigger refresh.
+ actions = {
+ IconButton(onClick = { state.startRefresh() }) {
+ Icon(Icons.Filled.Refresh, "Trigger Refresh")
+ }
+ }
+ )
+ }
+ ) {
+ Box(Modifier.padding(it)) {
+ LazyColumn(Modifier.fillMaxSize()) {
+ if (!state.isRefreshing) {
+ items(itemCount) {
+ ListItem({ Text(text = "Item ${itemCount - it}") })
+ }
}
}
+ PullToRefreshContainer(
+ modifier = Modifier.align(Alignment.TopCenter),
+ state = state,
+ )
}
- PullToRefreshContainer(
- modifier = Modifier.align(Alignment.TopCenter),
- state = state,
- )
}
}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ScaffoldSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ScaffoldSamples.kt
index 05ebde2..b5602ae 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ScaffoldSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ScaffoldSamples.kt
@@ -115,7 +115,6 @@
)
}
-@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Sampled
@Composable
@@ -149,7 +148,6 @@
)
}
-@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Sampled
@Composable
@@ -186,7 +184,6 @@
)
}
-@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Sampled
@Composable
@@ -264,7 +261,6 @@
)
}
-@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Sampled
@Composable
@@ -313,7 +309,6 @@
)
}
-@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Sampled
@Composable
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SearchBarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SearchBarSamples.kt
index da7d53d..5e3e751 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SearchBarSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SearchBarSamples.kt
@@ -35,6 +35,7 @@
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.SearchBar
+import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -56,7 +57,7 @@
@Composable
fun SearchBarSample() {
var text by rememberSaveable { mutableStateOf("") }
- var active by rememberSaveable { mutableStateOf(false) }
+ var expanded by rememberSaveable { mutableStateOf(false) }
Box(
Modifier
@@ -67,16 +68,20 @@
modifier = Modifier
.align(Alignment.TopCenter)
.semantics { traversalIndex = -1f },
- query = text,
- onQueryChange = { text = it },
- onSearch = { active = false },
- active = active,
- onActiveChange = {
- active = it
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = text,
+ onQueryChange = { text = it },
+ onSearch = { expanded = false },
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ placeholder = { Text("Hinted search text") },
+ leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
+ trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = null) },
+ )
},
- placeholder = { Text("Hinted search text") },
- leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
- trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = null) },
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
) {
repeat(4) { idx ->
val resultText = "Suggestion $idx"
@@ -88,7 +93,7 @@
modifier = Modifier
.clickable {
text = resultText
- active = false
+ expanded = false
}
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
@@ -119,7 +124,7 @@
@Composable
fun DockedSearchBarSample() {
var text by rememberSaveable { mutableStateOf("") }
- var active by rememberSaveable { mutableStateOf(false) }
+ var expanded by rememberSaveable { mutableStateOf(false) }
Box(
Modifier
@@ -131,14 +136,20 @@
.align(Alignment.TopCenter)
.padding(top = 8.dp)
.semantics { traversalIndex = -1f },
- query = text,
- onQueryChange = { text = it },
- onSearch = { active = false },
- active = active,
- onActiveChange = { active = it },
- placeholder = { Text("Hinted search text") },
- leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
- trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = null) },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = text,
+ onQueryChange = { text = it },
+ onSearch = { expanded = false },
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ placeholder = { Text("Hinted search text") },
+ leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
+ trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = null) },
+ )
+ },
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
) {
repeat(4) { idx ->
val resultText = "Suggestion $idx"
@@ -150,7 +161,7 @@
modifier = Modifier
.clickable {
text = resultText
- active = false
+ expanded = false
}
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt
index 0749843..73738d5 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt
@@ -462,7 +462,7 @@
colors = colors,
// update border thickness and shape
container = {
- OutlinedTextFieldDefaults.ContainerBox(
+ OutlinedTextFieldDefaults.Container(
enabled = enabled,
isError = false,
colors = colors,
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
index df864c1..006e50e 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/BottomSheetScaffoldTest.kt
@@ -817,6 +817,73 @@
}
@Test
+ fun bottomSheetScaffold_gesturesDisabled_doesNotParticipateInNestedScroll() {
+ lateinit var sheetState: SheetState
+ lateinit var sheetContentScrollState: ScrollState
+ lateinit var scope: CoroutineScope
+
+ rule.setContent {
+ sheetState = rememberStandardBottomSheetState()
+ scope = rememberCoroutineScope()
+ BottomSheetScaffold(
+ scaffoldState = rememberBottomSheetScaffoldState(bottomSheetState = sheetState),
+ sheetSwipeEnabled = false,
+ sheetContent = {
+ sheetContentScrollState = rememberScrollState()
+ Column(
+ Modifier
+ .verticalScroll(sheetContentScrollState)
+ .testTag(sheetTag)
+ ) {
+ repeat(100) {
+ Text(it.toString(), Modifier.requiredHeight(50.dp))
+ }
+ }
+ },
+ sheetPeekHeight = peekHeight,
+ ) {
+ Box(Modifier.fillMaxSize()) {
+ Text("Content")
+ }
+ }
+ }
+
+ // Initial scrollState is at 0 and sheetState is partially expanded
+ assertThat(sheetContentScrollState.value).isEqualTo(0)
+ assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+
+ // Scrolling up within the sheet causes content to scroll without changing sheet state
+ // because swipe gestures are disabled.
+ rule.onNodeWithTag(sheetTag)
+ .performTouchInput {
+ swipeUp()
+ }
+ rule.waitForIdle()
+ assertThat(sheetContentScrollState.value).isGreaterThan(0)
+ assertThat(sheetState.currentValue).isEqualTo(SheetValue.PartiallyExpanded)
+
+ scope.launch {
+ sheetState.snapTo(SheetValue.Expanded)
+ sheetContentScrollState.scrollTo(10)
+ }
+ rule.waitForIdle()
+
+ // Initial scrollState is > 0 and sheetState is fully expanded
+ assertThat(sheetContentScrollState.value).isEqualTo(10)
+ assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+
+ // Scrolling down within the sheet causes content to scroll without changing sheet state
+ // because swipe gestures are disabled.
+ rule.onNodeWithTag(sheetTag)
+ .performTouchInput {
+ swipeDown()
+ }
+ rule.waitForIdle()
+ assertThat(sheetContentScrollState.value).isEqualTo(0)
+ assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
+ }
+
+ @Test
fun bottomSheetScaffold_sheetMaxWidth_sizeChanges_snapsToNewTarget() {
lateinit var sheetMaxWidth: MutableState<Dp>
var screenWidth by mutableStateOf(0.dp)
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
index 27f5ee5..a3464b2 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
@@ -1262,7 +1262,7 @@
nestedScrollDispatcher.dispatchPostScroll(
consumed = Offset.Zero,
available = Offset(x = 0f, y = scrollableContentHeight / 2f),
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
)
scope.launch {
nestedScrollDispatcher.dispatchPostFling(
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt
index 2dde725..2176687 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt
@@ -41,6 +41,7 @@
import androidx.compose.material3.internal.MinTextLineHeight
import androidx.compose.material3.internal.Strings
import androidx.compose.material3.internal.SupportingTopPadding
+import androidx.compose.material3.internal.TextFieldAnimationDuration
import androidx.compose.material3.internal.TextFieldPadding
import androidx.compose.material3.internal.getString
import androidx.compose.runtime.CompositionLocalProvider
@@ -1661,8 +1662,8 @@
focusRequester.requestFocus()
}
- // animation duration is 150, advancing by 75 to get into middle of animation
- rule.mainClock.advanceTimeBy(75)
+ // advance to middle of animation
+ rule.mainClock.advanceTimeBy(TextFieldAnimationDuration.toLong() / 2)
rule.runOnIdle {
assertThat(textStyle.color).isEqualTo(expectedLabelColor)
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt
index 3c5554b..d93530b 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarScreenshotTest.kt
@@ -57,16 +57,24 @@
private val testTag = "SearchBar"
@Test
- fun searchBar_inactive() {
+ fun searchBar_notExpanded() {
rule.setMaterialContent(scheme.colorScheme) {
+ val expanded = false
+ val onExpandedChange: (Boolean) -> Unit = {}
SearchBar(
modifier = Modifier.testTag(testTag),
- query = "",
- onQueryChange = {},
- onSearch = {},
- active = false,
- onActiveChange = {},
- placeholder = { Text("Hint") },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = expanded,
+ onExpandedChange = onExpandedChange,
+ placeholder = { Text("Hint") },
+ )
+ },
+ expanded = expanded,
+ onExpandedChange = onExpandedChange,
content = {},
)
}
@@ -76,15 +84,23 @@
@Test
fun searchBar_disabled() {
rule.setMaterialContent(scheme.colorScheme) {
+ val expanded = false
+ val onExpandedChange: (Boolean) -> Unit = {}
SearchBar(
modifier = Modifier.testTag(testTag),
- query = "",
- onQueryChange = {},
- onSearch = {},
- active = false,
- onActiveChange = {},
- enabled = false,
- placeholder = { Text("Hint") },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = expanded,
+ onExpandedChange = onExpandedChange,
+ enabled = false,
+ placeholder = { Text("Hint") },
+ )
+ },
+ expanded = expanded,
+ onExpandedChange = onExpandedChange,
content = {},
)
}
@@ -92,15 +108,23 @@
}
@Test
- fun searchBar_active() {
+ fun searchBar_expanded() {
rule.setMaterialContent(scheme.colorScheme) {
+ val expanded = true
+ val onExpandedChange: (Boolean) -> Unit = {}
SearchBar(
modifier = Modifier.testTag(testTag),
- query = "Query",
- onQueryChange = {},
- onSearch = {},
- active = true,
- onActiveChange = {},
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = expanded,
+ onExpandedChange = onExpandedChange,
+ )
+ },
+ expanded = expanded,
+ onExpandedChange = onExpandedChange,
content = { Text("Content") },
)
}
@@ -108,17 +132,25 @@
}
@Test
- fun searchBar_active_withIcons() {
+ fun searchBar_expanded_withIcons() {
rule.setMaterialContent(scheme.colorScheme) {
+ val expanded = true
+ val onExpandedChange: (Boolean) -> Unit = {}
SearchBar(
modifier = Modifier.testTag(testTag),
- query = "Query",
- onQueryChange = {},
- onSearch = {},
- active = true,
- onActiveChange = {},
- leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
- trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = null) },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = expanded,
+ onExpandedChange = onExpandedChange,
+ leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
+ trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = null) },
+ )
+ },
+ expanded = expanded,
+ onExpandedChange = onExpandedChange,
content = { Text("Content") },
)
}
@@ -126,15 +158,23 @@
}
@Test
- fun searchBar_active_customColors() {
+ fun searchBar_expanded_customColors() {
rule.setMaterialContent(lightColorScheme()) {
+ val expanded = true
+ val onExpandedChange: (Boolean) -> Unit = {}
SearchBar(
modifier = Modifier.testTag(testTag),
- query = "Query",
- onQueryChange = {},
- onSearch = {},
- active = true,
- onActiveChange = {},
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = expanded,
+ onExpandedChange = onExpandedChange,
+ )
+ },
+ expanded = expanded,
+ onExpandedChange = onExpandedChange,
colors = SearchBarDefaults.colors(
containerColor = Color.Yellow,
dividerColor = Color.Green,
@@ -146,16 +186,24 @@
}
@Test
- fun searchBar_shadow_inactive() {
+ fun searchBar_shadow_notExpanded() {
rule.setMaterialContent(lightColorScheme()) {
+ val expanded = false
+ val onExpandedChange: (Boolean) -> Unit = {}
SearchBar(
modifier = Modifier.testTag(testTag),
- query = "",
- onQueryChange = {},
- onSearch = {},
- active = false,
- onActiveChange = {},
- placeholder = { Text("Hint") },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = expanded,
+ onExpandedChange = onExpandedChange,
+ placeholder = { Text("Hint") },
+ )
+ },
+ expanded = expanded,
+ onExpandedChange = onExpandedChange,
shadowElevation = 6.dp,
content = {},
)
@@ -164,15 +212,24 @@
}
@Test
- fun searchBar_shadow_active() {
+ fun searchBar_shadow_expanded() {
rule.setMaterialContent(lightColorScheme()) {
+ val expanded = true
+ val onExpandedChange: (Boolean) -> Unit = {}
SearchBar(
modifier = Modifier.testTag(testTag),
- query = "Query",
- onQueryChange = {},
- onSearch = {},
- active = true,
- onActiveChange = {},
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = expanded,
+ onExpandedChange = onExpandedChange,
+ placeholder = { Text("Hint") },
+ )
+ },
+ expanded = expanded,
+ onExpandedChange = onExpandedChange,
shadowElevation = 6.dp,
content = { Text("Content") },
)
@@ -221,16 +278,22 @@
}
@Test
- fun dockedSearchBar_inactive() {
+ fun dockedSearchBar_notExpanded() {
rule.setMaterialContent(scheme.colorScheme) {
DockedSearchBar(
modifier = Modifier.testTag(testTag),
- query = "",
- onQueryChange = {},
- onSearch = {},
- active = false,
- onActiveChange = {},
- placeholder = { Text("Hint") },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = false,
+ onExpandedChange = {},
+ placeholder = { Text("Hint") },
+ )
+ },
+ expanded = false,
+ onExpandedChange = {},
content = {},
)
}
@@ -242,13 +305,19 @@
rule.setMaterialContent(scheme.colorScheme) {
DockedSearchBar(
modifier = Modifier.testTag(testTag),
- query = "",
- onQueryChange = {},
- onSearch = {},
- active = false,
- onActiveChange = {},
- enabled = false,
- placeholder = { Text("Hint") },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = false,
+ onExpandedChange = {},
+ enabled = false,
+ placeholder = { Text("Hint") },
+ )
+ },
+ expanded = false,
+ onExpandedChange = {},
content = {},
)
}
@@ -256,15 +325,21 @@
}
@Test
- fun dockedSearchBar_active() {
+ fun dockedSearchBar_expanded() {
rule.setMaterialContent(scheme.colorScheme) {
DockedSearchBar(
modifier = Modifier.testTag(testTag),
- query = "Query",
- onQueryChange = {},
- onSearch = {},
- active = true,
- onActiveChange = {},
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = true,
+ onExpandedChange = {},
+ )
+ },
+ expanded = true,
+ onExpandedChange = {},
content = { Text("Content") },
)
}
@@ -272,17 +347,23 @@
}
@Test
- fun dockedSearchBar_active_withIcons() {
+ fun dockedSearchBar_expanded_withIcons() {
rule.setMaterialContent(scheme.colorScheme) {
DockedSearchBar(
modifier = Modifier.testTag(testTag),
- query = "Query",
- onQueryChange = {},
- onSearch = {},
- active = true,
- onActiveChange = {},
- leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
- trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = null) },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = true,
+ onExpandedChange = {},
+ leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
+ trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = null) },
+ )
+ },
+ expanded = true,
+ onExpandedChange = {},
content = { Text("Content") },
)
}
@@ -290,15 +371,21 @@
}
@Test
- fun dockedSearchBar_active_customShape() {
+ fun dockedSearchBar_expanded_customShape() {
rule.setMaterialContent(lightColorScheme()) {
DockedSearchBar(
modifier = Modifier.testTag(testTag),
- query = "Query",
- onQueryChange = {},
- onSearch = {},
- active = true,
- onActiveChange = {},
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = true,
+ onExpandedChange = {},
+ )
+ },
+ expanded = true,
+ onExpandedChange = {},
shape = CutCornerShape(24.dp),
content = { Text("Content") },
)
@@ -307,15 +394,21 @@
}
@Test
- fun dockedSearchBar_active_customColors() {
+ fun dockedSearchBar_expanded_customColors() {
rule.setMaterialContent(lightColorScheme()) {
DockedSearchBar(
modifier = Modifier.testTag(testTag),
- query = "Query",
- onQueryChange = {},
- onSearch = {},
- active = true,
- onActiveChange = {},
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = true,
+ onExpandedChange = {},
+ )
+ },
+ expanded = true,
+ onExpandedChange = {},
colors = SearchBarDefaults.colors(
containerColor = Color.Yellow,
dividerColor = Color.Green,
@@ -327,16 +420,22 @@
}
@Test
- fun dockedSearchBar_shadow_inactive() {
+ fun dockedSearchBar_shadow_notExpanded() {
rule.setMaterialContent(lightColorScheme()) {
DockedSearchBar(
modifier = Modifier.testTag(testTag),
- query = "",
- onQueryChange = {},
- onSearch = {},
- active = false,
- onActiveChange = {},
- placeholder = { Text("Hint") },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = false,
+ onExpandedChange = {},
+ placeholder = { Text("Hint") },
+ )
+ },
+ expanded = false,
+ onExpandedChange = {},
shadowElevation = 6.dp,
content = {},
)
@@ -345,15 +444,21 @@
}
@Test
- fun dockedSearchBar_shadow_active() {
+ fun dockedSearchBar_shadow_expanded() {
rule.setMaterialContent(lightColorScheme()) {
DockedSearchBar(
modifier = Modifier.testTag(testTag),
- query = "Query",
- onQueryChange = {},
- onSearch = {},
- active = true,
- onActiveChange = {},
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = true,
+ onExpandedChange = {},
+ )
+ },
+ expanded = true,
+ onExpandedChange = {},
shadowElevation = 6.dp,
content = { Text("Content") },
)
@@ -414,12 +519,12 @@
currentBackEvent = currentBackEvent,
modifier = Modifier.testTag(testTag),
inputField = {
- SearchBarInputField(
+ SearchBarDefaults.InputField(
query = "Query",
onQueryChange = {},
onSearch = {},
- active = true,
- onActiveChange = {},
+ expanded = true,
+ onExpandedChange = {},
)
},
content = { Text("Content") },
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarTest.kt
index b025a39..f9d8102 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SearchBarTest.kt
@@ -65,22 +65,31 @@
private val BackTestTag = "Back"
@Test
- fun searchBar_becomesActiveAndFocusedOnClick_andInactiveAndUnfocusedOnBack() {
+ fun searchBar_becomesExpandedAndFocusedOnClick_andNotExpandedAndUnfocusedOnBack() {
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.fillMaxSize()) {
val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
- var active by remember { mutableStateOf(false) }
+ var expanded by remember { mutableStateOf(false) }
// Extra item for initial focus.
- Box(Modifier.size(10.dp).focusable())
+ Box(
+ Modifier
+ .size(10.dp)
+ .focusable())
SearchBar(
modifier = Modifier.testTag(SearchBarTestTag),
- query = "Query",
- onQueryChange = {},
- onSearch = {},
- active = active,
- onActiveChange = { active = it },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ )
+ },
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
) {
Button(
onClick = { dispatcher.onBackPressed() },
@@ -111,17 +120,25 @@
Column(Modifier.fillMaxSize()) {
SearchBar(
modifier = Modifier.testTag(SearchBarTestTag),
- query = "Query",
- onQueryChange = {},
- onSearch = {},
- active = false,
- onActiveChange = {},
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = false,
+ onExpandedChange = {},
+ )
+ },
+ expanded = false,
+ onExpandedChange = {},
content = {},
)
TextField(
value = "",
onValueChange = {},
- modifier = Modifier.testTag("SIBLING").focusRequester(focusRequester)
+ modifier = Modifier
+ .testTag("SIBLING")
+ .focusRequester(focusRequester)
)
}
}
@@ -142,14 +159,20 @@
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.fillMaxSize()) {
- var active by remember { mutableStateOf(true) }
+ var expanded by remember { mutableStateOf(true) }
SearchBar(
- query = "Query",
- onQueryChange = {},
- onSearch = { capturedSearchQuery = it },
- active = active,
- onActiveChange = { active = it },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = { capturedSearchQuery = it },
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ )
+ },
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
content = { Text("Content") },
)
}
@@ -160,15 +183,21 @@
}
@Test
- fun searchBar_inactiveSize() {
+ fun searchBar_notExpandedSize() {
rule.setMaterialContentForSizeAssertions {
SearchBar(
- query = "",
- onQueryChange = {},
- onSearch = {},
- active = false,
- onActiveChange = {},
- placeholder = { Text("Hint") },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = false,
+ onExpandedChange = {},
+ placeholder = { Text("Hint") },
+ )
+ },
+ expanded = false,
+ onExpandedChange = {},
content = {},
)
}
@@ -177,7 +206,7 @@
}
@Test
- fun searchBar_activeSize() {
+ fun searchBar_expandedSize() {
val totalHeight = 500.dp
val totalWidth = 325.dp
val searchBarSize = Ref<IntSize>()
@@ -188,12 +217,18 @@
modifier = Modifier.onGloballyPositioned {
searchBarSize.value = it.size
},
- query = "",
- onQueryChange = {},
- onSearch = {},
- active = true,
- onActiveChange = {},
- placeholder = { Text("Hint") },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = true,
+ onExpandedChange = {},
+ placeholder = { Text("Hint") },
+ )
+ },
+ expanded = true,
+ onExpandedChange = {},
content = { Text("Content") },
)
}
@@ -211,23 +246,29 @@
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.fillMaxSize()) {
- var active by remember { mutableStateOf(false) }
+ var expanded by remember { mutableStateOf(false) }
SearchBar(
modifier = Modifier.testTag(SearchBarTestTag),
- query = "Query",
- onQueryChange = {},
- onSearch = {},
- active = active,
- onActiveChange = { active = it },
- trailingIcon = {
- IconButton(
- onClick = { iconClicked = true },
- modifier = Modifier.testTag(IconTestTag)
- ) {
- Icon(Icons.Default.MoreVert, null)
- }
- }
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ trailingIcon = {
+ IconButton(
+ onClick = { iconClicked = true },
+ modifier = Modifier.testTag(IconTestTag)
+ ) {
+ Icon(Icons.Default.MoreVert, null)
+ }
+ }
+ )
+ },
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
) {
Text("Content")
}
@@ -247,22 +288,31 @@
}
@Test
- fun dockedSearchBar_becomesActiveAndFocusedOnClick_andInactiveAndUnfocusedOnBack() {
+ fun dockedSearchBar_becomesExpandedAndFocusedOnClick_andNotExpandedAndUnfocusedOnBack() {
rule.setMaterialContent(lightColorScheme()) {
Column(Modifier.fillMaxSize()) {
val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
- var active by remember { mutableStateOf(false) }
+ var expanded by remember { mutableStateOf(false) }
// Extra item for initial focus.
- Box(Modifier.size(10.dp).focusable())
+ Box(
+ Modifier
+ .size(10.dp)
+ .focusable())
DockedSearchBar(
modifier = Modifier.testTag(SearchBarTestTag),
- query = "Query",
- onQueryChange = {},
- onSearch = {},
- active = active,
- onActiveChange = { active = it },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ )
+ },
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
) {
Button(
onClick = { dispatcher.onBackPressed() },
@@ -293,17 +343,25 @@
Column(Modifier.fillMaxSize()) {
DockedSearchBar(
modifier = Modifier.testTag(SearchBarTestTag),
- query = "Query",
- onQueryChange = {},
- onSearch = {},
- active = false,
- onActiveChange = {},
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = false,
+ onExpandedChange = {},
+ )
+ },
+ expanded = false,
+ onExpandedChange = {},
content = {},
)
TextField(
value = "",
onValueChange = {},
- modifier = Modifier.testTag("SIBLING").focusRequester(focusRequester)
+ modifier = Modifier
+ .testTag("SIBLING")
+ .focusRequester(focusRequester)
)
}
}
@@ -324,14 +382,20 @@
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.fillMaxSize()) {
- var active by remember { mutableStateOf(true) }
+ var expanded by remember { mutableStateOf(true) }
DockedSearchBar(
- query = "Query",
- onQueryChange = {},
- onSearch = { capturedSearchQuery = it },
- active = active,
- onActiveChange = { active = it },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = { capturedSearchQuery = it },
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ )
+ },
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
content = { Text("Content") },
)
}
@@ -342,15 +406,21 @@
}
@Test
- fun dockedSearchBar_inactiveSize() {
+ fun dockedSearchBar_notExpandedSize() {
rule.setMaterialContentForSizeAssertions {
DockedSearchBar(
- query = "",
- onQueryChange = {},
- onSearch = {},
- active = false,
- onActiveChange = {},
- placeholder = { Text("Hint") },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = false,
+ onExpandedChange = {},
+ placeholder = { Text("Hint") },
+ )
+ },
+ expanded = false,
+ onExpandedChange = {},
content = {},
)
}
@@ -359,20 +429,27 @@
}
@Test
- fun dockedSearchBar_activeSize() {
+ fun dockedSearchBar_expandedSize() {
rule.setMaterialContentForSizeAssertions {
DockedSearchBar(
- query = "",
- onQueryChange = {},
- onSearch = {},
- active = true,
- onActiveChange = {},
- placeholder = { Text("Hint") },
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = true,
+ onExpandedChange = {},
+ placeholder = { Text("Hint") },
+ )
+ },
+ expanded = true,
+ onExpandedChange = {},
content = { Text("Content") },
)
}
.assertWidthIsEqualTo(SearchBarMinWidth)
- .assertHeightIsEqualTo(SearchBarDefaults.InputFieldHeight + DockedActiveTableMinHeight)
+ .assertHeightIsEqualTo(
+ SearchBarDefaults.InputFieldHeight + DockedExpandedTableMinHeight)
}
@Test
@@ -381,23 +458,29 @@
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.fillMaxSize()) {
- var active by remember { mutableStateOf(false) }
+ var expanded by remember { mutableStateOf(false) }
DockedSearchBar(
modifier = Modifier.testTag(SearchBarTestTag),
- query = "Query",
- onQueryChange = {},
- onSearch = {},
- active = active,
- onActiveChange = { active = it },
- trailingIcon = {
- IconButton(
- onClick = { iconClicked = true },
- modifier = Modifier.testTag(IconTestTag)
- ) {
- Icon(Icons.Default.MoreVert, null)
- }
- }
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = "Query",
+ onQueryChange = {},
+ onSearch = {},
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ trailingIcon = {
+ IconButton(
+ onClick = { iconClicked = true },
+ modifier = Modifier.testTag(IconTestTag)
+ ) {
+ Icon(Icons.Default.MoreVert, null)
+ }
+ }
+ )
+ },
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
) {
Text("Content")
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt
index ad2893d..176aa44 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldDecorationBoxTest.kt
@@ -368,7 +368,7 @@
interactionSource = interactionSource,
singleLine = singleLine,
container = {
- OutlinedTextFieldDefaults.ContainerBox(
+ OutlinedTextFieldDefaults.Container(
enabled = true,
isError = false,
colors = colors,
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldTest.kt
index a93e323..aa7bf96 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/TextFieldTest.kt
@@ -46,6 +46,7 @@
import androidx.compose.material3.internal.MinTextLineHeight
import androidx.compose.material3.internal.Strings.Companion.DefaultErrorMessage
import androidx.compose.material3.internal.SupportingTopPadding
+import androidx.compose.material3.internal.TextFieldAnimationDuration
import androidx.compose.material3.internal.TextFieldPadding
import androidx.compose.material3.internal.getString
import androidx.compose.runtime.CompositionLocalProvider
@@ -1728,8 +1729,8 @@
focusRequester.requestFocus()
}
- // animation duration is 150, advancing by 75 to get into middle of animation
- rule.mainClock.advanceTimeBy(75)
+ // advance to middle of animation
+ rule.mainClock.advanceTimeBy(TextFieldAnimationDuration.toLong() / 2)
rule.runOnIdle {
assertThat(textStyle.color).isEqualTo(expectedLabelColor)
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
index a6050e0..95b8a25 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
@@ -265,7 +265,7 @@
state = it,
)
},
- content: @Composable CarouselScope.(item: Int) -> Unit = { Item(index = it) }
+ content: @Composable CarouselItemScope.(item: Int) -> Unit = { Item(index = it) }
) {
rule.setMaterialContent(lightColorScheme()) {
val state = rememberCarouselState(initialItem, itemCount).also {
@@ -299,7 +299,7 @@
modifier: Modifier = Modifier
.width(412.dp)
.height(221.dp),
- content: @Composable CarouselScope.(item: Int) -> Unit = { Item(index = it) }
+ content: @Composable CarouselItemScope.(item: Int) -> Unit = { Item(index = it) }
) {
rule.setMaterialContent(lightColorScheme()) {
val state = rememberCarouselState(initialItem, itemCount).also {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/pulltorefresh/PullToRefreshStateImplTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/pulltorefresh/PullToRefreshStateImplTest.kt
index 1233c28..998d030 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/pulltorefresh/PullToRefreshStateImplTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/pulltorefresh/PullToRefreshStateImplTest.kt
@@ -79,7 +79,8 @@
Box(
Modifier
.nestedScroll(state.nestedScrollConnection)
- .testTag(PullRefreshTag)) {
+ .testTag(PullRefreshTag)
+ ) {
LazyColumn {
items(100) {
Text("item $it")
@@ -137,7 +138,8 @@
Box(
Modifier
.nestedScroll(state.nestedScrollConnection)
- .testTag(PullRefreshTag)) {
+ .testTag(PullRefreshTag)
+ ) {
LazyColumn {
items(100) {
Text("item $it")
@@ -180,7 +182,8 @@
Box(
Modifier
.nestedScroll(state.nestedScrollConnection)
- .testTag(PullRefreshTag)) {
+ .testTag(PullRefreshTag)
+ ) {
LazyColumn {
items(100) {
Text("item $it")
@@ -234,7 +237,8 @@
Box(
Modifier
.nestedScroll(state.nestedScrollConnection)
- .testTag(PullRefreshTag)) {
+ .testTag(PullRefreshTag)
+ ) {
LazyColumn {
items(100) {
Text("item $it")
@@ -277,18 +281,21 @@
Box(
Modifier
.nestedScroll(state.nestedScrollConnection)
- .testTag(PullRefreshTag)) {
+ .testTag(PullRefreshTag)
+ ) {
Box(
Modifier
.size(100.dp)
- .nestedScroll(connection, dispatcher))
+ .nestedScroll(connection, dispatcher)
+ )
}
}
// 100 pixels up
val dragUpOffset = Offset(0f, -100f)
rule.runOnIdle {
- val preConsumed = dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.Drag)
+ val preConsumed =
+ dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.UserInput)
// Pull refresh is not showing, so we should consume nothing
assertThat(preConsumed).isEqualTo(Offset.Zero)
assertThat(state.verticalOffset).isEqualTo(0f)
@@ -300,7 +307,8 @@
rule.runOnIdle {
assertThat(state.calculateVerticalOffset())
.isEqualTo(100f /* 200 / 2 for drag multiplier */)
- val preConsumed = dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.Drag)
+ val preConsumed =
+ dispatcher.dispatchPreScroll(dragUpOffset, NestedScrollSource.UserInput)
// Pull refresh is currently showing, so we should consume all the delta
assertThat(preConsumed).isEqualTo(dragUpOffset)
assertThat(state.calculateVerticalOffset())
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/SearchBar.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/SearchBar.android.kt
index cec1418..fb08255 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/SearchBar.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/SearchBar.android.kt
@@ -125,13 +125,14 @@
* and get relevant information. It can be used as a way to navigate through an app via search
* queries.
*
- * An active search bar expands into a search "view" and can be used to display dynamic suggestions.
+ * A search bar expands into a search "view" and can be used to display dynamic suggestions or
+ * search results.
*
* 
*
- * A [SearchBar] expands to occupy the entirety of its allowed size when active. For full-screen
- * behavior as specified by Material guidelines, parent layouts of the [SearchBar] must not pass
- * any [Constraints] that limit its size, and the host activity should set
+ * A [SearchBar] tries to occupy the entirety of its allowed size in the expanded state. For
+ * full-screen behavior as specified by Material guidelines, parent layouts of the [SearchBar] must
+ * not pass any [Constraints] that limit its size, and the host activity should set
* `WindowCompat.setDecorFitsSystemWindows(window, false)`.
*
* If this expansion behavior is undesirable, for example on large tablet screens, [DockedSearchBar]
@@ -140,73 +141,54 @@
* An example looks like:
* @sample androidx.compose.material3.samples.SearchBarSample
*
- * @param query the query text to be shown in the search bar's input field
- * @param onQueryChange the callback to be invoked when the input service updates the query. An
- * updated text comes as a parameter of the callback.
- * @param onSearch the callback to be invoked when the input service triggers the [ImeAction.Search]
- * action. The current [query] comes as a parameter of the callback.
- * @param active whether this search bar is active
- * @param onActiveChange the callback to be invoked when this search bar's active state is changed
- * @param modifier the [Modifier] to be applied to this search bar
- * @param enabled controls the enabled state of this search bar. When `false`, this component will
- * not respond to user input, and it will appear visually disabled and disabled to accessibility
- * services.
- * @param placeholder the placeholder to be displayed when the search bar's [query] is empty.
- * @param leadingIcon the leading icon to be displayed at the beginning of the search bar container
- * @param trailingIcon the trailing icon to be displayed at the end of the search bar container
- * @param shape the shape of this search bar when it is not [active]. When [active], the shape will
- * always be [SearchBarDefaults.fullScreenShape].
+ * @param inputField the input field of this search bar that allows entering a query, typically a
+ * [SearchBarDefaults.InputField].
+ * @param expanded whether this search bar is expanded and showing search results.
+ * @param onExpandedChange the callback to be invoked when this search bar's expanded state is
+ * changed.
+ * @param modifier the [Modifier] to be applied to this search bar.
+ * @param shape the shape of this search bar when it is not [expanded]. When [expanded], the shape
+ * will always be [SearchBarDefaults.fullScreenShape].
* @param colors [SearchBarColors] that will be used to resolve the colors used for this search bar
* in different states. See [SearchBarDefaults.colors].
* @param tonalElevation when [SearchBarColors.containerColor] is [ColorScheme.surface], a
* translucent primary color overlay is applied on top of the container. A higher tonal elevation
* value will result in a darker color in light theme and lighter color in dark theme. See also:
* [Surface].
- * @param shadowElevation the elevation for the shadow below the search bar
- * @param windowInsets the window insets that the search bar will respect
- * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
- * emitting [Interaction]s for this search bar. You can use this to change the search bar's
- * appearance or preview the search bar in different states. Note that if `null` is provided,
- * interactions will still happen internally.
- * @param content the content of this search bar that will be displayed below the input field
+ * @param shadowElevation the elevation for the shadow below this search bar
+ * @param windowInsets the window insets that this search bar will respect
+ * @param content the content of this search bar to display search results below the [inputField].
*/
@ExperimentalMaterial3Api
@Composable
fun SearchBar(
- query: String,
- onQueryChange: (String) -> Unit,
- onSearch: (String) -> Unit,
- active: Boolean,
- onActiveChange: (Boolean) -> Unit,
+ inputField: @Composable () -> Unit,
+ expanded: Boolean,
+ onExpandedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
- enabled: Boolean = true,
- placeholder: @Composable (() -> Unit)? = null,
- leadingIcon: @Composable (() -> Unit)? = null,
- trailingIcon: @Composable (() -> Unit)? = null,
shape: Shape = SearchBarDefaults.inputFieldShape,
colors: SearchBarColors = SearchBarDefaults.colors(),
tonalElevation: Dp = SearchBarDefaults.TonalElevation,
shadowElevation: Dp = SearchBarDefaults.ShadowElevation,
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
- interactionSource: MutableInteractionSource? = null,
content: @Composable ColumnScope.() -> Unit,
) {
- val animationProgress = remember { Animatable(initialValue = if (active) 1f else 0f) }
+ val animationProgress = remember { Animatable(initialValue = if (expanded) 1f else 0f) }
val finalBackProgress = remember { mutableFloatStateOf(Float.NaN) }
val firstBackEvent = remember { mutableStateOf<BackEventCompat?>(null) }
val currentBackEvent = remember { mutableStateOf<BackEventCompat?>(null) }
- LaunchedEffect(active) {
+ LaunchedEffect(expanded) {
val animationInProgress = animationProgress.value > 0 && animationProgress.value < 1
val animationSpec =
if (animationInProgress) AnimationPredictiveBackExitFloatSpec
- else if (active) AnimationEnterFloatSpec
+ else if (expanded) AnimationEnterFloatSpec
else AnimationExitFloatSpec
- val targetValue = if (active) 1f else 0f
+ val targetValue = if (expanded) 1f else 0f
if (animationProgress.value != targetValue) {
animationProgress.animateTo(targetValue, animationSpec)
}
- if (!active) {
+ if (!expanded) {
finalBackProgress.floatValue = Float.NaN
firstBackEvent.value = null
currentBackEvent.value = null
@@ -214,7 +196,7 @@
}
val mutatorMutex = remember { MutatorMutex() }
- PredictiveBackHandler(enabled = active) { progress ->
+ PredictiveBackHandler(enabled = expanded) { progress ->
mutatorMutex.mutate {
try {
finalBackProgress.floatValue = Float.NaN
@@ -227,7 +209,7 @@
animationProgress.snapTo(targetValue = 1 - interpolatedProgress)
}
finalBackProgress.floatValue = animationProgress.value
- onActiveChange(false)
+ onExpandedChange(false)
} catch (e: CancellationException) {
animationProgress.animateTo(
targetValue = 1f,
@@ -246,22 +228,7 @@
firstBackEvent = firstBackEvent,
currentBackEvent = currentBackEvent,
modifier = modifier,
- inputField = {
- SearchBarInputField(
- query = query,
- onQueryChange = onQueryChange,
- onSearch = onSearch,
- active = active,
- onActiveChange = onActiveChange,
- modifier = Modifier.fillMaxWidth(),
- enabled = enabled,
- placeholder = placeholder,
- leadingIcon = leadingIcon,
- trailingIcon = trailingIcon,
- colors = colors.inputFieldColors,
- interactionSource = interactionSource,
- )
- },
+ inputField = inputField,
shape = shape,
colors = colors,
tonalElevation = tonalElevation,
@@ -278,68 +245,47 @@
* and get relevant information. It can be used as a way to navigate through an app via search
* queries.
*
- * An active search bar expands into a search "view" and can be used to display dynamic suggestions.
+ * An search bar expands into a search "view" and can be used to display dynamic suggestions or
+ * search results.
*
* 
*
- * A [DockedSearchBar] displays search results in a bounded table below the input field. It is meant
- * to be an alternative to [SearchBar] when expanding to full-screen size is undesirable on large
- * screens such as tablets.
+ * A [DockedSearchBar] displays search results in a bounded table below the input field. It is an
+ * alternative to [SearchBar] when expanding to full-screen size is undesirable on large screens
+ * such as tablets.
*
* An example looks like:
* @sample androidx.compose.material3.samples.DockedSearchBarSample
*
- * @param query the query text to be shown in the search bar's input field
- * @param onQueryChange the callback to be invoked when the input service updates the query. An
- * updated text comes as a parameter of the callback.
- * @param onSearch the callback to be invoked when the input service triggers the [ImeAction.Search]
- * action. The current [query] comes as a parameter of the callback.
- * @param active whether this search bar is active
- * @param onActiveChange the callback to be invoked when this search bar's active state is changed
- * @param modifier the [Modifier] to be applied to this search bar
- * @param enabled controls the enabled state of this search bar. When `false`, this component will
- * not respond to user input, and it will appear visually disabled and disabled to accessibility
- * services.
- * @param placeholder the placeholder to be displayed when the search bar's [query] is empty.
- * @param leadingIcon the leading icon to be displayed at the beginning of the search bar container
- * @param trailingIcon the trailing icon to be displayed at the end of the search bar container
- * @param shape the shape of this search bar
+ * @param inputField the input field of this search bar that allows entering a query, typically a
+ * [SearchBarDefaults.InputField].
+ * @param expanded whether this search bar is expanded and showing search results.
+ * @param onExpandedChange the callback to be invoked when this search bar's expanded state is
+ * changed.
+ * @param modifier the [Modifier] to be applied to this search bar.
+ * @param shape the shape of this search bar.
* @param colors [SearchBarColors] that will be used to resolve the colors used for this search bar
* in different states. See [SearchBarDefaults.colors].
* @param tonalElevation when [SearchBarColors.containerColor] is [ColorScheme.surface], a
* translucent primary color overlay is applied on top of the container. A higher tonal elevation
* value will result in a darker color in light theme and lighter color in dark theme. See also:
* [Surface].
- * @param shadowElevation the elevation for the shadow below the search bar
- * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
- * emitting [Interaction]s for this search bar. You can use this to change the search bar's
- * appearance or preview the search bar in different states. Note that if `null` is provided,
- * interactions will still happen internally.
- * @param content the content of this search bar that will be displayed below the input field
+ * @param shadowElevation the elevation for the shadow below the search bar.
+ * @param content the content of this search bar to display search results below the [inputField].
*/
@ExperimentalMaterial3Api
@Composable
fun DockedSearchBar(
- query: String,
- onQueryChange: (String) -> Unit,
- onSearch: (String) -> Unit,
- active: Boolean,
- onActiveChange: (Boolean) -> Unit,
+ inputField: @Composable () -> Unit,
+ expanded: Boolean,
+ onExpandedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
- enabled: Boolean = true,
- placeholder: @Composable (() -> Unit)? = null,
- leadingIcon: @Composable (() -> Unit)? = null,
- trailingIcon: @Composable (() -> Unit)? = null,
shape: Shape = SearchBarDefaults.dockedShape,
colors: SearchBarColors = SearchBarDefaults.colors(),
tonalElevation: Dp = SearchBarDefaults.TonalElevation,
shadowElevation: Dp = SearchBarDefaults.ShadowElevation,
- interactionSource: MutableInteractionSource? = null,
content: @Composable ColumnScope.() -> Unit,
) {
- @Suppress("NAME_SHADOWING")
- val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
-
Surface(
shape = shape,
color = colors.containerColor,
@@ -351,32 +297,19 @@
.width(SearchBarMinWidth)
) {
Column {
- SearchBarInputField(
- modifier = Modifier.fillMaxWidth(),
- query = query,
- onQueryChange = onQueryChange,
- onSearch = onSearch,
- active = active,
- onActiveChange = onActiveChange,
- enabled = enabled,
- placeholder = placeholder,
- leadingIcon = leadingIcon,
- trailingIcon = trailingIcon,
- colors = colors.inputFieldColors,
- interactionSource = interactionSource,
- )
+ inputField()
AnimatedVisibility(
- visible = active,
+ visible = expanded,
enter = DockedEnterTransition,
exit = DockedExitTransition,
) {
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
val maxHeight = remember(screenHeight) {
- screenHeight * DockedActiveTableMaxHeightScreenRatio
+ screenHeight * DockedExpandedTableMaxHeightScreenRatio
}
val minHeight = remember(maxHeight) {
- DockedActiveTableMinHeight.coerceAtMost(maxHeight)
+ DockedExpandedTableMinHeight.coerceAtMost(maxHeight)
}
Column(Modifier.heightIn(min = minHeight, max = maxHeight)) {
@@ -387,98 +320,8 @@
}
}
- BackHandler(enabled = active) {
- onActiveChange(false)
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-internal fun SearchBarInputField(
- query: String,
- onQueryChange: (String) -> Unit,
- onSearch: (String) -> Unit,
- active: Boolean,
- onActiveChange: (Boolean) -> Unit,
- modifier: Modifier = Modifier,
- enabled: Boolean = true,
- placeholder: @Composable (() -> Unit)? = null,
- leadingIcon: @Composable (() -> Unit)? = null,
- trailingIcon: @Composable (() -> Unit)? = null,
- colors: TextFieldColors = SearchBarDefaults.inputFieldColors(),
- interactionSource: MutableInteractionSource? = null,
-) {
- @Suppress("NAME_SHADOWING")
- val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
- val focusRequester = remember { FocusRequester() }
- val focusManager = LocalFocusManager.current
- val searchSemantics = getString(Strings.SearchBarSearch)
- val suggestionsAvailableSemantics = getString(Strings.SuggestionsAvailable)
- val textColor = LocalTextStyle.current.color.takeOrElse {
- val focused = interactionSource.collectIsFocusedAsState().value
- colors.textColor(enabled, isError = false, focused = focused)
- }
-
- BasicTextField(
- value = query,
- onValueChange = onQueryChange,
- modifier = modifier
- .sizeIn(
- minWidth = SearchBarMinWidth,
- maxWidth = SearchBarMaxWidth,
- minHeight = InputFieldHeight,
- )
- .focusRequester(focusRequester)
- .onFocusChanged { if (it.isFocused) onActiveChange(true) }
- .semantics {
- contentDescription = searchSemantics
- if (active) {
- stateDescription = suggestionsAvailableSemantics
- }
- onClick {
- focusRequester.requestFocus()
- true
- }
- },
- enabled = enabled,
- singleLine = true,
- textStyle = LocalTextStyle.current.merge(TextStyle(color = textColor)),
- cursorBrush = SolidColor(colors.cursorColor(isError = false)),
- keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
- keyboardActions = KeyboardActions(onSearch = { onSearch(query) }),
- interactionSource = interactionSource,
- decorationBox = @Composable { innerTextField ->
- TextFieldDefaults.DecorationBox(
- value = query,
- innerTextField = innerTextField,
- enabled = enabled,
- singleLine = true,
- visualTransformation = VisualTransformation.None,
- interactionSource = interactionSource,
- placeholder = placeholder,
- leadingIcon = leadingIcon?.let { leading -> {
- Box(Modifier.offset(x = SearchBarIconOffsetX)) { leading() }
- } },
- trailingIcon = trailingIcon?.let { trailing -> {
- Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() }
- } },
- shape = SearchBarDefaults.inputFieldShape,
- colors = colors,
- contentPadding = TextFieldDefaults.contentPaddingWithoutLabel(),
- container = {},
- )
- }
- )
-
- val isFocused = interactionSource.collectIsFocusedAsState().value
- val shouldClearFocus = !active && isFocused
- LaunchedEffect(active) {
- if (shouldClearFocus) {
- // Not strictly needed according to the motion spec, but since the animation already has
- // a delay, this works around b/261632544.
- delay(AnimationDelayMillis.toLong())
- focusManager.clearFocus()
- }
+ BackHandler(enabled = expanded) {
+ onExpandedChange(false)
}
}
@@ -500,13 +343,13 @@
)
val Elevation: Dp = TonalElevation
- /** Default height for a search bar's input field, or a search bar in the inactive state. */
+ /** Default height for a search bar's input field, or a search bar in the unexpanded state. */
val InputFieldHeight: Dp = SearchBarTokens.ContainerHeight
- /** Default shape for a search bar's input field, or a search bar in the inactive state. */
+ /** Default shape for a search bar's input field, or a search bar in the unexpanded state. */
val inputFieldShape: Shape @Composable get() = SearchBarTokens.ContainerShape.value
- /** Default shape for a [SearchBar] in the active state. */
+ /** Default shape for a [SearchBar] in the expanded state. */
val fullScreenShape: Shape
@Composable get() = SearchViewTokens.FullScreenContainerShape.value
@@ -520,19 +363,20 @@
* Creates a [SearchBarColors] that represents the different colors used in parts of the
* search bar in different states.
*
+ * For colors used in the input field, see [SearchBarDefaults.inputFieldColors].
+ *
* @param containerColor the container color of the search bar
* @param dividerColor the color of the divider between the input field and the search results
- * @param inputFieldColors the colors of the input field
*/
+ @Suppress("DEPRECATION")
@Composable
fun colors(
containerColor: Color = SearchBarTokens.ContainerColor.value,
dividerColor: Color = SearchViewTokens.DividerColor.value,
- inputFieldColors: TextFieldColors = inputFieldColors(),
): SearchBarColors = SearchBarColors(
containerColor = containerColor,
dividerColor = dividerColor,
- inputFieldColors = inputFieldColors,
+ inputFieldColors = inputFieldColors(),
)
/**
@@ -597,6 +441,141 @@
disabledPlaceholderColor = disabledPlaceholderColor,
)
+ /**
+ * A text field to input a query in a search bar
+ *
+ * @param query the query text to be shown in the input field.
+ * @param onQueryChange the callback to be invoked when the input service updates the query. An
+ * updated text comes as a parameter of the callback.
+ * @param onSearch the callback to be invoked when the input service triggers the
+ * [ImeAction.Search] action. The current [query] comes as a parameter of the callback.
+ * @param expanded whether the search bar is expanded and showing search results.
+ * @param onExpandedChange the callback to be invoked when the search bar's expanded state is
+ * changed.
+ * @param modifier the [Modifier] to be applied to this input field.
+ * @param enabled the enabled state of this input field. When `false`, this component will
+ * not respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services.
+ * @param placeholder the placeholder to be displayed when the [query] is empty.
+ * @param leadingIcon the leading icon to be displayed at the start of the input field.
+ * @param trailingIcon the trailing icon to be displayed at the end of the input field.
+ * @param colors [TextFieldColors] that will be used to resolve the colors used for this input
+ * field in different states. See [SearchBarDefaults.inputFieldColors].
+ * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
+ * emitting [Interaction]s for this input field. You can use this to change the search bar's
+ * appearance or preview the search bar in different states. Note that if `null` is provided,
+ * interactions will still happen internally.
+ */
+ @ExperimentalMaterial3Api
+ @Composable
+ fun InputField(
+ query: String,
+ onQueryChange: (String) -> Unit,
+ onSearch: (String) -> Unit,
+ expanded: Boolean,
+ onExpandedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ placeholder: @Composable (() -> Unit)? = null,
+ leadingIcon: @Composable (() -> Unit)? = null,
+ trailingIcon: @Composable (() -> Unit)? = null,
+ colors: TextFieldColors = inputFieldColors(),
+ interactionSource: MutableInteractionSource? = null,
+ ) {
+ @Suppress("NAME_SHADOWING")
+ val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
+
+ val focused = interactionSource.collectIsFocusedAsState().value
+ val focusRequester = remember { FocusRequester() }
+ val focusManager = LocalFocusManager.current
+
+ val searchSemantics = getString(Strings.SearchBarSearch)
+ val suggestionsAvailableSemantics = getString(Strings.SuggestionsAvailable)
+
+ val textColor = LocalTextStyle.current.color.takeOrElse {
+ colors.textColor(enabled, isError = false, focused = focused)
+ }
+
+ BasicTextField(
+ value = query,
+ onValueChange = onQueryChange,
+ modifier = modifier
+ .sizeIn(
+ minWidth = SearchBarMinWidth,
+ maxWidth = SearchBarMaxWidth,
+ minHeight = InputFieldHeight,
+ )
+ .focusRequester(focusRequester)
+ .onFocusChanged { if (it.isFocused) onExpandedChange(true) }
+ .semantics {
+ contentDescription = searchSemantics
+ if (expanded) {
+ stateDescription = suggestionsAvailableSemantics
+ }
+ onClick {
+ focusRequester.requestFocus()
+ true
+ }
+ },
+ enabled = enabled,
+ singleLine = true,
+ textStyle = LocalTextStyle.current.merge(TextStyle(color = textColor)),
+ cursorBrush = SolidColor(colors.cursorColor(isError = false)),
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+ keyboardActions = KeyboardActions(onSearch = { onSearch(query) }),
+ interactionSource = interactionSource,
+ decorationBox = @Composable { innerTextField ->
+ TextFieldDefaults.DecorationBox(
+ value = query,
+ innerTextField = innerTextField,
+ enabled = enabled,
+ singleLine = true,
+ visualTransformation = VisualTransformation.None,
+ interactionSource = interactionSource,
+ placeholder = placeholder,
+ leadingIcon = leadingIcon?.let { leading -> {
+ Box(Modifier.offset(x = SearchBarIconOffsetX)) { leading() }
+ } },
+ trailingIcon = trailingIcon?.let { trailing -> {
+ Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() }
+ } },
+ shape = SearchBarDefaults.inputFieldShape,
+ colors = colors,
+ contentPadding = TextFieldDefaults.contentPaddingWithoutLabel(),
+ container = {},
+ )
+ }
+ )
+
+ val shouldClearFocus = !expanded && focused
+ LaunchedEffect(expanded) {
+ if (shouldClearFocus) {
+ // Not strictly needed according to the motion spec, but since the animation
+ // already has a delay, this works around b/261632544.
+ delay(AnimationDelayMillis.toLong())
+ focusManager.clearFocus()
+ }
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ @Deprecated(
+ message = "Search bars now take the input field as a parameter. `inputFieldColors` " +
+ "should be passed explicitly to the input field. This parameter will be removed in " +
+ "a future version of the library.",
+ replaceWith = ReplaceWith("colors(containerColor, dividerColor)"),
+ )
+ @Composable
+ fun colors(
+ containerColor: Color = SearchBarTokens.ContainerColor.value,
+ dividerColor: Color = SearchViewTokens.DividerColor.value,
+ inputFieldColors: TextFieldColors = inputFieldColors(),
+ ): SearchBarColors = SearchBarColors(
+ containerColor = containerColor,
+ dividerColor = dividerColor,
+ inputFieldColors = inputFieldColors,
+ )
+
@Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
@Composable
fun inputFieldColors(
@@ -640,18 +619,29 @@
* See [SearchBarDefaults.colors] for the default implementation that follows Material
* specifications.
*/
+@Suppress("DEPRECATION")
@ExperimentalMaterial3Api
@Immutable
-class SearchBarColors(
+class SearchBarColors
+@Deprecated("Search bars now take the input field as a parameter. TextFieldColors should be " +
+ "passed explicitly to the input field. The `inputFieldColors` parameter will be removed in " +
+ "a future version of the library.")
+constructor(
val containerColor: Color,
val dividerColor: Color,
+ @Deprecated("Search bars now take the input field as a parameter. TextFieldColors should be " +
+ "passed explicitly to the input field. The `inputFieldColors` property will be removed " +
+ "in a future version of the library.")
val inputFieldColors: TextFieldColors,
) {
+ constructor(
+ containerColor: Color,
+ dividerColor: Color,
+ ) : this(containerColor, dividerColor, UnspecifiedTextFieldColors)
+
override fun equals(other: Any?): Boolean {
if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as SearchBarColors
+ if (other !is SearchBarColors) return false
if (containerColor != other.containerColor) return false
if (dividerColor != other.dividerColor) return false
@@ -668,6 +658,159 @@
}
}
+@Suppress("DEPRECATION")
+@Deprecated(
+ message = "Use overload which takes inputField as a parameter",
+ replaceWith = ReplaceWith("SearchBar(\n" +
+ " inputField = {\n" +
+ " SearchBarDefaults.InputField(\n" +
+ " query = query,\n" +
+ " onQueryChange = onQueryChange,\n" +
+ " onSearch = onSearch,\n" +
+ " expanded = active,\n" +
+ " onExpandedChange = onActiveChange,\n" +
+ " enabled = enabled,\n" +
+ " placeholder = placeholder,\n" +
+ " leadingIcon = leadingIcon,\n" +
+ " trailingIcon = trailingIcon,\n" +
+ " colors = colors.inputFieldColors,\n" +
+ " interactionSource = interactionSource,\n" +
+ " )\n" +
+ " },\n" +
+ " expanded = active,\n" +
+ " onExpandedChange = onActiveChange,\n" +
+ " modifier = modifier,\n" +
+ " shape = shape,\n" +
+ " colors = colors,\n" +
+ " tonalElevation = tonalElevation,\n" +
+ " shadowElevation = shadowElevation,\n" +
+ " windowInsets = windowInsets,\n" +
+ " content = content,\n" +
+ ")"),
+)
+@ExperimentalMaterial3Api
+@Composable
+fun SearchBar(
+ query: String,
+ onQueryChange: (String) -> Unit,
+ onSearch: (String) -> Unit,
+ active: Boolean,
+ onActiveChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ placeholder: @Composable (() -> Unit)? = null,
+ leadingIcon: @Composable (() -> Unit)? = null,
+ trailingIcon: @Composable (() -> Unit)? = null,
+ shape: Shape = SearchBarDefaults.inputFieldShape,
+ colors: SearchBarColors = SearchBarDefaults.colors(),
+ tonalElevation: Dp = SearchBarDefaults.TonalElevation,
+ shadowElevation: Dp = SearchBarDefaults.ShadowElevation,
+ windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
+ interactionSource: MutableInteractionSource? = null,
+ content: @Composable ColumnScope.() -> Unit,
+) = SearchBar(
+ inputField = {
+ SearchBarDefaults.InputField(
+ modifier = Modifier.fillMaxWidth(),
+ query = query,
+ onQueryChange = onQueryChange,
+ onSearch = onSearch,
+ expanded = active,
+ onExpandedChange = onActiveChange,
+ enabled = enabled,
+ placeholder = placeholder,
+ leadingIcon = leadingIcon,
+ trailingIcon = trailingIcon,
+ colors = colors.inputFieldColors,
+ interactionSource = interactionSource,
+ )
+ },
+ expanded = active,
+ onExpandedChange = onActiveChange,
+ modifier = modifier,
+ shape = shape,
+ colors = colors,
+ tonalElevation = tonalElevation,
+ shadowElevation = shadowElevation,
+ windowInsets = windowInsets,
+ content = content,
+)
+
+@Suppress("DEPRECATION")
+@Deprecated(
+ message = "Use overload which takes inputField as a parameter",
+ replaceWith = ReplaceWith("DockedSearchBar(\n" +
+ " inputField = {\n" +
+ " SearchBarDefaults.InputField(\n" +
+ " query = query,\n" +
+ " onQueryChange = onQueryChange,\n" +
+ " onSearch = onSearch,\n" +
+ " expanded = active,\n" +
+ " onExpandedChange = onActiveChange,\n" +
+ " enabled = enabled,\n" +
+ " placeholder = placeholder,\n" +
+ " leadingIcon = leadingIcon,\n" +
+ " trailingIcon = trailingIcon,\n" +
+ " colors = colors.inputFieldColors,\n" +
+ " interactionSource = interactionSource,\n" +
+ " )\n" +
+ " },\n" +
+ " expanded = active,\n" +
+ " onExpandedChange = onActiveChange,\n" +
+ " modifier = modifier,\n" +
+ " shape = shape,\n" +
+ " colors = colors,\n" +
+ " tonalElevation = tonalElevation,\n" +
+ " shadowElevation = shadowElevation,\n" +
+ " content = content,\n" +
+ ")"),
+)
+@ExperimentalMaterial3Api
+@Composable
+fun DockedSearchBar(
+ query: String,
+ onQueryChange: (String) -> Unit,
+ onSearch: (String) -> Unit,
+ active: Boolean,
+ onActiveChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ placeholder: @Composable (() -> Unit)? = null,
+ leadingIcon: @Composable (() -> Unit)? = null,
+ trailingIcon: @Composable (() -> Unit)? = null,
+ shape: Shape = SearchBarDefaults.dockedShape,
+ colors: SearchBarColors = SearchBarDefaults.colors(),
+ tonalElevation: Dp = SearchBarDefaults.TonalElevation,
+ shadowElevation: Dp = SearchBarDefaults.ShadowElevation,
+ interactionSource: MutableInteractionSource? = null,
+ content: @Composable ColumnScope.() -> Unit,
+) = DockedSearchBar(
+ inputField = {
+ SearchBarDefaults.InputField(
+ modifier = Modifier.fillMaxWidth(),
+ query = query,
+ onQueryChange = onQueryChange,
+ onSearch = onSearch,
+ expanded = active,
+ onExpandedChange = onActiveChange,
+ enabled = enabled,
+ placeholder = placeholder,
+ leadingIcon = leadingIcon,
+ trailingIcon = trailingIcon,
+ colors = colors.inputFieldColors,
+ interactionSource = interactionSource,
+ )
+ },
+ expanded = active,
+ onExpandedChange = onActiveChange,
+ modifier = modifier,
+ shape = shape,
+ colors = colors,
+ tonalElevation = tonalElevation,
+ shadowElevation = shadowElevation,
+ content = content,
+)
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun SearchBarImpl(
@@ -707,7 +850,6 @@
}
val surface = @Composable {
Surface(
- modifier = Modifier,
shape = animatedShape,
color = colors.containerColor,
contentColor = contentColorFor(colors.containerColor),
@@ -813,22 +955,30 @@
predictiveBackMultiplier
)
- val endWidth = constraints.maxWidth
- val endHeight = constraints.maxHeight
+ val maxWidth = constraints.maxWidth
+ val maxHeight = constraints.maxHeight
- val width = lerp(startWidth, endWidth, animationProgress)
- val height = lerp(startHeight, endHeight, animationProgress)
+ val minWidth = lerp(startWidth, maxWidth, animationProgress)
+ val height = lerp(startHeight, maxHeight, animationProgress)
// Note: animatedTopPadding decreases w.r.t. animationProgress
val animatedTopPadding = lerp(topPadding, 0, animationProgress)
val animatedBottomPadding = lerp(0, bottomPadding, animationProgress)
+ val inputFieldPlaceable = inputFieldMeasurable.measure(
+ Constraints(
+ minWidth = minWidth,
+ maxWidth = maxWidth,
+ minHeight = defaultStartHeight,
+ maxHeight = defaultStartHeight,
+ )
+ )
+ val width = inputFieldPlaceable.width
+
// As the animation proceeds, the surface loses its padding
// and expands to cover the entire container.
val surfacePlaceable = surfaceMeasurable
.measure(Constraints.fixed(width, height - animatedTopPadding))
- val inputFieldPlaceable = inputFieldMeasurable
- .measure(Constraints.fixed(width, defaultStartHeight))
val contentPlaceable = contentMeasurable?.measure(
Constraints(
minWidth = width,
@@ -932,6 +1082,52 @@
return (interpolatedOffsetY * predictiveBackMultiplier * directionMultiplier).roundToInt()
}
+private val UnspecifiedTextFieldColors: TextFieldColors = TextFieldColors(
+ focusedTextColor = Color.Unspecified,
+ unfocusedTextColor = Color.Unspecified,
+ disabledTextColor = Color.Unspecified,
+ errorTextColor = Color.Unspecified,
+ focusedContainerColor = Color.Unspecified,
+ unfocusedContainerColor = Color.Unspecified,
+ disabledContainerColor = Color.Unspecified,
+ errorContainerColor = Color.Unspecified,
+ cursorColor = Color.Unspecified,
+ errorCursorColor = Color.Unspecified,
+ textSelectionColors = TextSelectionColors(Color.Unspecified, Color.Unspecified),
+ focusedIndicatorColor = Color.Unspecified,
+ unfocusedIndicatorColor = Color.Unspecified,
+ disabledIndicatorColor = Color.Unspecified,
+ errorIndicatorColor = Color.Unspecified,
+ focusedLeadingIconColor = Color.Unspecified,
+ unfocusedLeadingIconColor = Color.Unspecified,
+ disabledLeadingIconColor = Color.Unspecified,
+ errorLeadingIconColor = Color.Unspecified,
+ focusedTrailingIconColor = Color.Unspecified,
+ unfocusedTrailingIconColor = Color.Unspecified,
+ disabledTrailingIconColor = Color.Unspecified,
+ errorTrailingIconColor = Color.Unspecified,
+ focusedLabelColor = Color.Unspecified,
+ unfocusedLabelColor = Color.Unspecified,
+ disabledLabelColor = Color.Unspecified,
+ errorLabelColor = Color.Unspecified,
+ focusedPlaceholderColor = Color.Unspecified,
+ unfocusedPlaceholderColor = Color.Unspecified,
+ disabledPlaceholderColor = Color.Unspecified,
+ errorPlaceholderColor = Color.Unspecified,
+ focusedSupportingTextColor = Color.Unspecified,
+ unfocusedSupportingTextColor = Color.Unspecified,
+ disabledSupportingTextColor = Color.Unspecified,
+ errorSupportingTextColor = Color.Unspecified,
+ focusedPrefixColor = Color.Unspecified,
+ unfocusedPrefixColor = Color.Unspecified,
+ disabledPrefixColor = Color.Unspecified,
+ errorPrefixColor = Color.Unspecified,
+ focusedSuffixColor = Color.Unspecified,
+ unfocusedSuffixColor = Color.Unspecified,
+ disabledSuffixColor = Color.Unspecified,
+ errorSuffixColor = Color.Unspecified,
+)
+
private const val LayoutIdInputField = "InputField"
private const val LayoutIdSurface = "Surface"
private const val LayoutIdSearchContent = "Content"
@@ -939,8 +1135,8 @@
// Measurement specs
@OptIn(ExperimentalMaterial3Api::class)
private val SearchBarCornerRadius: Dp = InputFieldHeight / 2
-internal val DockedActiveTableMinHeight: Dp = 240.dp
-private const val DockedActiveTableMaxHeightScreenRatio: Float = 2f / 3f
+internal val DockedExpandedTableMinHeight: Dp = 240.dp
+private const val DockedExpandedTableMaxHeightScreenRatio: Float = 2f / 3f
internal val SearchBarMinWidth: Dp = 360.dp
private val SearchBarMaxWidth: Dp = 720.dp
internal val SearchBarVerticalPadding: Dp = 8.dp
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
index 595e34c..e19e370 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
@@ -589,6 +589,40 @@
assertThat(firstNonAnchorLeft).isWithin(.01f).of(0f)
}
+ @Test
+ fun testStartStrategy_twoLargeOneSmall_shouldAccountForPadding() {
+ val strategy = Strategy { availableSpace, itemSpacing ->
+ keylineListOf(availableSpace, itemSpacing, CarouselAlignment.Start) {
+ add(10f, isAnchor = true)
+ add(186f)
+ add(186f)
+ add(56f)
+ add(10f, isAnchor = true)
+ }
+ }.apply(
+ availableSpace = 444f,
+ itemSpacing = 8f,
+ beforeContentPadding = 16f,
+ afterContentPadding = 16f
+ )
+
+ assertThat(strategy.itemMainAxisSize).isEqualTo(186f)
+
+ val lastStartStepSmallItem = strategy.startKeylineSteps.last()[3]
+ assertThat(lastStartStepSmallItem.offset + (lastStartStepSmallItem.size / 2f))
+ .isWithin(.001f)
+ .of(444f)
+
+ val lastEndSteps = strategy.endKeylineSteps.last()
+ assertThat(lastEndSteps[1].size + 8f + lastEndSteps[2].size + 8f + lastEndSteps[3].size)
+ .isEqualTo(444f - 16f)
+
+ val lastEndStepSmallItem = strategy.endKeylineSteps.last()[1]
+ assertThat(lastEndStepSmallItem.offset - (lastEndStepSmallItem.size / 2f))
+ .isWithin(.001f)
+ .of(0f)
+ }
+
private fun assertEqualWithFloatTolerance(
tolerance: Float,
actual: Keyline,
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 62820f5..39f902f 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
@@ -226,20 +226,25 @@
val scope = rememberCoroutineScope()
val orientation = Orientation.Vertical
val peekHeightPx = with(LocalDensity.current) { peekHeight.toPx() }
+ val nestedScroll = if (sheetSwipeEnabled) {
+ Modifier.nestedScroll(
+ remember(state.anchoredDraggableState) {
+ ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
+ sheetState = state,
+ orientation = orientation,
+ onFling = { scope.launch { state.settle(it) } }
+ )
+ }
+ )
+ } else {
+ Modifier
+ }
Surface(
modifier = Modifier
.widthIn(max = sheetMaxWidth)
.fillMaxWidth()
.requiredHeightIn(min = peekHeight)
- .nestedScroll(
- remember(state.anchoredDraggableState) {
- ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
- sheetState = state,
- orientation = orientation,
- onFling = { scope.launch { state.settle(it) } }
- )
- }
- )
+ .then(nestedScroll)
.draggableAnchors(state.anchoredDraggableState, orientation) { sheetSize, constraints ->
val layoutHeight = constraints.maxHeight.toFloat()
val sheetHeight = sheetSize.height.toFloat()
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
index 79d2960..c93dafa 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
@@ -151,6 +151,46 @@
val surfaceContainerLow: Color,
val surfaceContainerLowest: Color,
) {
+ @Deprecated(
+ level = DeprecationLevel.WARNING,
+ message = "Use constructor with additional 'surfaceContainer' roles.",
+ replaceWith = ReplaceWith("ColorScheme(primary,\n" +
+ "onPrimary,\n" +
+ "primaryContainer,\n" +
+ "onPrimaryContainer,\n" +
+ "inversePrimary,\n" +
+ "secondary,\n" +
+ "onSecondary,\n" +
+ "secondaryContainer,\n" +
+ "onSecondaryContainer,\n" +
+ "tertiary,\n" +
+ "onTertiary,\n" +
+ "tertiaryContainer,\n" +
+ "onTertiaryContainer,\n" +
+ "background,\n" +
+ "onBackground,\n" +
+ "surface,\n" +
+ "onSurface,\n" +
+ "surfaceVariant,\n" +
+ "onSurfaceVariant,\n" +
+ "surfaceTint,\n" +
+ "inverseSurface,\n" +
+ "inverseOnSurface,\n" +
+ "error,\n" +
+ "onError,\n" +
+ "errorContainer,\n" +
+ "onErrorContainer,\n" +
+ "outline,\n" +
+ "outlineVariant,\n" +
+ "scrim,\n" +
+ "surfaceBright,\n" +
+ "surfaceDim,\n" +
+ "surfaceContainer,\n" +
+ "surfaceContainerHigh,\n" +
+ "surfaceContainerHighest,\n" +
+ "surfaceContainerLow,\n" +
+ "surfaceContainerLowest,)")
+ )
constructor(
primary: Color,
onPrimary: Color,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
index c849bed9..ed4970e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
@@ -244,12 +244,12 @@
interactionSource = interactionSource,
colors = colors,
container = {
- OutlinedTextFieldDefaults.ContainerBox(
- enabled,
- isError,
- interactionSource,
- colors,
- shape
+ OutlinedTextFieldDefaults.Container(
+ enabled = enabled,
+ isError = isError,
+ interactionSource = interactionSource,
+ colors = colors,
+ shape = shape,
)
}
)
@@ -408,12 +408,12 @@
interactionSource = interactionSource,
colors = colors,
container = {
- OutlinedTextFieldDefaults.ContainerBox(
- enabled,
- isError,
- interactionSource,
- colors,
- shape
+ OutlinedTextFieldDefaults.Container(
+ enabled = enabled,
+ isError = isError,
+ interactionSource = interactionSource,
+ colors = colors,
+ shape = shape,
)
}
)
@@ -980,12 +980,6 @@
Alignment.CenterVertically.align(leadingPlaceable.height, height)
)
- // placed center vertically and to the end edge horizontally
- trailingPlaceable?.placeRelative(
- width - trailingPlaceable.width,
- Alignment.CenterVertically.align(trailingPlaceable.height, height)
- )
-
// label position is animated
// in single line text field, label is centered vertically before animation starts
labelPlaceable?.let {
@@ -1022,11 +1016,6 @@
calculateVerticalPosition(prefixPlaceable)
)
- suffixPlaceable?.placeRelative(
- width - widthOrZero(trailingPlaceable) - suffixPlaceable.width,
- calculateVerticalPosition(suffixPlaceable)
- )
-
val textHorizontalPosition = widthOrZero(leadingPlaceable) + widthOrZero(prefixPlaceable)
textFieldPlaceable.placeRelative(
@@ -1040,13 +1029,25 @@
calculateVerticalPosition(placeholderPlaceable)
)
+ suffixPlaceable?.placeRelative(
+ width - widthOrZero(trailingPlaceable) - suffixPlaceable.width,
+ calculateVerticalPosition(suffixPlaceable)
+ )
+
+ // placed center vertically and to the end edge horizontally
+ trailingPlaceable?.placeRelative(
+ width - trailingPlaceable.width,
+ Alignment.CenterVertically.align(trailingPlaceable.height, height)
+ )
+
// place supporting text
supportingPlaceable?.placeRelative(0, height)
}
-internal fun Modifier.outlineCutout(labelSize: Size, paddingValues: PaddingValues) =
+internal fun Modifier.outlineCutout(labelSize: () -> Size, paddingValues: PaddingValues) =
this.drawWithContent {
- val labelWidth = labelSize.width
+ val labelSizeValue = labelSize()
+ val labelWidth = labelSizeValue.width
if (labelWidth > 0f) {
val innerPadding = OutlinedTextFieldInnerPadding.toPx()
val leftLtr = paddingValues.calculateLeftPadding(layoutDirection).toPx() - innerPadding
@@ -1059,7 +1060,7 @@
LayoutDirection.Rtl -> size.width - leftLtr.coerceAtLeast(0f)
else -> rightLtr
}
- val labelHeight = labelSize.height
+ val labelHeight = labelSizeValue.height
// using label height as a cutout area to make sure that no hairline artifacts are
// left when we clip the border
clipRect(left, -labelHeight / 2, right, labelHeight / 2, ClipOp.Difference) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
index 0bc9184..a8977c5 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
@@ -376,7 +376,7 @@
): NestedScrollConnection = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
- return if (delta < 0 && source == NestedScrollSource.Drag) {
+ return if (delta < 0 && source == NestedScrollSource.UserInput) {
sheetState.anchoredDraggableState.dispatchRawDelta(delta).toOffset()
} else {
Offset.Zero
@@ -388,7 +388,7 @@
available: Offset,
source: NestedScrollSource
): Offset {
- return if (source == NestedScrollSource.Drag) {
+ return if (source == NestedScrollSource.UserInput) {
sheetState.anchoredDraggableState.dispatchRawDelta(available.toFloat()).toOffset()
} else {
Offset.Zero
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
index 6eb7ad4..79ee41b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
@@ -57,6 +57,7 @@
import androidx.compose.material3.internal.widthOrZero
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -81,7 +82,6 @@
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.coerceAtLeast
import androidx.compose.ui.unit.dp
@@ -964,10 +964,6 @@
0,
Alignment.CenterVertically.align(leadingPlaceable.height, height)
)
- trailingPlaceable?.placeRelative(
- width - trailingPlaceable.width,
- Alignment.CenterVertically.align(trailingPlaceable.height, height)
- )
labelPlaceable?.let {
// if it's a single line, the label's start position is in the center of the
// container. When it's a multiline text field, the label's start position is at the
@@ -985,14 +981,20 @@
}
prefixPlaceable?.placeRelative(widthOrZero(leadingPlaceable), textPosition)
+
+ val textHorizontalPosition = widthOrZero(leadingPlaceable) + widthOrZero(prefixPlaceable)
+ textfieldPlaceable.placeRelative(textHorizontalPosition, textPosition)
+ placeholderPlaceable?.placeRelative(textHorizontalPosition, textPosition)
+
suffixPlaceable?.placeRelative(
width - widthOrZero(trailingPlaceable) - suffixPlaceable.width,
textPosition,
)
- val textHorizontalPosition = widthOrZero(leadingPlaceable) + widthOrZero(prefixPlaceable)
- textfieldPlaceable.placeRelative(textHorizontalPosition, textPosition)
- placeholderPlaceable?.placeRelative(textHorizontalPosition, textPosition)
+ trailingPlaceable?.placeRelative(
+ width - trailingPlaceable.width,
+ Alignment.CenterVertically.align(trailingPlaceable.height, height)
+ )
supportingPlaceable?.placeRelative(0, height)
}
@@ -1028,10 +1030,6 @@
0,
Alignment.CenterVertically.align(leadingPlaceable.height, height)
)
- trailingPlaceable?.placeRelative(
- width - trailingPlaceable.width,
- Alignment.CenterVertically.align(trailingPlaceable.height, height)
- )
// Single line text field without label places its text components centered vertically.
// Multiline text field without label places its text components at the top with padding.
@@ -1048,11 +1046,6 @@
calculateVerticalPosition(prefixPlaceable)
)
- suffixPlaceable?.placeRelative(
- width - widthOrZero(trailingPlaceable) - suffixPlaceable.width,
- calculateVerticalPosition(suffixPlaceable),
- )
-
val textHorizontalPosition = widthOrZero(leadingPlaceable) + widthOrZero(prefixPlaceable)
textPlaceable.placeRelative(textHorizontalPosition, calculateVerticalPosition(textPlaceable))
@@ -1062,21 +1055,29 @@
calculateVerticalPosition(placeholderPlaceable)
)
+ suffixPlaceable?.placeRelative(
+ width - widthOrZero(trailingPlaceable) - suffixPlaceable.width,
+ calculateVerticalPosition(suffixPlaceable),
+ )
+
+ trailingPlaceable?.placeRelative(
+ width - trailingPlaceable.width,
+ Alignment.CenterVertically.align(trailingPlaceable.height, height)
+ )
+
supportingPlaceable?.placeRelative(0, height)
}
/**
* A draw modifier that draws a bottom indicator line in [TextField]
*/
-internal fun Modifier.drawIndicatorLine(indicatorBorder: BorderStroke): Modifier {
- val strokeWidthDp = indicatorBorder.width
+internal fun Modifier.drawIndicatorLine(indicatorBorder: State<BorderStroke>): Modifier {
return drawWithContent {
drawContent()
- if (strokeWidthDp == Dp.Hairline) return@drawWithContent
- val strokeWidth = strokeWidthDp.value * density
+ val strokeWidth = indicatorBorder.value.width.toPx()
val y = size.height - strokeWidth / 2
drawLine(
- indicatorBorder.brush,
+ indicatorBorder.value.brush,
Offset(0f, y),
Offset(size.width, y),
strokeWidth
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
index 8816844..96612a6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldDefaults.kt
@@ -17,10 +17,7 @@
package androidx.compose.material3
import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.InteractionSource
@@ -31,24 +28,22 @@
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
-import androidx.compose.material3.internal.AnimationDuration
import androidx.compose.material3.internal.CommonDecorationBox
import androidx.compose.material3.internal.SupportingTopPadding
+import androidx.compose.material3.internal.TextFieldAnimationDuration
import androidx.compose.material3.internal.TextFieldPadding
import androidx.compose.material3.internal.TextFieldType
+import androidx.compose.material3.internal.animateBorderStrokeAsState
+import androidx.compose.material3.internal.textFieldBackground
import androidx.compose.material3.tokens.FilledTextFieldTokens
import androidx.compose.material3.tokens.OutlinedTextFieldTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.text.input.VisualTransformation
@@ -87,50 +82,70 @@
val FocusedIndicatorThickness = 2.dp
/**
- * Composable that draws a default container for the content of [TextField], with an indicator
- * line at the bottom. You can use it to draw a container for your custom text field based on
- * [TextFieldDefaults.DecorationBox]. [TextField] applies it automatically.
+ * Composable that draws a default container for a [TextField] with an indicator line at the
+ * bottom. You can apply it to a [BasicTextField] using [DecorationBox] to create a custom text
+ * field based on the styling of a Material filled text field. The [TextField] component
+ * applies it automatically.
*
* @param enabled whether the text field is enabled
* @param isError whether the text field's current value is in error
- * @param interactionSource the [InteractionSource] of this text field. Helps to determine if
+ * @param interactionSource the [InteractionSource] of the text field. Used to determine if
* the text field is in focus or not
+ * @param modifier the [Modifier] of this container
* @param colors [TextFieldColors] used to resolve colors of the text field
- * @param shape shape of the container
+ * @param shape the shape of this container
+ * @param focusedIndicatorLineThickness thickness of the indicator line when the text field is
+ * focused
+ * @param unfocusedIndicatorLineThickness thickness of the indicator line when the text field is
+ * not focused
*/
@ExperimentalMaterial3Api
@Composable
- fun ContainerBox(
+ fun Container(
enabled: Boolean,
isError: Boolean,
interactionSource: InteractionSource,
- colors: TextFieldColors,
+ modifier: Modifier = Modifier,
+ colors: TextFieldColors = colors(),
shape: Shape = TextFieldDefaults.shape,
+ focusedIndicatorLineThickness: Dp = FocusedIndicatorThickness,
+ unfocusedIndicatorLineThickness: Dp = UnfocusedIndicatorThickness,
) {
val focused = interactionSource.collectIsFocusedAsState().value
- val containerColor = colors.containerColor(enabled, isError, focused)
- val containerColorState =
- animateColorAsState(containerColor, tween(durationMillis = AnimationDuration))
+ val containerColor = animateColorAsState(
+ targetValue = colors.containerColor(enabled, isError, focused),
+ animationSpec = tween(durationMillis = TextFieldAnimationDuration),
+ )
Box(
- Modifier
- .background(containerColorState.value, shape)
- .indicatorLine(enabled, isError, interactionSource, colors)
+ modifier
+ .textFieldBackground(containerColor::value, shape)
+ .indicatorLine(
+ enabled = enabled,
+ isError = isError,
+ interactionSource = interactionSource,
+ colors = colors,
+ focusedIndicatorLineThickness = focusedIndicatorLineThickness,
+ unfocusedIndicatorLineThickness = unfocusedIndicatorLineThickness,
+ )
)
}
/**
- * A modifier to draw a default bottom indicator line in [TextField]. You can use this modifier
- * if you build your custom text field using [TextFieldDefaults.DecorationBox] whilst the
- * [TextField] applies it automatically.
+ * A modifier to draw a default bottom indicator line for [TextField]. You can apply it to a
+ * [BasicTextField] or to [DecorationBox] to create a custom text field based on the styling
+ * of a Material filled text field.
+ *
+ * Consider using [Container], which automatically applies this modifier as well as other text
+ * field container styling.
*
* @param enabled whether the text field is enabled
* @param isError whether the text field's current value is in error
- * @param interactionSource the [InteractionSource] of this text field. Helps to determine if
+ * @param interactionSource the [InteractionSource] of the text field. Used to determine if
* the text field is in focus or not
* @param colors [TextFieldColors] used to resolve colors of the text field
- * @param focusedIndicatorLineThickness thickness of the indicator line when text field is
+ * @param focusedIndicatorLineThickness thickness of the indicator line when the text field is
* focused
- * @param unfocusedIndicatorLineThickness thickness of the indicator line when text field is
+ * @param unfocusedIndicatorLineThickness thickness of the indicator line when the text field is
* not focused
*/
@ExperimentalMaterial3Api
@@ -150,25 +165,138 @@
properties["focusedIndicatorLineThickness"] = focusedIndicatorLineThickness
properties["unfocusedIndicatorLineThickness"] = unfocusedIndicatorLineThickness
}) {
+ val focused = interactionSource.collectIsFocusedAsState().value
val stroke = animateBorderStrokeAsState(
enabled,
isError,
- interactionSource,
+ focused,
colors,
focusedIndicatorLineThickness,
unfocusedIndicatorLineThickness
)
- Modifier.drawIndicatorLine(stroke.value)
+ Modifier.drawIndicatorLine(stroke)
+ }
+
+ /**
+ * A decoration box used to create custom text fields based on
+ * <a href="https://m3.material.io/components/text-fields/overview" class="external" target="_blank">Material Design filled text field</a>.
+ *
+ * If your text field requires customising elements that aren't exposed by [TextField],
+ * consider using this decoration box to achieve the desired design.
+ *
+ * For example, if you wish to customise the bottom indicator line, you can pass a custom
+ * [Container] to this decoration box's [container].
+ *
+ * An example of building a custom text field using [DecorationBox]:
+ * @sample androidx.compose.material3.samples.CustomTextFieldBasedOnDecorationBox
+ *
+ * @param value the input [String] shown by the text field
+ * @param innerTextField input text field that this decoration box wraps. You will pass here a
+ * framework-controlled composable parameter "innerTextField" from the decorationBox lambda of
+ * the [BasicTextField]
+ * @param enabled the enabled state of the text field. When `false`, this decoration box will
+ * appear visually disabled. This must be the same value that is passed to [BasicTextField].
+ * @param singleLine indicates if this is a single line or multi line text field. This must be
+ * the same value that is passed to [BasicTextField].
+ * @param visualTransformation transforms the visual representation of the input [value]. This
+ * must be the same value that is passed to [BasicTextField].
+ * @param interactionSource the read-only [InteractionSource] representing the stream of
+ * [Interaction]s for this text field. You must first create and pass in your own `remember`ed
+ * [MutableInteractionSource] instance to the [BasicTextField] for it to dispatch events. And
+ * then pass the same instance to this decoration box to observe [Interaction]s and customize
+ * the appearance / behavior of this text field in different states.
+ * @param isError indicates if the text field's current value is in an error state. When `true`,
+ * this decoration box will display its contents in an error color.
+ * @param label the optional label to be displayed inside the text field container. The default
+ * text style for internal [Text] is [Typography.bodySmall] when the text field is in focus and
+ * [Typography.bodyLarge] when the text field is not in focus.
+ * @param placeholder the optional placeholder to be displayed when the text field is in focus
+ * and the input text is empty. The default text style for internal [Text] is
+ * [Typography.bodyLarge].
+ * @param leadingIcon the optional leading icon to be displayed at the beginning of the text
+ * field container
+ * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
+ * container
+ * @param prefix the optional prefix to be displayed before the input text in the text field
+ * @param suffix the optional suffix to be displayed after the input text in the text field
+ * @param supportingText the optional supporting text to be displayed below the text field
+ * @param shape defines the shape of this decoration box's container
+ * @param colors [TextFieldColors] that will be used to resolve the colors used for this text
+ * field decoration box in different states. See [TextFieldDefaults.colors].
+ * @param contentPadding the padding applied between the internal elements of this decoration
+ * box and the edge of its container. If a [label] is present, the top padding represents
+ * the distance from the top edge of the container to the top of the label when the text field
+ * is focused. When [label] is null, the top padding represents the distance from the top edge
+ * of the container to the top of the input field. All other paddings represent the distance
+ * from the edge of the container to the corresponding edge of the closest element.
+ * @param container the container to be drawn behind the text field. By default, this uses
+ * [Container]. Default colors for the container come from the [colors].
+ */
+ @Composable
+ @ExperimentalMaterial3Api
+ fun DecorationBox(
+ value: String,
+ innerTextField: @Composable () -> Unit,
+ enabled: Boolean,
+ singleLine: Boolean,
+ visualTransformation: VisualTransformation,
+ interactionSource: InteractionSource,
+ isError: Boolean = false,
+ label: @Composable (() -> Unit)? = null,
+ placeholder: @Composable (() -> Unit)? = null,
+ leadingIcon: @Composable (() -> Unit)? = null,
+ trailingIcon: @Composable (() -> Unit)? = null,
+ prefix: @Composable (() -> Unit)? = null,
+ suffix: @Composable (() -> Unit)? = null,
+ supportingText: @Composable (() -> Unit)? = null,
+ shape: Shape = TextFieldDefaults.shape,
+ colors: TextFieldColors = colors(),
+ contentPadding: PaddingValues =
+ if (label == null) {
+ contentPaddingWithoutLabel()
+ } else {
+ contentPaddingWithLabel()
+ },
+ container: @Composable () -> Unit = {
+ Container(
+ enabled = enabled,
+ isError = isError,
+ interactionSource = interactionSource,
+ modifier = Modifier,
+ colors = colors,
+ shape = shape,
+ focusedIndicatorLineThickness = FocusedIndicatorThickness,
+ unfocusedIndicatorLineThickness = UnfocusedIndicatorThickness,
+ )
+ }
+ ) {
+ CommonDecorationBox(
+ type = TextFieldType.Filled,
+ value = value,
+ innerTextField = innerTextField,
+ visualTransformation = visualTransformation,
+ placeholder = placeholder,
+ label = label,
+ leadingIcon = leadingIcon,
+ trailingIcon = trailingIcon,
+ prefix = prefix,
+ suffix = suffix,
+ supportingText = supportingText,
+ singleLine = singleLine,
+ enabled = enabled,
+ isError = isError,
+ interactionSource = interactionSource,
+ colors = colors,
+ contentPadding = contentPadding,
+ container = container
+ )
}
/**
* Default content padding applied to [TextField] when there is a label.
*
- * Note that when the label is present, the "top" padding is a distance between the top edge of
- * the [TextField] and the top of the label, not to the top of the input field. The input field
- * is placed directly beneath the label.
- *
- * See [PaddingValues] for more details.
+ * The top padding represents ths distance between the top edge of the [TextField] and the top
+ * of the label in the focused state. The input field is placed directly beneath the label.
*/
fun contentPaddingWithLabel(
start: Dp = TextFieldPadding,
@@ -179,7 +307,6 @@
/**
* Default content padding applied to [TextField] when the label is null.
- * See [PaddingValues] for more details.
*/
fun contentPaddingWithoutLabel(
start: Dp = TextFieldPadding,
@@ -193,7 +320,6 @@
* See [PaddingValues] for more details.
*/
// TODO(246775477): consider making this public
- @ExperimentalMaterial3Api
internal fun supportingTextPadding(
start: Dp = TextFieldPadding,
top: Dp = SupportingTopPadding,
@@ -417,118 +543,35 @@
}
}
- /**
- * A decoration box which helps creating custom text fields based on
- * <a href="https://material.io/components/text-fields#filled-text-field" class="external" target="_blank">Material Design filled text field</a>.
- *
- * If your text field requires customising elements that aren't exposed by [TextField],
- * consider using this decoration box to achieve the desired design.
- *
- * For example, if you need to create a dense text field, use [contentPadding] parameter to
- * decrease the paddings around the input field. If you need to customise the bottom indicator,
- * apply [indicatorLine] modifier to achieve that.
- *
- * See example of using [DecorationBox] to build your own custom text field
- * @sample androidx.compose.material3.samples.CustomTextFieldBasedOnDecorationBox
- *
- * @param value the input [String] shown by the text field
- * @param innerTextField input text field that this decoration box wraps. You will pass here a
- * framework-controlled composable parameter "innerTextField" from the decorationBox lambda of
- * the [BasicTextField]
- * @param enabled controls the enabled state of the text field. When `false`, this component
- * will not respond to user input, and it will appear visually disabled and disabled to
- * accessibility services. You must also pass the same value to the [BasicTextField] for it to
- * adjust the behavior accordingly.
- * @param singleLine indicates if this is a single line or multi line text field. You must pass
- * the same value as to [BasicTextField].
- * @param visualTransformation transforms the visual representation of the input [value]. You
- * must pass the same value as to [BasicTextField].
- * @param interactionSource the read-only [InteractionSource] representing the stream of
- * [Interaction]s for this text field. You must first create and pass in your own `remember`ed
- * [MutableInteractionSource] instance to the [BasicTextField] for it to dispatch events. And
- * then pass the same instance to this decoration box to observe [Interaction]s and customize
- * the appearance / behavior of this text field in different states.
- * @param isError indicates if the text field's current value is in error state. If set to
- * true, the label, bottom indicator and trailing icon by default will be displayed in error
- * color.
- * @param label the optional label to be displayed inside the text field container. The default
- * text style for internal [Text] is [Typography.bodySmall] when the text field is in focus and
- * [Typography.bodyLarge] when the text field is not in focus.
- * @param placeholder the optional placeholder to be displayed when the text field is in focus
- * and the input text is empty. The default text style for internal [Text] is
- * [Typography.bodyLarge].
- * @param leadingIcon the optional leading icon to be displayed at the beginning of the text
- * field container
- * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
- * container
- * @param prefix the optional prefix to be displayed before the input text in the text field
- * @param suffix the optional suffix to be displayed after the input text in the text field
- * @param supportingText the optional supporting text to be displayed below the text field
- * @param shape defines the shape of this text field's container
- * @param colors [TextFieldColors] that will be used to resolve the colors used for this text
- * field in different states. See [TextFieldDefaults.colors].
- * @param contentPadding the spacing values to apply internally between the internals of text
- * field and the decoration box container. You can use it to implement dense text fields or
- * simply to control horizontal padding. See [TextFieldDefaults.contentPaddingWithLabel] and
- * [TextFieldDefaults.contentPaddingWithoutLabel].
- * Note that if there's a label in the text field, the [top][PaddingValues.calculateTopPadding]
- * padding represents the distance from the top edge of the container to the top of the label.
- * Otherwise if label is null, it represents the distance from the top edge of the container to
- * the top of the input field. All other paddings represent the distance from the corresponding
- * edge of the container to the corresponding edge of the closest element.
- * @param container the container to be drawn behind the text field. By default, this includes
- * the bottom indicator line. Default colors for the container come from the [colors].
- */
- @Composable
+ @Deprecated(
+ message = "Renamed to TextFieldDefaults.Container",
+ replaceWith = ReplaceWith("Container(\n" +
+ " enabled = enabled,\n" +
+ " isError = isError,\n" +
+ " interactionSource = interactionSource,\n" +
+ " colors = colors,\n" +
+ " shape = shape,\n" +
+ ")"),
+ level = DeprecationLevel.WARNING
+ )
@ExperimentalMaterial3Api
- fun DecorationBox(
- value: String,
- innerTextField: @Composable () -> Unit,
+ @Composable
+ fun ContainerBox(
enabled: Boolean,
- singleLine: Boolean,
- visualTransformation: VisualTransformation,
+ isError: Boolean,
interactionSource: InteractionSource,
- isError: Boolean = false,
- label: @Composable (() -> Unit)? = null,
- placeholder: @Composable (() -> Unit)? = null,
- leadingIcon: @Composable (() -> Unit)? = null,
- trailingIcon: @Composable (() -> Unit)? = null,
- prefix: @Composable (() -> Unit)? = null,
- suffix: @Composable (() -> Unit)? = null,
- supportingText: @Composable (() -> Unit)? = null,
+ colors: TextFieldColors,
shape: Shape = TextFieldDefaults.shape,
- colors: TextFieldColors = colors(),
- contentPadding: PaddingValues =
- if (label == null) {
- contentPaddingWithoutLabel()
- } else {
- contentPaddingWithLabel()
- },
- container: @Composable () -> Unit = {
- ContainerBox(enabled, isError, interactionSource, colors, shape)
- }
- ) {
- CommonDecorationBox(
- type = TextFieldType.Filled,
- value = value,
- innerTextField = innerTextField,
- visualTransformation = visualTransformation,
- placeholder = placeholder,
- label = label,
- leadingIcon = leadingIcon,
- trailingIcon = trailingIcon,
- prefix = prefix,
- suffix = suffix,
- supportingText = supportingText,
- singleLine = singleLine,
- enabled = enabled,
- isError = isError,
- interactionSource = interactionSource,
- colors = colors,
- contentPadding = contentPadding,
- container = container
- )
- }
+ ) = Container(
+ enabled = enabled,
+ isError = isError,
+ interactionSource = interactionSource,
+ modifier = Modifier,
+ colors = colors,
+ shape = shape,
+ focusedIndicatorLineThickness = FocusedIndicatorThickness,
+ unfocusedIndicatorLineThickness = UnfocusedIndicatorThickness,
+ )
@Deprecated(
message = "Renamed to `OutlinedTextFieldDefaults.shape`",
@@ -661,48 +704,156 @@
val FocusedBorderThickness = 2.dp
/**
- * Composable that draws a default container for [OutlinedTextField] with a border stroke. You
- * can use it to draw a border stroke in your custom text field based on
- * [OutlinedTextFieldDefaults.DecorationBox]. The [OutlinedTextField] applies it automatically.
+ * Composable that draws a default container for an [OutlinedTextField] with a border stroke.
+ * You can apply it to a [BasicTextField] using [DecorationBox] to create a custom text field
+ * based on the styling of a Material outlined text field. The [OutlinedTextField] component
+ * applies it automatically.
*
* @param enabled whether the text field is enabled
* @param isError whether the text field's current value is in error
- * @param interactionSource the [InteractionSource] of this text field. Helps to determine if
+ * @param interactionSource the [InteractionSource] of the text field. Used to determine if
* the text field is in focus or not
+ * @param modifier the [Modifier] of this container
* @param colors [TextFieldColors] used to resolve colors of the text field
- * @param shape shape of the container
- * @param focusedBorderThickness thickness of the [OutlinedTextField]'s border when it is in
- * focused state
- * @param unfocusedBorderThickness thickness of the [OutlinedTextField]'s border when it is not
- * in focused state
+ * @param shape the shape of this container
+ * @param focusedBorderThickness thickness of the border when the text field is focused
+ * @param unfocusedBorderThickness thickness of the border when the text field is not focused
*/
@ExperimentalMaterial3Api
@Composable
- fun ContainerBox(
+ fun Container(
enabled: Boolean,
isError: Boolean,
interactionSource: InteractionSource,
- colors: TextFieldColors,
- shape: Shape = OutlinedTextFieldTokens.ContainerShape.value,
+ modifier: Modifier = Modifier,
+ colors: TextFieldColors = colors(),
+ shape: Shape = OutlinedTextFieldDefaults.shape,
focusedBorderThickness: Dp = FocusedBorderThickness,
- unfocusedBorderThickness: Dp = UnfocusedBorderThickness
+ unfocusedBorderThickness: Dp = UnfocusedBorderThickness,
) {
+ val focused = interactionSource.collectIsFocusedAsState().value
val borderStroke = animateBorderStrokeAsState(
enabled,
isError,
- interactionSource,
+ focused,
colors,
focusedBorderThickness,
- unfocusedBorderThickness
+ unfocusedBorderThickness,
)
- val focused = interactionSource.collectIsFocusedAsState().value
- val containerColor = colors.containerColor(enabled, isError, focused)
- val containerColorState =
- animateColorAsState(containerColor, tween(durationMillis = AnimationDuration))
+ val containerColor = animateColorAsState(
+ targetValue = colors.containerColor(enabled, isError, focused),
+ animationSpec = tween(durationMillis = TextFieldAnimationDuration),
+ )
Box(
- Modifier
+ modifier
.border(borderStroke.value, shape)
- .background(containerColorState.value, shape)
+ .textFieldBackground(containerColor::value, shape)
+ )
+ }
+
+ /**
+ * A decoration box used to create custom text fields based on
+ * <a href="https://m3.material.io/components/text-fields/overview" class="external" target="_blank">Material Design outlined text field</a>.
+ *
+ * If your text field requires customising elements that aren't exposed by [OutlinedTextField],
+ * consider using this decoration box to achieve the desired design.
+ *
+ * For example, if you wish to customize the thickness of the border, you can pass a custom
+ * [Container] to this decoration box's [container].
+ *
+ * An example of building a custom text field using [DecorationBox]:
+ * @sample androidx.compose.material3.samples.CustomOutlinedTextFieldBasedOnDecorationBox
+ *
+ * @param value the input [String] shown by the text field
+ * @param innerTextField input text field that this decoration box wraps. You will pass here a
+ * framework-controlled composable parameter "innerTextField" from the decorationBox lambda of
+ * the [BasicTextField]
+ * @param enabled the enabled state of the text field. When `false`, this decoration box will
+ * appear visually disabled. This must be the same value that is passed to [BasicTextField].
+ * @param singleLine indicates if this is a single line or multi line text field. This must be
+ * the same value that is passed to [BasicTextField].
+ * @param visualTransformation transforms the visual representation of the input [value]. This
+ * must be the same value that is passed to [BasicTextField].
+ * @param interactionSource the read-only [InteractionSource] representing the stream of
+ * [Interaction]s for this text field. You must first create and pass in your own `remember`ed
+ * [MutableInteractionSource] instance to the [BasicTextField] for it to dispatch events. And
+ * then pass the same instance to this decoration box to observe [Interaction]s and customize
+ * the appearance / behavior of this text field in different states.
+ * @param isError indicates if the text field's current value is in an error state. When `true`,
+ * this decoration box will display its contents in an error color.
+ * @param label the optional label to be displayed inside the text field container. The default
+ * text style for internal [Text] is [Typography.bodySmall] when the text field is in focus and
+ * [Typography.bodyLarge] when the text field is not in focus.
+ * @param placeholder the optional placeholder to be displayed when the text field is in focus
+ * and the input text is empty. The default text style for internal [Text] is
+ * [Typography.bodyLarge].
+ * @param leadingIcon the optional leading icon to be displayed at the beginning of the text
+ * field container
+ * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
+ * container
+ * @param prefix the optional prefix to be displayed before the input text in the text field
+ * @param suffix the optional suffix to be displayed after the input text in the text field
+ * @param supportingText the optional supporting text to be displayed below the text field
+ * @param colors [TextFieldColors] that will be used to resolve the colors used for this text
+ * field in different states. See [OutlinedTextFieldDefaults.colors].
+ * @param contentPadding the padding applied between the internal elements of this decoration
+ * box and the edge of its container
+ * @param container the container to be drawn behind the text field. By default, this is
+ * transparent and only includes a border. The cutout in the border to fit the [label] will be
+ * automatically added by the framework. Default colors for the container come from the
+ * [colors].
+ */
+ @Composable
+ @ExperimentalMaterial3Api
+ fun DecorationBox(
+ value: String,
+ innerTextField: @Composable () -> Unit,
+ enabled: Boolean,
+ singleLine: Boolean,
+ visualTransformation: VisualTransformation,
+ interactionSource: InteractionSource,
+ isError: Boolean = false,
+ label: @Composable (() -> Unit)? = null,
+ placeholder: @Composable (() -> Unit)? = null,
+ leadingIcon: @Composable (() -> Unit)? = null,
+ trailingIcon: @Composable (() -> Unit)? = null,
+ prefix: @Composable (() -> Unit)? = null,
+ suffix: @Composable (() -> Unit)? = null,
+ supportingText: @Composable (() -> Unit)? = null,
+ colors: TextFieldColors = colors(),
+ contentPadding: PaddingValues = contentPadding(),
+ container: @Composable () -> Unit = {
+ Container(
+ enabled = enabled,
+ isError = isError,
+ interactionSource = interactionSource,
+ modifier = Modifier,
+ colors = colors,
+ shape = shape,
+ focusedBorderThickness = FocusedBorderThickness,
+ unfocusedBorderThickness = UnfocusedBorderThickness,
+ )
+ }
+ ) {
+ CommonDecorationBox(
+ type = TextFieldType.Outlined,
+ value = value,
+ visualTransformation = visualTransformation,
+ innerTextField = innerTextField,
+ placeholder = placeholder,
+ label = label,
+ leadingIcon = leadingIcon,
+ trailingIcon = trailingIcon,
+ prefix = prefix,
+ suffix = suffix,
+ supportingText = supportingText,
+ singleLine = singleLine,
+ enabled = enabled,
+ isError = isError,
+ interactionSource = interactionSource,
+ colors = colors,
+ contentPadding = contentPadding,
+ container = container
)
}
@@ -935,112 +1086,40 @@
defaultOutlinedTextFieldColorsCached = it
}
}
- /**
- * A decoration box which helps creating custom text fields based on
- * <a href="https://material.io/components/text-fields#outlined-text-field" class="external" target="_blank">Material Design outlined text field</a>.
- *
- * If your text field requires customising elements that aren't exposed by [OutlinedTextField],
- * consider using this decoration box to achieve the desired design.
- *
- * For example, if you need to create a dense outlined text field, use [contentPadding]
- * parameter to decrease the paddings around the input field. If you need to change the
- * thickness of the border, use [container] parameter to achieve that.
- *
- * Example of custom text field based on [OutlinedTextFieldDefaults.DecorationBox]:
- * @sample androidx.compose.material3.samples.CustomOutlinedTextFieldBasedOnDecorationBox
- *
- * @param value the input [String] shown by the text field
- * @param innerTextField input text field that this decoration box wraps. You will pass here a
- * framework-controlled composable parameter "innerTextField" from the decorationBox lambda of
- * the [BasicTextField]
- * @param enabled controls the enabled state of the text field. When `false`, this component
- * will not respond to user input, and it will appear visually disabled and disabled to
- * accessibility services. You must also pass the same value to the [BasicTextField] for it to
- * adjust the behavior accordingly.
- * @param singleLine indicates if this is a single line or multi line text field. You must pass
- * the same value as to [BasicTextField].
- * @param visualTransformation transforms the visual representation of the input [value]. You
- * must pass the same value as to [BasicTextField].
- * @param interactionSource the read-only [InteractionSource] representing the stream of
- * [Interaction]s for this text field. You must first create and pass in your own `remember`ed
- * [MutableInteractionSource] instance to the [BasicTextField] for it to dispatch events. And
- * then pass the same instance to this decoration box to observe [Interaction]s and customize
- * the appearance / behavior of this text field in different states.
- * @param isError indicates if the text field's current value is in error state. If set to
- * true, the label, bottom indicator and trailing icon by default will be displayed in error
- * color.
- * @param label the optional label to be displayed inside the text field container. The default
- * text style for internal [Text] is [Typography.bodySmall] when the text field is in focus and
- * [Typography.bodyLarge] when the text field is not in focus.
- * @param placeholder the optional placeholder to be displayed when the text field is in focus
- * and the input text is empty. The default text style for internal [Text] is
- * [Typography.bodyLarge].
- * @param leadingIcon the optional leading icon to be displayed at the beginning of the text
- * field container
- * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
- * container
- * @param prefix the optional prefix to be displayed before the input text in the text field
- * @param suffix the optional suffix to be displayed after the input text in the text field
- * @param supportingText the optional supporting text to be displayed below the text field
- * @param colors [TextFieldColors] that will be used to resolve the colors used for this text
- * field in different states. See [OutlinedTextFieldDefaults.colors].
- * @param contentPadding the spacing values to apply internally between the internals of text
- * field and the decoration box container. You can use it to implement dense text fields or
- * simply to control horizontal padding. See [OutlinedTextFieldDefaults.contentPadding].
- * @param container the container to be drawn behind the text field. By default, this is
- * transparent and only includes a border. The cutout in the border to fit the [label] will be
- * automatically added by the framework. Note that by default the color of the border comes from
- * the [colors].
- */
- @Composable
+
+ @Deprecated(
+ message = "Renamed to OutlinedTextFieldDefaults.Container",
+ replaceWith = ReplaceWith("Container(\n" +
+ " enabled = enabled,\n" +
+ " isError = isError,\n" +
+ " interactionSource = interactionSource,\n" +
+ " colors = colors,\n" +
+ " shape = shape,\n" +
+ " focusedBorderThickness = focusedBorderThickness,\n" +
+ " unfocusedBorderThickness = unfocusedBorderThickness,\n" +
+ ")"),
+ level = DeprecationLevel.WARNING
+ )
@ExperimentalMaterial3Api
- fun DecorationBox(
- value: String,
- innerTextField: @Composable () -> Unit,
+ @Composable
+ fun ContainerBox(
enabled: Boolean,
- singleLine: Boolean,
- visualTransformation: VisualTransformation,
+ isError: Boolean,
interactionSource: InteractionSource,
- isError: Boolean = false,
- label: @Composable (() -> Unit)? = null,
- placeholder: @Composable (() -> Unit)? = null,
- leadingIcon: @Composable (() -> Unit)? = null,
- trailingIcon: @Composable (() -> Unit)? = null,
- prefix: @Composable (() -> Unit)? = null,
- suffix: @Composable (() -> Unit)? = null,
- supportingText: @Composable (() -> Unit)? = null,
colors: TextFieldColors = colors(),
- contentPadding: PaddingValues = contentPadding(),
- container: @Composable () -> Unit = {
- ContainerBox(
- enabled,
- isError,
- interactionSource,
- colors
- )
- }
- ) {
- CommonDecorationBox(
- type = TextFieldType.Outlined,
- value = value,
- visualTransformation = visualTransformation,
- innerTextField = innerTextField,
- placeholder = placeholder,
- label = label,
- leadingIcon = leadingIcon,
- trailingIcon = trailingIcon,
- prefix = prefix,
- suffix = suffix,
- supportingText = supportingText,
- singleLine = singleLine,
- enabled = enabled,
- isError = isError,
- interactionSource = interactionSource,
- colors = colors,
- contentPadding = contentPadding,
- container = container
- )
- }
+ shape: Shape = OutlinedTextFieldDefaults.shape,
+ focusedBorderThickness: Dp = FocusedBorderThickness,
+ unfocusedBorderThickness: Dp = UnfocusedBorderThickness,
+ ) = Container(
+ enabled = enabled,
+ isError = isError,
+ interactionSource = interactionSource,
+ modifier = Modifier,
+ colors = colors,
+ shape = shape,
+ focusedBorderThickness = focusedBorderThickness,
+ unfocusedBorderThickness = unfocusedBorderThickness,
+ )
}
/**
@@ -1542,30 +1621,3 @@
return result
}
}
-
-@Composable
-private fun animateBorderStrokeAsState(
- enabled: Boolean,
- isError: Boolean,
- interactionSource: InteractionSource,
- colors: TextFieldColors,
- focusedBorderThickness: Dp,
- unfocusedBorderThickness: Dp
-): State<BorderStroke> {
- val focused by interactionSource.collectIsFocusedAsState()
- val indicatorColor = colors.indicatorColor(enabled, isError, focused)
- val indicatorColorState = if (enabled) {
- animateColorAsState(indicatorColor, tween(durationMillis = AnimationDuration))
- } else {
- rememberUpdatedState(indicatorColor)
- }
- val targetThickness = if (focused) focusedBorderThickness else unfocusedBorderThickness
- val animatedThickness = if (enabled) {
- animateDpAsState(targetThickness, tween(durationMillis = AnimationDuration))
- } else {
- rememberUpdatedState(unfocusedBorderThickness)
- }
- return rememberUpdatedState(
- BorderStroke(animatedThickness.value, SolidColor(indicatorColorState.value))
- )
-}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
index dc8ea84..98413b8 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
@@ -1655,7 +1655,7 @@
interactionSource = interactionSource,
contentPadding = PaddingValues(0.dp),
container = {
- OutlinedTextFieldDefaults.ContainerBox(
+ OutlinedTextFieldDefaults.Container(
enabled = true,
isError = false,
interactionSource = interactionSource,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
index df65fa5..65effa4 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
@@ -112,7 +112,7 @@
minSmallItemWidth: Dp = CarouselDefaults.MinSmallItemSize,
maxSmallItemWidth: Dp = CarouselDefaults.MaxSmallItemSize,
contentPadding: PaddingValues = PaddingValues(0.dp),
- content: @Composable CarouselScope.(itemIndex: Int) -> Unit
+ content: @Composable CarouselItemScope.(itemIndex: Int) -> Unit
) {
val density = LocalDensity.current
Carousel(
@@ -174,7 +174,7 @@
itemSpacing: Dp = 0.dp,
flingBehavior: TargetedFlingBehavior = CarouselDefaults.noSnapFlingBehavior(),
contentPadding: PaddingValues = PaddingValues(0.dp),
- content: @Composable CarouselScope.(itemIndex: Int) -> Unit
+ content: @Composable CarouselItemScope.(itemIndex: Int) -> Unit
) {
val density = LocalDensity.current
Carousel(
@@ -228,7 +228,7 @@
itemSpacing: Dp = 0.dp,
flingBehavior: TargetedFlingBehavior =
CarouselDefaults.singleAdvanceFlingBehavior(state = state),
- content: @Composable CarouselScope.(itemIndex: Int) -> Unit
+ content: @Composable CarouselItemScope.(itemIndex: Int) -> Unit
) {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val beforeContentPadding = contentPadding.calculateBeforeContentPadding(orientation)
@@ -240,7 +240,6 @@
val beyondViewportPageCount = remember(pageSize.strategy.itemMainAxisSize) {
calculateBeyondViewportPageCount(pageSize.strategy)
}
- val carouselScope = CarouselScopeImpl
val snapPositionMap = remember(pageSize.strategy.itemMainAxisSize) {
calculateSnapPositions(
@@ -265,16 +264,20 @@
flingBehavior = flingBehavior,
modifier = modifier
) { page ->
+ val carouselItemInfo = remember { CarouselItemInfoImpl() }
+ val scope = remember { CarouselItemScopeImpl(itemInfo = carouselItemInfo) }
+
Box(
modifier = Modifier.carouselItem(
index = page,
state = state,
strategy = pageSize.strategy,
itemPositionMap = snapPositionMap,
+ carouselItemInfo = carouselItemInfo,
isRtl = isRtl
)
) {
- carouselScope.content(page)
+ scope.content(page)
}
}
} else if (orientation == Orientation.Vertical) {
@@ -292,16 +295,20 @@
flingBehavior = flingBehavior,
modifier = modifier
) { page ->
+ val carouselItemInfo = remember { CarouselItemInfoImpl() }
+ val scope = remember { CarouselItemScopeImpl(itemInfo = carouselItemInfo) }
+
Box(
modifier = Modifier.carouselItem(
index = page,
state = state,
strategy = pageSize.strategy,
itemPositionMap = snapPositionMap,
+ carouselItemInfo = carouselItemInfo,
isRtl = isRtl
)
) {
- carouselScope.content(page)
+ scope.content(page)
}
}
}
@@ -401,6 +408,7 @@
* @param state the carousel state
* @param strategy the strategy used to mask and translate items in the carousel
* @param itemPositionMap the position of each index when it is the current item
+ * @param carouselItemInfo the item info that should be updated with the changes in this modifier
* @param isRtl true if the layout direction is right-to-left
*/
@OptIn(ExperimentalMaterial3Api::class)
@@ -409,6 +417,7 @@
state: CarouselState,
strategy: Strategy,
itemPositionMap: IntIntMap,
+ carouselItemInfo: CarouselItemInfoImpl,
isRtl: Boolean
): Modifier {
if (!strategy.isValid()) return this
@@ -442,6 +451,11 @@
val maxScrollOffset = calculateMaxScrollOffset(state, strategy)
// TODO: Reduce the number of times a keyline for the same scroll offset is calculated
val keylines = strategy.getKeylineListForScrollOffset(scrollOffset, maxScrollOffset)
+ val roundedKeylines = strategy.getKeylineListForScrollOffset(
+ scrollOffset = scrollOffset,
+ maxScrollOffset = maxScrollOffset,
+ roundToNearestStep = true
+ )
// Find center of the item at this index
val itemSizeWithSpacing = strategy.itemMainAxisSize + strategy.itemSpacing
@@ -458,6 +472,26 @@
val interpolatedKeyline = lerp(keylineBefore, keylineAfter, progress)
val isOutOfKeylineBounds = keylineBefore == keylineAfter
+ val centerX =
+ if (isVertical) size.height / 2f else strategy.itemMainAxisSize / 2f
+ val centerY =
+ if (isVertical) strategy.itemMainAxisSize / 2f else size.height / 2f
+ val halfMaskWidth =
+ if (isVertical) size.width / 2f else interpolatedKeyline.size / 2f
+ val halfMaskHeight =
+ if (isVertical) interpolatedKeyline.size / 2f else size.height / 2f
+ val maskRect = Rect(
+ left = centerX - halfMaskWidth,
+ top = centerY - halfMaskHeight,
+ right = centerX + halfMaskWidth,
+ bottom = centerY + halfMaskHeight
+ )
+
+ // Update carousel item info
+ carouselItemInfo.sizeState.floatValue = interpolatedKeyline.size
+ carouselItemInfo.minSizeState.floatValue = roundedKeylines.minBy { it.size }.size
+ carouselItemInfo.maxSizeState.floatValue = roundedKeylines.firstFocal.size
+
// Clip the item
clip = true
shape = object : Shape {
@@ -469,29 +503,15 @@
layoutDirection: LayoutDirection,
density: Density
): Outline {
- val centerX =
- if (isVertical) size.height / 2f else strategy.itemMainAxisSize / 2f
- val centerY =
- if (isVertical) strategy.itemMainAxisSize / 2f else size.height / 2f
- val halfMaskWidth =
- if (isVertical) size.width / 2f else interpolatedKeyline.size / 2f
- val halfMaskHeight =
- if (isVertical) interpolatedKeyline.size / 2f else size.height / 2f
- val rect = Rect(
- left = centerX - halfMaskWidth,
- top = centerY - halfMaskHeight,
- right = centerX + halfMaskWidth,
- bottom = centerY + halfMaskHeight
- )
val cornerSize =
roundedCornerShape.topStart.toPx(
- Size(rect.width, rect.height),
+ Size(maskRect.width, maskRect.height),
density
)
val cornerRadius = CornerRadius(cornerSize)
return Outline.Rounded(
RoundRect(
- rect = rect,
+ rect = maskRect,
topLeft = cornerRadius,
topRight = cornerRadius,
bottomRight = cornerRadius,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselItemScope.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselItemScope.kt
new file mode 100644
index 0000000..fea9940
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselItemScope.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.carousel
+
+import androidx.compose.material3.ExperimentalMaterial3Api
+
+/**
+ * Receiver scope for [Carousel] item content.
+ */
+@ExperimentalMaterial3Api
+sealed interface CarouselItemScope {
+ /**
+ * Information regarding the carousel item, such as its minimum and maximum size.
+ *
+ * The item information is updated after every scroll. If you use it in a composable function,
+ * it will be recomposed on every change causing potential performance issues. Avoid using it
+ * in the composition.
+ */
+ val carouselItemInfo: CarouselItemInfo
+}
+
+@ExperimentalMaterial3Api
+internal class CarouselItemScopeImpl(
+ private val itemInfo: CarouselItemInfo
+) : CarouselItemScope {
+ override val carouselItemInfo: CarouselItemInfo
+ get() = itemInfo
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselScope.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselScope.kt
deleted file mode 100644
index 467d8a5..0000000
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselScope.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3.carousel
-
-import androidx.compose.material3.ExperimentalMaterial3Api
-
-/**
- * Receiver scope for [Carousel].
- */
-@ExperimentalMaterial3Api
-sealed interface CarouselScope
-
-@ExperimentalMaterial3Api
-internal object CarouselScopeImpl : CarouselScope
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt
index 4c26f8a..1f5f990 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt
@@ -23,6 +23,7 @@
import androidx.compose.foundation.pager.PagerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
@@ -108,3 +109,80 @@
itemCountState.value = itemCount
}
}
+
+/**
+ * Interface to hold information about a Carousel item and its size.
+ *
+ * Example of CarouselItemInfo usage:
+ * @sample androidx.compose.material3.samples.FadingHorizontalMultiBrowseCarouselSample
+ */
+@ExperimentalMaterial3Api
+sealed interface CarouselItemInfo {
+
+ /** The size of the carousel item in the main axis */
+ val size: Float
+
+ /**
+ * The minimum size in the main axis of the carousel item, eg. the size of the item when it
+ * scrolls off the sides of the carousel
+ */
+ val minSize: Float
+
+ /**
+ * The maximum size in the main axis of the carousel item, eg. the size of the item when it is
+ * at a focal position
+ */
+ val maxSize: Float
+}
+
+/**
+ * Gets the start offset of the carousel item from its full size. This offset can be used to pin any
+ * carousel item content to the left side of the item (or right if RTL).
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+fun CarouselItemInfo.startOffset(): Float {
+ return (maxSize - size) / 2f
+}
+
+/**
+ * Gets the end offset of the carousel item from its full size. This offset can be used to pin any
+ * carousel item content to the right side of the item (or left if RTL).
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+fun CarouselItemInfo.endOffset(): Float {
+ return maxSize - (maxSize - size) / 2f
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+internal class CarouselItemInfoImpl : CarouselItemInfo {
+
+ val sizeState = mutableFloatStateOf(0f)
+ override val size: Float
+ get() = sizeState.floatValue
+
+ val minSizeState = mutableFloatStateOf(0f)
+ override val minSize: Float
+ get() = minSizeState.floatValue
+
+ val maxSizeState = mutableFloatStateOf(0f)
+ override val maxSize: Float
+ get() = maxSizeState.floatValue
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is CarouselItemInfoImpl) return false
+
+ if (sizeState != other.sizeState) return false
+ if (minSizeState != other.minSizeState) return false
+ if (maxSizeState != other.maxSizeState) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = sizeState.hashCode()
+ result = 31 * result + minSizeState.hashCode()
+ result = 31 * result + maxSizeState.hashCode()
+ return result
+ }
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
index 916efe0..92a5bba 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
@@ -309,7 +309,9 @@
defaultKeylines,
carouselMainAxisSize,
itemSpacing,
- beforeContentPadding
+ beforeContentPadding,
+ defaultKeylines.firstFocal,
+ defaultKeylines.firstFocalIndex
)
)
}
@@ -364,7 +366,9 @@
steps.last(),
carouselMainAxisSize,
itemSpacing,
- beforeContentPadding
+ beforeContentPadding,
+ steps.last().firstFocal,
+ steps.last().firstFocalIndex
)
}
@@ -402,7 +406,9 @@
defaultKeylines,
carouselMainAxisSize,
itemSpacing,
- -afterContentPadding
+ -afterContentPadding,
+ defaultKeylines.lastFocal,
+ defaultKeylines.lastFocalIndex
))
}
return steps
@@ -456,7 +462,9 @@
steps.last(),
carouselMainAxisSize,
itemSpacing,
- -afterContentPadding
+ -afterContentPadding,
+ steps.last().lastFocal,
+ steps.last().lastFocalIndex
)
}
@@ -471,7 +479,9 @@
from: KeylineList,
carouselMainAxisSize: Float,
itemSpacing: Float,
- contentPadding: Float
+ contentPadding: Float,
+ pivot: Keyline,
+ pivotIndex: Int
): KeylineList {
val numberOfNonAnchorKeylines = from.fastFilter { !it.isAnchor }.count()
val sizeReduction = contentPadding / numberOfNonAnchorKeylines
@@ -480,8 +490,8 @@
val newKeylines = keylineListOf(
carouselMainAxisSize = carouselMainAxisSize,
itemSpacing = itemSpacing,
- pivotIndex = from.pivotIndex,
- pivotOffset = from.pivot.offset + contentPadding - (sizeReduction / 2f)
+ pivotIndex = pivotIndex,
+ pivotOffset = pivot.offset - (sizeReduction / 2f) + contentPadding
) {
from.fastForEach { k -> add(k.size - abs(sizeReduction), k.isAnchor) }
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/TextFieldImpl.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/TextFieldImpl.kt
index 886c2e2..d3f51de 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/TextFieldImpl.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/TextFieldImpl.kt
@@ -17,11 +17,14 @@
package androidx.compose.material3.internal
import androidx.compose.animation.animateColor
+import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
@@ -35,13 +38,21 @@
import androidx.compose.material3.outlineCutout
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorProducer
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawOutline
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.LayoutIdParentData
@@ -54,6 +65,7 @@
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.lerp
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
internal enum class TextFieldType {
@@ -100,33 +112,34 @@
val typography = MaterialTheme.typography
val bodyLarge = typography.bodyLarge
val bodySmall = typography.bodySmall
- val shouldOverrideTextStyleColor =
+ val overrideLabelTextStyleColor =
(bodyLarge.color == Color.Unspecified && bodySmall.color != Color.Unspecified) ||
(bodyLarge.color != Color.Unspecified && bodySmall.color == Color.Unspecified)
- TextFieldTransitionScope.Transition(
+ TextFieldTransitionScope(
inputState = inputState,
- focusedTextStyleColor = with(MaterialTheme.typography.bodySmall.color) {
- if (shouldOverrideTextStyleColor) this.takeOrElse { labelColor } else this
+ focusedLabelTextStyleColor = with(bodySmall.color) {
+ if (overrideLabelTextStyleColor) this.takeOrElse { labelColor } else this
},
- unfocusedTextStyleColor = with(MaterialTheme.typography.bodyLarge.color) {
- if (shouldOverrideTextStyleColor) this.takeOrElse { labelColor } else this
+ unfocusedLabelTextStyleColor = with(bodyLarge.color) {
+ if (overrideLabelTextStyleColor) this.takeOrElse { labelColor } else this
},
- contentColor = labelColor,
- showLabel = label != null
- ) { labelProgress, labelTextStyleColor, labelContentColor, placeholderAlphaProgress,
- prefixSuffixAlphaProgress ->
-
+ labelColor = labelColor,
+ showLabel = label != null,
+ ) { labelProgress, labelTextStyleColor, labelContentColor, placeholderAlpha,
+ prefixSuffixAlpha ->
+ val labelProgressValue = labelProgress.value
val decoratedLabel: @Composable (() -> Unit)? = label?.let {
@Composable {
- val labelTextStyle = lerp(
- MaterialTheme.typography.bodyLarge,
- MaterialTheme.typography.bodySmall,
- labelProgress
- ).let {
- if (shouldOverrideTextStyleColor) it.copy(color = labelTextStyleColor) else it
- }
- Decoration(labelContentColor, labelTextStyle, it)
+ val labelTextStyle =
+ lerp(bodyLarge, bodySmall, labelProgressValue).let { textStyle ->
+ if (overrideLabelTextStyleColor) {
+ textStyle.copy(color = labelTextStyleColor.value)
+ } else {
+ textStyle
+ }
+ }
+ Decoration(labelContentColor.value, labelTextStyle, it)
}
}
@@ -134,13 +147,18 @@
// have alpha == 0, we set the component to null instead.
val placeholderColor = colors.placeholderColor(enabled, isError, isFocused)
+ val showPlaceholder by remember {
+ derivedStateOf(structuralEqualityPolicy()) {
+ placeholderAlpha.value > 0f
+ }
+ }
val decoratedPlaceholder: @Composable ((Modifier) -> Unit)? =
- if (placeholder != null && transformedText.isEmpty() && placeholderAlphaProgress > 0f) {
+ if (placeholder != null && transformedText.isEmpty() && showPlaceholder) {
@Composable { modifier ->
- Box(modifier.alpha(placeholderAlphaProgress)) {
+ Box(modifier.graphicsLayer { alpha = placeholderAlpha.value }) {
Decoration(
contentColor = placeholderColor,
- typography = MaterialTheme.typography.bodyLarge,
+ textStyle = bodyLarge,
content = placeholder
)
}
@@ -148,13 +166,18 @@
} else null
val prefixColor = colors.prefixColor(enabled, isError, isFocused)
+ val showPrefixSuffix by remember {
+ derivedStateOf(structuralEqualityPolicy()) {
+ prefixSuffixAlpha.value > 0f
+ }
+ }
val decoratedPrefix: @Composable (() -> Unit)? =
- if (prefix != null && prefixSuffixAlphaProgress > 0f) {
+ if (prefix != null && showPrefixSuffix) {
@Composable {
- Box(Modifier.alpha(prefixSuffixAlphaProgress)) {
+ Box(Modifier.graphicsLayer { alpha = prefixSuffixAlpha.value }) {
Decoration(
contentColor = prefixColor,
- typography = bodyLarge,
+ textStyle = bodyLarge,
content = prefix
)
}
@@ -163,12 +186,12 @@
val suffixColor = colors.suffixColor(enabled, isError, isFocused)
val decoratedSuffix: @Composable (() -> Unit)? =
- if (suffix != null && prefixSuffixAlphaProgress > 0f) {
+ if (suffix != null && showPrefixSuffix) {
@Composable {
- Box(Modifier.alpha(prefixSuffixAlphaProgress)) {
+ Box(Modifier.graphicsLayer { alpha = prefixSuffixAlpha.value }) {
Decoration(
contentColor = suffixColor,
- typography = bodyLarge,
+ textStyle = bodyLarge,
content = suffix
)
}
@@ -193,7 +216,7 @@
colors.supportingTextColor(enabled, isError, isFocused)
val decoratedSupporting: @Composable (() -> Unit)? = supportingText?.let {
@Composable {
- Decoration(contentColor = supportingTextColor, typography = bodySmall, content = it)
+ Decoration(contentColor = supportingTextColor, textStyle = bodySmall, content = it)
}
}
@@ -218,7 +241,8 @@
container = containerWithId,
supporting = decoratedSupporting,
singleLine = singleLine,
- animationProgress = labelProgress,
+ // TODO(b/271000818): progress state read should be deferred to layout phase
+ animationProgress = labelProgressValue,
paddingValues = contentPadding
)
}
@@ -229,7 +253,7 @@
Box(
Modifier
.layoutId(ContainerId)
- .outlineCutout(labelSize.value, contentPadding),
+ .outlineCutout(labelSize::value, contentPadding),
propagateMinConstraints = true
) {
container()
@@ -248,15 +272,16 @@
supporting = decoratedSupporting,
singleLine = singleLine,
onLabelMeasured = {
- val labelWidth = it.width * labelProgress
- val labelHeight = it.height * labelProgress
+ val labelWidth = it.width * labelProgressValue
+ val labelHeight = it.height * labelProgressValue
if (labelSize.value.width != labelWidth ||
labelSize.value.height != labelHeight
) {
labelSize.value = Size(labelWidth, labelHeight)
}
},
- animationProgress = labelProgress,
+ // TODO(b/271000818): progress state read should be deferred to layout phase
+ animationProgress = labelProgressValue,
container = borderContainerWithId,
paddingValues = contentPadding
)
@@ -266,25 +291,23 @@
}
/**
- * Set content color, typography and emphasis for [content] composable
+ * Decorates [content] with [contentColor] and [textStyle].
*/
@Composable
-internal fun Decoration(
+private fun Decoration(
contentColor: Color,
- typography: TextStyle? = null,
+ textStyle: TextStyle,
content: @Composable () -> Unit
-) {
- val contentWithColor: @Composable () -> Unit = @Composable {
- CompositionLocalProvider(
- LocalContentColor provides contentColor,
- content = content
- )
- }
- if (typography != null)
- ProvideContentColorTextStyle(contentColor, typography, content)
- else
- contentWithColor()
-}
+) = ProvideContentColorTextStyle(contentColor, textStyle, content)
+
+/**
+ * Decorates [content] with [contentColor].
+ */
+@Composable
+private fun Decoration(
+ contentColor: Color,
+ content: @Composable () -> Unit
+) = CompositionLocalProvider(LocalContentColor provides contentColor, content = content)
// Developers need to handle invalid input manually. But since we don't provide an error message
// slot API, we can set the default error message in case developers forget about it.
@@ -293,105 +316,143 @@
defaultErrorMessage: String,
): Modifier = if (isError) semantics { error(defaultErrorMessage) } else this
+/**
+ * Replacement for Modifier.background which takes color as a State to avoid
+ * recomposition while animating.
+ */
+internal fun Modifier.textFieldBackground(
+ color: ColorProducer,
+ shape: Shape,
+): Modifier = this.drawWithCache {
+ val outline = shape.createOutline(size, layoutDirection, this)
+ onDrawBehind {
+ drawOutline(outline, color = color())
+ }
+}
+
internal fun widthOrZero(placeable: Placeable?) = placeable?.width ?: 0
internal fun heightOrZero(placeable: Placeable?) = placeable?.height ?: 0
-private object TextFieldTransitionScope {
- @Composable
- fun Transition(
- inputState: InputPhase,
- focusedTextStyleColor: Color,
- unfocusedTextStyleColor: Color,
- contentColor: Color,
- showLabel: Boolean,
- content: @Composable (
- labelProgress: Float,
- labelTextStyleColor: Color,
- labelContentColor: Color,
- placeholderOpacity: Float,
- prefixSuffixOpacity: Float,
- ) -> Unit
+@Composable
+private inline fun TextFieldTransitionScope(
+ inputState: InputPhase,
+ focusedLabelTextStyleColor: Color,
+ unfocusedLabelTextStyleColor: Color,
+ labelColor: Color,
+ showLabel: Boolean,
+ content: @Composable (
+ labelProgress: State<Float>,
+ labelTextStyleColor: State<Color>,
+ labelContentColor: State<Color>,
+ placeholderOpacity: State<Float>,
+ prefixSuffixOpacity: State<Float>,
+ ) -> Unit
+) {
+ // Transitions from/to InputPhase.Focused are the most critical in the transition below.
+ // UnfocusedEmpty <-> UnfocusedNotEmpty are needed when a single state is used to control
+ // multiple text fields.
+ val transition = updateTransition(inputState, label = "TextFieldInputState")
+
+ val labelProgress = transition.animateFloat(
+ label = "LabelProgress",
+ transitionSpec = { tween(durationMillis = TextFieldAnimationDuration) }
) {
- // Transitions from/to InputPhase.Focused are the most critical in the transition below.
- // UnfocusedEmpty <-> UnfocusedNotEmpty are needed when a single state is used to control
- // multiple text fields.
- val transition = updateTransition(inputState, label = "TextFieldInputState")
-
- val labelProgress by transition.animateFloat(
- label = "LabelProgress",
- transitionSpec = { tween(durationMillis = AnimationDuration) }
- ) {
- when (it) {
- InputPhase.Focused -> 1f
- InputPhase.UnfocusedEmpty -> 0f
- InputPhase.UnfocusedNotEmpty -> 1f
- }
+ when (it) {
+ InputPhase.Focused -> 1f
+ InputPhase.UnfocusedEmpty -> 0f
+ InputPhase.UnfocusedNotEmpty -> 1f
}
-
- val placeholderOpacity by transition.animateFloat(
- label = "PlaceholderOpacity",
- transitionSpec = {
- if (InputPhase.Focused isTransitioningTo InputPhase.UnfocusedEmpty) {
- tween(
- durationMillis = PlaceholderAnimationDelayOrDuration,
- easing = LinearEasing
- )
- } else if (InputPhase.UnfocusedEmpty isTransitioningTo InputPhase.Focused ||
- InputPhase.UnfocusedNotEmpty isTransitioningTo InputPhase.UnfocusedEmpty
- ) {
- tween(
- durationMillis = PlaceholderAnimationDuration,
- delayMillis = PlaceholderAnimationDelayOrDuration,
- easing = LinearEasing
- )
- } else {
- spring()
- }
- }
- ) {
- when (it) {
- InputPhase.Focused -> 1f
- InputPhase.UnfocusedEmpty -> if (showLabel) 0f else 1f
- InputPhase.UnfocusedNotEmpty -> 0f
- }
- }
-
- val prefixSuffixOpacity by transition.animateFloat(
- label = "PrefixSuffixOpacity",
- transitionSpec = { tween(durationMillis = AnimationDuration) }
- ) {
- when (it) {
- InputPhase.Focused -> 1f
- InputPhase.UnfocusedEmpty -> if (showLabel) 0f else 1f
- InputPhase.UnfocusedNotEmpty -> 1f
- }
- }
-
- val labelTextStyleColor by transition.animateColor(
- transitionSpec = { tween(durationMillis = AnimationDuration) },
- label = "LabelTextStyleColor"
- ) {
- when (it) {
- InputPhase.Focused -> focusedTextStyleColor
- else -> unfocusedTextStyleColor
- }
- }
-
- @Suppress("UnusedTransitionTargetStateParameter")
- val labelContentColor by transition.animateColor(
- transitionSpec = { tween(durationMillis = AnimationDuration) },
- label = "LabelContentColor",
- targetValueByState = { contentColor }
- )
-
- content(
- labelProgress,
- labelTextStyleColor,
- labelContentColor,
- placeholderOpacity,
- prefixSuffixOpacity,
- )
}
+
+ val placeholderOpacity = transition.animateFloat(
+ label = "PlaceholderOpacity",
+ transitionSpec = {
+ if (InputPhase.Focused isTransitioningTo InputPhase.UnfocusedEmpty) {
+ tween(
+ durationMillis = PlaceholderAnimationDelayOrDuration,
+ easing = LinearEasing
+ )
+ } else if (InputPhase.UnfocusedEmpty isTransitioningTo InputPhase.Focused ||
+ InputPhase.UnfocusedNotEmpty isTransitioningTo InputPhase.UnfocusedEmpty
+ ) {
+ tween(
+ durationMillis = PlaceholderAnimationDuration,
+ delayMillis = PlaceholderAnimationDelayOrDuration,
+ easing = LinearEasing
+ )
+ } else {
+ spring()
+ }
+ }
+ ) {
+ when (it) {
+ InputPhase.Focused -> 1f
+ InputPhase.UnfocusedEmpty -> if (showLabel) 0f else 1f
+ InputPhase.UnfocusedNotEmpty -> 0f
+ }
+ }
+
+ val prefixSuffixOpacity = transition.animateFloat(
+ label = "PrefixSuffixOpacity",
+ transitionSpec = { tween(durationMillis = TextFieldAnimationDuration) }
+ ) {
+ when (it) {
+ InputPhase.Focused -> 1f
+ InputPhase.UnfocusedEmpty -> if (showLabel) 0f else 1f
+ InputPhase.UnfocusedNotEmpty -> 1f
+ }
+ }
+
+ val labelTextStyleColor = transition.animateColor(
+ transitionSpec = { tween(durationMillis = TextFieldAnimationDuration) },
+ label = "LabelTextStyleColor"
+ ) {
+ when (it) {
+ InputPhase.Focused -> focusedLabelTextStyleColor
+ else -> unfocusedLabelTextStyleColor
+ }
+ }
+
+ @Suppress("UnusedTransitionTargetStateParameter")
+ val labelContentColor = transition.animateColor(
+ transitionSpec = { tween(durationMillis = TextFieldAnimationDuration) },
+ label = "LabelContentColor",
+ targetValueByState = { labelColor }
+ )
+
+ content(
+ labelProgress,
+ labelTextStyleColor,
+ labelContentColor,
+ placeholderOpacity,
+ prefixSuffixOpacity,
+ )
+}
+
+@Composable
+internal fun animateBorderStrokeAsState(
+ enabled: Boolean,
+ isError: Boolean,
+ focused: Boolean,
+ colors: TextFieldColors,
+ focusedBorderThickness: Dp,
+ unfocusedBorderThickness: Dp
+): State<BorderStroke> {
+ val targetColor = colors.indicatorColor(enabled, isError, focused)
+ val indicatorColor = if (enabled) {
+ animateColorAsState(targetColor, tween(durationMillis = TextFieldAnimationDuration))
+ } else {
+ rememberUpdatedState(targetColor)
+ }
+
+ val thickness = if (enabled) {
+ val targetThickness = if (focused) focusedBorderThickness else unfocusedBorderThickness
+ animateDpAsState(targetThickness, tween(durationMillis = TextFieldAnimationDuration))
+ } else {
+ rememberUpdatedState(unfocusedBorderThickness)
+ }
+
+ return rememberUpdatedState(BorderStroke(thickness.value, indicatorColor.value))
}
/**
@@ -422,7 +483,7 @@
internal const val ContainerId = "Container"
internal val ZeroConstraints = Constraints(0, 0, 0, 0)
-internal const val AnimationDuration = 150
+internal const val TextFieldAnimationDuration = 150
private const val PlaceholderAnimationDuration = 83
private const val PlaceholderAnimationDelayOrDuration = 67
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
index f46c597..67a0f7b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
@@ -318,7 +318,7 @@
): Offset = when {
!enabled() -> Offset.Zero
// Swiping up
- source == NestedScrollSource.Drag && available.y < 0 -> {
+ source == NestedScrollSource.UserInput && available.y < 0 -> {
consumeAvailableOffset(available)
}
else -> Offset.Zero
@@ -331,7 +331,7 @@
): Offset = when {
!enabled() -> Offset.Zero
// Swiping down
- source == NestedScrollSource.Drag && available.y > 0 -> {
+ source == NestedScrollSource.UserInput && available.y > 0 -> {
consumeAvailableOffset(available)
}
else -> Offset.Zero
diff --git a/compose/runtime/runtime-livedata/build.gradle b/compose/runtime/runtime-livedata/build.gradle
index dbebaba..0a09b7d 100644
--- a/compose/runtime/runtime-livedata/build.gradle
+++ b/compose/runtime/runtime-livedata/build.gradle
@@ -37,7 +37,7 @@
api(project(":compose:runtime:runtime"))
api("androidx.lifecycle:lifecycle-livedata:2.6.1")
api("androidx.lifecycle:lifecycle-runtime:2.6.1")
- implementation(project(":compose:ui:ui"))
+ api(project(":lifecycle:lifecycle-runtime-compose"))
androidTestImplementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
androidTestImplementation(project(":compose:test-utils"))
diff --git a/compose/runtime/runtime-saveable/src/androidUnitTest/kotlin/androidx/compose/runtime/saveable/SaveableStateRegistryTest.kt b/compose/runtime/runtime-saveable/src/androidUnitTest/kotlin/androidx/compose/runtime/saveable/SaveableStateRegistryTest.kt
index 1dbda70..c74d862 100644
--- a/compose/runtime/runtime-saveable/src/androidUnitTest/kotlin/androidx/compose/runtime/saveable/SaveableStateRegistryTest.kt
+++ b/compose/runtime/runtime-saveable/src/androidUnitTest/kotlin/androidx/compose/runtime/saveable/SaveableStateRegistryTest.kt
@@ -92,6 +92,17 @@
}
}
+ @Test
+ fun singleCharacterKeysAreAllowed() {
+ val registry = createRegistry()
+
+ registry.registerProvider("a") { 1 }
+
+ registry.performSave().apply {
+ assertThat(get("a")).isEqualTo(listOf(1))
+ }
+ }
+
@Test(expected = IllegalArgumentException::class)
fun emptyKeysAreNotAllowed() {
val registry = createRegistry()
diff --git a/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/SaveableStateRegistry.kt b/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/SaveableStateRegistry.kt
index 8af7791..0428ffe 100644
--- a/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/SaveableStateRegistry.kt
+++ b/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/SaveableStateRegistry.kt
@@ -93,7 +93,7 @@
// CharSequence.isBlank() allocates an iterator because it calls indices.all{}
private fun CharSequence.fastIsBlank(): Boolean {
var blank = true
- for (i in 0 until length - 1) {
+ for (i in 0 until length) {
if (!this[i].isWhitespace()) {
blank = false
break
diff --git a/compose/runtime/runtime/api/api_lint.ignore b/compose/runtime/runtime/api/api_lint.ignore
index ced886f..e36cd55 100644
--- a/compose/runtime/runtime/api/api_lint.ignore
+++ b/compose/runtime/runtime/api/api_lint.ignore
@@ -21,6 +21,8 @@
Getter for boolean property `hasInvalidations` is named `getHasInvalidations` but should match the property name. Use `@get:JvmName` to rename.
GetterSetterNames: androidx.compose.runtime.ControlledComposition#getHasPendingChanges():
Getter for boolean property `hasPendingChanges` is named `getHasPendingChanges` but should match the property name. Use `@get:JvmName` to rename.
+GetterSetterNames: androidx.compose.runtime.ProvidedValue#getCanOverride():
+ Getter for boolean property `canOverride` is named `getCanOverride` but should match the property name. Use `@get:JvmName` to rename.
GetterSetterNames: androidx.compose.runtime.Recomposer#getHasPendingWork():
Getter for boolean property `hasPendingWork` is named `getHasPendingWork` but should match the property name. Use `@get:JvmName` to rename.
GetterSetterNames: androidx.compose.runtime.RecomposerInfo#getHasPendingWork():
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 0163760..36648cc 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -227,6 +227,10 @@
property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final inline T current;
}
+ public interface CompositionLocalAccessorScope {
+ method public <T> T getCurrentValue(androidx.compose.runtime.CompositionLocal<T>);
+ }
+
@androidx.compose.runtime.Stable public final class CompositionLocalContext {
}
@@ -235,6 +239,7 @@
method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonSkippableComposable public static void CompositionLocalProvider(androidx.compose.runtime.ProvidedValue<?> value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonSkippableComposable public static void CompositionLocalProvider(androidx.compose.runtime.ProvidedValue<?>[] values, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public static <T> androidx.compose.runtime.ProvidableCompositionLocal<T> compositionLocalOf(optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> defaultFactory);
+ method public static <T> androidx.compose.runtime.ProvidableCompositionLocal<T> compositionLocalWithComputedDefaultOf(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CompositionLocalAccessorScope,? extends T> defaultComputation);
method public static <T> androidx.compose.runtime.ProvidableCompositionLocal<T> staticCompositionLocalOf(kotlin.jvm.functions.Function0<? extends T> defaultFactory);
}
@@ -473,6 +478,7 @@
@androidx.compose.runtime.Stable public abstract class ProvidableCompositionLocal<T> extends androidx.compose.runtime.CompositionLocal<T> {
method public final infix androidx.compose.runtime.ProvidedValue<T> provides(T value);
+ method public final infix androidx.compose.runtime.ProvidedValue<T> providesComputed(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CompositionLocalAccessorScope,? extends T> compute);
method public final infix androidx.compose.runtime.ProvidedValue<T> providesDefault(T value);
}
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 5240627..ee4de2df 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -245,6 +245,10 @@
property @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public final inline T current;
}
+ public interface CompositionLocalAccessorScope {
+ method public <T> T getCurrentValue(androidx.compose.runtime.CompositionLocal<T>);
+ }
+
@androidx.compose.runtime.Stable public final class CompositionLocalContext {
}
@@ -253,6 +257,7 @@
method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonSkippableComposable public static void CompositionLocalProvider(androidx.compose.runtime.ProvidedValue<?> value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonSkippableComposable public static void CompositionLocalProvider(androidx.compose.runtime.ProvidedValue<?>[] values, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public static <T> androidx.compose.runtime.ProvidableCompositionLocal<T> compositionLocalOf(optional androidx.compose.runtime.SnapshotMutationPolicy<T> policy, kotlin.jvm.functions.Function0<? extends T> defaultFactory);
+ method public static <T> androidx.compose.runtime.ProvidableCompositionLocal<T> compositionLocalWithComputedDefaultOf(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CompositionLocalAccessorScope,? extends T> defaultComputation);
method public static <T> androidx.compose.runtime.ProvidableCompositionLocal<T> staticCompositionLocalOf(kotlin.jvm.functions.Function0<? extends T> defaultFactory);
}
@@ -501,6 +506,7 @@
@androidx.compose.runtime.Stable public abstract class ProvidableCompositionLocal<T> extends androidx.compose.runtime.CompositionLocal<T> {
method public final infix androidx.compose.runtime.ProvidedValue<T> provides(T value);
+ method public final infix androidx.compose.runtime.ProvidedValue<T> providesComputed(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CompositionLocalAccessorScope,? extends T> compute);
method public final infix androidx.compose.runtime.ProvidedValue<T> providesDefault(T value);
}
diff --git a/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/CompositionLocalSamples.kt b/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/CompositionLocalSamples.kt
index 5369df3..06e3907 100644
--- a/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/CompositionLocalSamples.kt
+++ b/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/CompositionLocalSamples.kt
@@ -22,6 +22,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.compositionLocalWithComputedDefaultOf
@Sampled
fun createCompositionLocal() {
@@ -39,6 +40,31 @@
}
@Sampled
+fun compositionLocalComputedByDefault() {
+ val LocalBaseValue = compositionLocalOf { 10 }
+ val LocalLargerValue = compositionLocalWithComputedDefaultOf {
+ LocalBaseValue.currentValue + 10
+ }
+}
+
+@Sampled
+fun compositionLocalProvidedComputed() {
+ val LocalValue = compositionLocalOf { 10 }
+ val LocalLargerValue = compositionLocalOf { 12 }
+
+ @Composable
+ fun App() {
+ CompositionLocalProvider(
+ LocalLargerValue providesComputed {
+ LocalValue.currentValue + 10
+ }
+ ) {
+ SomeScreen()
+ }
+ }
+}
+
+@Sampled
fun someScreenSample() {
@Composable
fun SomeScreen() {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index d21e787..d11df0e 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -305,14 +305,61 @@
/**
* An instance to hold a value provided by [CompositionLocalProvider] and is created by the
- * [ProvidableCompositionLocal.provides] infixed operator. If [canOverride] is `false`, the
+ * [ProvidableCompositionLocal.provides] infix operator. If [canOverride] is `false`, the
* provided value will not overwrite a potentially already existing value in the scope.
+ *
+ * This value cannot be created directly. It can only be created by using one of the `provides`
+ * operators of [ProvidableCompositionLocal].
+ *
+ * @see ProvidableCompositionLocal.provides
+ * @see ProvidableCompositionLocal.providesDefault
+ * @see ProvidableCompositionLocal.providesComputed
*/
class ProvidedValue<T> internal constructor(
+ /**
+ * The composition local that is provided by this value. This is the left-hand side of the
+ * [ProvidableCompositionLocal.provides] infix operator.
+ */
val compositionLocal: CompositionLocal<T>,
- val value: T,
- val canOverride: Boolean
-)
+ value: T?,
+ private val explicitNull: Boolean,
+ internal val mutationPolicy: SnapshotMutationPolicy<T>?,
+ internal val state: MutableState<T>?,
+ internal val compute: (CompositionLocalAccessorScope.() -> T)?,
+ internal val isDynamic: Boolean
+) {
+ private val providedValue: T? = value
+
+ /**
+ * The value provided by the [ProvidableCompositionLocal.provides] infix operator. This is the
+ * right-hand side of the operator.
+ */
+ @Suppress("UNCHECKED_CAST")
+ val value: T get() = providedValue as T
+
+ /**
+ * This value is `true` if the provided value will override any value provided above it. This
+ * value is `true` when using [ProvidableCompositionLocal.provides] but `false` when using
+ * [ProvidableCompositionLocal.providesDefault].
+ *
+ * @see ProvidableCompositionLocal.provides
+ * @see ProvidableCompositionLocal.providesDefault
+ */
+ @get:JvmName("getCanOverride")
+ var canOverride: Boolean = true
+ private set
+ @Suppress("UNCHECKED_CAST")
+ internal val effectiveValue: T
+ get() = when {
+ explicitNull -> null as T
+ state != null -> state.value
+ providedValue != null -> providedValue
+ else -> composeRuntimeError("Unexpected form of a provided value")
+ }
+ internal val isStatic get() = (explicitNull || value != null) && !isDynamic
+
+ internal fun ifNotAlreadyProvided() = this.also { canOverride = false }
+}
/**
* A Compose compiler plugin API. DO NOT call directly.
@@ -2127,9 +2174,15 @@
writer.anchor(group)
} else null
} else {
- if (reader.isAfterFirstChild)
- reader.anchor(reader.currentGroup - 1)
- else null
+ if (reader.isAfterFirstChild) {
+ var group = reader.currentGroup - 1
+ var parent = reader.parent(group)
+ while (parent != reader.parent && parent >= 0) {
+ group = parent
+ parent = reader.parent(group)
+ }
+ reader.anchor(group)
+ } else null
}
override val compositionData: CompositionData get() = slotTable
@@ -2208,10 +2261,10 @@
startGroup(providerKey, provider)
val oldState = rememberedValue().let {
if (it == Composer.Empty) null
- else it as State<Any?>
+ else it as ValueHolder<Any?>
}
val local = value.compositionLocal as CompositionLocal<Any?>
- val state = local.updatedStateOf(value.value, oldState)
+ val state = local.updatedStateOf(value as ProvidedValue<Any?>, oldState)
val change = state != oldState
if (change) {
updateRememberedValue(state)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocal.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocal.kt
index 4f9cf8c..4af9eb7 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocal.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocal.kt
@@ -56,10 +56,13 @@
* @sample androidx.compose.runtime.samples.consumeCompositionLocal
*/
@Stable
-sealed class CompositionLocal<T> constructor(defaultFactory: () -> T) {
- internal val defaultValueHolder = LazyValueHolder(defaultFactory)
+sealed class CompositionLocal<T>(defaultFactory: () -> T) {
+ internal open val defaultValueHolder: ValueHolder<T> = LazyValueHolder(defaultFactory)
- internal abstract fun updatedStateOf(value: T, previous: State<T>?): State<T>
+ internal abstract fun updatedStateOf(
+ value: ProvidedValue<T>,
+ previous: ValueHolder<T>?
+ ): ValueHolder<T>
/**
* Return the value provided by the nearest [CompositionLocalProvider] component that invokes, directly or
@@ -85,6 +88,7 @@
@Stable
abstract class ProvidableCompositionLocal<T> internal constructor(defaultFactory: () -> T) :
CompositionLocal<T> (defaultFactory) {
+ internal abstract fun defaultProvidedValue(value: T): ProvidedValue<T>
/**
* Associates a [CompositionLocal] key to a value in a call to [CompositionLocalProvider].
@@ -92,16 +96,76 @@
* @see CompositionLocal
* @see ProvidableCompositionLocal
*/
- infix fun provides(value: T) = ProvidedValue(this, value, true)
+ infix fun provides(value: T) = defaultProvidedValue(value)
/**
- * Associates a [CompositionLocal] key to a value in a call to [CompositionLocalProvider] if the key does not
- * already have an associated value.
+ * Associates a [CompositionLocal] key to a value in a call to [CompositionLocalProvider] if the
+ * key does not already have an associated value.
*
* @see CompositionLocal
* @see ProvidableCompositionLocal
*/
- infix fun providesDefault(value: T) = ProvidedValue(this, value, false)
+ infix fun providesDefault(value: T) = defaultProvidedValue(value).ifNotAlreadyProvided()
+
+ /**
+ * Associates a [CompositionLocal] key to a lambda, [compute], in a call to [CompositionLocal].
+ * The [compute] lambda is invoked whenever the key is retrieved. The lambda is executed in
+ * the context of a [CompositionLocalContext] which allow retrieving the current values of
+ * other composition locals by calling [CompositionLocalAccessorScope.currentValue], which is an
+ * extension function provided by the context for a [CompositionLocal] key.
+ *
+ * @sample androidx.compose.runtime.samples.compositionLocalProvidedComputed
+ *
+ * @see CompositionLocal
+ * @see CompositionLocalContext
+ * @see ProvidableCompositionLocal
+ */
+ infix fun providesComputed(compute: CompositionLocalAccessorScope.() -> T) =
+ ProvidedValue(
+ compositionLocal = this,
+ value = null,
+ explicitNull = false,
+ mutationPolicy = null,
+ state = null,
+ compute = compute,
+ isDynamic = false
+ )
+
+ override fun updatedStateOf(
+ value: ProvidedValue<T>,
+ previous: ValueHolder<T>?
+ ): ValueHolder<T> {
+ return when (previous) {
+ is DynamicValueHolder ->
+ if (value.isDynamic) {
+ previous.state.value = value.effectiveValue
+ previous
+ } else null
+ is StaticValueHolder ->
+ if (value.isStatic && value.effectiveValue == previous.value)
+ previous
+ else null
+ is ComputedValueHolder ->
+ if (value.compute == previous.compute)
+ previous
+ else null
+ else -> null
+ } ?: valueHolderOf(value)
+ }
+
+ private fun valueHolderOf(value: ProvidedValue<T>): ValueHolder<T> =
+ when {
+ value.isDynamic ->
+ DynamicValueHolder(
+ value.state
+ ?: mutableStateOf(value.value, value.mutationPolicy
+ ?: structuralEqualityPolicy()
+ )
+ )
+ value.compute != null -> ComputedValueHolder(value.compute)
+ value.state != null -> DynamicValueHolder(value.state)
+ else -> StaticValueHolder(value.effectiveValue)
+ }
}
/**
@@ -113,18 +177,21 @@
*
* @see compositionLocalOf
*/
-internal class DynamicProvidableCompositionLocal<T> constructor(
+internal class DynamicProvidableCompositionLocal<T>(
private val policy: SnapshotMutationPolicy<T>,
defaultFactory: () -> T
) : ProvidableCompositionLocal<T>(defaultFactory) {
- override fun updatedStateOf(value: T, previous: State<T>?): State<T> =
- if (previous != null && previous is MutableState<T>) {
- previous.value = value
- previous
- } else {
- mutableStateOf(value, policy)
- }
+ override fun defaultProvidedValue(value: T) =
+ ProvidedValue(
+ compositionLocal = this,
+ value = value,
+ explicitNull = value === null,
+ mutationPolicy = policy,
+ state = null,
+ compute = null,
+ isDynamic = true
+ )
}
/**
@@ -135,9 +202,16 @@
internal class StaticProvidableCompositionLocal<T>(defaultFactory: () -> T) :
ProvidableCompositionLocal<T>(defaultFactory) {
- override fun updatedStateOf(value: T, previous: State<T>?): State<T> =
- if (previous != null && previous.value == value) previous
- else StaticValueHolder(value)
+ override fun defaultProvidedValue(value: T) =
+ ProvidedValue(
+ compositionLocal = this,
+ value = value,
+ explicitNull = value === null,
+ mutationPolicy = null,
+ state = null,
+ compute = null,
+ isDynamic = false
+ )
}
/**
@@ -197,6 +271,65 @@
StaticProvidableCompositionLocal(defaultFactory)
/**
+ * Create a [CompositionLocal] that behaves like it was provided using
+ * [ProvidableCompositionLocal.providesComputed] by default. If a value is provided using
+ * [ProvidableCompositionLocal.provides] it behaves as if the [CompositionLocal] was produced
+ * by calling [compositionLocalOf].
+ *
+ * In other words, a [CompositionLocal] produced by can be provided identically to
+ * [CompositionLocal] created with [compositionLocalOf] with the only difference is how it behaves
+ * when the value is not provided. For a [compositionLocalOf] the default value is returned. If no
+ * default value has be computed for [CompositionLocal] the default computation is called.
+ *
+ * @sample androidx.compose.runtime.samples.compositionLocalComputedByDefault
+ *
+ * @param defaultComputation the default computation to use when this [CompositionLocal] is not
+ * provided.
+ *
+ * @see CompositionLocal
+ * @see ProvidableCompositionLocal
+ */
+fun <T> compositionLocalWithComputedDefaultOf(
+ defaultComputation: CompositionLocalAccessorScope.() -> T
+): ProvidableCompositionLocal<T> =
+ ComputedProvidableCompositionLocal(defaultComputation)
+
+internal class ComputedProvidableCompositionLocal<T>(
+ defaultComputation: CompositionLocalAccessorScope.() -> T
+) : ProvidableCompositionLocal<T>({ composeRuntimeError("Unexpected call to default provider") }) {
+ override val defaultValueHolder = ComputedValueHolder(defaultComputation)
+
+ override fun defaultProvidedValue(value: T): ProvidedValue<T> =
+ ProvidedValue(
+ compositionLocal = this,
+ value = value,
+ explicitNull = value === null,
+ mutationPolicy = null,
+ state = null,
+ compute = null,
+ isDynamic = true
+ )
+}
+
+interface CompositionLocalAccessorScope {
+ /**
+ * An extension property that allows accessing the current value of a composition local in
+ * the context of this scope. This scope is the type of the `this` parameter when in a
+ * computed composition. Computed composition locals can be provided by either using
+ * [compositionLocalWithComputedDefaultOf] or by using the
+ * [ProvidableCompositionLocal.providesComputed] infix operator.
+ *
+ * @sample androidx.compose.runtime.samples.compositionLocalProvidedComputed
+ *
+ * @see ProvidableCompositionLocal
+ * @see ProvidableCompositionLocal.providesComputed
+ * @see ProvidableCompositionLocal.provides
+ * @see CompositionLocalProvider
+ */
+ val <T> CompositionLocal<T>.currentValue: T
+}
+
+/**
* Stores [CompositionLocal]'s and their values.
*
* Can be obtained via [currentCompositionLocalContext] and passed to another composition
@@ -266,9 +399,7 @@
@Composable
fun CompositionLocalProvider(context: CompositionLocalContext, content: @Composable () -> Unit) {
CompositionLocalProvider(
- *context.compositionLocals
- .map { it.key as ProvidableCompositionLocal<Any?> provides it.value.value }
- .toTypedArray(),
+ *context.compositionLocals.map { it.value.toProvided(it.key) }.toTypedArray(),
content = content
)
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocalMap.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocalMap.kt
index a34bc3a..45e4540 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocalMap.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocalMap.kt
@@ -54,10 +54,17 @@
* [CompositionLocal]s.
*/
internal interface PersistentCompositionLocalMap :
- PersistentMap<CompositionLocal<Any?>, State<Any?>>,
- CompositionLocalMap {
+ PersistentMap<CompositionLocal<Any?>, ValueHolder<Any?>>,
+ CompositionLocalMap,
+ CompositionLocalAccessorScope {
- fun putValue(key: CompositionLocal<Any?>, value: State<Any?>): PersistentCompositionLocalMap
+ fun putValue(
+ key: CompositionLocal<Any?>,
+ value: ValueHolder<Any?>
+ ): PersistentCompositionLocalMap
+
+ override val <T> CompositionLocal<T>.currentValue: T
+ get() = read(this)
// Override the builder APIs so that we can create new PersistentMaps that retain the type
// information of PersistentCompositionLocalMap. If we use the built-in implementation, we'll
@@ -65,13 +72,13 @@
// PersistentCompositionLocalMap
override fun builder(): Builder
- interface Builder : PersistentMap.Builder<CompositionLocal<Any?>, State<Any?>> {
+ interface Builder : PersistentMap.Builder<CompositionLocal<Any?>, ValueHolder<Any?>> {
override fun build(): PersistentCompositionLocalMap
}
}
internal inline fun PersistentCompositionLocalMap.mutate(
- mutator: (MutableMap<CompositionLocal<Any?>, State<Any?>>) -> Unit
+ mutator: (MutableMap<CompositionLocal<Any?>, ValueHolder<Any?>>) -> Unit
): PersistentCompositionLocalMap = builder().apply(mutator).build()
@Suppress("UNCHECKED_CAST")
@@ -81,7 +88,7 @@
@Suppress("UNCHECKED_CAST")
internal fun <T> PersistentCompositionLocalMap.read(
key: CompositionLocal<T>
-): T = getOrElse(key as CompositionLocal<Any?>) { key.defaultValueHolder }.value as T
+): T = getOrElse(key as CompositionLocal<Any?>) { key.defaultValueHolder }.readValue(this) as T
internal fun updateCompositionMap(
values: Array<out ProvidedValue<*>>,
@@ -90,14 +97,14 @@
): PersistentCompositionLocalMap {
val builder: PersistentCompositionLocalMap.Builder =
persistentCompositionLocalHashMapOf().builder()
- val map: PersistentMap<CompositionLocal<Any?>, State<Any?>> = previous
+ val map: PersistentMap<CompositionLocal<Any?>, ValueHolder<Any?>> = previous
@Suppress("UNCHECKED_CAST")
for (index in values.indices) {
val provided = values[index]
val local = provided.compositionLocal as ProvidableCompositionLocal<Any?>
if (provided.canOverride || !parentScope.contains(local)) {
val previousState = map[local]
- val newState = local.updatedStateOf(provided.value, previousState)
+ val newState = local.updatedStateOf(provided as ProvidedValue<Any?>, previousState)
builder[local] = newState
}
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index 6a41089..d119996 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -27,7 +27,9 @@
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.snapshots.SnapshotApplyResult
import androidx.compose.runtime.snapshots.StateObjectImpl
+import androidx.compose.runtime.snapshots.fastAll
import androidx.compose.runtime.snapshots.fastAny
+import androidx.compose.runtime.snapshots.fastFilterIndexed
import androidx.compose.runtime.snapshots.fastForEach
import androidx.compose.runtime.snapshots.fastGroupBy
import androidx.compose.runtime.snapshots.fastMap
@@ -1214,7 +1216,35 @@
compositionValuesRemoved.removeLastMultiValue(reference.content)
}
}
- composition.insertMovableContent(pairs)
+
+ // Avoid mixing creating new content with moving content as the moved content
+ // may release content when it is moved as it is recomposed when move.
+ val toInsert = if (
+ pairs.fastAll { it.second == null } || pairs.fastAll { it.second != null }
+ ) { pairs } else {
+ // Return the content not moving to the awaiting list. These will come back
+ // here in the next iteration of the caller's loop and either have content
+ // to move or by still needing to create the content.
+ val toReturn = pairs.fastMapNotNull { item ->
+ if (item.second == null) item.first else null
+ }
+ synchronized(stateLock) {
+ compositionValuesAwaitingInsert += toReturn
+ }
+
+ // Only insert the moving content this time
+ pairs.fastFilterIndexed { _, item -> item.second != null }
+ }
+
+ // toInsert is guaranteed to be not empty as,
+ // 1) refs is guaranteed to be not empty as a condition of groupBy
+ // 2) pairs is guaranteed to be not empty as it is a map of refs
+ // 3) toInsert is guaranteed to not be empty because the toReturn and toInsert
+ // lists have at least one item by the condition of the guard in the if
+ // expression. If one would be empty the condition is true and the filter is not
+ // performed. As both have at least one item toInsert has at least one item. If
+ // the filter is not performed the list is pairs which has at least one item.
+ composition.insertMovableContent(toInsert)
}
}
return tasks.keys.toList()
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ValueHolders.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ValueHolders.kt
index 0c7d125..fa3eca0 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ValueHolders.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ValueHolders.kt
@@ -16,17 +16,67 @@
package androidx.compose.runtime
+internal interface ValueHolder<T> {
+ fun readValue(map: PersistentCompositionLocalMap): T
+ fun toProvided(local: CompositionLocal<T>): ProvidedValue<T>
+}
+
/**
* A StaticValueHolder holds a value that will never change.
*/
-internal data class StaticValueHolder<T>(override val value: T) : State<T>
+internal data class StaticValueHolder<T>(val value: T) : ValueHolder<T> {
+ override fun readValue(map: PersistentCompositionLocalMap): T = value
+ override fun toProvided(local: CompositionLocal<T>): ProvidedValue<T> =
+ ProvidedValue(
+ compositionLocal = local,
+ value = value,
+ explicitNull = value === null,
+ mutationPolicy = null,
+ state = null,
+ compute = null,
+ isDynamic = false
+ )
+}
/**
* A lazy value holder is static value holder for which the value is produced by the valueProducer
* parameter which is called once and the result is remembered for the life of LazyValueHolder.
*/
-internal class LazyValueHolder<T>(valueProducer: () -> T) : State<T> {
+internal class LazyValueHolder<T>(valueProducer: () -> T) : ValueHolder<T> {
private val current by lazy(valueProducer)
- override val value: T get() = current
+ override fun readValue(map: PersistentCompositionLocalMap): T = current
+ override fun toProvided(local: CompositionLocal<T>): ProvidedValue<T> =
+ composeRuntimeError("Cannot produce a provider from a lazy value holder")
+}
+
+internal data class ComputedValueHolder<T>(
+ val compute: CompositionLocalAccessorScope.() -> T
+) : ValueHolder<T> {
+ override fun readValue(map: PersistentCompositionLocalMap): T =
+ map.compute()
+ override fun toProvided(local: CompositionLocal<T>): ProvidedValue<T> =
+ ProvidedValue(
+ compositionLocal = local,
+ value = null,
+ explicitNull = false,
+ mutationPolicy = null,
+ state = null,
+ compute = compute,
+ isDynamic = false
+ )
+}
+
+internal data class DynamicValueHolder<T>(val state: MutableState<T>) : ValueHolder<T> {
+ override fun readValue(map: PersistentCompositionLocalMap): T = state.value
+ override fun toProvided(local: CompositionLocal<T>): ProvidedValue<T> =
+ ProvidedValue(
+ compositionLocal = local,
+ value = null,
+ explicitNull = false,
+ mutationPolicy = null,
+ state = state,
+ compute = null,
+ isDynamic = true
+ )
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/PersistentCompositionLocalMap.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/PersistentCompositionLocalMap.kt
index cac2470..c0c60e6 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/PersistentCompositionLocalMap.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/PersistentCompositionLocalMap.kt
@@ -18,7 +18,7 @@
import androidx.compose.runtime.CompositionLocal
import androidx.compose.runtime.PersistentCompositionLocalMap
-import androidx.compose.runtime.State
+import androidx.compose.runtime.ValueHolder
import androidx.compose.runtime.external.kotlinx.collections.immutable.ImmutableSet
import androidx.compose.runtime.external.kotlinx.collections.immutable.implementations.immutableMap.PersistentHashMap
import androidx.compose.runtime.external.kotlinx.collections.immutable.implementations.immutableMap.PersistentHashMapBuilder
@@ -28,19 +28,19 @@
import androidx.compose.runtime.read
internal class PersistentCompositionLocalHashMap(
- node: TrieNode<CompositionLocal<Any?>, State<Any?>>,
+ node: TrieNode<CompositionLocal<Any?>, ValueHolder<Any?>>,
size: Int
-) : PersistentHashMap<CompositionLocal<Any?>, State<Any?>>(node, size),
+) : PersistentHashMap<CompositionLocal<Any?>, ValueHolder<Any?>>(node, size),
PersistentCompositionLocalMap {
- override val entries: ImmutableSet<Map.Entry<CompositionLocal<Any?>, State<Any?>>>
+ override val entries: ImmutableSet<Map.Entry<CompositionLocal<Any?>, ValueHolder<Any?>>>
get() = super.entries
override fun <T> get(key: CompositionLocal<T>): T = read(key)
override fun putValue(
key: CompositionLocal<Any?>,
- value: State<Any?>
+ value: ValueHolder<Any?>
): PersistentCompositionLocalMap {
val newNodeResult = node.put(key.hashCode(), key, value, 0) ?: return this
return PersistentCompositionLocalHashMap(
@@ -55,7 +55,7 @@
class Builder(
internal var map: PersistentCompositionLocalHashMap
- ) : PersistentHashMapBuilder<CompositionLocal<Any?>, State<Any?>>(map),
+ ) : PersistentHashMapBuilder<CompositionLocal<Any?>, ValueHolder<Any?>>(map),
PersistentCompositionLocalMap.Builder {
override fun build(): PersistentCompositionLocalHashMap {
map = if (node === map.node) {
@@ -71,7 +71,7 @@
companion object {
@Suppress("UNCHECKED_CAST")
val Empty = PersistentCompositionLocalHashMap(
- node = TrieNode.EMPTY as TrieNode<CompositionLocal<Any?>, State<Any?>>,
+ node = TrieNode.EMPTY as TrieNode<CompositionLocal<Any?>, ValueHolder<Any?>>,
size = 0
)
}
@@ -80,5 +80,5 @@
internal fun persistentCompositionLocalHashMapOf() = PersistentCompositionLocalHashMap.Empty
internal fun persistentCompositionLocalHashMapOf(
- vararg pairs: Pair<CompositionLocal<Any?>, State<Any?>>
+ vararg pairs: Pair<CompositionLocal<Any?>, ValueHolder<Any?>>
): PersistentCompositionLocalMap = PersistentCompositionLocalHashMap.Empty.mutate { it += pairs }
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionLocalTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionLocalTests.kt
index ddfcc02..3e4c0db 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionLocalTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionLocalTests.kt
@@ -714,6 +714,66 @@
}
}
}
+
+ @Test
+ fun testDefaultComputedLocal() = compositionTest {
+ val local1 = compositionLocalOf { 0 }
+ val local2 = compositionLocalWithComputedDefaultOf { local1.currentValue + 10 }
+ compose {
+ val valueOfLocal1 = local1.current
+ val valueOfLocal2 = local2.current
+ assertEquals(valueOfLocal1 + 10, valueOfLocal2)
+ CompositionLocalProvider(local1 provides 20) {
+ val nestedValueOfLocal1 = local1.current
+ val nestedValueOfLocal2 = local2.current
+ assertEquals(nestedValueOfLocal1 + 10, nestedValueOfLocal2)
+ }
+ }
+ }
+
+ @Test
+ fun testProvidingDynamicLocalAsComputed() = compositionTest {
+ val local1 = compositionLocalOf { 0 }
+ val local2 = compositionLocalOf { 0 }
+ compose {
+ val valueOfLocal1A = local1.current
+ val valueOfLocal2A = local2.current
+ assertEquals(0, valueOfLocal1A)
+ assertEquals(0, valueOfLocal2A)
+ CompositionLocalProvider(local2 providesComputed { local1.currentValue + 10 }) {
+ val valueOfLocal1B = local1.current
+ val valueOfLocal2B = local2.current
+ assertEquals(valueOfLocal1B + 10, valueOfLocal2B)
+ CompositionLocalProvider(local1 provides 10) {
+ val valueOfLocal1C = local1.current
+ val valueOfLocal2C = local2.current
+ assertEquals(valueOfLocal1C + 10, valueOfLocal2C)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testProvidingAStaticLocalAsComputed() = compositionTest {
+ val local1 = staticCompositionLocalOf { 0 }
+ val local2 = staticCompositionLocalOf { 0 }
+ compose {
+ val valueOfLocal1A = local1.current
+ val valueOfLocal2A = local2.current
+ assertEquals(0, valueOfLocal1A)
+ assertEquals(0, valueOfLocal2A)
+ CompositionLocalProvider(local2 providesComputed { local1.currentValue + 10 }) {
+ val valueOfLocal1B = local1.current
+ val valueOfLocal2B = local2.current
+ assertEquals(valueOfLocal1B + 10, valueOfLocal2B)
+ CompositionLocalProvider(local1 provides 10) {
+ val valueOfLocal1C = local1.current
+ val valueOfLocal2C = local2.current
+ assertEquals(valueOfLocal1C + 10, valueOfLocal2C)
+ }
+ }
+ }
+ }
}
val cacheLocal = staticCompositionLocalOf { "Unset" }
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/MovableContentTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/MovableContentTests.kt
index 7f0a6d3..f6feff2 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/MovableContentTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/MovableContentTests.kt
@@ -1592,6 +1592,85 @@
revalidate()
}
+
+ @Test
+ fun movableContent_rememberOrdering() = compositionTest {
+ val movableContent1 = movableContentOf {
+ repeat(100) {
+ Text("Some content")
+ }
+ }
+ var includeContent by mutableStateOf(true)
+ var rememberKey by mutableStateOf(0)
+
+ compose {
+ if (includeContent) {
+ movableContent1()
+ }
+ val a = remember(rememberKey) {
+ SimpleRememberedObject("Key $rememberKey")
+ }
+ Text(a.name)
+ }
+
+ rememberKey++
+ expectChanges()
+
+ includeContent = false
+ rememberKey++
+ expectChanges()
+ }
+
+ @Test
+ fun movableContent_nestedMovableContent() = compositionTest {
+ var data = 0
+
+ var condition by mutableStateOf(true)
+
+ val nestedContent = movableContentOf {
+ val state = remember {
+ data++
+ }
+ Text("Generated state: $state")
+ }
+
+ val contentHost = movableContentOf {
+ Text("Host")
+ if (condition) {
+ nestedContent()
+ }
+ }
+
+ compose {
+ if (condition) {
+ contentHost()
+ }
+ Text("Outer")
+ if (!condition) {
+ contentHost()
+ nestedContent()
+ }
+ }
+
+ validate {
+ if (condition) {
+ Text("Host")
+ Text("Generated state: 0")
+ }
+ Text("Outer")
+ if (!condition) {
+ Text("Host")
+ Text("Generated state: 0")
+ }
+ }
+
+ condition = false
+ expectChanges()
+ revalidate()
+ condition = true
+ expectChanges()
+ revalidate()
+ }
}
@Composable
@@ -1756,3 +1835,14 @@
if (count == 0) died = true
}
}
+
+class SimpleRememberedObject(val name: String) : RememberObserver {
+ override fun onRemembered() {
+ }
+
+ override fun onForgotten() {
+ }
+
+ override fun onAbandoned() {
+ }
+}
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index db1a98b..2e7e6e7e 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -1293,7 +1293,6 @@
}
@androidx.compose.ui.graphics.drawscope.DrawScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface DrawScope extends androidx.compose.ui.unit.Density {
- method public default androidx.compose.ui.graphics.layer.GraphicsLayer buildLayer(androidx.compose.ui.graphics.layer.GraphicsLayer, optional long size, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
method public void drawArc(androidx.compose.ui.graphics.Brush brush, float startAngle, float sweepAngle, boolean useCenter, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
method public void drawArc(long color, float startAngle, float sweepAngle, boolean useCenter, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
method public void drawCircle(androidx.compose.ui.graphics.Brush brush, optional float radius, optional long center, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
@@ -1317,6 +1316,7 @@
method public androidx.compose.ui.graphics.drawscope.DrawContext getDrawContext();
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
method public default long getSize();
+ method public default void record(androidx.compose.ui.graphics.layer.GraphicsLayer, optional long size, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
property public default long center;
property public abstract androidx.compose.ui.graphics.drawscope.DrawContext drawContext;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
@@ -1421,7 +1421,6 @@
}
public final class GraphicsLayer {
- method public androidx.compose.ui.graphics.layer.GraphicsLayer buildLayer(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection, long size, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
method public float getAlpha();
method public long getAmbientShadowColor();
method public int getBlendMode();
@@ -1446,6 +1445,7 @@
method public float getTranslationX();
method public float getTranslationY();
method public boolean isReleased();
+ method public void record(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection, long size, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
method public void setAlpha(float);
method public void setAmbientShadowColor(long);
method public void setBlendMode(int);
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index c66b65d..5224391 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -1388,7 +1388,6 @@
}
@androidx.compose.ui.graphics.drawscope.DrawScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface DrawScope extends androidx.compose.ui.unit.Density {
- method public default androidx.compose.ui.graphics.layer.GraphicsLayer buildLayer(androidx.compose.ui.graphics.layer.GraphicsLayer, optional long size, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
method public void drawArc(androidx.compose.ui.graphics.Brush brush, float startAngle, float sweepAngle, boolean useCenter, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
method public void drawArc(long color, float startAngle, float sweepAngle, boolean useCenter, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
method public void drawCircle(androidx.compose.ui.graphics.Brush brush, optional float radius, optional long center, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional int blendMode);
@@ -1412,6 +1411,7 @@
method public androidx.compose.ui.graphics.drawscope.DrawContext getDrawContext();
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
method public default long getSize();
+ method public default void record(androidx.compose.ui.graphics.layer.GraphicsLayer, optional long size, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
property public default long center;
property public abstract androidx.compose.ui.graphics.drawscope.DrawContext drawContext;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
@@ -1516,7 +1516,6 @@
}
public final class GraphicsLayer {
- method public androidx.compose.ui.graphics.layer.GraphicsLayer buildLayer(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection, long size, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
method public float getAlpha();
method public long getAmbientShadowColor();
method public int getBlendMode();
@@ -1541,6 +1540,7 @@
method public float getTranslationX();
method public float getTranslationY();
method public boolean isReleased();
+ method public void record(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection, long size, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
method public void setAlpha(float);
method public void setAmbientShadowColor(long);
method public void setBlendMode(int);
diff --git a/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/GraphicsLayerSamples.kt b/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/GraphicsLayerSamples.kt
index 045723b..6f5be44 100644
--- a/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/GraphicsLayerSamples.kt
+++ b/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/GraphicsLayerSamples.kt
@@ -48,9 +48,10 @@
// Build the layer with the density, layout direction and size from the DrawScope
// and position the top left to be 20 pixels from the left and 30 pixels from the top.
// This will the bounds of the layer with a red rectangle
- layer.buildLayer {
- drawRect(Color.Red)
- }.apply {
+ layer.apply {
+ record {
+ drawRect(Color.Red)
+ }
this.topLeft = IntOffset(20, 30)
}
@@ -62,7 +63,7 @@
fun DrawScope.GraphicsLayerSizeSample(layer: GraphicsLayer) {
// Build the layer with the density, layout direction from the DrawScope that is
// sized to 200 x 100 pixels and draw a red rectangle that occupies these bounds
- layer.buildLayer(size = IntSize(200, 100)) {
+ layer.record(size = IntSize(200, 100)) {
drawRect(Color.Red)
}
@@ -74,12 +75,13 @@
fun DrawScope.GraphicsLayerScaleAndPivotSample(layer: GraphicsLayer) {
// Create a 200 x 200 pixel layer that has a red rectangle drawn in the lower right
// corner.
- layer.buildLayer(size = IntSize(200, 200)) {
- drawRect(
- Color.Red,
- topLeft = Offset(size.width / 2f, size.height / 2f)
- )
- }.apply {
+ layer.apply {
+ record(size = IntSize(200, 200)) {
+ drawRect(
+ Color.Red,
+ topLeft = Offset(size.width / 2f, size.height / 2f)
+ )
+ }
// Scale the layer by 1.5x in both the x and y axis relative to the bottom
// right corner
scaleX = 1.5f
@@ -97,9 +99,10 @@
@Sampled
fun DrawScope.GraphicsLayerTranslateSample(layer: GraphicsLayer) {
// Create a 200 x 200 pixel layer that draws a red square
- layer.buildLayer(size = IntSize(200, 200)) {
- drawRect(Color.Red)
- }.apply {
+ layer.apply {
+ record(size = IntSize(200, 200)) {
+ drawRect(Color.Red)
+ }
// Configuring the translationX + Y will translate the red square
// by 100 pixels to the right and 50 pixels from the top when drawn
// into the destination DrawScope
@@ -114,9 +117,10 @@
@Sampled
fun DrawScope.GraphicsLayerShadowSample(layer: GraphicsLayer) {
// Create a 200 x 200 pixel layer that draws a red square
- layer.buildLayer(size = IntSize(200, 200)) {
- drawRect(Color.Red)
- }.apply {
+ layer.apply {
+ record(size = IntSize(200, 200)) {
+ drawRect(Color.Red)
+ }
// Apply a shadow with specified colors that has an elevation of 20f when this layer is
// drawn into the destination DrawScope.
shadowElevation = 20f
@@ -142,9 +146,10 @@
// Build the GraphicsLayer with the specified offset and size that is filled
// with a red rectangle.
- layer.buildLayer(size = layerSize) {
- drawRect(Color.Red)
- }.apply {
+ layer.apply {
+ record(size = layerSize) {
+ drawRect(Color.Red)
+ }
this.topLeft = topLeft
// Specify the Xor blend mode here so that layer contents will be shown in the
// destination only if it is transparent, otherwise the destination would be cleared
@@ -170,9 +175,10 @@
fun DrawScope.GraphicsLayerColorFilterSample(layer: GraphicsLayer) {
// Create a layer with the same configuration as the destination DrawScope
// and draw a red rectangle in the layer
- layer.buildLayer {
- drawRect(Color.Red)
- }.apply {
+ layer.apply {
+ record {
+ drawRect(Color.Red)
+ }
// Apply a ColorFilter that will tint the contents of the layer to blue
// when it is drawn into the destination DrawScope
colorFilter = ColorFilter.tint(Color.Blue)
@@ -186,11 +192,12 @@
fun DrawScope.GraphicsLayerRenderEffectSample(layer: GraphicsLayer) {
// Create a layer sized to the destination draw scope that is comprised
// of an inset red rectangle
- layer.buildLayer {
- inset(20f, 20f) {
- drawRect(Color.Red)
+ layer.apply {
+ record {
+ inset(20f, 20f) {
+ drawRect(Color.Red)
+ }
}
- }.apply {
// Configure a blur to the contents of the layer that is applied
// when drawn to the destination DrawScope
renderEffect = BlurEffect(20f, 20f, TileMode.Decal)
@@ -203,11 +210,12 @@
fun DrawScope.GraphicsLayerAlphaSample(layer: GraphicsLayer) {
// Create a layer sized to the destination draw scope that is comprised
// of an inset red rectangle
- layer.buildLayer {
- inset(20f, 20f) {
- drawRect(Color.Red)
+ layer.apply {
+ record {
+ inset(20f, 20f) {
+ drawRect(Color.Red)
+ }
}
- }.apply {
// Renders the content of the layer with 50% alpha when it is drawn
// into the destination
alpha = 0.5f
@@ -220,9 +228,10 @@
fun DrawScope.GraphicsLayerOutlineSample(layer: GraphicsLayer) {
// Create a layer sized to the destination draw scope that is comprised
// of an inset red rectangle
- layer.buildLayer {
- drawRect(Color.Red)
- }.apply {
+ layer.apply {
+ record {
+ drawRect(Color.Red)
+ }
// Apply a shadow that is clipped to the specified round rect
shadowElevation = 20f
setRoundRectOutline(IntOffset.Zero, IntSize(300, 180), 30f)
@@ -235,9 +244,10 @@
fun DrawScope.GraphicsLayerRoundRectOutline(layer: GraphicsLayer) {
// Create a layer sized to the destination draw scope that is comprised
// of an inset red rectangle
- layer.buildLayer {
- drawRect(Color.Red)
- }.apply {
+ layer.apply {
+ record {
+ drawRect(Color.Red)
+ }
// Apply a shadow and have the contents of the layer be clipped
// to the size of the layer with a 20 pixel corner radius
shadowElevation = 20f
@@ -251,9 +261,10 @@
fun DrawScope.GraphicsLayerRectOutline(layer: GraphicsLayer) {
// Create a layer sized to the destination draw scope that is comprised
// of an inset red rectangle
- layer.buildLayer {
- drawRect(Color.Red)
- }.apply {
+ layer.apply {
+ record {
+ drawRect(Color.Red)
+ }
// Apply a shadow and have the contents of the layer be clipped
// to the size of the layer with a 20 pixel corner radius
shadowElevation = 20f
@@ -265,9 +276,10 @@
@Sampled
fun DrawScope.GraphicsLayerRotationX(layer: GraphicsLayer) {
- layer.buildLayer {
- drawRect(Color.Yellow)
- }.apply {
+ layer.apply {
+ record {
+ drawRect(Color.Yellow)
+ }
// Rotates the yellow rect 45f clockwise relative to the x axis
rotationX = 45f
}
@@ -277,9 +289,10 @@
@Sampled
fun DrawScope.GraphicsLayerRotationYWithCameraDistance(layer: GraphicsLayer) {
- layer.buildLayer {
- drawRect(Color.Yellow)
- }.apply {
+ layer.apply {
+ record {
+ drawRect(Color.Yellow)
+ }
// Rotates the yellow rect 45f clockwise relative to the y axis
rotationY = 45f
cameraDistance = 5.0f
@@ -291,7 +304,7 @@
@OptIn(DelicateCoroutinesApi::class)
@Sampled
fun GraphicsLayerToImageBitmap(context: Context, layer: GraphicsLayer) {
- layer.buildLayer(Density(1f), LayoutDirection.Ltr, IntSize(300, 200)) {
+ layer.record(Density(1f), LayoutDirection.Ltr, IntSize(300, 200)) {
val half = Size(size.width / 2, size.height)
drawRect(Color.Red, size = half)
drawRect(Color.Blue, topLeft = Offset(size.width / 2f, 0f), size = half)
diff --git a/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayerTest.kt b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayerTest.kt
index a5b0b00..0b9f951 100644
--- a/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayerTest.kt
+++ b/compose/ui/ui-graphics/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayerTest.kt
@@ -144,7 +144,7 @@
block = { graphicsContext ->
graphicsContext.createGraphicsLayer().apply {
assertEquals(IntSize.Zero, this.size)
- buildLayer {
+ record {
drawRect(
Color.Red,
size = size / 2f
@@ -193,7 +193,7 @@
graphicsContext.createGraphicsLayer().apply {
graphicsLayer = this
assertEquals(IntSize.Zero, this.size)
- buildLayer {
+ record {
drawRect(provider!!.color)
}
}
@@ -219,7 +219,7 @@
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
assertEquals(IntSize.Zero, this.size)
- buildLayer {
+ record {
drawRect(Color.Red)
}
}
@@ -234,11 +234,11 @@
}
@Test
- fun testBuildLayerWithSize() {
+ fun testRecordLayerWithSize() {
graphicsLayerTest(
block = { graphicsContext ->
val layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer(IntSize(TEST_WIDTH / 2, TEST_HEIGHT / 2)) {
+ record(IntSize(TEST_WIDTH / 2, TEST_HEIGHT / 2)) {
drawRect(Color.Red)
}
}
@@ -251,17 +251,16 @@
}
@Test
- fun testBuildLayerWithOffset() {
+ fun testRecordLayerWithOffset() {
var layer: GraphicsLayer? = null
val topLeft = IntOffset(TEST_WIDTH / 2, TEST_HEIGHT / 2)
val size = IntSize(TEST_WIDTH, TEST_HEIGHT)
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(Color.Red)
}
- }.apply {
this.topLeft = topLeft
}
drawLayer(layer!!)
@@ -282,7 +281,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
inset(0f, 0f, -4f, -4f) {
drawRect(Color.Red)
}
@@ -310,7 +309,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(Color.Red)
}
alpha = 0.5f
@@ -339,7 +338,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(
Color.Red,
size = Size(this.size.width / 2, this.size.height / 2)
@@ -366,7 +365,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(
Color.Red,
size = Size(this.size.width / 2, this.size.height / 2)
@@ -393,7 +392,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
inset(this.size.width / 4, this.size.height / 4) {
drawRect(Color.Red)
}
@@ -419,7 +418,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(Color.Red)
}
scaleY = 0.5f
@@ -444,7 +443,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(Color.Red, size = this.size / 2f)
}
translationX = this.size.width / 2f
@@ -467,7 +466,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(Color.Red, size = this.size / 2f)
}
translationY = this.size.height / 2f
@@ -490,7 +489,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(
Color.Red,
size = Size(this.size.width, this.size.height / 2)
@@ -520,7 +519,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(Color.Red)
}
pivotOffset = Offset(0f, this.size.height / 2f)
@@ -549,7 +548,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(
Color.Red,
topLeft = Offset(
@@ -598,7 +597,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(
Color.Red,
size = Size(100000f, 100000f)
@@ -625,7 +624,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(
Color.Red,
size = Size(100000f, 100000f)
@@ -670,7 +669,7 @@
)
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer(halfSize) {
+ record(halfSize) {
drawRect(targetColor)
}
shadowElevation = 10f
@@ -721,7 +720,7 @@
)
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer(halfSize) {
+ record(halfSize) {
drawRect(targetColor)
}
setPathOutline(
@@ -790,7 +789,7 @@
bottom = top + halfSize.height
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer(halfSize) {
+ record(halfSize) {
drawRect(targetColor)
}
setRoundRectOutline(IntOffset.Zero, halfSize, radius)
@@ -871,7 +870,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(Color.Red)
}
renderEffect = BlurEffect(blurRadius, blurRadius, TileMode.Decal)
@@ -907,7 +906,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
inset(0f, 0f, size.width / 3, size.height / 3) {
drawRect(color = Color.Red)
}
@@ -945,7 +944,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
inset(0f, 0f, size.width / 3, size.height / 3) {
drawRect(color = Color.Red)
}
@@ -978,7 +977,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
inset(0f, 0f, size.width / 3, size.height / 3) {
drawRect(color = Color.Red)
}
@@ -1017,7 +1016,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(Color.Red)
}
cameraDistance = 5.0f
@@ -1045,11 +1044,10 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(Color.Red)
- }.apply {
- colorFilter = tint(Color.Blue)
}
+ colorFilter = tint(Color.Blue)
}
drawLayer(layer!!)
},
@@ -1080,12 +1078,11 @@
(drawScopeSize.width / 2).toInt(),
(drawScopeSize.height / 2).toInt()
)
- buildLayer(layerSize) {
+ record(layerSize) {
drawRect(Color.Red)
- }.apply {
- this.topLeft = topLeft
- this.blendMode = BlendMode.Xor
}
+ this.topLeft = topLeft
+ this.blendMode = BlendMode.Xor
}
drawRect(Color.Green)
drawLayer(layer!!)
@@ -1128,7 +1125,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(targetColor)
}
setRectOutline(this.size.center, this.size / 2)
@@ -1173,7 +1170,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(targetColor)
}
setPathOutline(Path().apply {
@@ -1228,7 +1225,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(targetColor)
}
setRoundRectOutline(
@@ -1367,9 +1364,9 @@
if (usePixelCopy && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
verify(target.captureToImage().toPixelMap())
} else {
- val buildLayerLatch = CountDownLatch(1)
+ val recordLatch = CountDownLatch(1)
testActivity!!.runOnUiThread {
- rootGraphicsLayer!!.buildLayer(
+ rootGraphicsLayer!!.record(
density,
Ltr,
IntSize(target.width, target.height)
@@ -1378,9 +1375,9 @@
target.draw(canvas.nativeCanvas)
}
}
- buildLayerLatch.countDown()
+ recordLatch.countDown()
}
- assertTrue(buildLayerLatch.await(3000, TimeUnit.MILLISECONDS))
+ assertTrue(recordLatch.await(3000, TimeUnit.MILLISECONDS))
val bitmap = runBlocking {
rootGraphicsLayer!!.toImageBitmap()
}
@@ -1406,7 +1403,7 @@
var root = rootGraphicsLayer
if (root == null) {
root = graphicsContext.createGraphicsLayer()
- root.buildLayer(Density(1f, 1f), Ltr, IntSize(width.toInt(), height.toInt())) {
+ root.record(Density(1f, 1f), Ltr, IntSize(width.toInt(), height.toInt())) {
block(graphicsContext)
}
rootGraphicsLayer = root
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
index db290e9..5713996 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
@@ -83,7 +83,7 @@
// actually doing a placeholder render before the activity is setup
// (ex in unit tests) causes the emulator to crash with an NPE in native code
// on the HWUI canvas implementation
- layer.buildLayer(
+ layer.record(
DefaultDensity,
LayoutDirection.Ltr,
IntSize(1, 1),
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayer.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayer.android.kt
index 2013787..033690d 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayer.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/AndroidGraphicsLayer.android.kt
@@ -106,7 +106,7 @@
/**
* Offset in pixels where this [GraphicsLayer] will render within a provided canvas when
- * [drawLayer] is called. This is configured by calling [buildLayer]
+ * [drawLayer] is called. This is configured by calling [record]
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerTopLeftSample
*/
@@ -122,7 +122,7 @@
* Size in pixels of the [GraphicsLayer]. By default [GraphicsLayer] contents can draw outside
* of the bounds specified by [topLeft] and [size], however, rasterization of this layer into
* an offscreen buffer will be sized according to the specified size. This is configured
- * by calling [buildLayer]
+ * by calling [record]
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerSizeSample
*/
@@ -397,12 +397,12 @@
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerBlendModeSample
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerTranslateSample
*/
- actual fun buildLayer(
+ actual fun record(
density: Density,
layoutDirection: LayoutDirection,
size: IntSize,
block: DrawScope.() -> Unit
- ): GraphicsLayer {
+ ) {
if (this.size != size) {
setPosition(topLeft, size)
this.size = size
@@ -415,10 +415,8 @@
childDependenciesTracker.withTracking(
onDependencyRemoved = { it.onRemovedFromParentLayer() }
) {
- impl.buildLayer(density, layoutDirection, this, drawBlock)
+ impl.record(density, layoutDirection, this, drawBlock)
}
-
- return this
}
private fun addSubLayer(graphicsLayer: GraphicsLayer) {
@@ -781,7 +779,7 @@
/**
* Create an [ImageBitmap] with the contents of this [GraphicsLayer] instance. Note that
- * [GraphicsLayer.buildLayer] must be invoked first to record drawing operations before invoking
+ * [GraphicsLayer.record] must be invoked first to record drawing operations before invoking
* this method.
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerToImageBitmap
@@ -930,9 +928,9 @@
fun draw(canvas: Canvas)
/**
- * @see GraphicsLayer.buildLayer
+ * @see GraphicsLayer.record
*/
- fun buildLayer(
+ fun record(
density: Density,
layoutDirection: LayoutDirection,
layer: GraphicsLayer,
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt
index 9a9c22d..db4635e 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt
@@ -268,7 +268,7 @@
override var isInvalidated: Boolean = true
- override fun buildLayer(
+ override fun record(
density: Density,
layoutDirection: LayoutDirection,
layer: GraphicsLayer,
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV29.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV29.android.kt
index 7a97b5c..1c62892 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV29.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV29.android.kt
@@ -208,7 +208,7 @@
override var isInvalidated: Boolean = true
- override fun buildLayer(
+ override fun record(
density: Density,
layoutDirection: LayoutDirection,
layer: GraphicsLayer,
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsViewLayer.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsViewLayer.android.kt
index ec3b901..f98f7bf 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsViewLayer.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsViewLayer.android.kt
@@ -370,7 +370,7 @@
}
}
- override fun buildLayer(
+ override fun record(
density: Density,
layoutDirection: LayoutDirection,
layer: GraphicsLayer,
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerSnapshot.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerSnapshot.android.kt
index 4382634..349eccf 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerSnapshot.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerSnapshot.android.kt
@@ -60,7 +60,7 @@
override fun endRecording() {
// NO-OP. The GraphicsLayer used here already has its drawing commands recorded via
- // GraphicsLayer.buildLayer, so there is no additional work to be done here.
+ // GraphicsLayer.record, so there is no additional work to be done here.
}
override fun getWidth(): Int = graphicsLayer.size.width
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt
index 93b5576..5be8224 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt
@@ -935,17 +935,17 @@
* This will retarget the underlying canvas of the provided DrawScope to draw within the layer
* itself and reset it to the original canvas on the conclusion of this method call.
*/
- fun GraphicsLayer.buildLayer(
+ fun GraphicsLayer.record(
size: IntSize = this@DrawScope.size.toIntSize(),
block: DrawScope.() -> Unit
- ): GraphicsLayer = buildLayer(
+ ) = record(
this@DrawScope,
this@DrawScope.layoutDirection,
size
) {
this@DrawScope.draw(
- // we can use this@buildLayer.drawContext directly as the values in this@DrawScope
- // and this@buildLayer are the same
+ // we can use this@record.drawContext directly as the values in this@DrawScope
+ // and this@record are the same
drawContext.density,
drawContext.layoutDirection,
drawContext.canvas,
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayer.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayer.kt
index ca57529..62019b8 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayer.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayer.kt
@@ -39,7 +39,7 @@
/**
* Draw the provided [GraphicsLayer] into the current [DrawScope].
- * The [GraphicsLayer] provided must have [GraphicsLayer.buildLayer] invoked on it otherwise
+ * The [GraphicsLayer] provided must have [GraphicsLayer.record] invoked on it otherwise
* no visual output will be seen in the rendered result.
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerTopLeftSample
@@ -71,7 +71,7 @@
* Usage of a [GraphicsLayer] requires a minimum of 2 steps.
*
* 1) The [GraphicsLayer] must be built, which involves specifying the position alongside a list
- * of drawing commands using [GraphicsLayer.buildLayer]
+ * of drawing commands using [GraphicsLayer.record]
*
* 2) The [GraphicsLayer] is then drawn into another destination [Canvas] using
* [GraphicsLayer.draw].
@@ -108,7 +108,7 @@
* Size in pixels of the [GraphicsLayer]. By default [GraphicsLayer] contents can draw outside
* of the bounds specified by [topLeft] and [size], however, rasterization of this layer into
* an offscreen buffer will be sized according to the specified size. This is configured
- * by calling [buildLayer]
+ * by calling [record]
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerSizeSample
*/
@@ -230,7 +230,7 @@
/**
* Returns the outline specified by either [setPathOutline] or [setRoundRectOutline].
* By default this will return [Outline.Rectangle] with the size of the [GraphicsLayer]
- * specified by [buildLayer] or [IntSize.Zero] if [buildLayer] was not previously invoked.
+ * specified by [record] or [IntSize.Zero] if [record] was not previously invoked.
*/
val outline: Outline
@@ -373,16 +373,16 @@
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerBlendModeSample
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerTranslateSample
*/
- fun buildLayer(
+ fun record(
density: Density,
layoutDirection: LayoutDirection,
size: IntSize,
block: DrawScope.() -> Unit
- ): GraphicsLayer
+ )
/**
* Create an [ImageBitmap] with the contents of this [GraphicsLayer] instance. Note that
- * [GraphicsLayer.buildLayer] must be invoked first to record drawing operations before invoking
+ * [GraphicsLayer.record] must be invoked first to record drawing operations before invoking
* this method.
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerToImageBitmap
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayer.desktop.kt b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayer.desktop.kt
index d7d9c3f..eaf601f 100644
--- a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayer.desktop.kt
+++ b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayer.desktop.kt
@@ -163,12 +163,12 @@
invalidateMatrix()
}
- actual fun buildLayer(
+ actual fun record(
density: Density,
layoutDirection: LayoutDirection,
size: IntSize,
block: DrawScope.() -> Unit
- ): GraphicsLayer {
+ ) {
this.density = density
this.size = size
updateLayerConfiguration()
@@ -200,7 +200,6 @@
)
}
picture = pictureRecorder.finishRecordingAsPicture()
- return this
}
private fun addSubLayer(graphicsLayer: GraphicsLayer) {
@@ -406,7 +405,7 @@
/**
* Returns the outline specified by either [setPathOutline] or [setRoundRectOutline].
* By default this will return [Outline.Rectangle] with the size of the [GraphicsLayer]
- * specified by [buildLayer] or [IntSize.Zero] if [buildLayer] was not previously invoked.
+ * specified by [record] or [IntSize.Zero] if [record] was not previously invoked.
*/
actual val outline: Outline
get() = configureOutline()
@@ -547,7 +546,7 @@
/**
* Create an [ImageBitmap] with the contents of this [GraphicsLayer] instance. Note that
- * [GraphicsLayer.buildLayer] must be invoked first to record drawing operations before invoking
+ * [GraphicsLayer.record] must be invoked first to record drawing operations before invoking
* this method.
*/
actual suspend fun toImageBitmap(): ImageBitmap =
diff --git a/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/layer/DesktopGraphicsLayerTest.kt b/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/layer/DesktopGraphicsLayerTest.kt
index 463b121..f8941c3 100644
--- a/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/layer/DesktopGraphicsLayerTest.kt
+++ b/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/layer/DesktopGraphicsLayerTest.kt
@@ -52,7 +52,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(Color.Red)
}
}
@@ -67,11 +67,11 @@
}
@Test
- fun testBuildLayerWithSize() {
+ fun testRecordWithSize() {
graphicsLayerTest(
block = { graphicsContext ->
val layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer(
+ record(
size = IntSize(TEST_WIDTH / 2, TEST_HEIGHT / 2)
) {
drawRect(Color.Red)
@@ -86,16 +86,17 @@
}
@Test
- fun testBuildLayerWithOffset() {
+ fun testRecordWithOffset() {
var layer: GraphicsLayer? = null
val topLeft = IntOffset(TEST_WIDTH / 2, TEST_HEIGHT / 2)
val size = IntSize(TEST_WIDTH, TEST_HEIGHT)
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(Color.Red)
- }.topLeft = topLeft
+ }
+ this.topLeft = topLeft
}
drawLayer(layer!!)
},
@@ -115,11 +116,12 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
inset(0f, 0f, -4f, -4f) {
drawRect(Color.Red)
}
- }.topLeft = topLeft
+ }
+ this.topLeft = topLeft
}
drawLayer(layer!!)
},
@@ -142,7 +144,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(
Color.Red,
size = Size(this.size.width / 2, this.size.height / 2)
@@ -169,7 +171,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(
Color.Red,
size = Size(this.size.width / 2, this.size.height / 2)
@@ -196,7 +198,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
inset(this.size.width / 4, this.size.height / 4) {
drawRect(Color.Red)
}
@@ -222,7 +224,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(Color.Red)
}
scaleY = 0.5f
@@ -247,7 +249,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(Color.Red, size = this.size / 2f)
}
translationX = this.size.width / 2f
@@ -270,7 +272,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(Color.Red, size = this.size / 2f)
}
translationY = this.size.height / 2f
@@ -291,7 +293,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(
Color.Red,
size = Size(100000f, 100000f)
@@ -318,7 +320,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(
Color.Red,
size = Size(100000f, 100000f)
@@ -362,7 +364,7 @@
)
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer(halfSize) {
+ record(halfSize) {
drawRect(targetColor)
}
shadowElevation = 10f
@@ -409,7 +411,7 @@
)
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer(halfSize) {
+ record(halfSize) {
drawRect(targetColor)
}
setPathOutline(
@@ -474,7 +476,7 @@
bottom = top + halfSize.height
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer(halfSize) {
+ record(halfSize) {
drawRect(targetColor)
}
setRoundRectOutline(IntOffset.Zero, halfSize, radius)
@@ -555,7 +557,7 @@
layer = graphicsContext.createGraphicsLayer().apply {
compositingStrategy = CompositingStrategy.Auto
alpha = 0.5f
- buildLayer {
+ record {
inset(0f, 0f, size.width / 3, size.height / 3) {
drawRect(color = Color.Red)
}
@@ -592,7 +594,7 @@
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
compositingStrategy = CompositingStrategy.Offscreen
- buildLayer {
+ record {
inset(0f, 0f, size.width / 3, size.height / 3) {
drawRect(color = Color.Red)
}
@@ -625,7 +627,7 @@
layer = graphicsContext.createGraphicsLayer().apply {
compositingStrategy = CompositingStrategy.ModulateAlpha
alpha = 0.5f
- buildLayer {
+ record {
inset(0f, 0f, size.width / 3, size.height / 3) {
drawRect(color = Color.Red)
}
@@ -662,7 +664,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(Color.Red)
}.apply {
colorFilter = ColorFilter.tint(Color.Blue)
@@ -697,12 +699,11 @@
(drawScopeSize.width / 2).toInt(),
(drawScopeSize.height / 2).toInt()
)
- buildLayer(layerSize) {
+ record(layerSize) {
drawRect(Color.Red)
- }.apply {
- this.topLeft = topLeft
- this.blendMode = BlendMode.Xor
}
+ this.topLeft = topLeft
+ this.blendMode = BlendMode.Xor
}
drawRect(Color.Green)
drawLayer(layer!!)
@@ -745,7 +746,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(targetColor)
}
setRectOutline(this.size.center, this.size / 2)
@@ -790,7 +791,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(targetColor)
}
setPathOutline(Path().apply {
@@ -845,7 +846,7 @@
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
- buildLayer {
+ record {
drawRect(targetColor)
}
setRoundRectOutline(
diff --git a/compose/ui/ui-inspection/build.gradle b/compose/ui/ui-inspection/build.gradle
index 5f0952ee..20b1101 100644
--- a/compose/ui/ui-inspection/build.gradle
+++ b/compose/ui/ui-inspection/build.gradle
@@ -40,6 +40,7 @@
// because compose:ui-inspector can be run only in app with compose:ui:ui
// thus all its transitive dependencies will be present too.
compileOnly(libs.kotlinStdlib)
+ compileOnly("androidx.collection:collection:1.4.0")
compileOnly("androidx.inspection:inspection:1.0.0")
compileOnly("androidx.compose.runtime:runtime:1.2.1")
compileOnly(project(":compose:ui:ui-graphics"))
@@ -52,7 +53,6 @@
implementation(libs.kotlinReflect, {
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib"
})
- implementation("androidx.collection:collection:1.4.0")
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.kotlinCoroutinesAndroid)
androidTestImplementation(libs.testCore)
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index ec08ba2..7a707ab 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -360,7 +360,6 @@
}
public final class CacheDrawScope implements androidx.compose.ui.unit.Density {
- method public androidx.compose.ui.graphics.layer.GraphicsLayer buildLayer(androidx.compose.ui.graphics.layer.GraphicsLayer, optional androidx.compose.ui.unit.Density density, optional androidx.compose.ui.unit.LayoutDirection layoutDirection, optional long size, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> block);
method public float getDensity();
method public float getFontScale();
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
@@ -368,6 +367,7 @@
method public androidx.compose.ui.graphics.layer.GraphicsLayer obtainGraphicsLayer();
method public androidx.compose.ui.draw.DrawResult onDrawBehind(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> block);
+ method public void record(androidx.compose.ui.graphics.layer.GraphicsLayer, optional androidx.compose.ui.unit.Density density, optional androidx.compose.ui.unit.LayoutDirection layoutDirection, optional long size, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> block);
property public float density;
property public float fontScale;
property public final androidx.compose.ui.unit.LayoutDirection layoutDirection;
@@ -1702,14 +1702,18 @@
}
public static final class NestedScrollSource.Companion {
- method public int getDrag();
- method public int getFling();
+ method @Deprecated public int getDrag();
+ method @Deprecated public int getFling();
method @Deprecated @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public int getRelocate();
- method public int getWheel();
- property public final int Drag;
- property public final int Fling;
+ method public int getSideEffect();
+ method public int getUserInput();
+ method @Deprecated public int getWheel();
+ property @Deprecated public final int Drag;
+ property @Deprecated public final int Fling;
property @Deprecated @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public final int Relocate;
- property public final int Wheel;
+ property public final int SideEffect;
+ property public final int UserInput;
+ property @Deprecated public final int Wheel;
}
}
@@ -2065,17 +2069,17 @@
}
public interface ApproachLayoutModifierNode extends androidx.compose.ui.node.LayoutModifierNode {
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.layout.MeasureResult approachMeasure(androidx.compose.ui.layout.ApproachMeasureScope, androidx.compose.ui.layout.Measurable measurable, long constraints);
- method public boolean isMeasurementApproachComplete(long lookaheadSize);
- method public default boolean isPlacementApproachComplete(androidx.compose.ui.layout.Placeable.PlacementScope, androidx.compose.ui.layout.LayoutCoordinates lookaheadCoordinates);
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public default int maxApproachIntrinsicHeight(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int width);
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public default int maxApproachIntrinsicWidth(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int height);
+ method public androidx.compose.ui.layout.MeasureResult approachMeasure(androidx.compose.ui.layout.ApproachMeasureScope, androidx.compose.ui.layout.Measurable measurable, long constraints);
+ method public boolean isMeasurementApproachInProgress(long lookaheadSize);
+ method public default boolean isPlacementApproachInProgress(androidx.compose.ui.layout.Placeable.PlacementScope, androidx.compose.ui.layout.LayoutCoordinates lookaheadCoordinates);
+ method public default int maxApproachIntrinsicHeight(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int width);
+ method public default int maxApproachIntrinsicWidth(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int height);
method public default androidx.compose.ui.layout.MeasureResult measure(androidx.compose.ui.layout.MeasureScope, androidx.compose.ui.layout.Measurable measurable, long constraints);
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public default int minApproachIntrinsicHeight(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int width);
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public default int minApproachIntrinsicWidth(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int height);
+ method public default int minApproachIntrinsicHeight(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int width);
+ method public default int minApproachIntrinsicWidth(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int height);
}
- @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public sealed interface ApproachMeasureScope extends androidx.compose.ui.layout.ApproachIntrinsicMeasureScope androidx.compose.ui.layout.MeasureScope {
+ public sealed interface ApproachMeasureScope extends androidx.compose.ui.layout.ApproachIntrinsicMeasureScope androidx.compose.ui.layout.MeasureScope {
}
public interface BeyondBoundsLayout {
@@ -2157,9 +2161,6 @@
ctor public HorizontalRuler();
}
- @Deprecated @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public interface IntermediateMeasureScope extends androidx.compose.ui.layout.ApproachMeasureScope kotlinx.coroutines.CoroutineScope androidx.compose.ui.layout.LookaheadScope {
- }
-
public interface IntrinsicMeasurable {
method public Object? getParentData();
method public int maxIntrinsicHeight(int width);
@@ -2273,8 +2274,7 @@
public final class LookaheadScopeKt {
method @androidx.compose.runtime.Composable @androidx.compose.ui.UiComposable public static void LookaheadScope(kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LookaheadScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier approachLayout(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.IntSize,java.lang.Boolean> isMeasurementApproachComplete, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.Placeable.PlacementScope,? super androidx.compose.ui.layout.LayoutCoordinates,java.lang.Boolean> isPlacementApproachComplete, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.ApproachMeasureScope,? super androidx.compose.ui.layout.Measurable,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> approachMeasure);
- method @Deprecated @SuppressCompatibility @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);
+ method public static androidx.compose.ui.Modifier approachLayout(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.IntSize,java.lang.Boolean> isMeasurementApproachInProgress, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.Placeable.PlacementScope,? super androidx.compose.ui.layout.LayoutCoordinates,java.lang.Boolean> isPlacementApproachInProgress, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.ApproachMeasureScope,? super androidx.compose.ui.layout.Measurable,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> approachMeasure);
}
public interface Measurable extends androidx.compose.ui.layout.IntrinsicMeasurable {
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 4ba9b32..fd944d2 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -360,7 +360,6 @@
}
public final class CacheDrawScope implements androidx.compose.ui.unit.Density {
- method public androidx.compose.ui.graphics.layer.GraphicsLayer buildLayer(androidx.compose.ui.graphics.layer.GraphicsLayer, optional androidx.compose.ui.unit.Density density, optional androidx.compose.ui.unit.LayoutDirection layoutDirection, optional long size, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> block);
method public float getDensity();
method public float getFontScale();
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
@@ -368,6 +367,7 @@
method public androidx.compose.ui.graphics.layer.GraphicsLayer obtainGraphicsLayer();
method public androidx.compose.ui.draw.DrawResult onDrawBehind(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> block);
+ method public void record(androidx.compose.ui.graphics.layer.GraphicsLayer, optional androidx.compose.ui.unit.Density density, optional androidx.compose.ui.unit.LayoutDirection layoutDirection, optional long size, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> block);
property public float density;
property public float fontScale;
property public final androidx.compose.ui.unit.LayoutDirection layoutDirection;
@@ -1702,14 +1702,18 @@
}
public static final class NestedScrollSource.Companion {
- method public int getDrag();
- method public int getFling();
+ method @Deprecated public int getDrag();
+ method @Deprecated public int getFling();
method @Deprecated @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public int getRelocate();
- method public int getWheel();
- property public final int Drag;
- property public final int Fling;
+ method public int getSideEffect();
+ method public int getUserInput();
+ method @Deprecated public int getWheel();
+ property @Deprecated public final int Drag;
+ property @Deprecated public final int Fling;
property @Deprecated @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public final int Relocate;
- property public final int Wheel;
+ property public final int SideEffect;
+ property public final int UserInput;
+ property @Deprecated public final int Wheel;
}
}
@@ -2065,17 +2069,17 @@
}
public interface ApproachLayoutModifierNode extends androidx.compose.ui.node.LayoutModifierNode {
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.layout.MeasureResult approachMeasure(androidx.compose.ui.layout.ApproachMeasureScope, androidx.compose.ui.layout.Measurable measurable, long constraints);
- method public boolean isMeasurementApproachComplete(long lookaheadSize);
- method public default boolean isPlacementApproachComplete(androidx.compose.ui.layout.Placeable.PlacementScope, androidx.compose.ui.layout.LayoutCoordinates lookaheadCoordinates);
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public default int maxApproachIntrinsicHeight(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int width);
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public default int maxApproachIntrinsicWidth(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int height);
+ method public androidx.compose.ui.layout.MeasureResult approachMeasure(androidx.compose.ui.layout.ApproachMeasureScope, androidx.compose.ui.layout.Measurable measurable, long constraints);
+ method public boolean isMeasurementApproachInProgress(long lookaheadSize);
+ method public default boolean isPlacementApproachInProgress(androidx.compose.ui.layout.Placeable.PlacementScope, androidx.compose.ui.layout.LayoutCoordinates lookaheadCoordinates);
+ method public default int maxApproachIntrinsicHeight(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int width);
+ method public default int maxApproachIntrinsicWidth(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int height);
method public default androidx.compose.ui.layout.MeasureResult measure(androidx.compose.ui.layout.MeasureScope, androidx.compose.ui.layout.Measurable measurable, long constraints);
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public default int minApproachIntrinsicHeight(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int width);
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public default int minApproachIntrinsicWidth(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int height);
+ method public default int minApproachIntrinsicHeight(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int width);
+ method public default int minApproachIntrinsicWidth(androidx.compose.ui.layout.ApproachIntrinsicMeasureScope, androidx.compose.ui.layout.IntrinsicMeasurable measurable, int height);
}
- @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public sealed interface ApproachMeasureScope extends androidx.compose.ui.layout.ApproachIntrinsicMeasureScope androidx.compose.ui.layout.MeasureScope {
+ public sealed interface ApproachMeasureScope extends androidx.compose.ui.layout.ApproachIntrinsicMeasureScope androidx.compose.ui.layout.MeasureScope {
}
public interface BeyondBoundsLayout {
@@ -2157,9 +2161,6 @@
ctor public HorizontalRuler();
}
- @Deprecated @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public interface IntermediateMeasureScope extends androidx.compose.ui.layout.ApproachMeasureScope kotlinx.coroutines.CoroutineScope androidx.compose.ui.layout.LookaheadScope {
- }
-
public interface IntrinsicMeasurable {
method public Object? getParentData();
method public int maxIntrinsicHeight(int width);
@@ -2276,8 +2277,7 @@
public final class LookaheadScopeKt {
method @androidx.compose.runtime.Composable @androidx.compose.ui.UiComposable public static void LookaheadScope(kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LookaheadScope,kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier approachLayout(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.IntSize,java.lang.Boolean> isMeasurementApproachComplete, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.Placeable.PlacementScope,? super androidx.compose.ui.layout.LayoutCoordinates,java.lang.Boolean> isPlacementApproachComplete, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.ApproachMeasureScope,? super androidx.compose.ui.layout.Measurable,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> approachMeasure);
- method @Deprecated @SuppressCompatibility @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);
+ method public static androidx.compose.ui.Modifier approachLayout(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.IntSize,java.lang.Boolean> isMeasurementApproachInProgress, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.Placeable.PlacementScope,? super androidx.compose.ui.layout.LayoutCoordinates,java.lang.Boolean> isPlacementApproachInProgress, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.ApproachMeasureScope,? super androidx.compose.ui.layout.Measurable,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> approachMeasure);
}
public interface Measurable extends androidx.compose.ui.layout.IntrinsicMeasurable {
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt
index 2f557b3..e4c7320 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/NestedScrollingBenchmark.kt
@@ -137,8 +137,12 @@
}
override fun toggleState() {
- scrollResult = dispatcher.dispatchPreScroll(delta, NestedScrollSource.Drag)
- scrollResult = dispatcher.dispatchPostScroll(delta, scrollResult, NestedScrollSource.Drag)
+ scrollResult = dispatcher.dispatchPreScroll(delta, NestedScrollSource.UserInput)
+ scrollResult = dispatcher.dispatchPostScroll(
+ delta,
+ scrollResult,
+ NestedScrollSource.UserInput
+ )
runBlocking {
velocityResult = dispatcher.dispatchPreFling(velocity)
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 8f79926..000723b 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -99,6 +99,18 @@
implementation("androidx.emoji2:emoji2:1.2.0")
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+
+ // `compose-ui` has a transitive dependency on `lifecycle-livedata-core`, and
+ // converting `lifecycle-runtime-compose` to KMP triggered a Gradle bug. Adding
+ // the `livedata` dependency directly works around the issue.
+ // See https://github.com/gradle/gradle/issues/14220 for details.
+ compileOnly(projectOrArtifact(":lifecycle:lifecycle-livedata-core"))
+
+ // `compose-ui` has a transitive dependency on `lifecycle-viewmodel-savedstate`, and
+ // converting `lifecycle-runtime-compose` to KMP triggered a Gradle bug. Adding
+ // the `lifecycle-viewmodel-savedstate` dependency directly works around the issue.
+ // See https://github.com/gradle/gradle/issues/14220 for details.
+ compileOnly(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
}
}
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadScopeSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadScopeSample.kt
index 036583e..c34126c 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadScopeSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadScopeSample.kt
@@ -82,12 +82,12 @@
sizeAnimation: DeferredTargetAnimation<IntSize, AnimationVector2D>,
coroutineScope: CoroutineScope
) = this.approachLayout(
- isMeasurementApproachComplete = { lookaheadSize ->
+ isMeasurementApproachInProgress = { lookaheadSize ->
// Update the target of the size animation.
sizeAnimation.updateTarget(lookaheadSize, coroutineScope)
- // Return true if the size animation has no pending target change and has finished
+ // Return true if the size animation has pending target change or hasn't finished
// running.
- sizeAnimation.isIdle
+ !sizeAnimation.isIdle
}
) { measurable, _ ->
// In the measurement approach, the goal is to gradually reach the destination size
@@ -150,21 +150,21 @@
IntOffset.VectorConverter
)
- override fun isMeasurementApproachComplete(lookaheadSize: IntSize): Boolean {
+ override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
// Since we only animate the placement here, we can consider measurement approach
// complete.
- return true
+ return false
}
- // Returns true when the offset animation is complete, false otherwise.
- override fun Placeable.PlacementScope.isPlacementApproachComplete(
+ // Returns true when the offset animation is in progress, false otherwise.
+ override fun Placeable.PlacementScope.isPlacementApproachInProgress(
lookaheadCoordinates: LayoutCoordinates
): Boolean {
val target = with(lookaheadScope) {
lookaheadScopeCoordinates.localLookaheadPositionOf(lookaheadCoordinates).round()
}
offsetAnimation.updateTarget(target, coroutineScope)
- return offsetAnimation.isIdle
+ return !offsetAnimation.isIdle
}
@ExperimentalComposeUiApi
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/NestedScrollSamples.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/NestedScrollSamples.kt
index 1d3cc92..cb2b988 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/NestedScrollSamples.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/NestedScrollSamples.kt
@@ -147,7 +147,7 @@
// want to pre consume (it's a nested scroll contract)
val parentsConsumed = nestedScrollDispatcher.dispatchPreScroll(
available = Offset(x = 0f, y = delta),
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
)
// adjust what's available to us since might have consumed smth
val adjustedAvailable = delta - parentsConsumed.y
@@ -159,7 +159,7 @@
nestedScrollDispatcher.dispatchPostScroll(
consumed = totalConsumed,
available = Offset(x = 0f, y = left),
- source = NestedScrollSource.Drag
+ source = NestedScrollSource.UserInput
)
// we won't dispatch pre/post fling events as we have no flinging here, but the
// idea is very similar:
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
index b591a0e..911aac3 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
@@ -164,7 +164,7 @@
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- fun testBuildLayerWithCache() {
+ fun testRecordWithCache() {
var graphicsLayer: GraphicsLayer? = null
val testTag = "TestTag"
val size = 120.dp
@@ -179,13 +179,12 @@
.then(
Modifier.drawWithCache {
val layer = obtainGraphicsLayer().also { graphicsLayer = it }
- layer
- .buildLayer {
+ layer.apply {
+ record {
drawContent()
}
- .apply {
- this.colorFilter = ColorFilter.tint(tintColor)
- }
+ this.colorFilter = ColorFilter.tint(tintColor)
+ }
onDrawWithContent {
drawLayer(layer)
}
@@ -217,7 +216,7 @@
val drawGraphicsLayer = mutableStateOf(0)
val rectColor = Color.Red
val bgColor = Color.Blue
- var isLayerBuilt = false
+ var isLayerRecorded = false
rule.setContent {
val graphicsLayer = rememberGraphicsLayer()
assertEquals(IntSize.Zero, graphicsLayer.size)
@@ -226,11 +225,11 @@
.testTag(testTag)
.then(
Modifier.drawWithCache {
- if (!isLayerBuilt) {
- graphicsLayer.buildLayer {
+ if (!isLayerRecorded) {
+ graphicsLayer.record {
drawRect(rectColor)
}
- isLayerBuilt = true
+ isLayerRecorded = true
}
onDrawWithContent {
drawRect(bgColor)
@@ -271,7 +270,7 @@
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- fun testBuildLayerDrawContent() {
+ fun testRecordDrawContent() {
val testTag = "TestTag"
val targetColor = Color.Blue
rule.setContent {
@@ -282,7 +281,7 @@
.size(40.dp)
.background(Color.Green)
.drawWithContent {
- layer.buildLayer {
+ layer.record {
this@drawWithContent.drawContent()
}
drawLayer(layer)
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawingPrebuiltGraphicsLayerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawingPrebuiltGraphicsLayerTest.kt
index c9f8888..a2ae451 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawingPrebuiltGraphicsLayerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawingPrebuiltGraphicsLayerTest.kt
@@ -365,7 +365,7 @@
layer: GraphicsLayer = obtainLayer()
): Modifier {
return drawWithContent {
- layer.buildLayer {
+ layer.record {
this@drawWithContent.drawContent()
}
drawLayer(layer)
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
index c523c91..f5f45b3 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/nestedscroll/NestedScrollModifierTest.kt
@@ -124,12 +124,12 @@
assertThat(counter).isEqualTo(0)
counter++
- childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.UserInput)
assertThat(counter).isEqualTo(2)
counter++
childDispatcher
- .dispatchPostScroll(scrollOffset, scrollLeftOffset, NestedScrollSource.Drag)
+ .dispatchPostScroll(scrollOffset, scrollLeftOffset, NestedScrollSource.UserInput)
assertThat(counter).isEqualTo(4)
counter++
@@ -175,7 +175,7 @@
assertThat(counter).isEqualTo(0)
counter++
- childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.UserInput)
assertThat(counter).isEqualTo(3)
counter++
}
@@ -224,7 +224,7 @@
counter++
childDispatcher
- .dispatchPostScroll(scrollOffset, scrollLeftOffset, NestedScrollSource.Drag)
+ .dispatchPostScroll(scrollOffset, scrollLeftOffset, NestedScrollSource.UserInput)
assertThat(counter).isEqualTo(3)
counter++
}
@@ -316,7 +316,7 @@
fun nestedScroll_twoNodes_hierarchyDispatch(): Unit = runBlocking {
val preScrollReturn = Offset(60f, 30f)
val preFlingReturn = Velocity(154f, 56f)
- var currentsource = NestedScrollSource.Drag
+ var currentsource = NestedScrollSource.UserInput
val childConnection = object : NestedScrollConnection {}
val parentConnection = object : NestedScrollConnection {
@@ -365,7 +365,7 @@
childDispatcher.dispatchPostScroll(scrollOffset, scrollLeftOffset, currentsource)
// flip to fling to test again below
- currentsource = NestedScrollSource.Fling
+ currentsource = NestedScrollSource.SideEffect
val preRes2 = childDispatcher.dispatchPreScroll(preScrollOffset, currentsource)
assertThat(preRes2).isEqualTo(preScrollReturn)
@@ -410,7 +410,7 @@
rule.runOnIdle {
val preRes =
- childDispatcher.dispatchPreScroll(dispatchedPreScroll, NestedScrollSource.Drag)
+ childDispatcher.dispatchPreScroll(dispatchedPreScroll, NestedScrollSource.UserInput)
assertThat(preRes).isEqualTo(grandParentConsumesPreScroll + parentConsumedPreScroll)
}
}
@@ -460,7 +460,7 @@
childDispatcher.dispatchPostScroll(
dispatchedConsumedScroll,
dispatchedScroll,
- NestedScrollSource.Drag
+ NestedScrollSource.UserInput
)
}
}
@@ -549,7 +549,7 @@
fun nestedScroll_twoNodes_flatDispatch(): Unit = runBlocking {
val preScrollReturn = Offset(60f, 30f)
val preFlingReturn = Velocity(154f, 56f)
- var currentsource = NestedScrollSource.Drag
+ var currentsource = NestedScrollSource.UserInput
val childConnection = object : NestedScrollConnection {}
val parentConnection = object : NestedScrollConnection {
@@ -599,7 +599,7 @@
childDispatcher.dispatchPostScroll(scrollOffset, scrollLeftOffset, currentsource)
// flip to fling to test again below
- currentsource = NestedScrollSource.Fling
+ currentsource = NestedScrollSource.SideEffect
val preRes2 = childDispatcher.dispatchPreScroll(preScrollOffset, currentsource)
assertThat(preRes2).isEqualTo(preScrollReturn)
@@ -649,9 +649,9 @@
}
}
- childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.UserInput)
childDispatcher
- .dispatchPostScroll(scrollOffset, scrollLeftOffset, NestedScrollSource.Fling)
+ .dispatchPostScroll(scrollOffset, scrollLeftOffset, NestedScrollSource.SideEffect)
childDispatcher.dispatchPreFling(preFling)
childDispatcher.dispatchPostFling(postFlingConsumed, postFlingLeft)
@@ -822,14 +822,14 @@
repeat(2) {
counter = 1
- childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.UserInput)
assertThat(counter).isEqualTo(3)
counter = 1
childDispatcher.dispatchPostScroll(
scrollOffset,
scrollLeftOffset,
- NestedScrollSource.Drag
+ NestedScrollSource.UserInput
)
assertThat(counter).isEqualTo(3)
counter = 1
@@ -962,14 +962,14 @@
repeat(2) {
counter = 1
- childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.UserInput)
assertThat(counter).isEqualTo(3)
counter = 1
childDispatcher.dispatchPostScroll(
scrollOffset,
scrollLeftOffset,
- NestedScrollSource.Drag
+ NestedScrollSource.UserInput
)
assertThat(counter).isEqualTo(3)
counter = 1
@@ -1118,14 +1118,14 @@
rule.runOnIdle {
val res =
- childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.UserInput)
assertThat(res).isEqualTo(rootParentPreConsumed + parentToRemovePreConsumed)
emitNewParent.value = false
}
rule.runOnIdle {
val res =
- childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.UserInput)
assertThat(res).isEqualTo(rootParentPreConsumed)
emitNewParent.value = true
@@ -1133,7 +1133,7 @@
rule.runOnIdle {
val res =
- childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.UserInput)
assertThat(res).isEqualTo(rootParentPreConsumed + parentToRemovePreConsumed)
}
}
@@ -1161,20 +1161,29 @@
}
rule.runOnIdle {
- val res = childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ val res = childDispatcher.dispatchPreScroll(
+ preScrollOffset,
+ NestedScrollSource.UserInput
+ )
assertThat(res).isEqualTo(preScrollReturn)
emitParentNestedScroll.value = false
}
rule.runOnIdle {
- val res = childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ val res = childDispatcher.dispatchPreScroll(
+ preScrollOffset,
+ NestedScrollSource.UserInput
+ )
assertThat(res).isEqualTo(Offset.Zero)
emitParentNestedScroll.value = true
}
rule.runOnIdle {
- val res = childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ val res = childDispatcher.dispatchPreScroll(
+ preScrollOffset,
+ NestedScrollSource.UserInput
+ )
assertThat(res).isEqualTo(preScrollReturn)
}
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index 67479b38..626fed9 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -251,6 +251,351 @@
assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
}
+ // Inserts a new Node at the top of an existing branch (tests removal of duplicate Nodes too).
+ @Test
+ fun addHitPath_dynamicNodeAddedTopPartiallyMatchingTreeWithOnePointerId_correctResult() {
+ val pif1 = PointerInputNodeMock()
+ val pif2 = PointerInputNodeMock()
+ val pif3 = PointerInputNodeMock()
+ val pif4 = PointerInputNodeMock()
+ val pifNew1 = PointerInputNodeMock()
+
+ val pointerId1 = PointerId(1)
+ hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
+ hitPathTracker.addHitPath(pointerId1, listOf(pifNew1, pif1, pif2, pif3, pif4))
+
+ val expectedRoot = NodeParent().apply {
+ children.add(
+ Node(pifNew1).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif1).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif2).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif3).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif4).apply {
+ pointerIds.add(pointerId1)
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
+ }
+
+ @Test
+ fun addHitPath_dynamicNodeAddedTopPartiallyMatchingTreeWithTwoPointerIds_correctResult() {
+ val pif1 = PointerInputNodeMock()
+ val pif2 = PointerInputNodeMock()
+ val pif3 = PointerInputNodeMock()
+ val pif4 = PointerInputNodeMock()
+ val pif5 = PointerInputNodeMock()
+ val pif6 = PointerInputNodeMock()
+ val pif7 = PointerInputNodeMock()
+ val pif8 = PointerInputNodeMock()
+
+ val pifNew1 = PointerInputNodeMock()
+
+ val pointerId1 = PointerId(1)
+ val pointerId2 = PointerId(2)
+
+ hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
+ hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
+
+ hitPathTracker.addHitPath(pointerId2, listOf(pifNew1, pif5, pif6, pif7, pif8))
+
+ val expectedRoot = NodeParent().apply {
+ children.add(
+ Node(pif1).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif2).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif3).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif4).apply {
+ pointerIds.add(pointerId1)
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+
+ children.add(
+ Node(pifNew1).apply {
+ pointerIds.add(pointerId2)
+ children.add(
+ Node(pif5).apply {
+ pointerIds.add(pointerId2)
+ children.add(
+ Node(pif6).apply {
+ pointerIds.add(pointerId2)
+ children.add(
+ Node(pif7).apply {
+ pointerIds.add(pointerId2)
+ children.add(
+ Node(pif8).apply {
+ pointerIds.add(pointerId2)
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
+ }
+
+ // Inserts a new Node inside an existing branch (tests removal of duplicate Nodes too).
+ @Test
+ fun addHitPath_dynamicNodeAddedInsidePartiallyMatchingTreeWithOnePointerId_correctResult() {
+ val pif1 = PointerInputNodeMock()
+ val pif2 = PointerInputNodeMock()
+ val pif3 = PointerInputNodeMock()
+ val pif4 = PointerInputNodeMock()
+ val pifNew1 = PointerInputNodeMock()
+
+ val pointerId1 = PointerId(1)
+ hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
+ hitPathTracker.addHitPath(pointerId1, listOf(pif1, pifNew1, pif2, pif3, pif4))
+
+ val expectedRoot = NodeParent().apply {
+ children.add(
+ Node(pif1).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pifNew1).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif2).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif3).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif4).apply {
+ pointerIds.add(pointerId1)
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
+ }
+
+ @Test
+ fun addHitPath_dynamicNodeAddedInsidePartiallyMatchingTreeWithTwoPointerIds_correctResult() {
+ val pif1 = PointerInputNodeMock()
+ val pif2 = PointerInputNodeMock()
+ val pif3 = PointerInputNodeMock()
+ val pif4 = PointerInputNodeMock()
+ val pif5 = PointerInputNodeMock()
+ val pif6 = PointerInputNodeMock()
+ val pif7 = PointerInputNodeMock()
+ val pif8 = PointerInputNodeMock()
+
+ val pifNew1 = PointerInputNodeMock()
+
+ val pointerId1 = PointerId(1)
+ val pointerId2 = PointerId(2)
+
+ hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
+ hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
+
+ hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pifNew1, pif7, pif8))
+
+ val expectedRoot = NodeParent().apply {
+ children.add(
+ Node(pif1).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif2).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif3).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif4).apply {
+ pointerIds.add(pointerId1)
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+
+ children.add(
+ Node(pif5).apply {
+ pointerIds.add(pointerId2)
+ children.add(
+ Node(pif6).apply {
+ pointerIds.add(pointerId2)
+ children.add(
+ Node(pifNew1).apply {
+ pointerIds.add(pointerId2)
+ children.add(
+ Node(pif7).apply {
+ pointerIds.add(pointerId2)
+ children.add(
+ Node(pif8).apply {
+ pointerIds.add(pointerId2)
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
+ }
+
+ // Inserts a Node in the bottom of an existing branch (tests removal of duplicate Nodes too).
+ @Test
+ fun addHitPath_dynamicNodeAddedBelowPartiallyMatchingTreeWithOnePointerId_correctResult() {
+ val pif1 = PointerInputNodeMock()
+ val pif2 = PointerInputNodeMock()
+ val pif3 = PointerInputNodeMock()
+ val pif4 = PointerInputNodeMock()
+ val pifNew1 = PointerInputNodeMock()
+
+ val pointerId1 = PointerId(1)
+ hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
+ hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4, pifNew1))
+
+ val expectedRoot = NodeParent().apply {
+ children.add(
+ Node(pif1).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif2).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif3).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif4).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pifNew1).apply {
+ pointerIds.add(pointerId1)
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
+ }
+
+ @Test
+ fun addHitPath_dynamicNodeAddedBelowPartiallyMatchingTreeWithTwoPointerIds_correctResult() {
+ val pif1 = PointerInputNodeMock()
+ val pif2 = PointerInputNodeMock()
+ val pif3 = PointerInputNodeMock()
+ val pif4 = PointerInputNodeMock()
+ val pif5 = PointerInputNodeMock()
+ val pif6 = PointerInputNodeMock()
+ val pif7 = PointerInputNodeMock()
+ val pif8 = PointerInputNodeMock()
+
+ val pifNew1 = PointerInputNodeMock()
+
+ val pointerId1 = PointerId(1)
+ val pointerId2 = PointerId(2)
+
+ hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
+ hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
+
+ hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8, pifNew1))
+
+ val expectedRoot = NodeParent().apply {
+ children.add(
+ Node(pif1).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif2).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif3).apply {
+ pointerIds.add(pointerId1)
+ children.add(
+ Node(pif4).apply {
+ pointerIds.add(pointerId1)
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+
+ children.add(
+ Node(pif5).apply {
+ pointerIds.add(pointerId2)
+ children.add(
+ Node(pif6).apply {
+ pointerIds.add(pointerId2)
+ children.add(
+ Node(pif7).apply {
+ pointerIds.add(pointerId2)
+ children.add(
+ Node(pif8).apply {
+ pointerIds.add(pointerId2)
+ children.add(
+ Node(pifNew1).apply {
+ pointerIds.add(pointerId2)
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
+ }
+
@Test
fun dispatchChanges_noNodes_doesNotCrash() {
hitPathTracker.dispatchChanges(internalPointerEventOf(down(0)))
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
index ad79089..31f0e27 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
@@ -930,6 +930,37 @@
.assertHeightIsEqualTo(10.dp)
}
+ @Test
+ fun pointerInput_badStartingSinglePointer_composeIgnores() {
+ val events = mutableListOf<PointerEventType>()
+ val tag = "input rect"
+ rule.setContent {
+ Box(
+ Modifier.fillMaxSize()
+ .testTag(tag)
+ .pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ events += event.type
+ }
+ }
+ }
+ )
+ }
+
+ rule.onNodeWithTag(tag).performTouchInput {
+ // Starts gestures with bad data. Because the bad x/y are part of the
+ // MotionEvent, Compose won't process the entire event or any following events
+ // until the bad data is removed.
+ down(Offset(Float.NaN, Float.NaN)) // Compose ignores
+ moveBy(0, Offset(10f, 10f)) // Compose ignores
+ up() // Compose ignores
+ }
+ assertThat(events).hasSize(0)
+ assertThat(events).containsExactly()
+ }
+
// Tests pointerInput with bad pointer data
@Test
fun pointerInput_badSinglePointer_composeIgnores() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/ApproachLayoutTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/ApproachLayoutTest.kt
index c1168d2..0f191a2 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/ApproachLayoutTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/ApproachLayoutTest.kt
@@ -18,16 +18,28 @@
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableStateOf
+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.Modifier.Node
+import androidx.compose.ui.background
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.LayoutCoordinatesStub
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
@@ -41,7 +53,9 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import kotlin.test.assertEquals
+import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -56,16 +70,16 @@
val excessiveAssertions = AndroidOwnerExtraAssertionsRule()
// Test that measurement approach has no effect on parent or child when
- // isMeasurementApproachComplete returns true
+ // isMeasurementApproachProgress returns false
@OptIn(ExperimentalComposeUiApi::class)
@Test
- fun toggleIsMeasurementApproachComplete() {
+ fun toggleIsMeasurementApproachInProgress() {
var isComplete by mutableStateOf(true)
var parentLookaheadSize = IntSize(-1, -1)
var childLookaheadConstraints: Constraints? = null
var childLookaheadSize = IntSize(-1, -1)
// This fraction change triggers a lookahead pass, which will be required to
- // do a `isMeasurementApproachComplete` after its prior completion.
+ // do a `isMeasurementApproachInProgress` after its prior completion.
var fraction by mutableStateOf(0.5f)
var lookaheadPositionInParent = androidx.compose.ui.geometry.Offset(Float.NaN, Float.NaN)
rule.setContent {
@@ -92,7 +106,7 @@
}
}
.approachLayout(
- isMeasurementApproachComplete = { isComplete }
+ isMeasurementApproachInProgress = { !isComplete }
) { measurable, _ ->
// Intentionally use different constraints, placement and report different
// measure result than lookahead, to verify that they have no effect on
@@ -202,8 +216,8 @@
}
}
.approachLayout(
- isMeasurementApproachComplete = { isMeasurementApproachComplete },
- isPlacementApproachComplete = { isPlacementApproachComplete }
+ isMeasurementApproachInProgress = { !isMeasurementApproachComplete },
+ isPlacementApproachInProgress = { !isPlacementApproachComplete }
) { measurable, _ ->
// Intentionally use different constraints, placement and report different
// measure result than lookahead, to verify that they have no effect on
@@ -322,8 +336,8 @@
var childLookaheadSize: IntSize? = null
var childApproachSize: IntSize? = null
val parentApproachNode = object : TestApproachLayoutModifierNode() {
- override fun isMeasurementApproachComplete(lookaheadSize: IntSize): Boolean {
- return parentMeasureApproachComplete
+ override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
+ return !parentMeasureApproachComplete
}
@ExperimentalComposeUiApi
@@ -380,7 +394,7 @@
}
}
}
- .approachLayout({ childMeasureApproachComplete }) { m, _ ->
+ .approachLayout({ !childMeasureApproachComplete }) { m, _ ->
m
.measure(Constraints.fixed(500, 500))
.run {
@@ -458,8 +472,8 @@
var childLookaheadSize: IntSize? = null
var childApproachSize: IntSize? = null
val parentApproachNode = object : TestApproachLayoutModifierNode() {
- override fun isMeasurementApproachComplete(lookaheadSize: IntSize): Boolean {
- return parentMeasureApproachComplete
+ override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
+ return !parentMeasureApproachComplete
}
@ExperimentalComposeUiApi
@@ -516,7 +530,7 @@
}
}
}
- .approachLayout({ childMeasureApproachComplete }) { m, _ ->
+ .approachLayout({ !childMeasureApproachComplete }) { m, _ ->
m
.measure(Constraints.fixed(500, 500))
.run {
@@ -584,8 +598,8 @@
fun testDefaultPlacementApproachComplete() {
var measurementComplete = true
val node = object : ApproachLayoutModifierNode {
- override fun isMeasurementApproachComplete(lookaheadSize: IntSize): Boolean {
- return measurementComplete
+ override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
+ return !measurementComplete
}
@ExperimentalComposeUiApi
@@ -603,23 +617,23 @@
override val node: Node = object : Node() {}
}
- assertEquals(true, node.isMeasurementApproachComplete(IntSize.Zero))
+ assertFalse(node.isMeasurementApproachInProgress(IntSize.Zero))
with(TestPlacementScope()) {
with(node) {
- isPlacementApproachComplete(LayoutCoordinatesStub())
+ isPlacementApproachInProgress(LayoutCoordinatesStub())
}
}.also {
- assertEquals(true, it)
+ assertFalse(it)
}
measurementComplete = false
- assertEquals(false, node.isMeasurementApproachComplete(IntSize.Zero))
+ assertTrue(node.isMeasurementApproachInProgress(IntSize.Zero))
with(TestPlacementScope()) {
with(node) {
- isPlacementApproachComplete(LayoutCoordinatesStub())
+ isPlacementApproachInProgress(LayoutCoordinatesStub())
}
}.also {
- assertEquals(true, it)
+ assertFalse(it)
}
}
@@ -632,14 +646,14 @@
var measureWithFixedConstraints by mutableStateOf(false)
var removeChild by mutableStateOf(false)
val parentNode = object : TestApproachLayoutModifierNode() {
- override fun isMeasurementApproachComplete(lookaheadSize: IntSize): Boolean {
- return false
+ override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
+ return true
}
- override fun Placeable.PlacementScope.isPlacementApproachComplete(
+ override fun Placeable.PlacementScope.isPlacementApproachInProgress(
lookaheadCoordinates: LayoutCoordinates
): Boolean {
- return true
+ return false
}
@ExperimentalComposeUiApi
@@ -661,14 +675,14 @@
}
val childNode = object : TestApproachLayoutModifierNode() {
- override fun isMeasurementApproachComplete(lookaheadSize: IntSize): Boolean {
- return true
+ override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
+ return false
}
- override fun Placeable.PlacementScope.isPlacementApproachComplete(
+ override fun Placeable.PlacementScope.isPlacementApproachInProgress(
lookaheadCoordinates: LayoutCoordinates
): Boolean {
- return true
+ return false
}
@ExperimentalComposeUiApi
@@ -718,6 +732,126 @@
}
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Test
+ fun testIsApproachCompleteCalledWhenSiblingRemovedInScroll() {
+ var isInColumn by mutableStateOf(false)
+
+ var lastTargetPosition by mutableStateOf(Offset.Zero)
+ var lastPosition by mutableStateOf(Offset.Zero)
+
+ rule.setContent {
+ CompositionLocalProvider(LocalDensity provides Density(1f)) {
+ val movableContent = remember {
+ movableContentOf {
+ Box(
+ Modifier
+ .let {
+ if (isInColumn) {
+ // Same layout as all other boxes in the Column
+ it
+ .height(100.dp)
+ .fillMaxWidth()
+ } else {
+ it.size(50.dp)
+ }
+ }
+ )
+ }
+ }
+
+ LookaheadScope {
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .height(300.dp)
+ ) {
+ Column(
+ Modifier
+ .fillMaxSize()
+ // Scroll is part of the trigger that skips ApproachLayout
+ .verticalScroll(rememberScrollState(0))
+ ) {
+ // First, fixed box.
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .height(100.dp)
+ )
+ // Second box is movableContent if `isInColumn` is `true`
+ if (isInColumn) {
+ movableContent()
+ }
+
+ // Last box. We keep track of its ApproachLayout placement callbacks.
+ // It should receive an isPlacementIsComplete as part of its post
+ // lookahead pass whenever the state changes and the movableContent is
+ // placed as a sibling or the secondary slot.
+ Box(
+ Modifier
+ .approachLayout(
+ isMeasurementApproachInProgress = {
+ return@approachLayout false
+ },
+ isPlacementApproachInProgress = { coordinates ->
+ lastTargetPosition =
+ lookaheadScopeCoordinates.localLookaheadPositionOf(
+ coordinates
+ )
+ return@approachLayout false
+ },
+ approachMeasure = { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ )
+ .onPlaced {
+ // Also consume the coordinates here, this is necessary to
+ // trigger the right access flags
+ lastPosition = it.positionInParent()
+ }
+ .padding(10.dp)
+ .background(Color.Cyan)
+ .height(100.dp)
+ .fillMaxWidth()
+ )
+ }
+ // Secondary slot - when not in Column
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.BottomEnd
+ ) {
+ if (!isInColumn) {
+ movableContent()
+ }
+ }
+ }
+ }
+ }
+ }
+ rule.runOnIdle {
+ // Second item in column
+ assertEquals(100f, lastTargetPosition.y)
+ assertEquals(100f, lastPosition.y)
+ }
+
+ isInColumn = true
+ rule.runOnIdle {
+ // Third item in column
+ assertEquals(200f, lastTargetPosition.y)
+ assertEquals(200f, lastPosition.y)
+ }
+
+ isInColumn = false
+ rule.runOnIdle {
+ // Second item in column
+ assertEquals(100f, lastTargetPosition.y)
+ assertEquals(100f, lastPosition.y)
+ }
+ }
+
private class TestPlacementScope : Placeable.PlacementScope() {
override val parentWidth: Int
get() = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
index 9157baad..1255231 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
@@ -211,10 +211,10 @@
Modifier
.requiredSize(targetSize.width.dp, targetSize.height.dp)
.createIntermediateElement(object : TestApproachLayoutModifierNode() {
- override fun isMeasurementApproachComplete(
+ override fun isMeasurementApproachInProgress(
lookaheadSize: IntSize
): Boolean {
- return iteration > 6
+ return iteration <= 6
}
@ExperimentalComposeUiApi
@@ -428,10 +428,10 @@
.createIntermediateElement(object :
TestApproachLayoutModifierNode() {
- override fun isMeasurementApproachComplete(
+ override fun isMeasurementApproachInProgress(
lookaheadSize: IntSize
): Boolean {
- return rootPostPlace >= 12
+ return rootPostPlace < 12
}
@ExperimentalComposeUiApi
@@ -471,10 +471,10 @@
.createIntermediateElement(object :
TestApproachLayoutModifierNode() {
- override fun isMeasurementApproachComplete(
+ override fun isMeasurementApproachInProgress(
lookaheadSize: IntSize
): Boolean {
- return rootPostPlace >= 12
+ return rootPostPlace < 12
}
@ExperimentalComposeUiApi
@@ -556,7 +556,7 @@
MyLookaheadLayout {
Box(modifier = Modifier
.approachLayout(
- isMeasurementApproachComplete = { scaleFactor > 3f }
+ isMeasurementApproachInProgress = { scaleFactor <= 3f }
) { measurable, constraints ->
assertEquals(width, lookaheadSize.width)
assertEquals(height, lookaheadSize.height)
@@ -2777,8 +2777,8 @@
with(scope) {
this@composed
.createIntermediateElement(object : TestApproachLayoutModifierNode() {
- override fun isMeasurementApproachComplete(lookaheadSize: IntSize): Boolean {
- return true
+ override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
+ return false
}
@ExperimentalComposeUiApi
@@ -2892,7 +2892,7 @@
measurable: Measurable,
constraints: Constraints,
) -> MeasureResult,
- ): Modifier = approachLayout({ testFinished }, approachMeasure = measure)
+ ): Modifier = approachLayout({ !testFinished }, approachMeasure = measure)
}
private fun Modifier.createIntermediateElement(node: TestApproachLayoutModifierNode): Modifier =
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt
index 2e93bb2..f1e4df3 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt
@@ -833,7 +833,7 @@
Box(
Modifier
.approachLayout({
- intermediateLayoutBlockCalls > 20
+ intermediateLayoutBlockCalls <= 20
}) { measurable, constraints ->
val p = measurable.measure(constraints)
layout(p.width, p.height) {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureDrawTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureDrawTest.kt
index 88b2c1c..67fe52c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureDrawTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureDrawTest.kt
@@ -66,7 +66,7 @@
}
@Test
- fun capture_drawsScrollContents_withCaptureHeight1px() {
+ fun capture_drawsScrollContents_fromTop_withCaptureHeight1px() = captureTester.runTest {
val scrollState = ScrollState(0)
captureTester.setContent {
TestContent(scrollState)
@@ -82,14 +82,33 @@
}
@Test
- fun capture_drawsScrollContents_withCaptureHeightFullViewport() {
+ fun capture_drawsScrollContents_fromTop_withCaptureHeightFullViewport() =
+ captureTester.runTest {
+ val scrollState = ScrollState(0)
+ captureTester.setContent {
+ TestContent(scrollState)
+ }
+ val target = captureTester.findCaptureTargets().single()
+ val bitmaps = captureTester.captureBitmapsVertically(target, captureHeight = 10)
+ assertThat(bitmaps).hasSize(3)
+ bitmaps.joinVerticallyToBitmap().use { joined ->
+ joined.assertRect(Rect(0, 0, 10, 9), Color.Red)
+ joined.assertRect(Rect(0, 10, 10, 18), Color.Blue)
+ joined.assertRect(Rect(0, 19, 10, 27), Color.Green)
+ }
+ }
+
+ @Test
+ fun capture_drawsScrollContents_fromMiddle_withCaptureHeight1px() = captureTester.runTest {
val scrollState = ScrollState(0)
captureTester.setContent {
TestContent(scrollState)
}
+
+ scrollState.scrollTo(scrollState.maxValue / 2)
+
val target = captureTester.findCaptureTargets().single()
- val bitmaps = captureTester.captureBitmapsVertically(target, captureHeight = 10)
- assertThat(bitmaps).hasSize(3)
+ val bitmaps = captureTester.captureBitmapsVertically(target, captureHeight = 1)
bitmaps.joinVerticallyToBitmap().use { joined ->
joined.assertRect(Rect(0, 0, 10, 9), Color.Red)
joined.assertRect(Rect(0, 10, 10, 18), Color.Blue)
@@ -98,7 +117,65 @@
}
@Test
- fun capture_resetsScrollPosition_from0() {
+ fun capture_drawsScrollContents_fromMiddle_withCaptureHeightFullViewport() =
+ captureTester.runTest {
+ val scrollState = ScrollState(0)
+ captureTester.setContent {
+ TestContent(scrollState)
+ }
+
+ scrollState.scrollTo(scrollState.maxValue / 2)
+
+ val target = captureTester.findCaptureTargets().single()
+ val bitmaps = captureTester.captureBitmapsVertically(target, captureHeight = 10)
+ assertThat(bitmaps).hasSize(3)
+ bitmaps.joinVerticallyToBitmap().use { joined ->
+ joined.assertRect(Rect(0, 0, 10, 9), Color.Red)
+ joined.assertRect(Rect(0, 10, 10, 18), Color.Blue)
+ joined.assertRect(Rect(0, 19, 10, 27), Color.Green)
+ }
+ }
+
+ @Test
+ fun capture_drawsScrollContents_fromBottom_withCaptureHeight1px() = captureTester.runTest {
+ val scrollState = ScrollState(0)
+ captureTester.setContent {
+ TestContent(scrollState)
+ }
+
+ scrollState.scrollTo(scrollState.maxValue)
+
+ val target = captureTester.findCaptureTargets().single()
+ val bitmaps = captureTester.captureBitmapsVertically(target, captureHeight = 1)
+ bitmaps.joinVerticallyToBitmap().use { joined ->
+ joined.assertRect(Rect(0, 0, 10, 9), Color.Red)
+ joined.assertRect(Rect(0, 10, 10, 18), Color.Blue)
+ joined.assertRect(Rect(0, 19, 10, 27), Color.Green)
+ }
+ }
+
+ @Test
+ fun capture_drawsScrollContents_fromBottom_withCaptureHeightFullViewport() =
+ captureTester.runTest {
+ val scrollState = ScrollState(0)
+ captureTester.setContent {
+ TestContent(scrollState)
+ }
+
+ scrollState.scrollTo(scrollState.maxValue)
+
+ val target = captureTester.findCaptureTargets().single()
+ val bitmaps = captureTester.captureBitmapsVertically(target, captureHeight = 10)
+ assertThat(bitmaps).hasSize(3)
+ bitmaps.joinVerticallyToBitmap().use { joined ->
+ joined.assertRect(Rect(0, 0, 10, 9), Color.Red)
+ joined.assertRect(Rect(0, 10, 10, 18), Color.Blue)
+ joined.assertRect(Rect(0, 19, 10, 27), Color.Green)
+ }
+ }
+
+ @Test
+ fun capture_resetsScrollPosition_from0() = captureTester.runTest {
val scrollState = ScrollState(0)
captureTester.setContent {
TestContent(scrollState)
@@ -106,13 +183,12 @@
val target = captureTester.findCaptureTargets().single()
val bitmaps = captureTester.captureBitmapsVertically(target, captureHeight = 10)
bitmaps.forEach { it.recycle() }
- rule.runOnIdle {
- assertThat(scrollState.value).isEqualTo(0)
- }
+ rule.awaitIdle()
+ assertThat(scrollState.value).isEqualTo(0)
}
@Test
- fun capture_resetsScrollPosition_fromNonZero() {
+ fun capture_resetsScrollPosition_fromNonZero() = captureTester.runTest {
val scrollState = ScrollState(5)
captureTester.setContent {
TestContent(scrollState)
@@ -120,9 +196,8 @@
val target = captureTester.findCaptureTargets().single()
val bitmaps = captureTester.captureBitmapsVertically(target, captureHeight = 10)
bitmaps.forEach { it.recycle() }
- rule.runOnIdle {
- assertThat(scrollState.value).isEqualTo(5)
- }
+ rule.awaitIdle()
+ assertThat(scrollState.value).isEqualTo(5)
}
@Composable
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureIntegrationTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureIntegrationTest.kt
index 9ee0de8..9981268 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureIntegrationTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureIntegrationTest.kt
@@ -71,7 +71,7 @@
}
@Test
- fun search_finds_verticalScrollModifier() {
+ fun search_finds_verticalScrollModifier() = captureTester.runTest {
captureTester.setContent {
with(LocalDensity.current) {
Box(
@@ -92,7 +92,7 @@
}
@Test
- fun search_doesNotFind_horizontalScrollModifier() {
+ fun search_doesNotFind_horizontalScrollModifier() = captureTester.runTest {
captureTester.setContent {
with(LocalDensity.current) {
Box(
@@ -110,7 +110,7 @@
}
@Test
- fun search_finds_LazyColumn() {
+ fun search_finds_LazyColumn() = captureTester.runTest {
captureTester.setContent {
with(LocalDensity.current) {
LazyColumn(Modifier.size(10.toDp())) {
@@ -129,7 +129,7 @@
}
@Test
- fun search_doesNotFind_LazyRow() {
+ fun search_doesNotFind_LazyRow() = captureTester.runTest {
captureTester.setContent {
with(LocalDensity.current) {
LazyRow(Modifier.size(10.toDp())) {
@@ -145,7 +145,7 @@
}
@Test
- fun search_finds_LazyVerticalGrid() {
+ fun search_finds_LazyVerticalGrid() = captureTester.runTest {
captureTester.setContent {
with(LocalDensity.current) {
LazyVerticalGrid(
@@ -165,7 +165,7 @@
}
@Test
- fun search_finds_LazyVerticalStaggeredGrid() {
+ fun search_finds_LazyVerticalStaggeredGrid() = captureTester.runTest {
captureTester.setContent {
with(LocalDensity.current) {
LazyVerticalStaggeredGrid(
@@ -185,7 +185,7 @@
}
@Test
- fun search_doesNotFind_TextField1_singleLine() {
+ fun search_doesNotFind_TextField1_singleLine() = captureTester.runTest {
captureTester.setContent {
BasicTextField(
"really long value to ensure that the field will scroll horizontally",
@@ -200,7 +200,7 @@
}
@Test
- fun search_doesNotFind_TextField1_multiLine_scrollable() {
+ fun search_doesNotFind_TextField1_multiLine_scrollable() = captureTester.runTest {
captureTester.setContent {
BasicTextField(
"lots\n\nof\n\nnewlines\n\nto\n\nmake\n\nvertically\n\nscrollable",
@@ -215,7 +215,7 @@
}
@Test
- fun search_doesNotFind_TextField2_singleLine() {
+ fun search_doesNotFind_TextField2_singleLine() = captureTester.runTest {
val state =
TextFieldState("really long value to ensure that the field will scroll horizontally")
captureTester.setContent {
@@ -231,7 +231,7 @@
}
@Test
- fun search_doesNotFind_TextField2_multiLine_scrollable() {
+ fun search_doesNotFind_TextField2_multiLine_scrollable() = captureTester.runTest {
val state =
TextFieldState("lots\n\nof\n\nnewlines\n\nto\n\nmake\n\nvertically\n\nscrollable")
captureTester.setContent {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTest.kt
index bb4d86f..8a470b2 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTest.kt
@@ -16,10 +16,7 @@
package androidx.compose.ui.scrollcapture
-import android.graphics.Canvas
import android.graphics.Rect
-import android.view.ScrollCaptureSession
-import android.view.Surface
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@@ -48,23 +45,12 @@
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import kotlin.math.roundToInt
-import kotlin.test.fail
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.async
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.currentCoroutineContext
-import kotlinx.coroutines.job
import kotlinx.coroutines.launch
-import kotlinx.coroutines.selects.onTimeout
-import kotlinx.coroutines.selects.select
-import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.kotlin.mock
/**
* Tests the scroll capture implementation's integration with semantics. Tests in this class should
@@ -94,7 +80,7 @@
}
@Test
- fun search_findsScrollableTarget() {
+ fun search_findsScrollableTarget() = captureTester.runTest {
lateinit var coordinates: LayoutCoordinates
captureTester.setContent {
TestVerticalScrollable(
@@ -115,7 +101,7 @@
}
@Test
- fun search_usesTargetsCoordinates() {
+ fun search_usesTargetsCoordinates() = captureTester.runTest {
lateinit var coordinates: LayoutCoordinates
val padding = 15
captureTester.setContent {
@@ -147,7 +133,7 @@
}
@Test
- fun search_findsLargestTarget_whenMultipleMatches() {
+ fun search_findsLargestTarget_whenMultipleMatches() = captureTester.runTest {
val smallerSize = 10
val largerSize = 11
captureTester.setContent {
@@ -165,7 +151,7 @@
}
@Test
- fun search_findsDeepestTarget() {
+ fun search_findsDeepestTarget() = captureTester.runTest {
captureTester.setContent {
TestVerticalScrollable(size = 11) {
TestVerticalScrollable(size = 10)
@@ -179,7 +165,7 @@
}
@Test
- fun search_findsDeepestTarget_whenLargerParentSibling() {
+ fun search_findsDeepestTarget_whenLargerParentSibling() = captureTester.runTest {
captureTester.setContent {
Column {
TestVerticalScrollable(size = 10) {
@@ -196,7 +182,7 @@
}
@Test
- fun search_findsDeepestLargestTarget_whenMultipleMatches() {
+ fun search_findsDeepestLargestTarget_whenMultipleMatches() = captureTester.runTest {
captureTester.setContent {
Column {
TestVerticalScrollable(size = 10) {
@@ -215,7 +201,7 @@
}
@Test
- fun search_usesClippedSize() {
+ fun search_usesClippedSize() = captureTester.runTest {
captureTester.setContent {
TestVerticalScrollable(size = 10) {
TestVerticalScrollable(size = 100)
@@ -229,7 +215,7 @@
}
@Test
- fun search_doesNotFindTarget_whenFeatureFlagDisabled() {
+ fun search_doesNotFindTarget_whenFeatureFlagDisabled() = captureTester.runTest {
@Suppress("DEPRECATION")
ComposeFeatureFlag_LongScreenshotsEnabled = false
@@ -242,10 +228,7 @@
}
@Test
- fun search_doesNotFindTarget_whenInvisibleToUser() {
- @Suppress("DEPRECATION")
- ComposeFeatureFlag_LongScreenshotsEnabled = false
-
+ fun search_doesNotFindTarget_whenInvisibleToUser() = captureTester.runTest {
captureTester.setContent {
TestVerticalScrollable(Modifier.semantics {
invisibleToUser()
@@ -257,10 +240,7 @@
}
@Test
- fun search_doesNotFindTarget_whenZeroSize() {
- @Suppress("DEPRECATION")
- ComposeFeatureFlag_LongScreenshotsEnabled = false
-
+ fun search_doesNotFindTarget_whenZeroSize() = captureTester.runTest {
captureTester.setContent {
TestVerticalScrollable(Modifier.size(0.dp))
}
@@ -270,7 +250,7 @@
}
@Test
- fun search_doesNotFindTarget_whenZeroMaxValue() {
+ fun search_doesNotFindTarget_whenZeroMaxValue() = captureTester.runTest {
captureTester.setContent {
TestVerticalScrollable(maxValue = 0f)
}
@@ -280,7 +260,7 @@
}
@Test
- fun search_doesNotFindTarget_whenNoScrollAxisRange() {
+ fun search_doesNotFindTarget_whenNoScrollAxisRange() = captureTester.runTest {
captureTester.setContent {
Box(
Modifier
@@ -296,7 +276,7 @@
}
@Test
- fun search_doesNotFindTarget_whenNoVerticalScrollAxisRange() {
+ fun search_doesNotFindTarget_whenNoVerticalScrollAxisRange() = captureTester.runTest {
captureTester.setContent {
Box(
Modifier
@@ -316,7 +296,7 @@
}
@Test
- fun search_doesNotFindTarget_whenNoScrollByImmediately() {
+ fun search_doesNotFindTarget_whenNoScrollByImmediately() = captureTester.runTest {
captureTester.setContent {
Box(
Modifier
@@ -335,7 +315,7 @@
}
@Test
- fun callbackOnSearch_returnsViewportBounds() = runTest {
+ fun callbackOnSearch_returnsViewportBounds() = captureTester.runTest {
lateinit var coordinates: LayoutCoordinates
val padding = 15
captureTester.setContent {
@@ -367,104 +347,50 @@
}
}
- // TODO this is flaky, figure out why
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
- fun callbackOnImageCapture_scrollsBackwardsThenForwards() = runTest {
- data class ScrollRequest(
- val requestedOffset: Offset,
- val consumeScroll: (Offset) -> Unit
- )
-
- val scrollRequests = Channel<ScrollRequest>(capacity = Channel.RENDEZVOUS)
- suspend fun expectScrollRequest(expectedOffset: Offset, consume: Offset = expectedOffset) {
- val request = select {
- scrollRequests.onReceive { it }
- onTimeout(1000) { fail("No scroll request received after 1000ms") }
+ fun callbackOnImageCapture_scrollsBackwardsThenForwards() = captureTester.runTest {
+ expectingScrolls(rule) {
+ val size = 10
+ val captureHeight = size / 2
+ captureTester.setContent {
+ TestVerticalScrollable(
+ size = size,
+ // Can't be a reference, see https://youtrack.jetbrains.com/issue/KT-49665
+ onScrollByOffset = { respondToScrollExpectation(it) }
+ )
}
- assertThat(request.requestedOffset).isEqualTo(expectedOffset)
- request.consumeScroll(consume)
- // Allow the scroll request to be consumed.
- rule.awaitIdle()
- }
- suspend fun expectNoScrollRequests() {
- rule.awaitIdle()
- if (!scrollRequests.isEmpty) {
- val requests = buildList {
- do {
- val request = scrollRequests.tryReceive()
- request.getOrNull()?.let(::add)
- } while (request.isSuccess)
- }
- fail("Expected no scroll requests, but had ${requests.size}: " +
- requests.joinToString { it.requestedOffset.toString() })
+ val target = captureTester.findCaptureTargets().single()
+ captureTester.capture(target, captureHeight) {
+ // First request is at origin, no scrolling required.
+ assertThat(performCaptureDiscardingBitmap()).isEqualTo(Rect(0, 0, 10, 5))
+ assertNoPendingScrollRequests()
+
+ // Back one half-page, but only respond to part of it.
+ expectScrollRequest(Offset(0f, -5f), consume = Offset(0f, -4f))
+ shiftWindowBy(-5)
+ assertThat(performCaptureDiscardingBitmap()).isEqualTo(Rect(0, -4, 10, 0))
+
+ // Forward one half-page – already in viewport, no scrolling required.
+ shiftWindowBy(5)
+ assertThat(performCaptureDiscardingBitmap()).isEqualTo(Rect(0, 0, 10, 5))
+ assertNoPendingScrollRequests()
+
+ // Forward another half-page. This time we need to scroll.
+ expectScrollRequest(Offset(0f, 4f))
+ shiftWindowBy(5)
+ assertThat(performCaptureDiscardingBitmap()).isEqualTo(Rect(0, 5, 10, 10))
+
+ // Forward another half-page, scroll again so now we're past the original viewport.
+ expectScrollRequest(Offset(0f, 5f))
+ shiftWindowBy(5)
+ assertThat(performCaptureDiscardingBitmap()).isEqualTo(Rect(0, 10, 10, 15))
+
+ // When capture ends expect one last scroll request to reset to original offset.
+ // Note that this request will be made _after_ this capture{} lambda returns.
+ expectScrollRequest(Offset(0f, -5f))
}
- }
-
- val size = 10
- captureTester.setContent {
- TestVerticalScrollable(
- size = size,
- onScrollByImmediately = { offset ->
- val result = CompletableDeferred<Offset>(parent = currentCoroutineContext().job)
- scrollRequests.send(ScrollRequest(offset, consumeScroll = result::complete))
- result.await()
- }
- )
- }
-
- val callback = captureTester.findCaptureTargets().single().callback
- val canvas = mock<Canvas>()
- val surface = mock<Surface> {
- on(it.lockHardwareCanvas()).thenReturn(canvas)
- }
- val session = mock<ScrollCaptureSession> {
- on(it.surface).thenReturn(surface)
- }
-
- launch {
- callback.onScrollCaptureStart(session)
-
- // First request is at origin, no scrolling required.
- async { callback.onScrollCaptureImageRequest(session, Rect(0, 0, 10, 10)) }
- .let { captureResult ->
- expectNoScrollRequests()
- assertThat(captureResult.await()).isEqualTo(Rect(0, 0, 10, 10))
- }
-
- // Back one half-page, but only respond to part of it.
- async { callback.onScrollCaptureImageRequest(session, Rect(0, -5, 10, 0)) }
- .let { captureResult ->
- expectScrollRequest(Offset(0f, -5f), consume = Offset(0f, -4f))
- assertThat(captureResult.await()).isEqualTo(Rect(0, -4, 10, 0))
- }
-
- // Forward one half-page – already in viewport, no scrolling required.
- async { callback.onScrollCaptureImageRequest(session, Rect(0, 0, 10, 5)) }
- .let { captureResult ->
- expectNoScrollRequests()
- assertThat(captureResult.await()).isEqualTo(Rect(0, 0, 10, 5))
- }
-
- // Forward another half-page. This time we need to scroll.
- async { callback.onScrollCaptureImageRequest(session, Rect(0, 5, 10, 10)) }
- .let { captureResult ->
- expectScrollRequest(Offset(0f, 4f))
- assertThat(captureResult.await()).isEqualTo(Rect(0, 5, 10, 10))
- }
-
- // Forward another half-page, scroll again so now we're past the original viewport.
- async { callback.onScrollCaptureImageRequest(session, Rect(0, 10, 10, 15)) }
- .let { captureResult ->
- expectScrollRequest(Offset(0f, 5f))
- assertThat(captureResult.await()).isEqualTo(Rect(0, 10, 10, 15))
- }
-
- launch { callback.onScrollCaptureEnd() }
- // One last scroll request to reset to original offset.
- expectScrollRequest(Offset(0f, -5f))
- expectNoScrollRequests()
+ assertNoPendingScrollRequests()
}
}
@@ -476,7 +402,7 @@
modifier: Modifier = Modifier,
size: Int = 10,
maxValue: Float = 1f,
- onScrollByImmediately: suspend (Offset) -> Offset = { Offset.Zero },
+ onScrollByOffset: suspend (Offset) -> Offset = { Offset.Zero },
content: (@Composable () -> Unit)? = null
) {
with(LocalDensity.current) {
@@ -492,10 +418,15 @@
.size(size.toDp())
.semantics {
verticalScrollAxisRange = scrollAxisRange
- scrollByOffset(onScrollByImmediately)
+ scrollByOffset(onScrollByOffset)
},
content = { content?.invoke() }
)
}
}
+
+ private suspend fun ScrollCaptureTester.CaptureSessionScope.performCaptureDiscardingBitmap() =
+ performCapture()
+ .also { it.bitmap?.recycle() }
+ .capturedRect
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTester.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTester.kt
index 8c75dc6..8ee57ed 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTester.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollCaptureTester.kt
@@ -36,13 +36,14 @@
import android.view.View
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.internal.checkPreconditionNotNull
import androidx.compose.ui.internal.requirePrecondition
import androidx.compose.ui.platform.AndroidComposeView
+import androidx.compose.ui.platform.AndroidUiDispatcher
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import java.util.concurrent.CountDownLatch
import kotlin.coroutines.resume
import kotlin.math.roundToInt
import kotlin.test.fail
@@ -59,83 +60,100 @@
import kotlinx.coroutines.selects.onTimeout
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
/**
* Helps tests pretend to be the Android platform performing scroll capture search and image
- * capture. Tests must call [setContent] on this class instead of on [rule].
+ * capture. Tests must call [setContent] on this class instead of on [rule], and the entire test
+ * should be run in the coroutine started by the [runTest] method on this class.
*/
@RequiresApi(31)
class ScrollCaptureTester(private val rule: ComposeContentTestRule) {
+
+ interface CaptureSessionScope {
+ val windowHeight: Int
+
+ suspend fun performCapture(): CaptureResult
+ fun shiftWindowBy(offset: Int)
+ }
+
+ class CaptureResult(
+ val bitmap: Bitmap?,
+ val capturedRect: Rect
+ )
+
private var view: View? = null
- private var coroutineScope: CoroutineScope? = null
fun setContent(content: @Composable () -> Unit) {
rule.setContent {
this.view = LocalView.current
- this.coroutineScope = rememberCoroutineScope()
content()
}
}
/**
+ * Workaround for standard kotlin runTest because it deadlocks when a composition coroutine
+ * calls `delay()`.
+ */
+ fun runTest(timeoutMillis: Long = 5_000, block: suspend CoroutineScope.() -> Unit) {
+ val scope = CoroutineScope(AndroidUiDispatcher.Main)
+ val latch = CountDownLatch(1)
+ var result: Result<Unit>? = null
+ scope.launch {
+ result = runCatching {
+ block()
+ }
+ latch.countDown()
+ }
+ rule.waitUntil("Test coroutine completed", timeoutMillis) { result != null }
+ return result!!.getOrThrow()
+ }
+
+ /**
* Calls [View.onScrollCaptureSearch] on the Compose host view, which searches the composition
* from [setContent] for scroll containers, and returns all the [ScrollCaptureTarget]s produced
* that would be given to the platform in production.
*/
- fun findCaptureTargets(): List<ScrollCaptureTarget> = rule.runOnIdle {
- val view = checkNotNull(view as? AndroidComposeView) {
- "Must call setContent on ScrollCaptureTester before capturing."
+ suspend fun findCaptureTargets(): List<ScrollCaptureTarget> {
+ rule.awaitIdle()
+ return withContext(AndroidUiDispatcher.Main) {
+ val view = checkNotNull(view as? AndroidComposeView) {
+ "Must call setContent on ScrollCaptureTester before capturing."
+ }
+ val localVisibleRect = Rect().also(view::getLocalVisibleRect)
+ val windowOffset = view.calculatePositionInWindow(Offset.Zero).roundToPoint()
+ val targets = mutableListOf<ScrollCaptureTarget>()
+ view.onScrollCaptureSearch(localVisibleRect, windowOffset, targets::add)
+ targets
}
- val localVisibleRect = Rect().also(view::getLocalVisibleRect)
- val windowOffset = view.calculatePositionInWindow(Offset.Zero).roundToPoint()
- val targets = mutableListOf<ScrollCaptureTarget>()
- view.onScrollCaptureSearch(localVisibleRect, windowOffset, targets::add)
- targets
}
/**
- * Emulates (roughly) how the platform interacts with [ScrollCaptureCallback] to iteratively
- * assemble a screenshot of the entire contents of the [target]. Unlike the platform, this
- * method will not limit itself to a certain size, it always captures the entire scroll
- * contents, so tests should make sure to use small enough scroll contents or the test might
- * run out of memory.
+ * Runs a capture session. [block] should call methods on [CaptureSessionScope] to incrementally
+ * capture bitmaps of [target].
*
- * @param captureHeight The height of the capture window. Must not be greater than viewport
+ * @param captureWindowHeight The height of the capture window. Must not be greater than viewport
* height.
*/
- fun captureBitmapsVertically(target: ScrollCaptureTarget, captureHeight: Int): List<Bitmap> {
- val scope = rule.runOnIdle {
- checkNotNull(coroutineScope) {
- "Must call setContent on ScrollCaptureTest before capturing."
- }
- }
- val bitmapsFromTop = mutableListOf<Bitmap>()
-
- // This coroutine will run on the main thread, no need to use runOnUiThread.
- val captureJob = scope.launch {
- runCaptureSession(target, captureHeight, onBitmap = bitmapsFromTop::add)
- }
-
- rule.waitUntil(3_000) { captureJob.isCompleted }
- return bitmapsFromTop
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- private suspend fun runCaptureSession(
+ suspend fun <T> capture(
target: ScrollCaptureTarget,
- captureHeight: Int,
- onBitmap: (Bitmap) -> Unit
- ) {
+ captureWindowHeight: Int,
+ block: suspend CaptureSessionScope.() -> T
+ ): T = withContext(AndroidUiDispatcher.Main) {
val callback = target.callback
// Use the bounds returned from the callback, not the ones from the target, because that's
// what the system does.
val scrollBounds = callback.onScrollCaptureSearch()
val captureWidth = scrollBounds.width()
- requirePrecondition(captureHeight <= scrollBounds.height()) {
- "Expected windowSize ($captureHeight) ≤ viewport height (${scrollBounds.height()})"
+ requirePrecondition(captureWindowHeight <= scrollBounds.height()) {
+ "Expected windowSize ($captureWindowHeight) ≤ viewport height " +
+ "(${scrollBounds.height()})"
}
- withSurfaceBitmaps(captureWidth, captureHeight) { surface, bitmapsFromSurface ->
+ val result = withSurfaceBitmaps(
+ captureWidth,
+ captureWindowHeight
+ ) { surface, bitmapsFromSurface ->
val session = ScrollCaptureSession(
surface,
scrollBounds,
@@ -143,97 +161,83 @@
)
callback.onScrollCaptureStart(session)
- var captureOffset = Point(0, 0)
- var goingUp = true
- // Starting with the original viewport, scrolls all the way to the top, then all the way
- // back down, capturing images on the way down until it hits the bottom.
- while (true) {
- val requestedCaptureArea = Rect(
- captureOffset.x,
- captureOffset.y,
- captureOffset.x + captureWidth,
- captureOffset.y + captureHeight
- )
- val resultCaptureArea =
- callback.onScrollCaptureImageRequest(session, requestedCaptureArea)
+ block(object : CaptureSessionScope {
+ private var captureOffset = Point(0, 0)
- // Empty results shouldn't produce an image.
- if (!resultCaptureArea.isEmpty) {
- val bitmap = bitmapsFromSurface.receiveWithTimeout(1_000) {
- "No bitmap received after 1 second for capture area $resultCaptureArea"
- }
+ override val windowHeight: Int
+ get() = captureWindowHeight
- // Only collect the returned images on the way down.
- if (!goingUp) {
- onBitmap(bitmap)
- } else {
- bitmap.recycle()
- }
+ override fun shiftWindowBy(offset: Int) {
+ captureOffset = Point(0, captureOffset.y + offset)
}
- if (resultCaptureArea != requestedCaptureArea) {
- // We found the top or bottom.
- if (goingUp) {
- // "Bounce" off the top: Change direction and start re-capturing down.
- goingUp = false
- captureOffset = Point(0, resultCaptureArea.top)
+ override suspend fun performCapture(): CaptureResult {
+ val requestedCaptureArea = Rect(
+ captureOffset.x,
+ captureOffset.y,
+ captureOffset.x + captureWidth,
+ captureOffset.y + captureWindowHeight
+ )
+ val resultCaptureArea =
+ callback.onScrollCaptureImageRequest(session, requestedCaptureArea)
+
+ // Empty results shouldn't produce an image.
+ val bitmap = if (!resultCaptureArea.isEmpty) {
+ bitmapsFromSurface.receiveWithTimeout(1_000) {
+ "No bitmap received after 1 second for capture area " +
+ resultCaptureArea
+ }
} else {
- // If we hit the bottom then we're done.
- break
+ null
}
- } else {
- // We can keep going in the same direction, offset the capture window and loop.
- captureOffset = if (goingUp) {
- Point(0, resultCaptureArea.top - captureHeight)
- } else {
- Point(0, resultCaptureArea.bottom)
- }
+ return CaptureResult(
+ bitmap = bitmap,
+ capturedRect = resultCaptureArea
+ )
}
- }
+ })
}
-
callback.onScrollCaptureEnd()
+ return@withContext result
}
/**
* Creates a [Surface] passes it to [block] along with a channel that will receive all images
* written to the [Surface].
*/
- private suspend inline fun withSurfaceBitmaps(
+ private suspend inline fun <T> withSurfaceBitmaps(
width: Int,
height: Int,
- crossinline block: suspend (Surface, ReceiveChannel<Bitmap>) -> Unit
- ) {
- coroutineScope {
- // ImageReader gives us the Surface that we'll provide to the session.
- ImageReader.newInstance(
- width,
- height,
- PixelFormat.RGBA_8888,
- // Each image is read, processed, and closed before the next request to draw is made,
- // so we don't need multiple images.
- /* maxImages= */ 1,
- USAGE_GPU_SAMPLED_IMAGE or USAGE_GPU_COLOR_OUTPUT
- ).use { imageReader ->
- val bitmapsChannel = Channel<Bitmap>(capacity = Channel.RENDEZVOUS)
+ crossinline block: suspend (Surface, ReceiveChannel<Bitmap>) -> T
+ ): T = coroutineScope {
+ // ImageReader gives us the Surface that we'll provide to the session.
+ ImageReader.newInstance(
+ width,
+ height,
+ PixelFormat.RGBA_8888,
+ // Each image is read, processed, and closed before the next request to draw is made,
+ // so we don't need multiple images.
+ /* maxImages= */ 1,
+ USAGE_GPU_SAMPLED_IMAGE or USAGE_GPU_COLOR_OUTPUT
+ ).use { imageReader ->
+ val bitmapsChannel = Channel<Bitmap>(capacity = Channel.RENDEZVOUS)
- // Must register the OnImageAvailableListener before any code in block runs to avoid
- // race conditions.
- val imageCollectorJob = launch(start = CoroutineStart.UNDISPATCHED) {
- imageReader.collectImages {
- val bitmap = it.toSoftwareBitmap()
- bitmapsChannel.send(bitmap)
- }
+ // Must register the OnImageAvailableListener before any code in block runs to avoid
+ // race conditions.
+ val imageCollectorJob = launch(start = CoroutineStart.UNDISPATCHED) {
+ imageReader.collectImages {
+ val bitmap = it.toSoftwareBitmap()
+ bitmapsChannel.send(bitmap)
}
+ }
- try {
- block(imageReader.surface, bitmapsChannel)
- // ImageReader has no signal that it's finished, so in the happy path we have to
- // stop the collector job explicitly.
- imageCollectorJob.cancel()
- } finally {
- bitmapsChannel.close()
- }
+ try {
+ block(imageReader.surface, bitmapsChannel)
+ } finally {
+ // ImageReader has no signal that it's finished, so in the happy path we have to
+ // stop the collector job explicitly.
+ imageCollectorJob.cancel()
+ bitmapsChannel.close()
}
}
}
@@ -301,6 +305,69 @@
}
/**
+ * Emulates (roughly) how the platform interacts with [ScrollCaptureCallback] to iteratively
+ * assemble a screenshot of the entire contents of the [target]. Unlike the platform, this
+ * method will not limit itself to a certain size, it always captures the entire scroll
+ * contents, so tests should make sure to use small enough scroll contents or the test might
+ * run out of memory.
+ *
+ * @param captureHeight The height of the capture window. Must not be greater than viewport
+ * height.
+ */
+@RequiresApi(31)
+suspend fun ScrollCaptureTester.captureBitmapsVertically(
+ target: ScrollCaptureTarget,
+ captureHeight: Int
+): List<Bitmap> = capture(target, captureHeight) {
+ buildList {
+ captureAllFromTop(::add)
+ }
+}
+
+@RequiresApi(31)
+suspend fun ScrollCaptureTester.CaptureSessionScope.captureAllFromTop(
+ onBitmap: suspend (Bitmap) -> Unit
+) {
+ // Starting with the original viewport, scrolls all the way to the top, then all the way
+ // back down, capturing images on the way down until it hits the bottom.
+ var goingUp = true
+ while (true) {
+ val result = performCapture()
+ val bitmap = result.bitmap
+ if (bitmap != null) {
+ // Only collect the returned images on the way down.
+ if (!goingUp) {
+ onBitmap(bitmap)
+ } else {
+ bitmap.recycle()
+ }
+ }
+
+ val consumed = result.capturedRect.height()
+ if (consumed < windowHeight) {
+ // We found the top or bottom.
+ if (goingUp) {
+ // "Bounce" off the top: Change direction and start re-capturing down.
+ goingUp = false
+ // Move the window to the top of the content.
+ shiftWindowBy(windowHeight - consumed)
+ } else {
+ // If we hit the bottom then we're done.
+ break
+ }
+ } else {
+ // We can keep going in the same direction, offset the capture window and
+ // loop.
+ if (goingUp) {
+ shiftWindowBy(-windowHeight)
+ } else {
+ shiftWindowBy(windowHeight)
+ }
+ }
+ }
+}
+
+/**
* Helper for calling [ScrollCaptureCallback.onScrollCaptureSearch] from a suspend function.
* The [CancellationSignal] and continuation callback are generated from the coroutine.
*/
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollExpecter.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollExpecter.kt
new file mode 100644
index 0000000..744e4d9
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/scrollcapture/ScrollExpecter.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.scrollcapture
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.semantics.scrollByOffset
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.fail
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.selects.onTimeout
+import kotlinx.coroutines.selects.select
+
+internal suspend fun expectingScrolls(
+ rule: ComposeTestRule,
+ block: suspend ScrollExpecter.() -> Unit
+) {
+ coroutineScope {
+ val scrollExpecter = ScrollExpecter(this, rule::awaitIdle)
+ block(scrollExpecter)
+ }
+}
+
+internal class ScrollExpecter(
+ private val coroutineScope: CoroutineScope,
+ private val awaitIdle: suspend () -> Unit
+) {
+ private val scrollRequests = Channel<ScrollRequest>(capacity = Channel.RENDEZVOUS)
+
+ /** This must be wired up to be called from [scrollByOffset]. */
+ suspend fun respondToScrollExpectation(offset: Offset): Offset {
+ val result = CompletableDeferred<Offset>(parent = currentCoroutineContext().job)
+ scrollRequests.send(ScrollRequest(offset, consumeScroll = result::complete))
+ return result.await()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ suspend fun expectScrollRequest(
+ expectedOffset: Offset,
+ consume: Offset = expectedOffset
+ ) {
+ coroutineScope.launch {
+ val request = select {
+ scrollRequests.onReceive { it }
+ onTimeout(1000) { fail("No scroll request received after 1000ms") }
+ }
+ assertThat(request.requestedOffset).isEqualTo(expectedOffset)
+ request.consumeScroll(consume)
+ }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ suspend fun assertNoPendingScrollRequests() {
+ awaitIdle()
+ if (!scrollRequests.isEmpty) {
+ val requests = buildList {
+ do {
+ val request = scrollRequests.tryReceive()
+ request.getOrNull()?.let(::add)
+ } while (request.isSuccess)
+ }
+ fail("Expected no scroll requests, but had ${requests.size}: " +
+ requests.joinToString { it.requestedOffset.toString() })
+ }
+ }
+
+ private data class ScrollRequest(
+ val requestedOffset: Offset,
+ val consumeScroll: (Offset) -> Unit
+ )
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchBackwardInteropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchBackwardInteropTest.kt
index a5de4b7..c2220bf 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchBackwardInteropTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchBackwardInteropTest.kt
@@ -18,7 +18,6 @@
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.O
-import android.os.Build.VERSION_CODES.P
import android.view.KeyEvent
import android.view.KeyEvent.ACTION_DOWN
import android.view.KeyEvent.META_SHIFT_ON as Shift
@@ -195,17 +194,22 @@
rule.runOnIdle { assertThat(view.isFocused).isTrue() }
}
- @SdkSuppress(minSdkVersion = P) // b/328143586
@Test
fun focusedComposableWithFocusableView_view_inLinearLayout() {
// Arrange.
+ var isComposableFocused = false
setContent {
- AndroidView({
- LinearLayout(it).apply {
- addView(FocusableView(it).apply { view2 = this })
- addView(ComposeView(it).apply {
+ AndroidView({ context ->
+ LinearLayout(context).apply {
+ addView(FocusableView(context).apply { view2 = this })
+ addView(ComposeView(context).apply {
setContent {
- Row(Modifier.testTag(composable).focusable()) {
+ Row(
+ Modifier
+ .testTag(composable)
+ .onFocusChanged { isComposableFocused = it.isFocused }
+ .focusable()
+ ) {
AndroidView({ FocusableView(it).apply { view1 = this } })
}
}
@@ -214,13 +218,16 @@
})
}
rule.onNodeWithTag(composable).requestFocus()
+ rule.waitUntil { isComposableFocused }
// Act.
- rule.focusSearchBackward()
+ rule.focusSearchBackward(waitForIdle = false)
// Assert.
- rule.onNodeWithTag(composable).assertIsNotFocused()
- rule.runOnIdle { assertThat(view2.isFocused).isTrue() }
+ rule.waitUntil { !isComposableFocused }
+
+ // TODO(b/332345953) Figure out why this fails on sdk 25 and below.
+ if (SDK_INT >= O) rule.waitUntil { view2.isFocused }
}
@Test
@@ -655,11 +662,11 @@
rule.onNodeWithTag(composable).assertIsNotFocused()
}
- private fun ComposeContentTestRule.focusSearchBackward() {
+ private fun ComposeContentTestRule.focusSearchBackward(waitForIdle: Boolean = true) {
+ if (waitForIdle) waitForIdle()
if (moveFocusProgrammatically) {
- runOnIdle { focusManager.moveFocus(FocusDirection.Previous) }
+ runOnUiThread { focusManager.moveFocus(FocusDirection.Previous) }
} else {
- waitForIdle()
InstrumentationRegistry
.getInstrumentation()
.sendKeySync(KeyEvent(0L, 0L, ACTION_DOWN, Key.Tab.nativeKeyCode, 0, Shift))
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchDownInteropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchDownInteropTest.kt
index a471c4a..b96dda8 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchDownInteropTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchDownInteropTest.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.viewinterop
-import android.os.Build.VERSION_CODES.P
import android.view.KeyEvent
import android.view.View
import android.widget.LinearLayout
@@ -46,7 +45,6 @@
import androidx.compose.ui.test.requestFocus
import androidx.compose.ui.unit.dp
import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -188,33 +186,38 @@
rule.runOnIdle { assertThat(view.isFocused).isTrue() }
}
- @SdkSuppress(minSdkVersion = P) // b/328143586
@Test
fun focusedComposableWithFocusableView_view_inLinearLayout() {
// Arrange.
+ var isComposableFocused = false
setContent {
- AndroidView({
- LinearLayout(it).apply {
+ AndroidView({ context ->
+ LinearLayout(context).apply {
orientation = VERTICAL
- addView(ComposeView(it).apply {
+ addView(ComposeView(context).apply {
setContent {
- Column(Modifier.testTag(composable).focusable()) {
+ Column(
+ Modifier
+ .testTag(composable)
+ .onFocusChanged { isComposableFocused = it.isFocused }
+ .focusable()
+ ) {
AndroidView({ FocusableView(it).apply { view1 = this } })
}
}
})
- addView(FocusableView(it).apply { view2 = this })
+ addView(FocusableView(context).apply { view2 = this })
}
})
}
rule.onNodeWithTag(composable).requestFocus()
+ rule.waitUntil { isComposableFocused }
// Act.
- rule.focusSearchDown()
+ rule.focusSearchDown(waitForIdle = false)
// Assert.
- rule.onNodeWithTag(composable).assertIsNotFocused()
- rule.runOnIdle { assertThat(view2.isFocused).isTrue() }
+ rule.waitUntil { !isComposableFocused && view2.isFocused }
}
@Test
@@ -652,11 +655,11 @@
rule.onNodeWithTag(composable).assertIsNotFocused()
}
- private fun ComposeContentTestRule.focusSearchDown() {
+ private fun ComposeContentTestRule.focusSearchDown(waitForIdle: Boolean = true) {
+ if (waitForIdle) waitForIdle()
if (moveFocusProgrammatically) {
- runOnIdle { focusManager.moveFocus(FocusDirection.Down) }
+ runOnUiThread { focusManager.moveFocus(FocusDirection.Down) }
} else {
- waitForIdle()
InstrumentationRegistry
.getInstrumentation()
.sendKeySync(KeyEvent(KeyEvent.ACTION_DOWN, Key.DirectionDown.nativeKeyCode))
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchForwardInteropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchForwardInteropTest.kt
index 189bea1..1d22244 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchForwardInteropTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchForwardInteropTest.kt
@@ -17,7 +17,6 @@
package androidx.compose.ui.viewinterop
import android.os.Build.VERSION_CODES.O
-import android.os.Build.VERSION_CODES.P
import android.view.KeyEvent as AndroidKeyEvent
import android.view.KeyEvent.ACTION_DOWN
import android.view.View
@@ -187,32 +186,37 @@
rule.runOnIdle { assertThat(view.isFocused).isTrue() }
}
- @SdkSuppress(minSdkVersion = P) // b/328143586
@Test
fun focusedComposableWithFocusableView_view_inLinearLayout() {
// Arrange.
+ var isComposableFocused = false
setContent {
- AndroidView({
- LinearLayout(it).apply {
- addView(ComposeView(it).apply {
+ AndroidView({ context ->
+ LinearLayout(context).apply {
+ addView(ComposeView(context).apply {
setContent {
- Row(Modifier.testTag(composable).focusable()) {
+ Row(
+ Modifier
+ .testTag(composable)
+ .onFocusChanged { isComposableFocused = it.isFocused }
+ .focusable()
+ ) {
AndroidView({ FocusableView(it).apply { view1 = this } })
}
}
})
- addView(FocusableView(it).apply { view2 = this })
+ addView(FocusableView(context).apply { view2 = this })
}
})
}
rule.onNodeWithTag(composable).requestFocus()
+ rule.waitUntil { isComposableFocused }
// Act.
- rule.focusSearchForward()
+ rule.focusSearchForward(waitForIdle = false)
// Assert.
- rule.onNodeWithTag(composable).assertIsNotFocused()
- rule.runOnIdle { assertThat(view1.isFocused).isTrue() }
+ rule.waitUntil { !isComposableFocused && view1.isFocused }
}
@Test
@@ -639,11 +643,11 @@
rule.onNodeWithTag(composable).assertIsNotFocused()
}
- private fun ComposeContentTestRule.focusSearchForward() {
+ private fun ComposeContentTestRule.focusSearchForward(waitForIdle: Boolean = true) {
+ if (waitForIdle) waitForIdle()
if (moveFocusProgrammatically) {
- runOnIdle { focusManager.moveFocus(FocusDirection.Next) }
+ runOnUiThread { focusManager.moveFocus(FocusDirection.Next) }
} else {
- waitForIdle()
InstrumentationRegistry
.getInstrumentation()
.sendKeySync(AndroidKeyEvent(ACTION_DOWN, Key.Tab.nativeKeyCode))
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchLeftInteropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchLeftInteropTest.kt
index 35e2277..fdfa90e 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchLeftInteropTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchLeftInteropTest.kt
@@ -18,7 +18,6 @@
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.O
-import android.os.Build.VERSION_CODES.P
import android.view.KeyEvent
import android.view.View
import android.widget.LinearLayout
@@ -48,7 +47,6 @@
import androidx.compose.ui.test.requestFocus
import androidx.compose.ui.unit.dp
import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -195,18 +193,23 @@
rule.runOnIdle { assertThat(view.isFocused).isTrue() }
}
- @SdkSuppress(minSdkVersion = P) // b/328143586
@Test
fun focusedComposableWithFocusableView_view_inLinearLayout() {
// Arrange.
+ var isComposableFocused = false
setContent {
- AndroidView({
- LinearLayout(it).apply {
+ AndroidView({ context ->
+ LinearLayout(context).apply {
orientation = HORIZONTAL
- addView(FocusableView(it).apply { view2 = this })
- addView(ComposeView(it).apply {
+ addView(FocusableView(context).apply { view2 = this })
+ addView(ComposeView(context).apply {
setContent {
- Row(Modifier.testTag(composable).focusable()) {
+ Row(
+ Modifier
+ .testTag(composable)
+ .onFocusChanged { isComposableFocused = it.isFocused }
+ .focusable()
+ ) {
AndroidView({ FocusableView(it).apply { view1 = this } })
}
}
@@ -215,13 +218,13 @@
})
}
rule.onNodeWithTag(composable).requestFocus()
+ rule.waitUntil { isComposableFocused }
// Act.
- rule.focusSearchLeft()
+ rule.focusSearchLeft(waitForIdle = false)
// Assert.
- rule.onNodeWithTag(composable).assertIsNotFocused()
- rule.runOnIdle { assertThat(view2.isFocused).isTrue() }
+ rule.waitUntil { !isComposableFocused && view2.isFocused }
}
@Test
@@ -669,11 +672,11 @@
rule.onNodeWithTag(composable).assertIsNotFocused()
}
- private fun ComposeContentTestRule.focusSearchLeft() {
+ private fun ComposeContentTestRule.focusSearchLeft(waitForIdle: Boolean = true) {
+ if (waitForIdle) waitForIdle()
if (moveFocusProgrammatically) {
- runOnIdle { focusManager.moveFocus(FocusDirection.Left) }
+ runOnUiThread { focusManager.moveFocus(FocusDirection.Left) }
} else {
- waitForIdle()
InstrumentationRegistry
.getInstrumentation()
.sendKeySync(KeyEvent(KeyEvent.ACTION_DOWN, Key.DirectionLeft.nativeKeyCode))
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchRightInteropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchRightInteropTest.kt
index 4eac80b..2d3eee6 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchRightInteropTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchRightInteropTest.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.viewinterop
-import android.os.Build.VERSION_CODES.P
import android.view.KeyEvent
import android.view.View
import android.widget.LinearLayout
@@ -46,7 +45,6 @@
import androidx.compose.ui.test.requestFocus
import androidx.compose.ui.unit.dp
import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -188,33 +186,38 @@
rule.runOnIdle { assertThat(view.isFocused).isTrue() }
}
- @SdkSuppress(minSdkVersion = P) // b/328143586
@Test
fun focusedComposableWithFocusableView_view_inLinearLayout() {
// Arrange.
+ var isComposableFocused = false
setContent {
- AndroidView({
- LinearLayout(it).apply {
+ AndroidView({ context ->
+ LinearLayout(context).apply {
orientation = HORIZONTAL
- addView(ComposeView(it).apply {
+ addView(ComposeView(context).apply {
setContent {
- Row(Modifier.testTag(composable).focusable()) {
+ Row(
+ Modifier
+ .testTag(composable)
+ .onFocusChanged { isComposableFocused = it.isFocused }
+ .focusable()
+ ) {
AndroidView({ FocusableView(it).apply { view1 = this } })
}
}
})
- addView(FocusableView(it).apply { view2 = this })
+ addView(FocusableView(context).apply { view2 = this })
}
})
}
rule.onNodeWithTag(composable).requestFocus()
+ rule.waitUntil { isComposableFocused }
// Act.
- rule.focusSearchRight()
+ rule.focusSearchRight(waitForIdle = false)
// Assert.
- rule.onNodeWithTag(composable).assertIsNotFocused()
- rule.runOnIdle { assertThat(view2.isFocused).isTrue() }
+ rule.waitUntil { !isComposableFocused && view2.isFocused }
}
@Test
@@ -652,11 +655,11 @@
rule.onNodeWithTag(composable).assertIsNotFocused()
}
- private fun ComposeContentTestRule.focusSearchRight() {
+ private fun ComposeContentTestRule.focusSearchRight(waitForIdle: Boolean = true) {
+ if (waitForIdle) waitForIdle()
if (moveFocusProgrammatically) {
- runOnIdle { focusManager.moveFocus(FocusDirection.Right) }
+ runOnUiThread { focusManager.moveFocus(FocusDirection.Right) }
} else {
- waitForIdle()
InstrumentationRegistry
.getInstrumentation()
.sendKeySync(KeyEvent(KeyEvent.ACTION_DOWN, Key.DirectionRight.nativeKeyCode))
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchUpInteropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchUpInteropTest.kt
index 4780c97..4d2998a 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchUpInteropTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/FocusSearchUpInteropTest.kt
@@ -18,7 +18,6 @@
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.O
-import android.os.Build.VERSION_CODES.P
import android.view.KeyEvent
import android.view.View
import android.widget.LinearLayout
@@ -48,7 +47,6 @@
import androidx.compose.ui.test.requestFocus
import androidx.compose.ui.unit.dp
import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -195,18 +193,23 @@
rule.runOnIdle { assertThat(view.isFocused).isTrue() }
}
- @SdkSuppress(minSdkVersion = P) // b/328143586
@Test
fun focusedComposableWithFocusableView_view_inLinearLayout() {
// Arrange.
+ var isComposableFocused = false
setContent {
- AndroidView({
- LinearLayout(it).apply {
+ AndroidView({ context ->
+ LinearLayout(context).apply {
orientation = VERTICAL
- addView(FocusableView(it).apply { view2 = this })
- addView(ComposeView(it).apply {
+ addView(FocusableView(context).apply { view2 = this })
+ addView(ComposeView(context).apply {
setContent {
- Column(Modifier.testTag(composable).focusable()) {
+ Column(
+ Modifier
+ .testTag(composable)
+ .onFocusChanged { isComposableFocused = it.isFocused }
+ .focusable()
+ ) {
AndroidView({ FocusableView(it).apply { view1 = this } })
}
}
@@ -215,13 +218,13 @@
})
}
rule.onNodeWithTag(composable).requestFocus()
+ rule.waitUntil { isComposableFocused }
// Act.
- rule.focusSearchUp()
+ rule.focusSearchUp(waitForIdle = false)
// Assert.
- rule.onNodeWithTag(composable).assertIsNotFocused()
- rule.runOnIdle { assertThat(view2.isFocused).isTrue() }
+ rule.waitUntil { !isComposableFocused && view2.isFocused }
}
@Test
@@ -669,11 +672,11 @@
rule.onNodeWithTag(composable).assertIsNotFocused()
}
- private fun ComposeContentTestRule.focusSearchUp() {
+ private fun ComposeContentTestRule.focusSearchUp(waitForIdle: Boolean = true) {
+ if (waitForIdle) waitForIdle()
if (moveFocusProgrammatically) {
- runOnIdle { focusManager.moveFocus(FocusDirection.Up) }
+ runOnUiThread { focusManager.moveFocus(FocusDirection.Up) }
} else {
- waitForIdle()
InstrumentationRegistry
.getInstrumentation()
.sendKeySync(KeyEvent(KeyEvent.ACTION_DOWN, Key.DirectionUp.nativeKeyCode))
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 b618150..a1bff71 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
@@ -1051,14 +1051,14 @@
// `traversalAfter` from a non-sealed instance of an ANI
when (extraDataKey) {
composeAccessibilityDelegate.ExtraDataTestTraversalBeforeVal -> {
- composeAccessibilityDelegate.idToBeforeMap[virtualViewId]?.let {
- info.extras.putInt(extraDataKey, it)
- }
+ composeAccessibilityDelegate.idToBeforeMap
+ .getOrDefault(virtualViewId, -1)
+ .let { if (it != -1) { info.extras.putInt(extraDataKey, it) } }
}
composeAccessibilityDelegate.ExtraDataTestTraversalAfterVal -> {
- composeAccessibilityDelegate.idToAfterMap[virtualViewId]?.let {
- info.extras.putInt(extraDataKey, it)
- }
+ composeAccessibilityDelegate.idToAfterMap
+ .getOrDefault(virtualViewId, -1)
+ .let { if (it != -1) { info.extras.putInt(extraDataKey, it) } }
}
else -> {}
}
@@ -1132,8 +1132,9 @@
info.setParent(thisView, parentId)
val semanticsId = layoutNode.semanticsId
- val beforeId = composeAccessibilityDelegate.idToBeforeMap[semanticsId]
- beforeId?.let {
+ val beforeId =
+ composeAccessibilityDelegate.idToBeforeMap.getOrDefault(semanticsId, -1)
+ if (beforeId != -1) {
val beforeView = androidViewsHandler.semanticsIdToView(beforeId)
if (beforeView != null) {
// If the node that should come before this one is a view, we want to
@@ -1143,7 +1144,7 @@
} else {
// Otherwise, we'll just set the "before" value by passing in
// the semanticsId.
- info.setTraversalBefore(thisView, it)
+ info.setTraversalBefore(thisView, beforeId)
}
addExtraDataToAccessibilityNodeInfoHelper(
semanticsId, info.unwrap(),
@@ -1151,13 +1152,14 @@
)
}
- val afterId = composeAccessibilityDelegate.idToAfterMap[semanticsId]
- afterId?.let {
+ val afterId =
+ composeAccessibilityDelegate.idToAfterMap.getOrDefault(semanticsId, -1)
+ if (afterId != -1) {
val afterView = androidViewsHandler.semanticsIdToView(afterId)
if (afterView != null) {
info.setTraversalAfter(afterView)
} else {
- info.setTraversalAfter(thisView, it)
+ info.setTraversalAfter(thisView, afterId)
}
addExtraDataToAccessibilityNodeInfoHelper(
semanticsId, info.unwrap(),
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 078b92c..488d119 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -43,6 +43,7 @@
import androidx.annotation.VisibleForTesting
import androidx.collection.ArraySet
import androidx.collection.IntObjectMap
+import androidx.collection.MutableIntIntMap
import androidx.collection.MutableIntObjectMap
import androidx.collection.MutableIntSet
import androidx.collection.MutableObjectIntMap
@@ -315,8 +316,8 @@
}
private var paneDisplayed = MutableIntSet()
- internal var idToBeforeMap = MutableIntObjectMap<Int>()
- internal var idToAfterMap = MutableIntObjectMap<Int>()
+ internal var idToBeforeMap = MutableIntIntMap()
+ internal var idToAfterMap = MutableIntIntMap()
internal val ExtraDataTestTraversalBeforeVal =
@Suppress("SpellCheckingInspection")
"android.view.accessibility.extra.EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL"
@@ -1218,8 +1219,8 @@
info.isScreenReaderFocusable = isScreenReaderFocusable(semanticsNode)
// `beforeId` refers to the semanticsId that should be read before this `virtualViewId`.
- val beforeId = idToBeforeMap[virtualViewId]
- beforeId?.let {
+ val beforeId = idToBeforeMap.getOrDefault(virtualViewId, -1)
+ if (beforeId != -1) {
val beforeView = view.androidViewsHandler.semanticsIdToView(beforeId)
if (beforeView != null) {
// If the node that should come before this one is a view, we want to pass in the
@@ -1234,8 +1235,8 @@
)
}
- val afterId = idToAfterMap[virtualViewId]
- afterId?.let {
+ val afterId = idToAfterMap.getOrDefault(virtualViewId, -1)
+ if (afterId != -1) {
val afterView = view.androidViewsHandler.semanticsIdToView(afterId)
// Specially use `traversalAfter` value if the node after is a View,
// as expressing the order using traversalBefore in this case would require mutating the
@@ -1952,13 +1953,13 @@
// This extra is just for testing: needed a way to retrieve `traversalBefore` and
// `traversalAfter` from a non-sealed instance of an ANI
if (extraDataKey == ExtraDataTestTraversalBeforeVal) {
- idToBeforeMap[virtualViewId]?.let {
- info.extras.putInt(extraDataKey, it)
- }
+ idToBeforeMap
+ .getOrDefault(virtualViewId, -1)
+ .let { if (it != -1) { info.extras.putInt(extraDataKey, it) } }
} else if (extraDataKey == ExtraDataTestTraversalAfterVal) {
- idToAfterMap[virtualViewId]?.let {
- info.extras.putInt(extraDataKey, it)
- }
+ idToAfterMap
+ .getOrDefault(virtualViewId, -1)
+ .let { if (it != -1) { info.extras.putInt(extraDataKey, it) } }
} else if (node.unmergedConfig.contains(SemanticsActions.GetTextLayoutResult) &&
arguments != null && extraDataKey == EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY
) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/GraphicsLayerOwnerLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/GraphicsLayerOwnerLayer.android.kt
index d99860d..cecabef 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/GraphicsLayerOwnerLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/GraphicsLayerOwnerLayer.android.kt
@@ -110,7 +110,7 @@
override fun updateDisplayList() {
if (isDirty) {
- graphicsLayer.buildLayer(density, layoutDirection, size) {
+ graphicsLayer.record(density, layoutDirection, size) {
drawIntoCanvas { canvas ->
drawBlock?.let { it(canvas) }
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/NestedScrollInteropConnection.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/NestedScrollInteropConnection.android.kt
index 5d125ee..d579246 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/NestedScrollInteropConnection.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/NestedScrollInteropConnection.android.kt
@@ -189,7 +189,7 @@
}
private fun NestedScrollSource.toViewType(): Int = when (this) {
- NestedScrollSource.Drag -> TYPE_TOUCH
+ NestedScrollSource.UserInput -> TYPE_TOUCH
else -> TYPE_NON_TOUCH
}
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 c1d4642..6789cf6 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
@@ -614,6 +614,6 @@
private fun Float.toComposeVelocity(): Float = this * -1f
private fun toNestedScrollSource(type: Int): NestedScrollSource = when (type) {
- ViewCompat.TYPE_TOUCH -> NestedScrollSource.Drag
- else -> NestedScrollSource.Fling
+ ViewCompat.TYPE_TOUCH -> NestedScrollSource.UserInput
+ else -> NestedScrollSource.SideEffect
}
diff --git a/compose/ui/ui/src/androidMain/res/values-te/strings.xml b/compose/ui/ui/src/androidMain/res/values-te/strings.xml
index fdfdad5a..cecac59e 100644
--- a/compose/ui/ui/src/androidMain/res/values-te/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-te/strings.xml
@@ -28,8 +28,8 @@
<string name="tab" msgid="1672349317127674378">"ట్యాబ్"</string>
<string name="navigation_menu" msgid="542007171693138492">"నావిగేషన్ మెనూ"</string>
<string name="dropdown_menu" msgid="1890207353314751437">"డ్రాప్డౌన్ మెనూ"</string>
- <string name="close_drawer" msgid="406453423630273620">"నావిగేషన్ మెనూను మూసివేయి"</string>
- <string name="close_sheet" msgid="7573152094250666567">"షీట్ను మూసివేయి"</string>
+ <string name="close_drawer" msgid="406453423630273620">"నావిగేషన్ మెనూను మూసివేయండి"</string>
+ <string name="close_sheet" msgid="7573152094250666567">"షీట్ను మూసివేయండి"</string>
<string name="default_error_message" msgid="8038256446254964252">"ఇన్పుట్ చెల్లదు"</string>
<string name="default_popup_window_title" msgid="6312721426453364202">"పాప్-అప్ విండో"</string>
<string name="range_start" msgid="7097486360902471446">"పరిధి ప్రారంభమయింది"</string>
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
index afb75c1..9c4aca2 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
@@ -335,15 +335,15 @@
graphicsContextProvider!!.invoke().createGraphicsLayer()
/**
- * Create a [GraphicsLayer] with the [Density], [LayoutDirection] and [Size] are given from the
- * provided [CacheDrawScope]
+ * Record the drawing commands into the [GraphicsLayer] with the [Density], [LayoutDirection]
+ * and [Size] are given from the provided [CacheDrawScope]
*/
- fun GraphicsLayer.buildLayer(
+ fun GraphicsLayer.record(
density: Density = this@CacheDrawScope,
layoutDirection: LayoutDirection = this@CacheDrawScope.layoutDirection,
size: IntSize = this@CacheDrawScope.size.toIntSize(),
block: ContentDrawScope.() -> Unit
- ): GraphicsLayer = buildLayer(density, layoutDirection, size) {
+ ) = record(density, layoutDirection, size) {
val contentDrawScope = this@CacheDrawScope.contentDrawScope!!
drawIntoCanvas { canvas ->
contentDrawScope.draw(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt
index d4f1682..55be271 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt
@@ -54,6 +54,8 @@
// end of the transaction, this state is stored as committed focus state.
private var committedFocusState: FocusStateImpl? = null
+ override val shouldAutoInvalidate = false
+
@OptIn(ExperimentalComposeUiApi::class)
override var focusState: FocusStateImpl
get() = focusTransactionManager?.run { uncommittedFocusState }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt
index b2ff604..9cf5728 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt
@@ -18,7 +18,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ComposableOpenTarget
-import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.layer.GraphicsLayer
@@ -225,6 +225,25 @@
get() = Size.Unspecified
}
+private class GraphicsContextObserver(
+ private val graphicsContext: GraphicsContext
+) : RememberObserver {
+
+ val graphicsLayer = graphicsContext.createGraphicsLayer()
+
+ override fun onRemembered() {
+ // NO-OP
+ }
+
+ override fun onForgotten() {
+ graphicsContext.releaseGraphicsLayer(graphicsLayer)
+ }
+
+ override fun onAbandoned() {
+ graphicsContext.releaseGraphicsLayer(graphicsLayer)
+ }
+}
+
/**
* Create a new [GraphicsLayer] instance that will automatically be released when the Composable
* is disposed.
@@ -235,15 +254,7 @@
@ComposableOpenTarget(-1)
fun rememberGraphicsLayer(): GraphicsLayer {
val graphicsContext = LocalGraphicsContext.current
- val layer = remember {
- graphicsContext.createGraphicsLayer()
- }
- DisposableEffect(layer) {
- onDispose {
- graphicsContext.releaseGraphicsLayer(layer)
- }
- }
- return layer
+ return remember { GraphicsContextObserver(graphicsContext) }.graphicsLayer
}
/**
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 457a782..3672fd2 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
@@ -229,25 +229,53 @@
override fun toString(): String {
@Suppress("DEPRECATION")
return when (this) {
- Drag -> "Drag"
- Fling -> "Fling"
+ UserInput -> "UserInput"
+ SideEffect -> "SideEffect"
@OptIn(ExperimentalComposeUiApi::class)
Relocate -> "Relocate"
- Wheel -> "Wheel"
else -> "Invalid"
}
}
companion object {
+
+ /**
+ * Represents any source of scroll events originated from a user interaction: mouse, touch,
+ * key events.
+ */
+ val UserInput: NestedScrollSource = NestedScrollSource(1)
+
+ /**
+ * Represents any other source of scroll events that are not a direct user input. (e.g
+ * animations, fling)
+ */
+ val SideEffect: NestedScrollSource = NestedScrollSource(2)
+
/**
* Dragging via mouse/touch/etc events.
*/
- val Drag: NestedScrollSource = NestedScrollSource(1)
+ @Deprecated(
+ "This has been replaced by UserInput.",
+ replaceWith = ReplaceWith(
+ "NestedScrollSource.UserInput",
+ "import androidx.compose.ui.input.nestedscroll." +
+ "NestedScrollSource.Companion.UserInput"
+ )
+ )
+ val Drag: NestedScrollSource = UserInput
/**
* Flinging after the drag has ended with velocity.
*/
- val Fling: NestedScrollSource = NestedScrollSource(2)
+ @Deprecated(
+ "This has been replaced by SideEffect.",
+ replaceWith = ReplaceWith(
+ "NestedScrollSource.SideEffect",
+ "import androidx.compose.ui.input.nestedscroll." +
+ "NestedScrollSource.Companion.SideEffect"
+ )
+ )
+ val Fling: NestedScrollSource = SideEffect
/**
* Relocating when a component asks parents to scroll to bring it into view.
@@ -261,7 +289,15 @@
/**
* Scrolling via mouse wheel.
*/
- val Wheel: NestedScrollSource = NestedScrollSource(4)
+ @Deprecated(
+ "This has been replaced by UserInput.",
+ replaceWith = ReplaceWith(
+ "NestedScrollSource.UserInput",
+ "import androidx.compose.ui.input.nestedscroll." +
+ "NestedScrollSource.Companion.UserInput"
+ )
+ )
+ val Wheel: NestedScrollSource = UserInput
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
index 8613ddd..dfa305b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
@@ -42,6 +42,9 @@
/*@VisibleForTesting*/
internal val root: NodeParent = NodeParent()
+ // Only used when removing duplicate Nodes from the Node tree ([removeDuplicateNode]).
+ private val vectorForHandlingDuplicateNodes: MutableVector<NodeParent> = mutableVectorOf()
+
/**
* Associates a [pointerId] to a list of hit [pointerInputNodes] and keeps track of them.
*
@@ -57,6 +60,8 @@
fun addHitPath(pointerId: PointerId, pointerInputNodes: List<Modifier.Node>) {
var parent: NodeParent = root
var merging = true
+ var nodeBranchPathToSkipDuringDuplicateNodeRemoval: Node? = null
+
eachPin@ for (i in pointerInputNodes.indices) {
val pointerInputNode = pointerInputNodes[i]
if (merging) {
@@ -76,11 +81,53 @@
val node = Node(pointerInputNode).apply {
pointerIds.add(pointerId)
}
+
+ if (nodeBranchPathToSkipDuringDuplicateNodeRemoval == null) {
+ // Null means this is the first new Node created that will need a new branch path
+ // (possibly from a pre-existing cached version of the node chain).
+ // If that is the case, we need to skip this path when looking for duplicate
+ // nodes to remove (that may have previously existed somewhere else in the tree).
+ nodeBranchPathToSkipDuringDuplicateNodeRemoval = node
+ } else {
+ // Every node after the top new node (that is, the top Node in the new path)
+ // could have potentially existed somewhere else in the cached node tree, and
+ // we need to remove it if we are adding it to this new branch.
+ removeDuplicateNode(node, nodeBranchPathToSkipDuringDuplicateNodeRemoval)
+ }
+
parent.children.add(node)
parent = node
}
}
+ /*
+ * Removes duplicate nodes when using a cached version of the node tree. Uses breadth-first
+ * search for simplicity (and because the tree will be very small).
+ */
+ private fun removeDuplicateNode(
+ duplicateNodeToRemove: Node,
+ headOfPathToSkip: Node
+ ) {
+ vectorForHandlingDuplicateNodes.clear()
+ vectorForHandlingDuplicateNodes.add(root)
+
+ while (vectorForHandlingDuplicateNodes.isNotEmpty()) {
+ val parent = vectorForHandlingDuplicateNodes.removeAt(0)
+
+ for (index in parent.children.indices) {
+ val child = parent.children[index]
+ if (child == headOfPathToSkip) continue
+ if (child.modifierNode == duplicateNodeToRemove.modifierNode) {
+ // Assumes there is only one unique Node in the tree (not copies).
+ // This also removes all children attached below the node.
+ parent.children.remove(child)
+ return
+ }
+ vectorForHandlingDuplicateNodes.add(child)
+ }
+ }
+ }
+
/**
* Dispatches [internalPointerEvent] through the hierarchy.
*
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachLayoutModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachLayoutModifierNode.kt
index c026b1f..0f58b93 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachLayoutModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachLayoutModifierNode.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.layout
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.NodeMeasuringIntrinsics
import androidx.compose.ui.unit.Constraints
@@ -42,65 +41,65 @@
* measure results without modification. This can be overridden as needed. [approachMeasure]
* will be invoked during the approach pass after lookahead.
*
- * [isMeasurementApproachComplete] signals whether the measurement has already reached the
+ * [isMeasurementApproachInProgress] signals whether the measurement is in progress of approaching
* destination size. It will be queried after the destination has been determined by the lookahead
* pass, before [approachMeasure] is invoked. The lookahead size is provided to
- * [isMeasurementApproachComplete] for convenience in deciding whether the destination size has
+ * [isMeasurementApproachInProgress] for convenience in deciding whether the destination size has
* been reached.
*
- * [isPlacementApproachComplete] indicates whether the position has approached
+ * [isPlacementApproachInProgress] indicates whether the position is actively approaching
* destination defined by the lookahead, hence it's a signal to the system for whether additional
- * approach placements are necessary. [isPlacementApproachComplete] will be invoked after the
+ * approach placements are necessary. [isPlacementApproachInProgress] will be invoked after the
* destination position has been determined by lookahead pass, and before the placement phase in
* [approachMeasure].
*
* **IMPORTANT**:
- * When both [isMeasurementApproachComplete] and [isPlacementApproachComplete] become true, the
+ * When both [isMeasurementApproachInProgress] and [isPlacementApproachInProgress] become false, the
* approach is considered complete. Approach pass will subsequently snap the measurement and
* placement to lookahead measurement and placement. Once approach is complete, [approachMeasure]
- * may never be invoked until either [isMeasurementApproachComplete] or
- * [isPlacementApproachComplete] becomes false again. Therefore it is important to ensure
+ * may never be invoked until either [isMeasurementApproachInProgress] or
+ * [isPlacementApproachInProgress] becomes true again. Therefore it is important to ensure
* [approachMeasure] and [measure] result in the same measurement and placement when the approach is
* complete. Otherwise, there may be visual discontinuity when we snap the measurement and placement
* to lookahead.
*
- * It is important to be accurate in [isPlacementApproachComplete] and
- * [isMeasurementApproachComplete]. A prolonged indication of incomplete approach will prevent the
+ * It is important to be accurate in [isPlacementApproachInProgress] and
+ * [isMeasurementApproachInProgress]. A prolonged indication of incomplete approach will prevent the
* system from potentially skipping approach pass when possible.
*
* @sample androidx.compose.ui.samples.LookaheadLayoutCoordinatesSample
*/
interface ApproachLayoutModifierNode : LayoutModifierNode {
/**
- * [isMeasurementApproachComplete] signals whether the measurement has already reached the
+ * [isMeasurementApproachInProgress] signals whether the measurement is currently approaching
* destination size. It will be queried after the destination has been determined by the
* lookahead pass, before [approachMeasure] is invoked. The lookahead size is provided to
- * [isMeasurementApproachComplete] for convenience in deciding whether the destination size has
- * been reached.
+ * [isMeasurementApproachInProgress] for convenience in deciding whether the destination size
+ * has been reached.
*
- * Note: It is important to be accurate in [isPlacementApproachComplete] and
- * [isMeasurementApproachComplete]. A prolonged indication of incomplete approach will prevent
+ * Note: It is important to be accurate in [isPlacementApproachInProgress] and
+ * [isMeasurementApproachInProgress]. A prolonged indication of incomplete approach will prevent
* the system from potentially skipping approach pass when possible.
*/
- fun isMeasurementApproachComplete(lookaheadSize: IntSize): Boolean
+ fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean
/**
- * [isPlacementApproachComplete] indicates whether the position has approached destination
+ * [isPlacementApproachInProgress] indicates whether the position is approaching destination
* defined by the lookahead, hence it's a signal to the system for whether additional
- * approach placements are necessary. [isPlacementApproachComplete] will be invoked after the
+ * approach placements are necessary. [isPlacementApproachInProgress] will be invoked after the
* destination position has been determined by lookahead pass, and before the placement phase in
* [approachMeasure].
*
- * Note: It is important to be accurate in [isPlacementApproachComplete] and
- * [isMeasurementApproachComplete]. A prolonged indication of incomplete approach will prevent
+ * Note: It is important to be accurate in [isPlacementApproachInProgress] and
+ * [isMeasurementApproachInProgress]. A prolonged indication of incomplete approach will prevent
* the system from potentially skipping approach pass when possible.
*
- * By default, [isPlacementApproachComplete] returns true.
+ * By default, [isPlacementApproachInProgress] returns false.
*/
- fun Placeable.PlacementScope.isPlacementApproachComplete(
+ fun Placeable.PlacementScope.isPlacementApproachInProgress(
lookaheadCoordinates: LayoutCoordinates
): Boolean {
- return true
+ return false
}
override fun MeasureScope.measure(
@@ -125,13 +124,12 @@
* achieved.
*
* Note: [approachMeasure] is only guaranteed to be invoked when either
- * [isPlacementApproachComplete] or [isMeasurementApproachComplete] is false. Otherwise, the
+ * [isMeasurementApproachInProgress] or [isMeasurementApproachInProgress] is true. Otherwise, the
* system will consider the approach complete (i.e. destination reached) and may skip the
* approach pass when possible.
*
* @sample androidx.compose.ui.samples.LookaheadLayoutCoordinatesSample
*/
- @ExperimentalComposeUiApi
fun ApproachMeasureScope.approachMeasure(
measurable: Measurable,
constraints: Constraints
@@ -140,7 +138,6 @@
/**
* The function used to calculate minIntrinsicWidth for the approach pass changes.
*/
- @ExperimentalComposeUiApi
fun ApproachIntrinsicMeasureScope.minApproachIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int
@@ -156,7 +153,6 @@
/**
* The function used to calculate minIntrinsicHeight for the approach pass changes.
*/
- @ExperimentalComposeUiApi
fun ApproachIntrinsicMeasureScope.minApproachIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
@@ -172,7 +168,6 @@
/**
* The function used to calculate maxIntrinsicWidth for the approach pass changes.
*/
- @ExperimentalComposeUiApi
fun ApproachIntrinsicMeasureScope.maxApproachIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int
@@ -188,7 +183,6 @@
/**
* The function used to calculate maxIntrinsicHeight for the approach pass changes.
*/
- @ExperimentalComposeUiApi
fun ApproachIntrinsicMeasureScope.maxApproachIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
index 94a755c..94e83fc 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
@@ -53,7 +53,6 @@
* in [ApproachLayoutModifierNode] to morph the layout gradually in both size and position
* to arrive at its precalculated bounds.
*/
-@ExperimentalComposeUiApi
sealed interface ApproachMeasureScope : ApproachIntrinsicMeasureScope, MeasureScope
internal class ApproachMeasureScopeImpl(
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
index b5c58fa..bbdeb4f 100644
--- 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
@@ -32,8 +32,6 @@
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
-import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.CoroutineScope
/**
* [LookaheadScope] creates a scope in which all layouts will first determine their destination
@@ -72,30 +70,6 @@
)
}
-@ExperimentalComposeUiApi
-@Deprecated(
- "IntermediateMeasureScope has been renamed to ApproachMeasureScope",
- replaceWith = ReplaceWith("ApproachMeasureScope")
-)
-interface IntermediateMeasureScope : ApproachMeasureScope, CoroutineScope, LookaheadScope
-
-@ExperimentalComposeUiApi
-@Deprecated(
- "intermediateLayout has been replaced with approachLayout, and requires an" +
- "additional parameter to signal if the approach is complete.",
- replaceWith = ReplaceWith(
- "approachLayout(isMeasurementApproachComplete = ," +
- "approachMeasure = measure)"
- )
-)
-fun Modifier.intermediateLayout(
- @Suppress("DEPRECATION")
- measure: IntermediateMeasureScope.(
- measurable: Measurable,
- constraints: Constraints,
- ) -> MeasureResult,
-) = this then IntermediateLayoutElement(measure)
-
/**
* Creates an approach layout intended to help gradually approach the destination layout calculated
* in the lookahead pass. This can be particularly helpful when the destination layout is
@@ -110,145 +84,77 @@
* [Placeable.PlacementScope.coordinates]. The sample code below illustrates how that can be
* achieved.
*
- * [isMeasurementApproachComplete] signals whether the measurement has already reached the
+ * [isMeasurementApproachInProgress] signals whether the measurement is in progress of approaching
* destination size. It will be queried after the destination has been determined by the lookahead
* pass, before [approachMeasure] is invoked. The lookahead size is provided to
- * [isMeasurementApproachComplete] for convenience in deciding whether the destination size has
+ * [isMeasurementApproachInProgress] for convenience in deciding whether the destination size has
* been reached.
*
- * [isPlacementApproachComplete] indicates whether the position has approached
+ * [isMeasurementApproachInProgress] indicates whether the position is currently approaching
* destination defined by the lookahead, hence it's a signal to the system for whether additional
- * approach placements are necessary. [isPlacementApproachComplete] will be invoked after the
+ * approach placements are necessary. [isPlacementApproachInProgress] will be invoked after the
* destination position has been determined by lookahead pass, and before the placement phase in
* [approachMeasure].
*
- * Once both [isMeasurementApproachComplete] and [isPlacementApproachComplete] return true, the
+ * Once both [isMeasurementApproachInProgress] and [isPlacementApproachInProgress] return false, the
* system may skip approach pass until additional approach passes are necessary as indicated by
- * [isMeasurementApproachComplete] and [isPlacementApproachComplete].
+ * [isMeasurementApproachInProgress] and [isPlacementApproachInProgress].
*
* **IMPORTANT**:
- * It is important to be accurate in [isPlacementApproachComplete] and
- * [isMeasurementApproachComplete]. A prolonged indication of incomplete approach will prevent the
+ * It is important to be accurate in [isPlacementApproachInProgress] and
+ * [isMeasurementApproachInProgress]. A prolonged indication of incomplete approach will prevent the
* system from potentially skipping approach pass when possible.
*
* @see ApproachLayoutModifierNode
* @sample androidx.compose.ui.samples.approachLayoutSample
*/
-@ExperimentalComposeUiApi
fun Modifier.approachLayout(
- isMeasurementApproachComplete: (lookaheadSize: IntSize) -> Boolean,
- isPlacementApproachComplete: Placeable.PlacementScope.(
+ isMeasurementApproachInProgress: (lookaheadSize: IntSize) -> Boolean,
+ isPlacementApproachInProgress: Placeable.PlacementScope.(
lookaheadCoordinates: LayoutCoordinates
- ) -> Boolean = defaultPlacementApproachComplete,
+ ) -> Boolean = defaultPlacementApproachInProgress,
approachMeasure: ApproachMeasureScope.(
measurable: Measurable,
constraints: Constraints,
) -> MeasureResult,
): Modifier = this then ApproachLayoutElement(
- isMeasurementApproachComplete = isMeasurementApproachComplete,
- isPlacementApproachComplete = isPlacementApproachComplete,
+ isMeasurementApproachInProgress = isMeasurementApproachInProgress,
+ isPlacementApproachInProgress = isPlacementApproachInProgress,
approachMeasure = approachMeasure
)
-private val defaultPlacementApproachComplete: Placeable.PlacementScope.(
+private val defaultPlacementApproachInProgress: Placeable.PlacementScope.(
lookaheadCoordinates: LayoutCoordinates
-) -> Boolean = { true }
+) -> Boolean = { false }
-@Suppress("DEPRECATION")
-@OptIn(ExperimentalComposeUiApi::class)
-private data class IntermediateLayoutElement(
- val measure: IntermediateMeasureScope.(
- measurable: Measurable,
- constraints: Constraints,
- ) -> MeasureResult,
-) : ModifierNodeElement<IntermediateLayoutModifierNodeImpl>() {
- override fun create() =
- IntermediateLayoutModifierNodeImpl(
- measure,
- )
-
- override fun update(node: IntermediateLayoutModifierNodeImpl) {
- node.measureBlock = measure
- }
-
- override fun InspectorInfo.inspectableProperties() {
- name = "intermediateLayout"
- properties["measure"] = measure
- }
-}
-
-@Suppress("DEPRECATION")
-@OptIn(ExperimentalComposeUiApi::class)
-private class IntermediateLayoutModifierNodeImpl(
- var measureBlock: IntermediateMeasureScope.(
- measurable: Measurable,
- constraints: Constraints,
- ) -> MeasureResult,
-) : ApproachLayoutModifierNode, Modifier.Node() {
- private var intermediateMeasureScope: IntermediateMeasureScopeImpl? = null
-
- private inner class IntermediateMeasureScopeImpl(
- val approachScope: ApproachMeasureScopeImpl
- ) : IntermediateMeasureScope, LookaheadScope by approachScope,
- ApproachMeasureScope by approachScope, CoroutineScope {
- override val coroutineContext: CoroutineContext
- get() = this@IntermediateLayoutModifierNodeImpl.coroutineScope.coroutineContext
- }
-
- override fun isMeasurementApproachComplete(lookaheadSize: IntSize): Boolean {
- // Important: Returning false here is strongly discouraged as it'll prevent layout
- // performance optimization. This ModifierNodeImpl is only intended to help devs transition
- // over to the new ApproachLayoutNodeModifier, and it'll be removed after a couple of
- // releases.
- return false
- }
-
- override fun ApproachMeasureScope.approachMeasure(
- measurable: Measurable,
- constraints: Constraints
- ): MeasureResult {
- val scope = intermediateMeasureScope
- val newScope = if (scope?.approachScope != this) {
- IntermediateMeasureScopeImpl(this as ApproachMeasureScopeImpl)
- } else {
- scope
- }
- intermediateMeasureScope = newScope
- return with(newScope) {
- measureBlock(measurable, constraints)
- }
- }
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
private data class ApproachLayoutElement(
val approachMeasure: ApproachMeasureScope.(
measurable: Measurable,
constraints: Constraints,
) -> MeasureResult,
- val isMeasurementApproachComplete: (IntSize) -> Boolean,
- val isPlacementApproachComplete: Placeable.PlacementScope.(
+ val isMeasurementApproachInProgress: (IntSize) -> Boolean,
+ val isPlacementApproachInProgress: Placeable.PlacementScope.(
lookaheadCoordinates: LayoutCoordinates
- ) -> Boolean = defaultPlacementApproachComplete,
+ ) -> Boolean = defaultPlacementApproachInProgress,
) : ModifierNodeElement<ApproachLayoutModifierNodeImpl>() {
override fun create() =
ApproachLayoutModifierNodeImpl(
approachMeasure,
- isMeasurementApproachComplete,
- isPlacementApproachComplete
+ isMeasurementApproachInProgress,
+ isPlacementApproachInProgress
)
override fun update(node: ApproachLayoutModifierNodeImpl) {
node.measureBlock = approachMeasure
- node.isMeasurementApproachComplete = isMeasurementApproachComplete
- node.isPlacementApproachComplete = isPlacementApproachComplete
+ node.isMeasurementApproachInProgress = isMeasurementApproachInProgress
+ node.isPlacementApproachInProgress = isPlacementApproachInProgress
}
override fun InspectorInfo.inspectableProperties() {
name = "approachLayout"
properties["approachMeasure"] = approachMeasure
- properties["isMeasurementApproachComplete"] = isMeasurementApproachComplete
- properties["isPlacementApproachComplete"] = isPlacementApproachComplete
+ properties["isMeasurementApproachInProgress"] = isMeasurementApproachInProgress
+ properties["isPlacementApproachInProgress"] = isPlacementApproachInProgress
}
}
@@ -258,18 +164,18 @@
measurable: Measurable,
constraints: Constraints,
) -> MeasureResult,
- var isMeasurementApproachComplete: (IntSize) -> Boolean,
- var isPlacementApproachComplete:
+ var isMeasurementApproachInProgress: (IntSize) -> Boolean,
+ var isPlacementApproachInProgress:
Placeable.PlacementScope.(LayoutCoordinates) -> Boolean,
) : ApproachLayoutModifierNode, Modifier.Node() {
- override fun isMeasurementApproachComplete(lookaheadSize: IntSize): Boolean {
- return isMeasurementApproachComplete.invoke(lookaheadSize)
+ override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
+ return isMeasurementApproachInProgress.invoke(lookaheadSize)
}
- override fun Placeable.PlacementScope.isPlacementApproachComplete(
+ override fun Placeable.PlacementScope.isPlacementApproachInProgress(
lookaheadCoordinates: LayoutCoordinates
): Boolean {
- return isPlacementApproachComplete.invoke(this, lookaheadCoordinates)
+ return isPlacementApproachInProgress.invoke(this, lookaheadCoordinates)
}
override fun ApproachMeasureScope.approachMeasure(
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 e491fd1e..fb22252 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
@@ -158,7 +158,7 @@
// approachMeasureScope is created/updated when layoutModifierNode is set. An
// ApproachLayoutModifierNode will lead to a non-null approachMeasureScope.
with(scope.approachNode) {
- scope.approachMeasureRequired = !isMeasurementApproachComplete(
+ scope.approachMeasureRequired = isMeasurementApproachInProgress(
scope.lookaheadSize
) || constraints != lookaheadConstraints
if (!scope.approachMeasureRequired) {
@@ -257,7 +257,7 @@
approachMeasureScope?.let {
with(it.approachNode) {
val approachComplete = with(placementScope) {
- isPlacementApproachComplete(
+ !isPlacementApproachInProgress(
lookaheadDelegate!!.lookaheadLayoutCoordinates
) && !it.approachMeasureRequired &&
size == lookaheadDelegate?.size &&
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 c0b271a..e176cc7e 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
@@ -237,6 +237,66 @@
}
/**
+ * Equivalent flag of [coordinatesAccessedDuringPlacement] but for [lookaheadPassDelegate].
+ */
+ var lookaheadCoordinatesAccessedDuringPlacement = false
+ set(value) {
+ val oldValue = field
+ if (oldValue != value) {
+ field = value
+ if (value && !lookaheadCoordinatesAccessedDuringModifierPlacement) {
+ // if first out of both flags changes to true increment
+ childrenAccessingLookaheadCoordinatesDuringPlacement++
+ } else if (!value && !lookaheadCoordinatesAccessedDuringModifierPlacement) {
+ // if both flags changes to false decrement
+ childrenAccessingLookaheadCoordinatesDuringPlacement--
+ }
+ }
+ }
+
+ /**
+ * Equivalent flag of [coordinatesAccessedDuringModifierPlacement] but for
+ * [lookaheadPassDelegate].
+ */
+ var lookaheadCoordinatesAccessedDuringModifierPlacement = false
+ set(value) {
+ val oldValue = field
+ if (oldValue != value) {
+ field = value
+ if (value && !lookaheadCoordinatesAccessedDuringPlacement) {
+ // if first out of both flags changes to true increment
+ childrenAccessingLookaheadCoordinatesDuringPlacement++
+ } else if (!value && !lookaheadCoordinatesAccessedDuringPlacement) {
+ // if both flags changes to false decrement
+ childrenAccessingLookaheadCoordinatesDuringPlacement--
+ }
+ }
+ }
+
+ /**
+ * Equivalent flag of [childrenAccessingCoordinatesDuringPlacement] but for
+ * [lookaheadPassDelegate].
+ *
+ * Naturally, this flag should only be affected by the lookahead coordinates access flags.
+ */
+ var childrenAccessingLookaheadCoordinatesDuringPlacement = 0
+ set(value) {
+ val oldValue = field
+ field = value
+ if ((oldValue == 0) != (value == 0)) {
+ // A child is either newly listening for coordinates or stopped listening
+ val parentLayoutDelegate = layoutNode.parent?.layoutDelegate
+ if (parentLayoutDelegate != null) {
+ if (value == 0) {
+ parentLayoutDelegate.childrenAccessingLookaheadCoordinatesDuringPlacement--
+ } else {
+ parentLayoutDelegate.childrenAccessingLookaheadCoordinatesDuringPlacement++
+ }
+ }
+ }
+ }
+
+ /**
* measurePassDelegate manages the measure/layout and alignmentLine related queries for the
* actual measure/layout pass.
*/
@@ -266,11 +326,10 @@
}
}
if (state == LayoutState.LookaheadLayingOut) {
- // TODO lookahead should have its own flags b/284153462
if (lookaheadPassDelegate?.layingOutChildren == true) {
- coordinatesAccessedDuringPlacement = true
+ lookaheadCoordinatesAccessedDuringPlacement = true
} else {
- coordinatesAccessedDuringModifierPlacement = true
+ lookaheadCoordinatesAccessedDuringModifierPlacement = true
}
}
}
@@ -1135,7 +1194,7 @@
val oldLayoutState = layoutState
layoutState = LayoutState.LookaheadLayingOut
val owner = layoutNode.requireOwner()
- coordinatesAccessedDuringPlacement = false
+ lookaheadCoordinatesAccessedDuringPlacement = false
owner.snapshotObserver.observeLayoutSnapshotReads(layoutNode) {
clearPlaceOrder()
forEachChildAlignmentLinesOwner { child ->
@@ -1160,7 +1219,7 @@
}
}
layoutState = oldLayoutState
- if (coordinatesAccessedDuringPlacement &&
+ if (lookaheadCoordinatesAccessedDuringPlacement &&
lookaheadDelegate.isPlacingForAlignment
) {
requestLayout()
@@ -1242,17 +1301,18 @@
* parents change their position on the same frame), it might be worth using a flag
* so that this call becomes cheap after the first one.
*/
- fun notifyChildrenUsingCoordinatesWhilePlacing() {
- if (childrenAccessingCoordinatesDuringPlacement > 0) {
+ fun notifyChildrenUsingLookaheadCoordinatesWhilePlacing() {
+ if (childrenAccessingLookaheadCoordinatesDuringPlacement > 0) {
layoutNode.forEachChild { child ->
val childLayoutDelegate = child.layoutDelegate
- val accessed = childLayoutDelegate.coordinatesAccessedDuringPlacement ||
- childLayoutDelegate.coordinatesAccessedDuringModifierPlacement
- if (accessed && !childLayoutDelegate.layoutPending) {
+ val accessed =
+ childLayoutDelegate.lookaheadCoordinatesAccessedDuringPlacement ||
+ childLayoutDelegate.lookaheadCoordinatesAccessedDuringModifierPlacement
+ if (accessed && !childLayoutDelegate.lookaheadLayoutPending) {
child.requestLookaheadRelayout()
}
childLayoutDelegate.lookaheadPassDelegate
- ?.notifyChildrenUsingCoordinatesWhilePlacing()
+ ?.notifyChildrenUsingLookaheadCoordinatesWhilePlacing()
}
}
}
@@ -1383,12 +1443,12 @@
placedOnce = true
onNodePlacedCalled = false
if (position != lastPosition) {
- if (coordinatesAccessedDuringModifierPlacement ||
- coordinatesAccessedDuringPlacement
+ if (lookaheadCoordinatesAccessedDuringModifierPlacement ||
+ lookaheadCoordinatesAccessedDuringPlacement
) {
lookaheadLayoutPending = true
}
- notifyChildrenUsingCoordinatesWhilePlacing()
+ notifyChildrenUsingLookaheadCoordinatesWhilePlacing()
}
val owner = layoutNode.requireOwner()
@@ -1396,7 +1456,7 @@
outerCoordinator.lookaheadDelegate!!.placeSelfApparentToRealOffset(position)
onNodePlaced()
} else {
- coordinatesAccessedDuringModifierPlacement = false
+ lookaheadCoordinatesAccessedDuringModifierPlacement = false
alignmentLines.usedByModifierLayout = false
owner.snapshotObserver.observeLayoutModifierSnapshotReads(layoutNode) {
val scope = if (layoutNode.isOutMostLookaheadRoot()) {
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 2c70aab..da054fe 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
@@ -87,8 +87,10 @@
abstract fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int
- // True when the coordinator is running its own placing block to obtain the position
- // in parent, but is not interested in the position of children.
+ /**
+ * True when the coordinator is running its own placing block to obtain the position
+ * in parent, but is not interested in the position of children.
+ */
internal var isShallowPlacing: Boolean = false
internal abstract val measureResult: MeasureResult
internal abstract fun replace()
@@ -415,7 +417,7 @@
if (this.position != position) {
this.position = position
layoutNode.layoutDelegate.lookaheadPassDelegate
- ?.notifyChildrenUsingCoordinatesWhilePlacing()
+ ?.notifyChildrenUsingLookaheadCoordinatesWhilePlacing()
coordinator.invalidateAlignmentLinesFromPositionChange()
}
if (!isPlacingForAlignment) {
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 380b4b4..6f94830 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
@@ -832,6 +832,7 @@
relativeToSource: Offset
): Offset {
if (sourceCoordinates is LookaheadLayoutCoordinates) {
+ sourceCoordinates.coordinator.onCoordinatesUsed()
return -sourceCoordinates.localPositionOf(this, -relativeToSource)
}
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/draw/DesktopDrawingPrebuiltGraphicsLayerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/draw/DesktopDrawingPrebuiltGraphicsLayerTest.kt
index 00083ea..daeb08a 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/draw/DesktopDrawingPrebuiltGraphicsLayerTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/draw/DesktopDrawingPrebuiltGraphicsLayerTest.kt
@@ -332,7 +332,7 @@
layer: GraphicsLayer = obtainLayer()
): Modifier {
return drawWithContent {
- layer.buildLayer {
+ layer.record {
this@drawWithContent.drawContent()
}
drawLayer(layer)
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/carousel/CarouselSwipeable.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/carousel/CarouselSwipeable.kt
index 4a1d531..b6ebbda 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/carousel/CarouselSwipeable.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/carousel/CarouselSwipeable.kt
@@ -831,7 +831,7 @@
get() = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
- return if (delta < 0 && source == NestedScrollSource.Drag) {
+ return if (delta < 0 && source == NestedScrollSource.UserInput) {
performDrag(delta).toOffset()
} else {
Offset.Zero
@@ -843,7 +843,7 @@
available: Offset,
source: NestedScrollSource
): Offset {
- return if (source == NestedScrollSource.Drag) {
+ return if (source == NestedScrollSource.UserInput) {
performDrag(available.toFloat()).toOffset()
} else {
Offset.Zero
diff --git a/core/core-i18n/lint-baseline.xml b/core/core-i18n/lint-baseline.xml
deleted file mode 100644
index 25cd21f..0000000
--- a/core/core-i18n/lint-baseline.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.4.0-alpha12" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha12)" variant="all" version="8.4.0-alpha12">
-
- <issue
- id="PrereleaseSdkCoreDependency"
- message="Prelease SDK check isAtLeastV cannot be called as this project has a versioned dependency on androidx.core:core"
- errorLine1=" "V: " + BuildCompat.isAtLeastV()"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/androidTest/java/androidx/core/i18n/DateTimeFormatterTest.kt"/>
- </issue>
-
-</issues>
diff --git a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
index a750a9b..1e24dd7 100644
--- a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
+++ b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
@@ -425,9 +425,11 @@
override fun install() {
setPostSplashScreenTheme(activity.theme, TypedValue())
- (activity.window.decorView as ViewGroup).setOnHierarchyChangeListener(
- hierarchyListener
- )
+ if (SDK_INT < 33) {
+ (activity.window.decorView as ViewGroup).setOnHierarchyChangeListener(
+ hierarchyListener
+ )
+ }
}
override fun setKeepOnScreenCondition(keepOnScreenCondition: KeepOnScreenCondition) {
@@ -454,7 +456,9 @@
exitAnimationListener: OnExitAnimationListener
) {
activity.splashScreen.setOnExitAnimationListener { splashScreenView ->
- applyAppSystemUiTheme()
+ if (SDK_INT < 33) {
+ applyAppSystemUiTheme()
+ }
val splashScreenViewProvider = SplashScreenViewProvider(splashScreenView, activity)
exitAnimationListener.onSplashScreenExit(splashScreenViewProvider)
}
diff --git a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreenViewProvider.kt b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreenViewProvider.kt
index 7820518..1ce8a15 100644
--- a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreenViewProvider.kt
+++ b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreenViewProvider.kt
@@ -133,10 +133,12 @@
override fun remove() {
platformView.remove()
- ThemeUtils.Api31.applyThemesSystemBarAppearance(
- activity.theme,
- activity.window.decorView
- )
+ if (Build.VERSION.SDK_INT < 33) {
+ ThemeUtils.Api31.applyThemesSystemBarAppearance(
+ activity.theme,
+ activity.window.decorView
+ )
+ }
}
}
}
diff --git a/core/core-splashscreen/src/main/res/values-night-v33/styles.xml b/core/core-splashscreen/src/main/res/values-night-v33/styles.xml
new file mode 100644
index 0000000..eb3f34e
--- /dev/null
+++ b/core/core-splashscreen/src/main/res/values-night-v33/styles.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<resources>
+ <style name="Base.Theme.SplashScreen.DayNight" parent="Base.Theme.SplashScreen">
+ <item name="android:windowLightNavigationBar">false</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/res/values-v33/styles.xml b/core/core-splashscreen/src/main/res/values-v33/styles.xml
new file mode 100644
index 0000000..6baf91ac
--- /dev/null
+++ b/core/core-splashscreen/src/main/res/values-v33/styles.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<resources>
+ <style name="Base.Theme.SplashScreen.DayNight" parent="Base.Theme.SplashScreen">
+ <item name="android:enforceNavigationBarContrast">true</item>
+ <item name="android:windowLightNavigationBar">true</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt
index 9de9603..dd37ec0 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/CallingMainActivity.kt
@@ -18,8 +18,6 @@
import android.annotation.SuppressLint
import android.app.Activity
-import android.media.AudioManager.AudioRecordingCallback
-import android.media.AudioRecord
import android.os.Bundle
import android.telecom.DisconnectCause
import android.util.Log
@@ -31,7 +29,6 @@
import androidx.core.view.WindowCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
-import java.util.concurrent.Executors
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -47,10 +44,6 @@
// Telecom
private var mCallsManager: CallsManager? = null
- // Audio Record
- private var mAudioRecord: AudioRecord? = null
- private var mAudioRecordingCallback: AudioRecordingCallback? = null
-
// Call Log objects
private var mRecyclerView: RecyclerView? = null
private var mCallObjects: ArrayList<CallRow> = ArrayList()
@@ -75,7 +68,6 @@
val addOutgoingCallButton = findViewById<Button>(R.id.addOutgoingCall)
addOutgoingCallButton.setOnClickListener {
mScope.launch {
- startAudioRecording()
addCallWithAttributes(Utilities.OUTGOING_CALL_ATTRIBUTES)
}
}
@@ -83,14 +75,12 @@
val addIncomingCallButton = findViewById<Button>(R.id.addIncomingCall)
addIncomingCallButton.setOnClickListener {
mScope.launch {
- startAudioRecording()
addCallWithAttributes(Utilities.INCOMING_CALL_ATTRIBUTES)
}
}
// Set up AudioRecord
- mAudioRecord = Utilities.createAudioRecord(applicationContext, this)
- mAdapter = CallListAdapter(mCallObjects, mAudioRecord)
+ mAdapter = CallListAdapter(mCallObjects, null)
// set up the call list view holder
mRecyclerView = findViewById(R.id.callListRecyclerView)
@@ -109,10 +99,6 @@
}
}
}
-
- // Clean up AudioRecord
- mAudioRecord?.release()
- mAudioRecord = null
}
@SuppressLint("WrongConstant")
@@ -198,12 +184,4 @@
mAdapter.notifyDataSetChanged()
}
}
-
- private fun startAudioRecording() {
- mAudioRecordingCallback = Utilities.TelecomAudioRecordingCallback(mAudioRecord!!)
- mAudioRecord?.registerAudioRecordingCallback(
- Executors.newSingleThreadExecutor(), mAudioRecordingCallback!!)
- mAdapter.mAudioRecordingCallback = mAudioRecordingCallback
- mAudioRecord?.startRecording()
- }
}
diff --git a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/Utilities.kt b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/Utilities.kt
index aa670ea..005cbed 100644
--- a/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/Utilities.kt
+++ b/core/core-telecom/integration-tests/testapp/src/main/java/androidx/core/telecom/test/Utilities.kt
@@ -64,7 +64,7 @@
// Audio recording config constants
private const val SAMPLE_RATE = 44100
- private const val AUDIO_SOURCE = MediaRecorder.AudioSource.CAMCORDER
+ private const val AUDIO_SOURCE = MediaRecorder.AudioSource.VOICE_COMMUNICATION
private const val CHANNEL_COUNT = 1
private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
private const val RECORD_AUDIO_REQUEST_CODE = 200
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/JetpackConnectionServiceTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/JetpackConnectionServiceTest.kt
index 116618f..0c199f0 100644
--- a/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/JetpackConnectionServiceTest.kt
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/test/JetpackConnectionServiceTest.kt
@@ -17,6 +17,7 @@
package androidx.core.telecom.test
import android.os.Build.VERSION_CODES
+import android.os.Bundle
import android.telecom.Connection
import android.telecom.ConnectionRequest
import androidx.annotation.RequiresApi
@@ -126,6 +127,54 @@
}
/**
+ * ensure JetpackConnectionService#onCreateOutgoingConnection does not throw an exception if
+ * any of the arguments are null.
+ */
+ @SmallTest
+ @Test
+ fun testOnCreateOutgoingConnectionWithNullArgs() {
+ mConnectionService.onCreateOutgoingConnection(
+ null /* connectionManagerPhoneAccount */,
+ null /* request */)
+ }
+
+ /**
+ * ensure JetpackConnectionService#onCreateOutgoingConnectionFailed does not throw an exception
+ * if any of the arguments are null.
+ */
+ @SmallTest
+ @Test
+ fun testOnCreateOutgoingConnectionFailedWithNullArgs() {
+ mConnectionService.onCreateOutgoingConnectionFailed(
+ null /* connectionManagerPhoneAccount */,
+ null /* request */)
+ }
+
+ /**
+ * ensure JetpackConnectionService#onCreateIncomingConnection does not throw an exception
+ * if any of the arguments are null.
+ */
+ @SmallTest
+ @Test
+ fun testOnCreateIncomingConnectionWithNullArgs() {
+ mConnectionService.onCreateIncomingConnection(
+ null /* connectionManagerPhoneAccount */,
+ null /* request */)
+ }
+
+ /**
+ * ensure JetpackConnectionService#onCreateIncomingConnectionFailed does not throw an exception
+ * if any of the arguments are null.
+ */
+ @SmallTest
+ @Test
+ fun testOnCreateIncomingConnectionFailedWithNullArgs() {
+ mConnectionService.onCreateIncomingConnectionFailed(
+ null /* connectionManagerPhoneAccount */,
+ null /* request */)
+ }
+
+ /**
* Ensure an outgoing Connection object has its extras set before sending it off to the
* platform.
*/
@@ -172,7 +221,13 @@
private fun createConnectionRequest(callAttributesCompat: CallAttributesCompat):
ConnectionRequest {
// wrap in PendingRequest
+ val pendingRequestId = "123"
+ val pendingRequestIdBundle = Bundle()
+ pendingRequestIdBundle.putString(
+ JetpackConnectionService.REQUEST_ID_MATCHER_KEY, pendingRequestId)
+
val pr = JetpackConnectionService.PendingConnectionRequest(
+ pendingRequestId,
callAttributesCompat, callChannels, mWorkerContext, null,
TestUtils.mOnAnswerLambda,
TestUtils.mOnDisconnectLambda,
@@ -184,6 +239,9 @@
// add to the list of pendingRequests
JetpackConnectionService.mPendingConnectionRequests.add(pr)
// create a ConnectionRequest
- return ConnectionRequest(mPackagePhoneAccountHandle, TEST_PHONE_NUMBER_9001, null)
+ return ConnectionRequest(
+ mPackagePhoneAccountHandle,
+ TEST_PHONE_NUMBER_9001,
+ pendingRequestIdBundle)
}
}
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
index d5224e3..f950cc4 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
@@ -39,6 +39,7 @@
import androidx.core.telecom.internal.CallSessionLegacy
import androidx.core.telecom.internal.JetpackConnectionService
import androidx.core.telecom.internal.utils.Utils
+import java.util.UUID
import java.util.concurrent.CancellationException
import java.util.concurrent.Executor
import kotlin.coroutines.coroutineContext
@@ -392,6 +393,7 @@
CompletableDeferred<CallSessionLegacy>(parent = coroutineContext.job)
val request = JetpackConnectionService.PendingConnectionRequest(
+ UUID.randomUUID().toString(),
callAttributes,
callChannels,
coroutineContext,
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt
index c7351e7..d26a399 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt
@@ -25,22 +25,37 @@
import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager
import android.telecom.VideoProfile
+import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.core.telecom.CallAttributesCompat
import androidx.core.telecom.CallsManager
+import androidx.core.telecom.CallsManager.Companion.CALL_CREATION_FAILURE_MSG
import androidx.core.telecom.extensions.voip.VoipExtensionManager
import androidx.core.telecom.internal.utils.Utils
import java.util.UUID
import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.CompletableDeferred
@RequiresApi(api = Build.VERSION_CODES.O)
internal class JetpackConnectionService : ConnectionService() {
+ private val TAG = JetpackConnectionService::class.java.simpleName
+
/**
* Wrap all the objects that are associated with a new CallSession request into a class
*/
data class PendingConnectionRequest(
+ /**
+ * requestIdMatcher - is important for matching requests sent to the platform via
+ * TelecomManage#placeCall(...,extras) or TelecomManager#addIncomingCall(..., extras)
+ * and receiving the same platform request (shortly after) via
+ * ConnectionService#onOutgoingConnection*(...,request.extras) and
+ * ConnectionService#onIncomingConnection*(...,request.extras). Without this, there is no
+ * way to match client CallsManager#addCall requests to Connections the ConnectionService
+ * gets from the platform.
+ */
+ val requestIdMatcher: String,
val callAttributes: CallAttributesCompat,
val callChannel: CallChannels,
val coroutineContext: CoroutineContext,
@@ -54,6 +69,8 @@
)
companion object {
+ const val REQUEST_ID_MATCHER_KEY = "JetpackConnectionService_requestIdMatcher_key"
+ const val KEY_NOT_FOUND = "requestIdMatcher KEY NOT FOUND"
const val CONNECTION_CREATION_TIMEOUT: Long = 5000 // time in milli-seconds
var mPendingConnectionRequests: ArrayList<PendingConnectionRequest> = ArrayList()
}
@@ -68,7 +85,10 @@
telecomManager: TelecomManager,
pendingConnectionRequest: PendingConnectionRequest,
) {
- // add request to list
+ Log.i(TAG, "CreationConnectionRequest:" +
+ " requestIdMatcher=[${pendingConnectionRequest.requestIdMatcher}]" +
+ " phoneAccountHandle=[${pendingConnectionRequest.callAttributes.mHandle}]")
+
mPendingConnectionRequests.add(pendingConnectionRequest)
val extras = Utils.getBundleWithPhoneAccountHandle(
@@ -76,13 +96,18 @@
pendingConnectionRequest.callAttributes.mHandle!!
)
+ val idBundle = Bundle()
+ idBundle.putString(REQUEST_ID_MATCHER_KEY, pendingConnectionRequest.requestIdMatcher)
+
// Call into the platform to start call
if (pendingConnectionRequest.callAttributes.isOutgoingCall()) {
+ extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, idBundle)
telecomManager.placeCall(
pendingConnectionRequest.callAttributes.address,
extras
)
} else {
+ extras.putBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, idBundle)
telecomManager.addNewIncomingCall(
pendingConnectionRequest.callAttributes.mHandle,
extras
@@ -94,9 +119,24 @@
* Outgoing Connections
*/
override fun onCreateOutgoingConnection(
- connectionManagerAccount: PhoneAccountHandle,
- request: ConnectionRequest
+ connectionManagerAccount: PhoneAccountHandle?,
+ request: ConnectionRequest?
): Connection? {
+ Log.i(TAG, "onCreateOutgoingConnection: " +
+ "connectionMgrAcct=[$connectionManagerAccount], request=[$request]")
+ if (request == null) {
+ // if the Platform provides a null request, there is no way to complete the new request
+ // for a backwards compat call. In this event, Core-Telecom needs to return a failed
+ // Connection to platform to end the call and ensure Telecom is left in a good state.
+ // The application will hit a timeout for the new addCall request and any other
+ // CallSessions will be unaffected.
+ return Connection.createFailedConnection(
+ DisconnectCause(
+ DisconnectCause.ERROR,
+ "ConnectionRequest is null, cannot complete the addCall request"
+ )
+ )
+ }
return createSelfManagedConnection(
request,
CallAttributesCompat.DIRECTION_OUTGOING
@@ -104,26 +144,45 @@
}
override fun onCreateOutgoingConnectionFailed(
- connectionManagerPhoneAccount: PhoneAccountHandle,
- request: ConnectionRequest
+ connectionManagerPhoneAccount: PhoneAccountHandle?,
+ request: ConnectionRequest?
) {
- val pendingRequest: PendingConnectionRequest? =
- findTargetPendingConnectionRequest(
- request,
- CallAttributesCompat.DIRECTION_OUTGOING
- )
- pendingRequest?.completableDeferred?.cancel()
-
+ Log.i(TAG, "onCreateOutgoingConnectionFailed: " +
+ "connectionMgrAcct=[$connectionManagerPhoneAccount], request=[$request]")
+ if (request == null) {
+ return
+ }
+ val pendingRequest: PendingConnectionRequest? = getPendingConnectionRequest(request)
mPendingConnectionRequests.remove(pendingRequest)
+ // Immediately throw a CancellationException out to the client to inform the Voip app that
+ // that call session cannot be created INSTEAD of waiting for the timeout. Otherwise, if the
+ // request is null, a timeout exception will be thrown.
+ pendingRequest?.completableDeferred?.cancel(
+ CancellationException(CALL_CREATION_FAILURE_MSG))
}
/**
* Incoming Connections
*/
override fun onCreateIncomingConnection(
- connectionManagerPhoneAccount: PhoneAccountHandle,
- request: ConnectionRequest
+ connectionManagerPhoneAccount: PhoneAccountHandle?,
+ request: ConnectionRequest?
): Connection? {
+ Log.i(TAG, "onCreateIncomingConnection:" +
+ " connectionManagerPhoneAccount=[$connectionManagerPhoneAccount], request=[$request]")
+ if (request == null) {
+ // if the Platform provides a null request, there is no way to complete the new request
+ // for a backwards compat call. In this event, Core-Telecom needs to return a failed
+ // Connection to platform to end the call and ensure Telecom is left in a good state.
+ // The application will hit a timeout for the new addCall request and any other
+ // CallSessions will be unaffected.
+ return Connection.createFailedConnection(
+ DisconnectCause(
+ DisconnectCause.ERROR,
+ "ConnectionRequest is null, cannot complete the addCall request"
+ )
+ )
+ }
return createSelfManagedConnection(
request,
CallAttributesCompat.DIRECTION_INCOMING
@@ -131,22 +190,30 @@
}
override fun onCreateIncomingConnectionFailed(
- connectionManagerPhoneAccount: PhoneAccountHandle,
- request: ConnectionRequest
+ connectionManagerPhoneAccount: PhoneAccountHandle?,
+ request: ConnectionRequest?
) {
- val pendingRequest: PendingConnectionRequest? =
- findTargetPendingConnectionRequest(
- request,
- CallAttributesCompat.DIRECTION_INCOMING
- )
- pendingRequest?.completableDeferred?.cancel()
+ Log.i(TAG, "onCreateIncomingConnectionFailed: " +
+ "connectionMgrAcct=[$connectionManagerPhoneAccount], request=[$request]")
+ if (request == null) {
+ return
+ }
+ val pendingRequest: PendingConnectionRequest? = getPendingConnectionRequest(request)
mPendingConnectionRequests.remove(pendingRequest)
+ // Immediately throw a CancellationException out to the client to inform the Voip app that
+ // that call session cannot be created INSTEAD of waiting for the timeout. Otherwise, if the
+ // request is null, a timeout exception will be thrown.
+ pendingRequest?.completableDeferred?.cancel(
+ CancellationException(CALL_CREATION_FAILURE_MSG))
}
+ /**
+ * Helper methods
+ */
internal fun createSelfManagedConnection(request: ConnectionRequest, direction: Int):
Connection? {
val targetRequest: PendingConnectionRequest =
- findTargetPendingConnectionRequest(request, direction) ?: return null
+ getPendingConnectionRequest(request) ?: return null
val jetpackConnection = CallSessionLegacy(
ParcelUuid.fromString(UUID.randomUUID().toString()),
@@ -208,36 +275,47 @@
return jetpackConnection
}
- /**
- * Helper methods
- */
- private fun findTargetPendingConnectionRequest(
- request: ConnectionRequest,
- direction: Int
- ): PendingConnectionRequest? {
+ private fun getPendingConnectionRequest(request: ConnectionRequest): PendingConnectionRequest? {
+ if (request.extras == null) {
+ Log.w(TAG, "no extras bundle found in the request")
+ return null
+ }
+ val targetId = getPlatformConnectionRequestId(request.extras)
+ if (targetId.equals(KEY_NOT_FOUND)) {
+ return getFirstPendingRequestFromApp(request) // return the first pending request
+ // as it is likely the application is not making multiple calls in parallel
+ }
for (pendingConnectionRequest in mPendingConnectionRequests) {
- if (isSameAddress(pendingConnectionRequest.callAttributes, request) &&
- isSameDirection(pendingConnectionRequest.callAttributes, direction) &&
- isSameHandle(pendingConnectionRequest.callAttributes.mHandle, request)
- ) {
+ Log.i(TAG, "targId=$targetId, currId=${pendingConnectionRequest.requestIdMatcher}")
+ if (pendingConnectionRequest.requestIdMatcher.equals(targetId)) {
+ return pendingConnectionRequest
+ }
+ }
+ Log.w(TAG, "request did not match any pending request elements")
+ return getFirstPendingRequestFromApp(request) // return the first pending request
+ // as it is likely the application is not making multiple calls in parallel
+ }
+
+ private fun getPlatformConnectionRequestId(extras: Bundle): String {
+ if (extras.containsKey(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS)) {
+ val incomingCallExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS)
+ if (incomingCallExtras == null) {
+ Log.w(TAG, "request did not match any pending request elements")
+ return KEY_NOT_FOUND
+ }
+ return incomingCallExtras.getString(REQUEST_ID_MATCHER_KEY, KEY_NOT_FOUND)
+ } else {
+ return extras.getString(REQUEST_ID_MATCHER_KEY, KEY_NOT_FOUND)
+ }
+ }
+
+ private fun getFirstPendingRequestFromApp(request: ConnectionRequest):
+ PendingConnectionRequest? {
+ for (pendingConnectionRequest in mPendingConnectionRequests) {
+ if (request.accountHandle.equals(pendingConnectionRequest.callAttributes.mHandle)) {
return pendingConnectionRequest
}
}
return null
}
-
- private fun isSameDirection(callAttributes: CallAttributesCompat, direction: Int): Boolean {
- return (callAttributes.direction == direction)
- }
-
- private fun isSameAddress(
- callAttributes: CallAttributesCompat,
- request: ConnectionRequest
- ): Boolean {
- return request.address?.equals(callAttributes.address) ?: false
- }
-
- private fun isSameHandle(handle: PhoneAccountHandle?, request: ConnectionRequest): Boolean {
- return request.accountHandle?.equals(handle) ?: false
- }
}
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index 11c7e35..de4ac69 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -5,8 +5,8 @@
method public static String capabilityToString(int);
method public static String feedbackTypeToString(int);
method public static String? flagToString(int);
- method public static int getCapabilities(android.accessibilityservice.AccessibilityServiceInfo);
- method public static String? loadDescription(android.accessibilityservice.AccessibilityServiceInfo, android.content.pm.PackageManager);
+ method @Deprecated public static int getCapabilities(android.accessibilityservice.AccessibilityServiceInfo);
+ method @Deprecated public static String? loadDescription(android.accessibilityservice.AccessibilityServiceInfo, android.content.pm.PackageManager);
field public static final int CAPABILITY_CAN_FILTER_KEY_EVENTS = 8; // 0x8
field public static final int CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 4; // 0x4
field public static final int CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION = 2; // 0x2
@@ -56,7 +56,7 @@
}
public final class ActivityManagerCompat {
- method public static boolean isLowRamDevice(android.app.ActivityManager);
+ method @Deprecated public static boolean isLowRamDevice(android.app.ActivityManager);
}
public class ActivityOptionsCompat {
@@ -83,7 +83,7 @@
method public static boolean canScheduleExactAlarms(android.app.AlarmManager);
method public static void setAlarmClock(android.app.AlarmManager, long, android.app.PendingIntent, android.app.PendingIntent);
method public static void setAndAllowWhileIdle(android.app.AlarmManager, int, long, android.app.PendingIntent);
- method public static void setExact(android.app.AlarmManager, int, long, android.app.PendingIntent);
+ method @Deprecated public static void setExact(android.app.AlarmManager, int, long, android.app.PendingIntent);
method public static void setExactAndAllowWhileIdle(android.app.AlarmManager, int, long, android.app.PendingIntent);
}
@@ -199,8 +199,8 @@
method public static String? getParentActivityName(android.app.Activity);
method public static String? getParentActivityName(android.content.Context, android.content.ComponentName) throws android.content.pm.PackageManager.NameNotFoundException;
method public static void navigateUpFromSameTask(android.app.Activity);
- method public static void navigateUpTo(android.app.Activity, android.content.Intent);
- method public static boolean shouldUpRecreateTask(android.app.Activity, android.content.Intent);
+ method @Deprecated public static void navigateUpTo(android.app.Activity, android.content.Intent);
+ method @Deprecated public static boolean shouldUpRecreateTask(android.app.Activity, android.content.Intent);
field public static final String PARENT_ACTIVITY = "android.support.PARENT_ACTIVITY";
}
@@ -273,7 +273,7 @@
method public static CharSequence? getContentInfo(android.app.Notification);
method public static CharSequence? getContentText(android.app.Notification);
method public static CharSequence? getContentTitle(android.app.Notification);
- method public static android.os.Bundle? getExtras(android.app.Notification);
+ method @Deprecated public static android.os.Bundle? getExtras(android.app.Notification);
method public static String? getGroup(android.app.Notification);
method public static int getGroupAlertBehavior(android.app.Notification);
method @RequiresApi(21) public static java.util.List<androidx.core.app.NotificationCompat.Action!> getInvisibleActions(android.app.Notification);
@@ -1109,11 +1109,11 @@
method public static java.io.File? getDataDir(android.content.Context);
method public static android.view.Display getDisplayOrDefault(@DisplayContext android.content.Context);
method public static android.graphics.drawable.Drawable? getDrawable(android.content.Context, @DrawableRes int);
- method public static java.io.File![] getExternalCacheDirs(android.content.Context);
- method public static java.io.File![] getExternalFilesDirs(android.content.Context, String?);
+ method @Deprecated public static java.io.File![] getExternalCacheDirs(android.content.Context);
+ method @Deprecated public static java.io.File![] getExternalFilesDirs(android.content.Context, String?);
method public static java.util.concurrent.Executor getMainExecutor(android.content.Context);
method public static java.io.File? getNoBackupFilesDir(android.content.Context);
- method public static java.io.File![] getObbDirs(android.content.Context);
+ method @Deprecated public static java.io.File![] getObbDirs(android.content.Context);
method public static String getString(android.content.Context, int);
method public static <T> T? getSystemService(android.content.Context, Class<T!>);
method public static String? getSystemServiceName(android.content.Context, Class<?>);
@@ -1122,7 +1122,7 @@
method public static android.content.Intent? registerReceiver(android.content.Context, android.content.BroadcastReceiver?, android.content.IntentFilter, String?, android.os.Handler?, int);
method public static boolean startActivities(android.content.Context, android.content.Intent![]);
method public static boolean startActivities(android.content.Context, android.content.Intent![], android.os.Bundle?);
- method public static void startActivity(android.content.Context, android.content.Intent, android.os.Bundle?);
+ method @Deprecated public static void startActivity(android.content.Context, android.content.Intent, android.os.Bundle?);
method public static void startForegroundService(android.content.Context, android.content.Intent);
field public static final int RECEIVER_EXPORTED = 2; // 0x2
field public static final int RECEIVER_NOT_EXPORTED = 4; // 0x4
@@ -1428,9 +1428,9 @@
public final class BitmapCompat {
method public static android.graphics.Bitmap createScaledBitmap(android.graphics.Bitmap, int, int, android.graphics.Rect?, boolean);
- method public static int getAllocationByteCount(android.graphics.Bitmap);
- method public static boolean hasMipMap(android.graphics.Bitmap);
- method public static void setHasMipMap(android.graphics.Bitmap, boolean);
+ method @Deprecated public static int getAllocationByteCount(android.graphics.Bitmap);
+ method @Deprecated public static boolean hasMipMap(android.graphics.Bitmap);
+ method @Deprecated public static void setHasMipMap(android.graphics.Bitmap, boolean);
}
public class BlendModeColorFilterCompat {
@@ -1558,13 +1558,13 @@
method public static void applyTheme(android.graphics.drawable.Drawable, android.content.res.Resources.Theme);
method public static boolean canApplyTheme(android.graphics.drawable.Drawable);
method public static void clearColorFilter(android.graphics.drawable.Drawable);
- method public static int getAlpha(android.graphics.drawable.Drawable);
+ method @Deprecated public static int getAlpha(android.graphics.drawable.Drawable);
method public static android.graphics.ColorFilter? getColorFilter(android.graphics.drawable.Drawable);
method public static int getLayoutDirection(android.graphics.drawable.Drawable);
method public static void inflate(android.graphics.drawable.Drawable, android.content.res.Resources, org.xmlpull.v1.XmlPullParser, android.util.AttributeSet, android.content.res.Resources.Theme?) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
- method public static boolean isAutoMirrored(android.graphics.drawable.Drawable);
+ method @Deprecated public static boolean isAutoMirrored(android.graphics.drawable.Drawable);
method @Deprecated public static void jumpToCurrentState(android.graphics.drawable.Drawable);
- method public static void setAutoMirrored(android.graphics.drawable.Drawable, boolean);
+ method @Deprecated public static void setAutoMirrored(android.graphics.drawable.Drawable, boolean);
method public static void setHotspot(android.graphics.drawable.Drawable, float, float);
method public static void setHotspotBounds(android.graphics.drawable.Drawable, int, int, int, int);
method public static boolean setLayoutDirection(android.graphics.drawable.Drawable, int);
@@ -1690,7 +1690,7 @@
public final class LocationCompat {
method public static float getBearingAccuracyDegrees(android.location.Location);
method public static long getElapsedRealtimeMillis(android.location.Location);
- method public static long getElapsedRealtimeNanos(android.location.Location);
+ method @Deprecated public static long getElapsedRealtimeNanos(android.location.Location);
method @FloatRange(from=0.0) public static float getMslAltitudeAccuracyMeters(android.location.Location);
method public static double getMslAltitudeMeters(android.location.Location);
method public static float getSpeedAccuracyMetersPerSecond(android.location.Location);
@@ -1700,7 +1700,7 @@
method public static boolean hasMslAltitudeAccuracy(android.location.Location);
method public static boolean hasSpeedAccuracy(android.location.Location);
method public static boolean hasVerticalAccuracy(android.location.Location);
- method public static boolean isMock(android.location.Location);
+ method @Deprecated public static boolean isMock(android.location.Location);
method public static void removeBearingAccuracy(android.location.Location);
method public static void removeMslAltitude(android.location.Location);
method public static void removeMslAltitudeAccuracy(android.location.Location);
@@ -1803,7 +1803,7 @@
public final class ConnectivityManagerCompat {
method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public static android.net.NetworkInfo? getNetworkInfoFromBroadcast(android.net.ConnectivityManager, android.content.Intent);
method public static int getRestrictBackgroundStatus(android.net.ConnectivityManager);
- method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public static boolean isActiveNetworkMetered(android.net.ConnectivityManager);
+ method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public static boolean isActiveNetworkMetered(android.net.ConnectivityManager);
field public static final int RESTRICT_BACKGROUND_STATUS_DISABLED = 1; // 0x1
field public static final int RESTRICT_BACKGROUND_STATUS_ENABLED = 3; // 0x3
field public static final int RESTRICT_BACKGROUND_STATUS_WHITELISTED = 2; // 0x2
@@ -1956,7 +1956,7 @@
method @RequiresApi(api=android.os.Build.VERSION_CODES.Q) public static <T> java.util.List<T!> readParcelableList(android.os.Parcel, java.util.List<T!>, ClassLoader?, Class<T!>);
method public static <T extends java.io.Serializable> T? readSerializable(android.os.Parcel, ClassLoader?, Class<T!>);
method public static <T> android.util.SparseArray<T!>? readSparseArray(android.os.Parcel, ClassLoader?, Class<? extends T!>);
- method public static void writeBoolean(android.os.Parcel, boolean);
+ method @Deprecated public static void writeBoolean(android.os.Parcel, boolean);
}
@Deprecated public final class ParcelableCompat {
@@ -2866,9 +2866,9 @@
}
public final class ScaleGestureDetectorCompat {
- method public static boolean isQuickScaleEnabled(android.view.ScaleGestureDetector);
+ method @Deprecated public static boolean isQuickScaleEnabled(android.view.ScaleGestureDetector);
method @Deprecated public static boolean isQuickScaleEnabled(Object!);
- method public static void setQuickScaleEnabled(android.view.ScaleGestureDetector, boolean);
+ method @Deprecated public static void setQuickScaleEnabled(android.view.ScaleGestureDetector, boolean);
method @Deprecated public static void setQuickScaleEnabled(Object!, boolean);
}
@@ -3142,11 +3142,11 @@
}
public final class ViewGroupCompat {
- method public static int getLayoutMode(android.view.ViewGroup);
+ method @Deprecated public static int getLayoutMode(android.view.ViewGroup);
method public static int getNestedScrollAxes(android.view.ViewGroup);
method public static boolean isTransitionGroup(android.view.ViewGroup);
method @Deprecated public static boolean onRequestSendAccessibilityEvent(android.view.ViewGroup!, android.view.View!, android.view.accessibility.AccessibilityEvent!);
- method public static void setLayoutMode(android.view.ViewGroup, int);
+ method @Deprecated public static void setLayoutMode(android.view.ViewGroup, int);
method @Deprecated public static void setMotionEventSplittingEnabled(android.view.ViewGroup!, boolean);
method public static void setTransitionGroup(android.view.ViewGroup, boolean);
field public static final int LAYOUT_MODE_CLIP_BOUNDS = 0; // 0x0
@@ -3154,7 +3154,7 @@
}
public final class ViewParentCompat {
- method public static void notifySubtreeAccessibilityStateChanged(android.view.ViewParent, android.view.View, android.view.View, int);
+ method @Deprecated public static void notifySubtreeAccessibilityStateChanged(android.view.ViewParent, android.view.View, android.view.View, int);
method public static boolean onNestedFling(android.view.ViewParent, android.view.View, float, float, boolean);
method public static boolean onNestedPreFling(android.view.ViewParent, android.view.View, float, float);
method public static void onNestedPreScroll(android.view.ViewParent, android.view.View, int, int, int[]);
@@ -3397,16 +3397,16 @@
public final class AccessibilityEventCompat {
method @Deprecated public static void appendRecord(android.view.accessibility.AccessibilityEvent!, androidx.core.view.accessibility.AccessibilityRecordCompat!);
method @Deprecated public static androidx.core.view.accessibility.AccessibilityRecordCompat! asRecord(android.view.accessibility.AccessibilityEvent!);
- method public static int getAction(android.view.accessibility.AccessibilityEvent);
- method public static int getContentChangeTypes(android.view.accessibility.AccessibilityEvent);
- method public static int getMovementGranularity(android.view.accessibility.AccessibilityEvent);
+ method @Deprecated public static int getAction(android.view.accessibility.AccessibilityEvent);
+ method @Deprecated public static int getContentChangeTypes(android.view.accessibility.AccessibilityEvent);
+ method @Deprecated public static int getMovementGranularity(android.view.accessibility.AccessibilityEvent);
method @Deprecated public static androidx.core.view.accessibility.AccessibilityRecordCompat! getRecord(android.view.accessibility.AccessibilityEvent!, int);
method @Deprecated public static int getRecordCount(android.view.accessibility.AccessibilityEvent!);
method public static boolean isAccessibilityDataSensitive(android.view.accessibility.AccessibilityEvent);
method public static void setAccessibilityDataSensitive(android.view.accessibility.AccessibilityEvent, boolean);
- method public static void setAction(android.view.accessibility.AccessibilityEvent, int);
- method public static void setContentChangeTypes(android.view.accessibility.AccessibilityEvent, int);
- method public static void setMovementGranularity(android.view.accessibility.AccessibilityEvent, int);
+ method @Deprecated public static void setAction(android.view.accessibility.AccessibilityEvent, int);
+ method @Deprecated public static void setContentChangeTypes(android.view.accessibility.AccessibilityEvent, int);
+ method @Deprecated public static void setMovementGranularity(android.view.accessibility.AccessibilityEvent, int);
field public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 4; // 0x4
field public static final int CONTENT_CHANGE_TYPE_CONTENT_INVALID = 1024; // 0x400
field public static final int CONTENT_CHANGE_TYPE_DRAG_CANCELLED = 512; // 0x200
@@ -3445,13 +3445,13 @@
public final class AccessibilityManagerCompat {
method @Deprecated public static boolean addAccessibilityStateChangeListener(android.view.accessibility.AccessibilityManager!, androidx.core.view.accessibility.AccessibilityManagerCompat.AccessibilityStateChangeListener!);
- method public static boolean addTouchExplorationStateChangeListener(android.view.accessibility.AccessibilityManager, androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener);
+ method @Deprecated public static boolean addTouchExplorationStateChangeListener(android.view.accessibility.AccessibilityManager, androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener);
method @Deprecated public static java.util.List<android.accessibilityservice.AccessibilityServiceInfo!>! getEnabledAccessibilityServiceList(android.view.accessibility.AccessibilityManager!, int);
method @Deprecated public static java.util.List<android.accessibilityservice.AccessibilityServiceInfo!>! getInstalledAccessibilityServiceList(android.view.accessibility.AccessibilityManager!);
method public static boolean isRequestFromAccessibilityTool(android.view.accessibility.AccessibilityManager);
method @Deprecated public static boolean isTouchExplorationEnabled(android.view.accessibility.AccessibilityManager!);
method @Deprecated public static boolean removeAccessibilityStateChangeListener(android.view.accessibility.AccessibilityManager!, androidx.core.view.accessibility.AccessibilityManagerCompat.AccessibilityStateChangeListener!);
- method public static boolean removeTouchExplorationStateChangeListener(android.view.accessibility.AccessibilityManager, androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener);
+ method @Deprecated public static boolean removeTouchExplorationStateChangeListener(android.view.accessibility.AccessibilityManager, androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener);
}
@Deprecated public static interface AccessibilityManagerCompat.AccessibilityStateChangeListener {
@@ -3816,9 +3816,9 @@
method @Deprecated public Object! getImpl();
method @Deprecated public int getItemCount();
method @Deprecated public int getMaxScrollX();
- method public static int getMaxScrollX(android.view.accessibility.AccessibilityRecord);
+ method @Deprecated public static int getMaxScrollX(android.view.accessibility.AccessibilityRecord);
method @Deprecated public int getMaxScrollY();
- method public static int getMaxScrollY(android.view.accessibility.AccessibilityRecord);
+ method @Deprecated public static int getMaxScrollY(android.view.accessibility.AccessibilityRecord);
method @Deprecated public android.os.Parcelable! getParcelableData();
method @Deprecated public int getRemovedCount();
method @Deprecated public int getScrollX();
@@ -3846,9 +3846,9 @@
method @Deprecated public void setFromIndex(int);
method @Deprecated public void setFullScreen(boolean);
method @Deprecated public void setItemCount(int);
- method public static void setMaxScrollX(android.view.accessibility.AccessibilityRecord, int);
+ method @Deprecated public static void setMaxScrollX(android.view.accessibility.AccessibilityRecord, int);
method @Deprecated public void setMaxScrollX(int);
- method public static void setMaxScrollY(android.view.accessibility.AccessibilityRecord, int);
+ method @Deprecated public static void setMaxScrollY(android.view.accessibility.AccessibilityRecord, int);
method @Deprecated public void setMaxScrollY(int);
method @Deprecated public void setParcelableData(android.os.Parcelable!);
method @Deprecated public void setPassword(boolean);
@@ -3856,7 +3856,7 @@
method @Deprecated public void setScrollX(int);
method @Deprecated public void setScrollY(int);
method @Deprecated public void setScrollable(boolean);
- method public static void setSource(android.view.accessibility.AccessibilityRecord, android.view.View?, int);
+ method @Deprecated public static void setSource(android.view.accessibility.AccessibilityRecord, android.view.View?, int);
method @Deprecated public void setSource(android.view.View!);
method @Deprecated public void setSource(android.view.View!, int);
method @Deprecated public void setToIndex(int);
@@ -4049,7 +4049,7 @@
}
public final class CheckedTextViewCompat {
- method public static android.graphics.drawable.Drawable? getCheckMarkDrawable(android.widget.CheckedTextView);
+ method @Deprecated public static android.graphics.drawable.Drawable? getCheckMarkDrawable(android.widget.CheckedTextView);
method public static android.content.res.ColorStateList? getCheckMarkTintList(android.widget.CheckedTextView);
method public static android.graphics.PorterDuff.Mode? getCheckMarkTintMode(android.widget.CheckedTextView);
method public static void setCheckMarkTintList(android.widget.CheckedTextView, android.content.res.ColorStateList?);
@@ -4097,7 +4097,7 @@
}
public final class ListPopupWindowCompat {
- method public static android.view.View.OnTouchListener? createDragToOpenListener(android.widget.ListPopupWindow, android.view.View);
+ method @Deprecated public static android.view.View.OnTouchListener? createDragToOpenListener(android.widget.ListPopupWindow, android.view.View);
method @Deprecated public static android.view.View.OnTouchListener! createDragToOpenListener(Object!, android.view.View!);
}
@@ -4167,7 +4167,7 @@
method public static int getWindowLayoutType(android.widget.PopupWindow);
method public static void setOverlapAnchor(android.widget.PopupWindow, boolean);
method public static void setWindowLayoutType(android.widget.PopupWindow, int);
- method public static void showAsDropDown(android.widget.PopupWindow, android.view.View, int, int, int);
+ method @Deprecated public static void showAsDropDown(android.widget.PopupWindow, android.view.View, int, int, int);
}
@Deprecated public final class ScrollerCompat {
@@ -4199,21 +4199,21 @@
method public static int getAutoSizeTextType(android.widget.TextView);
method public static android.content.res.ColorStateList? getCompoundDrawableTintList(android.widget.TextView);
method public static android.graphics.PorterDuff.Mode? getCompoundDrawableTintMode(android.widget.TextView);
- method public static android.graphics.drawable.Drawable![] getCompoundDrawablesRelative(android.widget.TextView);
+ method @Deprecated public static android.graphics.drawable.Drawable![] getCompoundDrawablesRelative(android.widget.TextView);
method public static int getFirstBaselineToTopHeight(android.widget.TextView);
method public static int getLastBaselineToBottomHeight(android.widget.TextView);
- method public static int getMaxLines(android.widget.TextView);
- method public static int getMinLines(android.widget.TextView);
+ method @Deprecated public static int getMaxLines(android.widget.TextView);
+ method @Deprecated public static int getMinLines(android.widget.TextView);
method public static androidx.core.text.PrecomputedTextCompat.Params getTextMetricsParams(android.widget.TextView);
method public static void setAutoSizeTextTypeUniformWithConfiguration(android.widget.TextView, int, int, int, int) throws java.lang.IllegalArgumentException;
method public static void setAutoSizeTextTypeUniformWithPresetSizes(android.widget.TextView, int[], int) throws java.lang.IllegalArgumentException;
method public static void setAutoSizeTextTypeWithDefaults(android.widget.TextView, int);
method public static void setCompoundDrawableTintList(android.widget.TextView, android.content.res.ColorStateList?);
method public static void setCompoundDrawableTintMode(android.widget.TextView, android.graphics.PorterDuff.Mode?);
- method public static void setCompoundDrawablesRelative(android.widget.TextView, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?);
- method public static void setCompoundDrawablesRelativeWithIntrinsicBounds(android.widget.TextView, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?);
- method public static void setCompoundDrawablesRelativeWithIntrinsicBounds(android.widget.TextView, @DrawableRes int, @DrawableRes int, @DrawableRes int, @DrawableRes int);
- method public static void setCustomSelectionActionModeCallback(android.widget.TextView, android.view.ActionMode.Callback);
+ method @Deprecated public static void setCompoundDrawablesRelative(android.widget.TextView, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?);
+ method @Deprecated public static void setCompoundDrawablesRelativeWithIntrinsicBounds(android.widget.TextView, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?);
+ method @Deprecated public static void setCompoundDrawablesRelativeWithIntrinsicBounds(android.widget.TextView, @DrawableRes int, @DrawableRes int, @DrawableRes int, @DrawableRes int);
+ method @Deprecated public static void setCustomSelectionActionModeCallback(android.widget.TextView, android.view.ActionMode.Callback);
method public static void setFirstBaselineToTopHeight(android.widget.TextView, @IntRange(from=0) @Px int);
method public static void setLastBaselineToBottomHeight(android.widget.TextView, @IntRange(from=0) @Px int);
method public static void setLineHeight(android.widget.TextView, @IntRange(from=0) @Px int);
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 3414ef2..fd443b0 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -18,8 +18,8 @@
method public static String capabilityToString(int);
method public static String feedbackTypeToString(int);
method public static String? flagToString(int);
- method public static int getCapabilities(android.accessibilityservice.AccessibilityServiceInfo);
- method public static String? loadDescription(android.accessibilityservice.AccessibilityServiceInfo, android.content.pm.PackageManager);
+ method @Deprecated public static int getCapabilities(android.accessibilityservice.AccessibilityServiceInfo);
+ method @Deprecated public static String? loadDescription(android.accessibilityservice.AccessibilityServiceInfo, android.content.pm.PackageManager);
field public static final int CAPABILITY_CAN_FILTER_KEY_EVENTS = 8; // 0x8
field public static final int CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 4; // 0x4
field public static final int CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION = 2; // 0x2
@@ -74,7 +74,7 @@
}
public final class ActivityManagerCompat {
- method public static boolean isLowRamDevice(android.app.ActivityManager);
+ method @Deprecated public static boolean isLowRamDevice(android.app.ActivityManager);
}
public class ActivityOptionsCompat {
@@ -101,7 +101,7 @@
method public static boolean canScheduleExactAlarms(android.app.AlarmManager);
method public static void setAlarmClock(android.app.AlarmManager, long, android.app.PendingIntent, android.app.PendingIntent);
method public static void setAndAllowWhileIdle(android.app.AlarmManager, int, long, android.app.PendingIntent);
- method public static void setExact(android.app.AlarmManager, int, long, android.app.PendingIntent);
+ method @Deprecated public static void setExact(android.app.AlarmManager, int, long, android.app.PendingIntent);
method public static void setExactAndAllowWhileIdle(android.app.AlarmManager, int, long, android.app.PendingIntent);
}
@@ -247,8 +247,8 @@
method public static String? getParentActivityName(android.app.Activity);
method public static String? getParentActivityName(android.content.Context, android.content.ComponentName) throws android.content.pm.PackageManager.NameNotFoundException;
method public static void navigateUpFromSameTask(android.app.Activity);
- method public static void navigateUpTo(android.app.Activity, android.content.Intent);
- method public static boolean shouldUpRecreateTask(android.app.Activity, android.content.Intent);
+ method @Deprecated public static void navigateUpTo(android.app.Activity, android.content.Intent);
+ method @Deprecated public static boolean shouldUpRecreateTask(android.app.Activity, android.content.Intent);
field public static final String PARENT_ACTIVITY = "android.support.PARENT_ACTIVITY";
}
@@ -325,7 +325,7 @@
method public static CharSequence? getContentInfo(android.app.Notification);
method public static CharSequence? getContentText(android.app.Notification);
method public static CharSequence? getContentTitle(android.app.Notification);
- method public static android.os.Bundle? getExtras(android.app.Notification);
+ method @Deprecated public static android.os.Bundle? getExtras(android.app.Notification);
method public static String? getGroup(android.app.Notification);
method @androidx.core.app.NotificationCompat.GroupAlertBehavior public static int getGroupAlertBehavior(android.app.Notification);
method @RequiresApi(21) public static java.util.List<androidx.core.app.NotificationCompat.Action!> getInvisibleActions(android.app.Notification);
@@ -1232,11 +1232,11 @@
method public static java.io.File? getDataDir(android.content.Context);
method public static android.view.Display getDisplayOrDefault(@DisplayContext android.content.Context);
method public static android.graphics.drawable.Drawable? getDrawable(android.content.Context, @DrawableRes int);
- method public static java.io.File![] getExternalCacheDirs(android.content.Context);
- method public static java.io.File![] getExternalFilesDirs(android.content.Context, String?);
+ method @Deprecated public static java.io.File![] getExternalCacheDirs(android.content.Context);
+ method @Deprecated public static java.io.File![] getExternalFilesDirs(android.content.Context, String?);
method public static java.util.concurrent.Executor getMainExecutor(android.content.Context);
method public static java.io.File? getNoBackupFilesDir(android.content.Context);
- method public static java.io.File![] getObbDirs(android.content.Context);
+ method @Deprecated public static java.io.File![] getObbDirs(android.content.Context);
method public static String getString(android.content.Context, int);
method public static <T> T? getSystemService(android.content.Context, Class<T!>);
method public static String? getSystemServiceName(android.content.Context, Class<?>);
@@ -1245,7 +1245,7 @@
method public static android.content.Intent? registerReceiver(android.content.Context, android.content.BroadcastReceiver?, android.content.IntentFilter, String?, android.os.Handler?, int);
method public static boolean startActivities(android.content.Context, android.content.Intent![]);
method public static boolean startActivities(android.content.Context, android.content.Intent![], android.os.Bundle?);
- method public static void startActivity(android.content.Context, android.content.Intent, android.os.Bundle?);
+ method @Deprecated public static void startActivity(android.content.Context, android.content.Intent, android.os.Bundle?);
method public static void startForegroundService(android.content.Context, android.content.Intent);
field public static final int RECEIVER_EXPORTED = 2; // 0x2
field public static final int RECEIVER_NOT_EXPORTED = 4; // 0x4
@@ -1670,9 +1670,9 @@
public final class BitmapCompat {
method public static android.graphics.Bitmap createScaledBitmap(android.graphics.Bitmap, int, int, android.graphics.Rect?, boolean);
- method public static int getAllocationByteCount(android.graphics.Bitmap);
- method public static boolean hasMipMap(android.graphics.Bitmap);
- method public static void setHasMipMap(android.graphics.Bitmap, boolean);
+ method @Deprecated public static int getAllocationByteCount(android.graphics.Bitmap);
+ method @Deprecated public static boolean hasMipMap(android.graphics.Bitmap);
+ method @Deprecated public static void setHasMipMap(android.graphics.Bitmap, boolean);
}
public class BlendModeColorFilterCompat {
@@ -1852,13 +1852,13 @@
method public static void applyTheme(android.graphics.drawable.Drawable, android.content.res.Resources.Theme);
method public static boolean canApplyTheme(android.graphics.drawable.Drawable);
method public static void clearColorFilter(android.graphics.drawable.Drawable);
- method public static int getAlpha(android.graphics.drawable.Drawable);
+ method @Deprecated public static int getAlpha(android.graphics.drawable.Drawable);
method public static android.graphics.ColorFilter? getColorFilter(android.graphics.drawable.Drawable);
method public static int getLayoutDirection(android.graphics.drawable.Drawable);
method public static void inflate(android.graphics.drawable.Drawable, android.content.res.Resources, org.xmlpull.v1.XmlPullParser, android.util.AttributeSet, android.content.res.Resources.Theme?) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
- method public static boolean isAutoMirrored(android.graphics.drawable.Drawable);
+ method @Deprecated public static boolean isAutoMirrored(android.graphics.drawable.Drawable);
method @Deprecated public static void jumpToCurrentState(android.graphics.drawable.Drawable);
- method public static void setAutoMirrored(android.graphics.drawable.Drawable, boolean);
+ method @Deprecated public static void setAutoMirrored(android.graphics.drawable.Drawable, boolean);
method public static void setHotspot(android.graphics.drawable.Drawable, float, float);
method public static void setHotspotBounds(android.graphics.drawable.Drawable, int, int, int, int);
method public static boolean setLayoutDirection(android.graphics.drawable.Drawable, int);
@@ -2077,7 +2077,7 @@
public final class LocationCompat {
method public static float getBearingAccuracyDegrees(android.location.Location);
method public static long getElapsedRealtimeMillis(android.location.Location);
- method public static long getElapsedRealtimeNanos(android.location.Location);
+ method @Deprecated public static long getElapsedRealtimeNanos(android.location.Location);
method @FloatRange(from=0.0) public static float getMslAltitudeAccuracyMeters(android.location.Location);
method public static double getMslAltitudeMeters(android.location.Location);
method public static float getSpeedAccuracyMetersPerSecond(android.location.Location);
@@ -2087,7 +2087,7 @@
method public static boolean hasMslAltitudeAccuracy(android.location.Location);
method public static boolean hasSpeedAccuracy(android.location.Location);
method public static boolean hasVerticalAccuracy(android.location.Location);
- method public static boolean isMock(android.location.Location);
+ method @Deprecated public static boolean isMock(android.location.Location);
method public static void removeBearingAccuracy(android.location.Location);
method public static void removeMslAltitude(android.location.Location);
method public static void removeMslAltitudeAccuracy(android.location.Location);
@@ -2190,7 +2190,7 @@
public final class ConnectivityManagerCompat {
method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public static android.net.NetworkInfo? getNetworkInfoFromBroadcast(android.net.ConnectivityManager, android.content.Intent);
method @androidx.core.net.ConnectivityManagerCompat.RestrictBackgroundStatus public static int getRestrictBackgroundStatus(android.net.ConnectivityManager);
- method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public static boolean isActiveNetworkMetered(android.net.ConnectivityManager);
+ method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public static boolean isActiveNetworkMetered(android.net.ConnectivityManager);
field public static final int RESTRICT_BACKGROUND_STATUS_DISABLED = 1; // 0x1
field public static final int RESTRICT_BACKGROUND_STATUS_ENABLED = 3; // 0x3
field public static final int RESTRICT_BACKGROUND_STATUS_WHITELISTED = 2; // 0x2
@@ -2346,7 +2346,7 @@
method @RequiresApi(api=android.os.Build.VERSION_CODES.Q) public static <T> java.util.List<T!> readParcelableList(android.os.Parcel, java.util.List<T!>, ClassLoader?, Class<T!>);
method public static <T extends java.io.Serializable> T? readSerializable(android.os.Parcel, ClassLoader?, Class<T!>);
method public static <T> android.util.SparseArray<T!>? readSparseArray(android.os.Parcel, ClassLoader?, Class<? extends T!>);
- method public static void writeBoolean(android.os.Parcel, boolean);
+ method @Deprecated public static void writeBoolean(android.os.Parcel, boolean);
}
@Deprecated public final class ParcelableCompat {
@@ -3352,9 +3352,9 @@
}
public final class ScaleGestureDetectorCompat {
- method public static boolean isQuickScaleEnabled(android.view.ScaleGestureDetector);
+ method @Deprecated public static boolean isQuickScaleEnabled(android.view.ScaleGestureDetector);
method @Deprecated public static boolean isQuickScaleEnabled(Object!);
- method public static void setQuickScaleEnabled(android.view.ScaleGestureDetector, boolean);
+ method @Deprecated public static void setQuickScaleEnabled(android.view.ScaleGestureDetector, boolean);
method @Deprecated public static void setQuickScaleEnabled(Object!, boolean);
}
@@ -3649,11 +3649,11 @@
}
public final class ViewGroupCompat {
- method public static int getLayoutMode(android.view.ViewGroup);
+ method @Deprecated public static int getLayoutMode(android.view.ViewGroup);
method @androidx.core.view.ViewCompat.ScrollAxis public static int getNestedScrollAxes(android.view.ViewGroup);
method public static boolean isTransitionGroup(android.view.ViewGroup);
method @Deprecated public static boolean onRequestSendAccessibilityEvent(android.view.ViewGroup!, android.view.View!, android.view.accessibility.AccessibilityEvent!);
- method public static void setLayoutMode(android.view.ViewGroup, int);
+ method @Deprecated public static void setLayoutMode(android.view.ViewGroup, int);
method @Deprecated public static void setMotionEventSplittingEnabled(android.view.ViewGroup!, boolean);
method public static void setTransitionGroup(android.view.ViewGroup, boolean);
field public static final int LAYOUT_MODE_CLIP_BOUNDS = 0; // 0x0
@@ -3661,7 +3661,7 @@
}
public final class ViewParentCompat {
- method public static void notifySubtreeAccessibilityStateChanged(android.view.ViewParent, android.view.View, android.view.View, int);
+ method @Deprecated public static void notifySubtreeAccessibilityStateChanged(android.view.ViewParent, android.view.View, android.view.View, int);
method public static boolean onNestedFling(android.view.ViewParent, android.view.View, float, float, boolean);
method public static boolean onNestedPreFling(android.view.ViewParent, android.view.View, float, float);
method public static void onNestedPreScroll(android.view.ViewParent, android.view.View, int, int, int[]);
@@ -3912,16 +3912,16 @@
public final class AccessibilityEventCompat {
method @Deprecated public static void appendRecord(android.view.accessibility.AccessibilityEvent!, androidx.core.view.accessibility.AccessibilityRecordCompat!);
method @Deprecated public static androidx.core.view.accessibility.AccessibilityRecordCompat! asRecord(android.view.accessibility.AccessibilityEvent!);
- method public static int getAction(android.view.accessibility.AccessibilityEvent);
- method @androidx.core.view.accessibility.AccessibilityEventCompat.ContentChangeType public static int getContentChangeTypes(android.view.accessibility.AccessibilityEvent);
- method public static int getMovementGranularity(android.view.accessibility.AccessibilityEvent);
+ method @Deprecated public static int getAction(android.view.accessibility.AccessibilityEvent);
+ method @Deprecated @androidx.core.view.accessibility.AccessibilityEventCompat.ContentChangeType public static int getContentChangeTypes(android.view.accessibility.AccessibilityEvent);
+ method @Deprecated public static int getMovementGranularity(android.view.accessibility.AccessibilityEvent);
method @Deprecated public static androidx.core.view.accessibility.AccessibilityRecordCompat! getRecord(android.view.accessibility.AccessibilityEvent!, int);
method @Deprecated public static int getRecordCount(android.view.accessibility.AccessibilityEvent!);
method public static boolean isAccessibilityDataSensitive(android.view.accessibility.AccessibilityEvent);
method public static void setAccessibilityDataSensitive(android.view.accessibility.AccessibilityEvent, boolean);
- method public static void setAction(android.view.accessibility.AccessibilityEvent, int);
- method public static void setContentChangeTypes(android.view.accessibility.AccessibilityEvent, @androidx.core.view.accessibility.AccessibilityEventCompat.ContentChangeType int);
- method public static void setMovementGranularity(android.view.accessibility.AccessibilityEvent, int);
+ method @Deprecated public static void setAction(android.view.accessibility.AccessibilityEvent, int);
+ method @Deprecated public static void setContentChangeTypes(android.view.accessibility.AccessibilityEvent, @androidx.core.view.accessibility.AccessibilityEventCompat.ContentChangeType int);
+ method @Deprecated public static void setMovementGranularity(android.view.accessibility.AccessibilityEvent, int);
field public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 4; // 0x4
field public static final int CONTENT_CHANGE_TYPE_CONTENT_INVALID = 1024; // 0x400
field public static final int CONTENT_CHANGE_TYPE_DRAG_CANCELLED = 512; // 0x200
@@ -3963,13 +3963,13 @@
public final class AccessibilityManagerCompat {
method @Deprecated public static boolean addAccessibilityStateChangeListener(android.view.accessibility.AccessibilityManager!, androidx.core.view.accessibility.AccessibilityManagerCompat.AccessibilityStateChangeListener!);
- method public static boolean addTouchExplorationStateChangeListener(android.view.accessibility.AccessibilityManager, androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener);
+ method @Deprecated public static boolean addTouchExplorationStateChangeListener(android.view.accessibility.AccessibilityManager, androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener);
method @Deprecated public static java.util.List<android.accessibilityservice.AccessibilityServiceInfo!>! getEnabledAccessibilityServiceList(android.view.accessibility.AccessibilityManager!, int);
method @Deprecated public static java.util.List<android.accessibilityservice.AccessibilityServiceInfo!>! getInstalledAccessibilityServiceList(android.view.accessibility.AccessibilityManager!);
method public static boolean isRequestFromAccessibilityTool(android.view.accessibility.AccessibilityManager);
method @Deprecated public static boolean isTouchExplorationEnabled(android.view.accessibility.AccessibilityManager!);
method @Deprecated public static boolean removeAccessibilityStateChangeListener(android.view.accessibility.AccessibilityManager!, androidx.core.view.accessibility.AccessibilityManagerCompat.AccessibilityStateChangeListener!);
- method public static boolean removeTouchExplorationStateChangeListener(android.view.accessibility.AccessibilityManager, androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener);
+ method @Deprecated public static boolean removeTouchExplorationStateChangeListener(android.view.accessibility.AccessibilityManager, androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener);
}
@Deprecated public static interface AccessibilityManagerCompat.AccessibilityStateChangeListener {
@@ -4341,9 +4341,9 @@
method @Deprecated public Object! getImpl();
method @Deprecated public int getItemCount();
method @Deprecated public int getMaxScrollX();
- method public static int getMaxScrollX(android.view.accessibility.AccessibilityRecord);
+ method @Deprecated public static int getMaxScrollX(android.view.accessibility.AccessibilityRecord);
method @Deprecated public int getMaxScrollY();
- method public static int getMaxScrollY(android.view.accessibility.AccessibilityRecord);
+ method @Deprecated public static int getMaxScrollY(android.view.accessibility.AccessibilityRecord);
method @Deprecated public android.os.Parcelable! getParcelableData();
method @Deprecated public int getRemovedCount();
method @Deprecated public int getScrollX();
@@ -4371,9 +4371,9 @@
method @Deprecated public void setFromIndex(int);
method @Deprecated public void setFullScreen(boolean);
method @Deprecated public void setItemCount(int);
- method public static void setMaxScrollX(android.view.accessibility.AccessibilityRecord, int);
+ method @Deprecated public static void setMaxScrollX(android.view.accessibility.AccessibilityRecord, int);
method @Deprecated public void setMaxScrollX(int);
- method public static void setMaxScrollY(android.view.accessibility.AccessibilityRecord, int);
+ method @Deprecated public static void setMaxScrollY(android.view.accessibility.AccessibilityRecord, int);
method @Deprecated public void setMaxScrollY(int);
method @Deprecated public void setParcelableData(android.os.Parcelable!);
method @Deprecated public void setPassword(boolean);
@@ -4381,7 +4381,7 @@
method @Deprecated public void setScrollX(int);
method @Deprecated public void setScrollY(int);
method @Deprecated public void setScrollable(boolean);
- method public static void setSource(android.view.accessibility.AccessibilityRecord, android.view.View?, int);
+ method @Deprecated public static void setSource(android.view.accessibility.AccessibilityRecord, android.view.View?, int);
method @Deprecated public void setSource(android.view.View!);
method @Deprecated public void setSource(android.view.View!, int);
method @Deprecated public void setToIndex(int);
@@ -4587,7 +4587,7 @@
}
public final class CheckedTextViewCompat {
- method public static android.graphics.drawable.Drawable? getCheckMarkDrawable(android.widget.CheckedTextView);
+ method @Deprecated public static android.graphics.drawable.Drawable? getCheckMarkDrawable(android.widget.CheckedTextView);
method public static android.content.res.ColorStateList? getCheckMarkTintList(android.widget.CheckedTextView);
method public static android.graphics.PorterDuff.Mode? getCheckMarkTintMode(android.widget.CheckedTextView);
method public static void setCheckMarkTintList(android.widget.CheckedTextView, android.content.res.ColorStateList?);
@@ -4635,7 +4635,7 @@
}
public final class ListPopupWindowCompat {
- method public static android.view.View.OnTouchListener? createDragToOpenListener(android.widget.ListPopupWindow, android.view.View);
+ method @Deprecated public static android.view.View.OnTouchListener? createDragToOpenListener(android.widget.ListPopupWindow, android.view.View);
method @Deprecated public static android.view.View.OnTouchListener! createDragToOpenListener(Object!, android.view.View!);
}
@@ -4705,7 +4705,7 @@
method public static int getWindowLayoutType(android.widget.PopupWindow);
method public static void setOverlapAnchor(android.widget.PopupWindow, boolean);
method public static void setWindowLayoutType(android.widget.PopupWindow, int);
- method public static void showAsDropDown(android.widget.PopupWindow, android.view.View, int, int, int);
+ method @Deprecated public static void showAsDropDown(android.widget.PopupWindow, android.view.View, int, int, int);
}
@Deprecated public final class ScrollerCompat {
@@ -4737,21 +4737,21 @@
method public static int getAutoSizeTextType(android.widget.TextView);
method public static android.content.res.ColorStateList? getCompoundDrawableTintList(android.widget.TextView);
method public static android.graphics.PorterDuff.Mode? getCompoundDrawableTintMode(android.widget.TextView);
- method public static android.graphics.drawable.Drawable![] getCompoundDrawablesRelative(android.widget.TextView);
+ method @Deprecated public static android.graphics.drawable.Drawable![] getCompoundDrawablesRelative(android.widget.TextView);
method public static int getFirstBaselineToTopHeight(android.widget.TextView);
method public static int getLastBaselineToBottomHeight(android.widget.TextView);
- method public static int getMaxLines(android.widget.TextView);
- method public static int getMinLines(android.widget.TextView);
+ method @Deprecated public static int getMaxLines(android.widget.TextView);
+ method @Deprecated public static int getMinLines(android.widget.TextView);
method public static androidx.core.text.PrecomputedTextCompat.Params getTextMetricsParams(android.widget.TextView);
method public static void setAutoSizeTextTypeUniformWithConfiguration(android.widget.TextView, int, int, int, int) throws java.lang.IllegalArgumentException;
method public static void setAutoSizeTextTypeUniformWithPresetSizes(android.widget.TextView, int[], int) throws java.lang.IllegalArgumentException;
method public static void setAutoSizeTextTypeWithDefaults(android.widget.TextView, int);
method public static void setCompoundDrawableTintList(android.widget.TextView, android.content.res.ColorStateList?);
method public static void setCompoundDrawableTintMode(android.widget.TextView, android.graphics.PorterDuff.Mode?);
- method public static void setCompoundDrawablesRelative(android.widget.TextView, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?);
- method public static void setCompoundDrawablesRelativeWithIntrinsicBounds(android.widget.TextView, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?);
- method public static void setCompoundDrawablesRelativeWithIntrinsicBounds(android.widget.TextView, @DrawableRes int, @DrawableRes int, @DrawableRes int, @DrawableRes int);
- method public static void setCustomSelectionActionModeCallback(android.widget.TextView, android.view.ActionMode.Callback);
+ method @Deprecated public static void setCompoundDrawablesRelative(android.widget.TextView, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?);
+ method @Deprecated public static void setCompoundDrawablesRelativeWithIntrinsicBounds(android.widget.TextView, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?, android.graphics.drawable.Drawable?);
+ method @Deprecated public static void setCompoundDrawablesRelativeWithIntrinsicBounds(android.widget.TextView, @DrawableRes int, @DrawableRes int, @DrawableRes int, @DrawableRes int);
+ method @Deprecated public static void setCustomSelectionActionModeCallback(android.widget.TextView, android.view.ActionMode.Callback);
method public static void setFirstBaselineToTopHeight(android.widget.TextView, @IntRange(from=0) @Px int);
method public static void setLastBaselineToBottomHeight(android.widget.TextView, @IntRange(from=0) @Px int);
method public static void setLineHeight(android.widget.TextView, @IntRange(from=0) @Px int);
diff --git a/core/core/build.gradle b/core/core/build.gradle
index 2799274..87f8666 100644
--- a/core/core/build.gradle
+++ b/core/core/build.gradle
@@ -21,7 +21,7 @@
implementation(project(":core:core-testing"))
}
- api("androidx.annotation:annotation:1.6.0")
+ api(project(":annotation:annotation"))
api("androidx.annotation:annotation-experimental:1.4.0")
api("androidx.lifecycle:lifecycle-runtime:2.6.2")
api("androidx.versionedparcelable:versionedparcelable:1.1.1")
diff --git a/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java b/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java
index d1dc409..eacb1fc 100644
--- a/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/accessibilityservice/AccessibilityServiceInfoCompat.java
@@ -190,7 +190,10 @@
* @param info The service info of interest
* @param packageManager The current package manager
* @return The localized description.
+ * @deprecated Call {@link AccessibilityServiceInfo#loadDescription()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "info.loadDescription(packageManager)")
@SuppressWarnings("deprecation")
@Nullable
public static String loadDescription(
@@ -277,7 +280,10 @@
* @see #CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION
* @see #CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY
* @see #CAPABILITY_CAN_FILTER_KEY_EVENTS
+ * @deprecated Call {@link AccessibilityServiceInfo#getCapabilities()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "info.getCapabilities()")
@SuppressWarnings("deprecation")
public static int getCapabilities(@NonNull AccessibilityServiceInfo info) {
return info.getCapabilities();
diff --git a/core/core/src/main/java/androidx/core/app/ActivityManagerCompat.java b/core/core/src/main/java/androidx/core/app/ActivityManagerCompat.java
index 696e01f..6afb250 100644
--- a/core/core/src/main/java/androidx/core/app/ActivityManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ActivityManagerCompat.java
@@ -34,7 +34,10 @@
* something in the class of a 512MB device with about a 800x480 or less screen.
* This is mostly intended to be used by apps to determine whether they should turn
* off certain features that require more RAM.
+ * @deprecated Call {@link ActivityManager#isLowRamDevice()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "activityManager.isLowRamDevice()")
public static boolean isLowRamDevice(@NonNull ActivityManager activityManager) {
return activityManager.isLowRamDevice();
}
diff --git a/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java b/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
index 34e3364..2242966 100644
--- a/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
@@ -162,7 +162,10 @@
* @see AlarmManager#ELAPSED_REALTIME_WAKEUP
* @see AlarmManager#RTC
* @see AlarmManager#RTC_WAKEUP
+ * @deprecated Call {@link AlarmManager#setExact()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "alarmManager.setExact(type, triggerAtMillis, operation)")
public static void setExact(@NonNull AlarmManager alarmManager, int type, long triggerAtMillis,
@NonNull PendingIntent operation) {
alarmManager.setExact(type, triggerAtMillis, operation);
diff --git a/core/core/src/main/java/androidx/core/app/BundleCompat.java b/core/core/src/main/java/androidx/core/app/BundleCompat.java
index a759f36..9be2181 100644
--- a/core/core/src/main/java/androidx/core/app/BundleCompat.java
+++ b/core/core/src/main/java/androidx/core/app/BundleCompat.java
@@ -38,7 +38,10 @@
* @param bundle The bundle to get the {@link IBinder}.
* @param key The key to use while getting the {@link IBinder}.
* @return The {@link IBinder} that was obtained.
+ * @deprecated Call {@link Bundle#getBinder()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "bundle.getBinder(key)")
@Nullable
public static IBinder getBinder(@NonNull Bundle bundle, @Nullable String key) {
return bundle.getBinder(key);
@@ -50,7 +53,10 @@
* @param bundle The bundle to insert the {@link IBinder}.
* @param key The key to use while putting the {@link IBinder}.
* @param binder The {@link IBinder} to put.
+ * @deprecated Call {@link Bundle#putBinder()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "bundle.putBinder(key, binder)")
public static void putBinder(@NonNull Bundle bundle, @Nullable String key,
@Nullable IBinder binder) {
bundle.putBinder(key, binder);
diff --git a/core/core/src/main/java/androidx/core/app/NavUtils.java b/core/core/src/main/java/androidx/core/app/NavUtils.java
index 3480592..56fffc4 100644
--- a/core/core/src/main/java/androidx/core/app/NavUtils.java
+++ b/core/core/src/main/java/androidx/core/app/NavUtils.java
@@ -54,7 +54,10 @@
* @param targetIntent An intent representing the target destination for up navigation
* @return true if navigating up should recreate a new task stack, false if the same task
* should be used for the destination
+ * @deprecated Call {@link Activity#shouldUpRecreateTask()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "sourceActivity.shouldUpRecreateTask(targetIntent)")
public static boolean shouldUpRecreateTask(@NonNull Activity sourceActivity,
@NonNull Intent targetIntent) {
return sourceActivity.shouldUpRecreateTask(targetIntent);
@@ -98,7 +101,10 @@
*
* @param sourceActivity The current activity from which the user is attempting to navigate up
* @param upIntent An intent representing the target destination for up navigation
+ * @deprecated Call {@link Activity#navigateUpTo()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "sourceActivity.navigateUpTo(upIntent)")
public static void navigateUpTo(@NonNull Activity sourceActivity, @NonNull Intent upIntent) {
sourceActivity.navigateUpTo(upIntent);
}
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 bee883a..6fd698c 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -8953,7 +8953,10 @@
* Gets the {@link Notification#extras} field from a notification in a backwards
* compatible manner. Extras field was supported from JellyBean (Api level 16)
* forwards. This function will return {@code null} on older api levels.
+ * @deprecated Call {@link Notification#extras} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "notification.extras")
@Nullable
public static Bundle getExtras(@NonNull Notification notification) {
return notification.extras;
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 bacc472..d6b1f9c 100644
--- a/core/core/src/main/java/androidx/core/content/ContextCompat.java
+++ b/core/core/src/main/java/androidx/core/content/ContextCompat.java
@@ -289,7 +289,10 @@
* {@link ActivityOptionsCompat} for how to build the Bundle
* supplied here; there are no supported definitions for
* building it manually.
+ * @deprecated Call {@link Context#startActivity()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "context.startActivity(intent, options)")
public static void startActivity(@NonNull Context context, @NonNull Intent intent,
@Nullable Bundle options) {
context.startActivity(intent, options);
@@ -362,7 +365,10 @@
*
* @see Context#getObbDir()
* @see EnvironmentCompat#getStorageState(File)
+ * @deprecated Call {@link Context#getObbDirs()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "context.getObbDirs()")
@NonNull
public static File[] getObbDirs(@NonNull Context context) {
return context.getObbDirs();
@@ -411,7 +417,10 @@
*
* @see Context#getExternalFilesDir(String)
* @see EnvironmentCompat#getStorageState(File)
+ * @deprecated Call {@link Context#getExternalFilesDirs()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "context.getExternalFilesDirs(type)")
@NonNull
public static File[] getExternalFilesDirs(@NonNull Context context, @Nullable String type) {
return context.getExternalFilesDirs(type);
@@ -460,7 +469,10 @@
*
* @see Context#getExternalCacheDir()
* @see EnvironmentCompat#getStorageState(File)
+ * @deprecated Call {@link Context#getExternalCacheDirs()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "context.getExternalCacheDirs()")
@NonNull
public static File[] getExternalCacheDirs(@NonNull Context context) {
return context.getExternalCacheDirs();
diff --git a/core/core/src/main/java/androidx/core/graphics/BitmapCompat.java b/core/core/src/main/java/androidx/core/graphics/BitmapCompat.java
index 25d0b7b..547918d 100644
--- a/core/core/src/main/java/androidx/core/graphics/BitmapCompat.java
+++ b/core/core/src/main/java/androidx/core/graphics/BitmapCompat.java
@@ -52,7 +52,10 @@
* @return true if the renderer should attempt to use mipmaps,
* false otherwise
* @see Bitmap#hasMipMap()
+ * @deprecated Call {@link Bitmap#hasMipMap()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "bitmap.hasMipMap()")
public static boolean hasMipMap(@NonNull Bitmap bitmap) {
return bitmap.hasMipMap();
}
@@ -76,7 +79,10 @@
* @param hasMipMap indicates whether the renderer should attempt
* to use mipmaps
* @see Bitmap#setHasMipMap(boolean)
+ * @deprecated Call {@link Bitmap#setHasMipMap()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "bitmap.setHasMipMap(hasMipMap)")
public static void setHasMipMap(@NonNull Bitmap bitmap, boolean hasMipMap) {
bitmap.setHasMipMap(hasMipMap);
}
@@ -87,7 +93,10 @@
* This value will not change over the lifetime of a Bitmap.
*
* @see Bitmap#getAllocationByteCount()
+ * @deprecated Call {@link Bitmap#getAllocationByteCount()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "bitmap.getAllocationByteCount()")
public static int getAllocationByteCount(@NonNull Bitmap bitmap) {
return bitmap.getAllocationByteCount();
}
diff --git a/core/core/src/main/java/androidx/core/graphics/drawable/DrawableCompat.java b/core/core/src/main/java/androidx/core/graphics/drawable/DrawableCompat.java
index 32130e6..3eb185e 100644
--- a/core/core/src/main/java/androidx/core/graphics/drawable/DrawableCompat.java
+++ b/core/core/src/main/java/androidx/core/graphics/drawable/DrawableCompat.java
@@ -60,6 +60,7 @@
*
* @deprecated Use {@link Drawable#jumpToCurrentState()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "drawable.jumpToCurrentState()")
@Deprecated
public static void jumpToCurrentState(@NonNull Drawable drawable) {
drawable.jumpToCurrentState();
@@ -76,7 +77,10 @@
* @param drawable The Drawable against which to invoke the method.
* @param mirrored Set to true if the Drawable should be mirrored, false if
* not.
+ * @deprecated Call {@link Drawable#setAutoMirrored()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "drawable.setAutoMirrored(mirrored)")
public static void setAutoMirrored(@NonNull Drawable drawable, boolean mirrored) {
drawable.setAutoMirrored(mirrored);
}
@@ -91,7 +95,10 @@
* @param drawable The Drawable against which to invoke the method.
* @return boolean Returns true if this Drawable will be automatically
* mirrored.
+ * @deprecated Call {@link Drawable#isAutoMirrored()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "drawable.isAutoMirrored()")
public static boolean isAutoMirrored(@NonNull Drawable drawable) {
return drawable.isAutoMirrored();
}
@@ -173,7 +180,10 @@
* 0 means fully transparent, 255 means fully opaque.
*
* @param drawable The Drawable against which to invoke the method.
+ * @deprecated Call {@link Drawable#getAlpha()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "drawable.getAlpha()")
@SuppressWarnings("unused")
public static int getAlpha(@NonNull Drawable drawable) {
return drawable.getAlpha();
diff --git a/core/core/src/main/java/androidx/core/location/LocationCompat.java b/core/core/src/main/java/androidx/core/location/LocationCompat.java
index 7d4c123..502ef2f 100644
--- a/core/core/src/main/java/androidx/core/location/LocationCompat.java
+++ b/core/core/src/main/java/androidx/core/location/LocationCompat.java
@@ -106,7 +106,10 @@
* based on the difference between system time and the location time. This should be taken as a
* best "guess" at what the elapsed realtime might have been, but if the clock used for
* location derivation is different from the system clock, the results may be inaccurate.
+ * @deprecated Call {@link Location#getElapsedRealtimeNanos()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "location.getElapsedRealtimeNanos()")
public static long getElapsedRealtimeNanos(@NonNull Location location) {
return location.getElapsedRealtimeNanos();
}
@@ -491,7 +494,10 @@
* this should be considered a mock location.
*
* @see android.location.LocationManager#addTestProvider
+ * @deprecated Call {@link Location#isFromMockProvider()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "location.isFromMockProvider()")
public static boolean isMock(@NonNull Location location) {
return location.isFromMockProvider();
}
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 e2fdc44..f678649 100644
--- a/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/net/ConnectivityManagerCompat.java
@@ -86,7 +86,10 @@
*
* @return {@code true} if large transfers should be avoided, otherwise
* {@code false}.
+ * @deprecated Call {@link ConnectivityManager#isActiveNetworkMetered()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "cm.isActiveNetworkMetered()")
@SuppressWarnings("deprecation")
@RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
public static boolean isActiveNetworkMetered(@NonNull ConnectivityManager cm) {
diff --git a/core/core/src/main/java/androidx/core/os/BundleCompat.java b/core/core/src/main/java/androidx/core/os/BundleCompat.java
index 5b72c26..3038657 100644
--- a/core/core/src/main/java/androidx/core/os/BundleCompat.java
+++ b/core/core/src/main/java/androidx/core/os/BundleCompat.java
@@ -190,6 +190,7 @@
*
* @deprecated Use {@link Bundle#getBinder(String)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "bundle.getBinder(key)")
@Deprecated
@Nullable
public static IBinder getBinder(@NonNull Bundle bundle, @Nullable String key) {
@@ -206,6 +207,7 @@
*
* @deprecated Use {@link Bundle#putBinder(String, IBinder)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "bundle.putBinder(key, binder)")
@Deprecated
public static void putBinder(@NonNull Bundle bundle, @Nullable String key,
@Nullable IBinder binder) {
diff --git a/core/core/src/main/java/androidx/core/os/ParcelCompat.java b/core/core/src/main/java/androidx/core/os/ParcelCompat.java
index cc52e51..fb67665 100644
--- a/core/core/src/main/java/androidx/core/os/ParcelCompat.java
+++ b/core/core/src/main/java/androidx/core/os/ParcelCompat.java
@@ -53,7 +53,10 @@
*
* <p>Note: This method currently delegates to {@link Parcel#writeInt} with a value of 1 or 0
* for true or false, respectively, but may change in the future.
+ * @deprecated Call {@link Parcel#writeInt()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "out.writeInt(value ? 1 : 0)")
public static void writeBoolean(@NonNull Parcel out, boolean value) {
out.writeInt(value ? 1 : 0);
}
diff --git a/core/core/src/main/java/androidx/core/view/MarginLayoutParamsCompat.java b/core/core/src/main/java/androidx/core/view/MarginLayoutParamsCompat.java
index a2859f5..876ca0b 100644
--- a/core/core/src/main/java/androidx/core/view/MarginLayoutParamsCompat.java
+++ b/core/core/src/main/java/androidx/core/view/MarginLayoutParamsCompat.java
@@ -43,6 +43,7 @@
* @return the margin along the starting edge in pixels
* @deprecated Use {@link ViewGroup.MarginLayoutParams#getMarginStart} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "lp.getMarginStart()")
@Deprecated
public static int getMarginStart(@NonNull ViewGroup.MarginLayoutParams lp) {
return lp.getMarginStart();
@@ -60,6 +61,7 @@
* @return the margin along the ending edge in pixels
* @deprecated Use {@link ViewGroup.MarginLayoutParams#getMarginStart} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "lp.getMarginEnd()")
@Deprecated
public static int getMarginEnd(@NonNull ViewGroup.MarginLayoutParams lp) {
return lp.getMarginEnd();
@@ -77,6 +79,7 @@
* @param marginStart the desired start margin in pixels
* @deprecated Use {@link ViewGroup.MarginLayoutParams#setMarginStart} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "lp.setMarginStart(marginStart)")
@Deprecated
public static void setMarginStart(@NonNull ViewGroup.MarginLayoutParams lp, int marginStart) {
lp.setMarginStart(marginStart);
@@ -94,6 +97,7 @@
* @param marginEnd the desired end margin in pixels
* @deprecated Use {@link ViewGroup.MarginLayoutParams#setMarginEnd} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "lp.setMarginEnd(marginEnd)")
@Deprecated
public static void setMarginEnd(@NonNull ViewGroup.MarginLayoutParams lp, int marginEnd) {
lp.setMarginEnd(marginEnd);
@@ -105,6 +109,7 @@
* @return true if either marginStart or marginEnd has been set.
* @deprecated Use {@link ViewGroup.MarginLayoutParams#isMarginRelative} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "lp.isMarginRelative()")
@Deprecated
public static boolean isMarginRelative(@NonNull ViewGroup.MarginLayoutParams lp) {
return lp.isMarginRelative();
@@ -140,6 +145,7 @@
* or {@link ViewCompat#LAYOUT_DIRECTION_RTL}.
* @deprecated Use {@link ViewGroup.MarginLayoutParams#setLayoutDirection} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "lp.setLayoutDirection(layoutDirection)")
@Deprecated
public static void setLayoutDirection(@NonNull ViewGroup.MarginLayoutParams lp,
int layoutDirection) {
@@ -152,6 +158,7 @@
*
* @deprecated Use {@link ViewGroup.MarginLayoutParams#resolveLayoutDirection} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "lp.resolveLayoutDirection(layoutDirection)")
@Deprecated
public static void resolveLayoutDirection(@NonNull ViewGroup.MarginLayoutParams lp,
int layoutDirection) {
diff --git a/core/core/src/main/java/androidx/core/view/MenuCompat.java b/core/core/src/main/java/androidx/core/view/MenuCompat.java
index de416fd..36b96d9 100644
--- a/core/core/src/main/java/androidx/core/view/MenuCompat.java
+++ b/core/core/src/main/java/androidx/core/view/MenuCompat.java
@@ -34,6 +34,7 @@
*
* @deprecated Use {@link MenuItem#setShowAsAction(int)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "item.setShowAsAction(actionEnum)")
@Deprecated
public static void setShowAsAction(MenuItem item, int actionEnum) {
item.setShowAsAction(actionEnum);
diff --git a/core/core/src/main/java/androidx/core/view/MenuItemCompat.java b/core/core/src/main/java/androidx/core/view/MenuItemCompat.java
index 715b9ba..5df61da 100644
--- a/core/core/src/main/java/androidx/core/view/MenuItemCompat.java
+++ b/core/core/src/main/java/androidx/core/view/MenuItemCompat.java
@@ -135,6 +135,7 @@
*
* @deprecated Use {@link MenuItem#setShowAsAction(int)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "item.setShowAsAction(actionEnum)")
@Deprecated
public static void setShowAsAction(MenuItem item, int actionEnum) {
item.setShowAsAction(actionEnum);
@@ -153,6 +154,7 @@
*
* @deprecated Use {@link MenuItem#setActionView(View)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "item.setActionView(view)")
@Deprecated
public static MenuItem setActionView(MenuItem item, View view) {
return item.setActionView(view);
@@ -175,6 +177,7 @@
*
* @deprecated Use {@link MenuItem#setActionView(int)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "item.setActionView(resId)")
@Deprecated
public static MenuItem setActionView(MenuItem item, int resId) {
return item.setActionView(resId);
@@ -188,6 +191,7 @@
*
* @deprecated Use {@link MenuItem#getActionView()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "item.getActionView()")
@Deprecated
public static View getActionView(MenuItem item) {
return item.getActionView();
@@ -252,6 +256,7 @@
*
* @deprecated Use {@link MenuItem#expandActionView()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "item.expandActionView()")
@Deprecated
public static boolean expandActionView(MenuItem item) {
return item.expandActionView();
@@ -271,6 +276,7 @@
*
* @deprecated Use {@link MenuItem#collapseActionView()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "item.collapseActionView()")
@Deprecated
public static boolean collapseActionView(MenuItem item) {
return item.collapseActionView();
@@ -287,6 +293,7 @@
*
* @deprecated Use {@link MenuItem#isActionViewExpanded()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "item.isActionViewExpanded()")
@Deprecated
public static boolean isActionViewExpanded(MenuItem item) {
return item.isActionViewExpanded();
diff --git a/core/core/src/main/java/androidx/core/view/MotionEventCompat.java b/core/core/src/main/java/androidx/core/view/MotionEventCompat.java
index b39a46c..bad4584 100644
--- a/core/core/src/main/java/androidx/core/view/MotionEventCompat.java
+++ b/core/core/src/main/java/androidx/core/view/MotionEventCompat.java
@@ -462,6 +462,7 @@
* @deprecated Call {@link MotionEvent#getAction()} directly. This method will be
* removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "event.getActionMasked()")
@Deprecated
public static int getActionMasked(MotionEvent event) {
return event.getActionMasked();
@@ -474,6 +475,7 @@
* @deprecated Call {@link MotionEvent#getActionIndex()} directly. This method will be
* removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "event.getActionIndex()")
@Deprecated
public static int getActionIndex(MotionEvent event) {
return event.getActionIndex();
@@ -485,6 +487,7 @@
* @deprecated Call {@link MotionEvent#findPointerIndex(int)} directly. This method will be
* removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "event.findPointerIndex(pointerId)")
@Deprecated
public static int findPointerIndex(MotionEvent event, int pointerId) {
return event.findPointerIndex(pointerId);
@@ -496,6 +499,7 @@
* @deprecated Call {@link MotionEvent#getPointerId(int)} directly. This method will be
* removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "event.getPointerId(pointerIndex)")
@Deprecated
public static int getPointerId(MotionEvent event, int pointerIndex) {
return event.getPointerId(pointerIndex);
@@ -507,6 +511,7 @@
* @deprecated Call {@link MotionEvent#getX()} directly. This method will be
* removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "event.getX(pointerIndex)")
@Deprecated
public static float getX(MotionEvent event, int pointerIndex) {
return event.getX(pointerIndex);
@@ -518,6 +523,7 @@
* @deprecated Call {@link MotionEvent#getY()} directly. This method will be
* removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "event.getY(pointerIndex)")
@Deprecated
public static float getY(MotionEvent event, int pointerIndex) {
return event.getY(pointerIndex);
@@ -529,6 +535,7 @@
* @deprecated Call {@link MotionEvent#getPointerCount()} directly. This method will be
* removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "event.getPointerCount()")
@Deprecated
public static int getPointerCount(MotionEvent event) {
return event.getPointerCount();
@@ -541,6 +548,7 @@
* @deprecated Call {@link MotionEvent#getSource()} directly. This method will be
* removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "event.getSource()")
@Deprecated
public static int getSource(MotionEvent event) {
return event.getSource();
@@ -569,6 +577,7 @@
* @deprecated Call {@link MotionEvent#getAxisValue(int)} directly. This method will be
* removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "event.getAxisValue(axis)")
@Deprecated
public static float getAxisValue(MotionEvent event, int axis) {
return event.getAxisValue(axis);
@@ -590,6 +599,7 @@
* @deprecated Call {@link MotionEvent#getAxisValue(int, int)} directly. This method will be
* removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "event.getAxisValue(axis, pointerIndex)")
@Deprecated
public static float getAxisValue(MotionEvent event, int axis, int pointerIndex) {
return event.getAxisValue(axis, pointerIndex);
@@ -599,6 +609,7 @@
* @deprecated Call {@link MotionEvent#getButtonState()} directly. This method will be
* removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "event.getButtonState()")
@Deprecated
public static int getButtonState(MotionEvent event) {
return event.getButtonState();
diff --git a/core/core/src/main/java/androidx/core/view/ScaleGestureDetectorCompat.java b/core/core/src/main/java/androidx/core/view/ScaleGestureDetectorCompat.java
index f24ea50..02cc957 100644
--- a/core/core/src/main/java/androidx/core/view/ScaleGestureDetectorCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ScaleGestureDetectorCompat.java
@@ -50,7 +50,10 @@
*
* @param scaleGestureDetector detector for which to set the scaling mode.
* @param enabled true to enable quick scaling, false to disable
+ * @deprecated Call {@link ScaleGestureDetector#setQuickScaleEnabled()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "scaleGestureDetector.setQuickScaleEnabled(enabled)")
public static void setQuickScaleEnabled(
@NonNull ScaleGestureDetector scaleGestureDetector, boolean enabled) {
scaleGestureDetector.setQuickScaleEnabled(enabled);
@@ -74,7 +77,10 @@
* Returns whether the quick scale gesture, in which the user performs a double tap followed by
* a swipe, should perform scaling. See
* {@link #setQuickScaleEnabled(ScaleGestureDetector, boolean)}.
+ * @deprecated Call {@link ScaleGestureDetector#isQuickScaleEnabled()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "scaleGestureDetector.isQuickScaleEnabled()")
public static boolean isQuickScaleEnabled(@NonNull ScaleGestureDetector scaleGestureDetector) {
return scaleGestureDetector.isQuickScaleEnabled();
}
diff --git a/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java b/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java
index 3f20831..aedbab2 100644
--- a/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java
@@ -85,6 +85,7 @@
*
* @deprecated Use {@link VelocityTracker#getXVelocity(int)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "tracker.getXVelocity(pointerId)")
@Deprecated
public static float getXVelocity(VelocityTracker tracker, int pointerId) {
return tracker.getXVelocity(pointerId);
@@ -97,6 +98,7 @@
*
* @deprecated Use {@link VelocityTracker#getYVelocity(int)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "tracker.getYVelocity(pointerId)")
@Deprecated
public static float getYVelocity(VelocityTracker tracker, int pointerId) {
return tracker.getYVelocity(pointerId);
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 0345c75..46875b0 100644
--- a/core/core/src/main/java/androidx/core/view/ViewCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewCompat.java
@@ -595,6 +595,7 @@
*
* @deprecated Use {@link View#canScrollHorizontally(int)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.canScrollHorizontally(direction)")
@Deprecated
public static boolean canScrollHorizontally(View view, int direction) {
return view.canScrollHorizontally(direction);
@@ -608,6 +609,7 @@
* @return true if this view can be scrolled in the specified direction, false otherwise.
* @deprecated Use {@link View#canScrollVertically(int)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.canScrollVertically(direction)")
@Deprecated
public static boolean canScrollVertically(View view, int direction) {
return view.canScrollVertically(direction);
@@ -624,6 +626,7 @@
* @deprecated Call {@link View#getOverScrollMode()} directly. This method will be
* removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getOverScrollMode()")
@Deprecated
@OverScroll
public static int getOverScrollMode(View view) {
@@ -645,6 +648,7 @@
* @deprecated Call {@link View#setOverScrollMode(int)} directly. This method will be
* removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setOverScrollMode(overScrollMode)")
@Deprecated
public static void setOverScrollMode(View view, @OverScroll int overScrollMode) {
view.setOverScrollMode(overScrollMode);
@@ -688,6 +692,7 @@
* @deprecated Call {@link View#onPopulateAccessibilityEvent(AccessibilityEvent)} directly.
* This method will be removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "v.onPopulateAccessibilityEvent(event)")
@Deprecated
public static void onPopulateAccessibilityEvent(View v, AccessibilityEvent event) {
v.onPopulateAccessibilityEvent(event);
@@ -720,6 +725,7 @@
* @deprecated Call {@link View#onInitializeAccessibilityEvent(AccessibilityEvent)} directly.
* This method will be removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "v.onInitializeAccessibilityEvent(event)")
@Deprecated
public static void onInitializeAccessibilityEvent(View v, AccessibilityEvent event) {
v.onInitializeAccessibilityEvent(event);
@@ -754,6 +760,7 @@
* @deprecated Call {@link View#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)}
* directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "v.onInitializeAccessibilityNodeInfo(info.unwrap())")
@Deprecated
public static void onInitializeAccessibilityNodeInfo(@NonNull View v,
@NonNull AccessibilityNodeInfoCompat info) {
@@ -1290,6 +1297,7 @@
* @return true if the view has transient state
* @deprecated Call {@link View#hasTransientState()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.hasTransientState()")
@Deprecated
public static boolean hasTransientState(@NonNull View view) {
return view.hasTransientState();
@@ -1303,6 +1311,7 @@
* @param hasTransientState true if this view has transient state
* @deprecated Call {@link View#setHasTransientState(boolean)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setHasTransientState(hasTransientState)")
@Deprecated
public static void setHasTransientState(@NonNull View view, boolean hasTransientState) {
view.setHasTransientState(hasTransientState);
@@ -1318,6 +1327,7 @@
* @param view View to invalidate
* @deprecated Call {@link View#postInvalidateOnAnimation()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.postInvalidateOnAnimation()")
@Deprecated
public static void postInvalidateOnAnimation(@NonNull View view) {
view.postInvalidateOnAnimation();
@@ -1337,6 +1347,7 @@
* @param bottom The bottom coordinate of the rectangle to invalidate.
* @deprecated Call {@link View#postInvalidateOnAnimation(int, int, int, int)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.postInvalidateOnAnimation(left, top, right, bottom)")
@Deprecated
public static void postInvalidateOnAnimation(@NonNull View view, int left, int top,
int right, int bottom) {
@@ -1354,6 +1365,7 @@
* @param action The Runnable that will be executed.
* @deprecated Call {@link View#postOnAnimation(Runnable)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.postOnAnimation(action)")
@Deprecated
public static void postOnAnimation(@NonNull View view, @NonNull Runnable action) {
view.postOnAnimation(action);
@@ -1373,6 +1385,7 @@
* will be executed.
* @deprecated Call {@link View#postOnAnimationDelayed(Runnable, long)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.postOnAnimationDelayed(action, delayMillis)")
@Deprecated
@SuppressLint("LambdaLast")
public static void postOnAnimationDelayed(@NonNull View view, @NonNull Runnable action,
@@ -1394,6 +1407,7 @@
* @see #IMPORTANT_FOR_ACCESSIBILITY_AUTO
* @deprecated Call {@link View#getImportantForAccessibility()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getImportantForAccessibility()")
@Deprecated
@ImportantForAccessibility
public static int getImportantForAccessibility(@NonNull View view) {
@@ -1420,6 +1434,7 @@
* @see #IMPORTANT_FOR_ACCESSIBILITY_AUTO
* @deprecated Call {@link View#setImportantForAccessibility(int)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setImportantForAccessibility(mode)")
@Deprecated
@UiThread
public static void setImportantForAccessibility(@NonNull View view,
@@ -1492,6 +1507,7 @@
* @return Whether the action was performed.
* @deprecated Call {@link View#performAccessibilityAction(int, Bundle)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.performAccessibilityAction(action, arguments)")
@Deprecated
public static boolean performAccessibilityAction(@NonNull View view, int action,
@Nullable Bundle arguments) {
@@ -1828,6 +1844,7 @@
*
* @deprecated Use {@link View#getAlpha()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getAlpha()")
@Deprecated
public static float getAlpha(View view) {
return view.getAlpha();
@@ -1867,6 +1884,7 @@
*
* @deprecated Use {@link View#setLayerType(int, Paint)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setLayerType(layerType, paint)")
@Deprecated
public static void setLayerType(View view, @LayerType int layerType, Paint paint) {
view.setLayerType(layerType, paint);
@@ -1890,6 +1908,7 @@
*
* @deprecated Use {@link View#getLayerType()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getLayerType()")
@Deprecated
@LayerType
public static int getLayerType(View view) {
@@ -1905,6 +1924,7 @@
* @return The labeled view id.
* @deprecated Call {@link View#getLabelFor()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getLabelFor()")
@Deprecated
public static int getLabelFor(@NonNull View view) {
return view.getLabelFor();
@@ -1918,6 +1938,7 @@
* @param labeledId The labeled view id.
* @deprecated Call {@link View#setLabelFor(int)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setLabelFor(labeledId)")
@Deprecated
public static void setLabelFor(@NonNull View view, @IdRes int labeledId) {
view.setLabelFor(labeledId);
@@ -1954,6 +1975,7 @@
* @see #setLayerType(View, int, android.graphics.Paint)
* @deprecated Call {@link View#setLayerPaint(Paint)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setLayerPaint(paint)")
@Deprecated
public static void setLayerPaint(@NonNull View view, @Nullable Paint paint) {
view.setLayerPaint(paint);
@@ -1971,6 +1993,7 @@
*
* @deprecated Call {@link View#getLayoutDirection()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getLayoutDirection()")
@Deprecated
@ResolvedLayoutDirectionMode
public static int getLayoutDirection(@NonNull View view) {
@@ -1995,6 +2018,7 @@
*
* @deprecated Call {@link View#setLayoutDirection(int)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setLayoutDirection(layoutDirection)")
@Deprecated
public static void setLayoutDirection(@NonNull View view,
@LayoutDirectionMode int layoutDirection) {
@@ -2010,6 +2034,7 @@
* @return The parent for use in accessibility inspection
* @deprecated Call {@link View#getParentForAccessibility()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getParentForAccessibility()")
@Deprecated
@Nullable
public static ViewParent getParentForAccessibility(@NonNull View view) {
@@ -2054,6 +2079,7 @@
* @deprecated Use {@link View#isOpaque()} directly. This method will be
* removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.isOpaque()")
@Deprecated
public static boolean isOpaque(View view) {
return view.isOpaque();
@@ -2092,6 +2118,7 @@
*
* @deprecated Use {@link View#getMeasuredWidth()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getMeasuredWidthAndState()")
@Deprecated
public static int getMeasuredWidthAndState(View view) {
return view.getMeasuredWidthAndState();
@@ -2109,6 +2136,7 @@
*
* @deprecated Use {@link View#getMeasuredHeightAndState()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getMeasuredHeightAndState()")
@Deprecated
public static int getMeasuredHeightAndState(View view) {
return view.getMeasuredHeightAndState();
@@ -2123,6 +2151,7 @@
*
* @deprecated Use {@link View#getMeasuredState()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getMeasuredState()")
@Deprecated
public static int getMeasuredState(View view) {
return view.getMeasuredState();
@@ -2152,6 +2181,7 @@
* @see ViewCompat#setAccessibilityLiveRegion(View, int)
* @deprecated Call {@link View#getAccessibilityLiveRegion()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getAccessibilityLiveRegion()")
@Deprecated
@AccessibilityLiveRegion
public static int getAccessibilityLiveRegion(@NonNull View view) {
@@ -2199,6 +2229,7 @@
* </ul>
* @deprecated Call {@link View#setAccessibilityLiveRegion(int)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setAccessibilityLiveRegion(mode)")
@Deprecated
public static void setAccessibilityLiveRegion(@NonNull View view,
@AccessibilityLiveRegion int mode) {
@@ -2214,6 +2245,7 @@
* @return the start padding in pixels
* @deprecated Call {@link View#getPaddingStart()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getPaddingStart()")
@Deprecated
@Px
public static int getPaddingStart(@NonNull View view) {
@@ -2229,6 +2261,7 @@
* @return the end padding in pixels
* @deprecated Call {@link View#getPaddingEnd()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getPaddingEnd()")
@Deprecated
@Px
public static int getPaddingEnd(@NonNull View view) {
@@ -2249,6 +2282,7 @@
* @param bottom the bottom padding in pixels
* @deprecated Call {@link View#setPaddingRelative(int, int, int, int)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setPaddingRelative(start, top, end, bottom)")
@Deprecated
public static void setPaddingRelative(@NonNull View view, @Px int start, @Px int top,
@Px int end, @Px int bottom) {
@@ -2322,6 +2356,7 @@
*
* @deprecated Use {@link View#getTranslationX()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getTranslationX()")
@Deprecated
public static float getTranslationX(View view) {
return view.getTranslationX();
@@ -2336,6 +2371,7 @@
*
* @deprecated Use {@link View#getTranslationY()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getTranslationY()")
@Deprecated
public static float getTranslationY(View view) {
return view.getTranslationY();
@@ -2357,6 +2393,7 @@
*
* @deprecated Use {@link View#getMatrix()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getMatrix()")
@Deprecated
@Nullable
public static Matrix getMatrix(View view) {
@@ -2371,6 +2408,7 @@
* @return the minimum width the view will try to be.
* @deprecated Call {@link View#getMinimumWidth()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getMinimumWidth()")
@Deprecated
@SuppressWarnings({"JavaReflectionMemberAccess", "ConstantConditions"})
// Reflective access to private field, unboxing result of reflective get()
@@ -2386,6 +2424,7 @@
* @return the minimum height the view will try to be.
* @deprecated Call {@link View#getMinimumHeight()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getMinimumHeight()")
@Deprecated
@SuppressWarnings({"JavaReflectionMemberAccess", "ConstantConditions"})
// Reflective access to private field, unboxing result of reflective get()
@@ -2425,6 +2464,7 @@
*
* @deprecated Use {@link View#setTranslationX(float)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setTranslationX(value)")
@Deprecated
public static void setTranslationX(View view, float value) {
view.setTranslationX(value);
@@ -2443,6 +2483,7 @@
*
* @deprecated Use {@link View#setTranslationY(float)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setTranslationY(value)")
@Deprecated
public static void setTranslationY(View view, float value) {
view.setTranslationY(value);
@@ -2461,6 +2502,7 @@
*
* @deprecated Use {@link View#setAlpha(float)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setAlpha(value)")
@Deprecated
public static void setAlpha(View view, @FloatRange(from = 0.0, to = 1.0) float value) {
view.setAlpha(value);
@@ -2477,6 +2519,7 @@
*
* @deprecated Use {@link View#setX(float)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setX(value)")
@Deprecated
public static void setX(View view, float value) {
view.setX(value);
@@ -2493,6 +2536,7 @@
*
* @deprecated Use {@link View#setY(float)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setY(value)")
@Deprecated
public static void setY(View view, float value) {
view.setY(value);
@@ -2507,6 +2551,7 @@
*
* @deprecated Use {@link View#setRotation(float)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setRotation(value)")
@Deprecated
public static void setRotation(View view, float value) {
view.setRotation(value);
@@ -2522,6 +2567,7 @@
*
* @deprecated Use {@link View#setRotationX(float)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setRotationX(value)")
@Deprecated
public static void setRotationX(View view, float value) {
view.setRotationX(value);
@@ -2537,6 +2583,7 @@
*
* @deprecated Use {@link View#setRotationY(float)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setRotationY(value)")
@Deprecated
public static void setRotationY(View view, float value) {
view.setRotationY(value);
@@ -2551,6 +2598,7 @@
*
* @deprecated Use {@link View#setScaleX(float)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setScaleX(value)")
@Deprecated
public static void setScaleX(View view, float value) {
view.setScaleX(value);
@@ -2565,6 +2613,7 @@
*
* @deprecated Use {@link View#setScaleY(float)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setScaleY(value)")
@Deprecated
public static void setScaleY(View view, float value) {
view.setScaleY(value);
@@ -2577,6 +2626,7 @@
* @param view view for which to get the pivot.
* @deprecated Use {@link View#getPivotX()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getPivotX()")
@Deprecated
public static float getPivotX(View view) {
return view.getPivotX();
@@ -2594,6 +2644,7 @@
*
* @deprecated Use {@link View#setPivotX(float)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setPivotX(value)")
@Deprecated
public static void setPivotX(View view, float value) {
view.setPivotX(value);
@@ -2608,6 +2659,7 @@
*
* @deprecated Use {@link View#getPivotY()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getPivotY()")
@Deprecated
public static float getPivotY(View view) {
return view.getPivotY();
@@ -2625,6 +2677,7 @@
*
* @deprecated Use {@link View#setPivotX(float)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setPivotY(value)")
@Deprecated
public static void setPivotY(View view, float value) {
view.setPivotY(value);
@@ -2634,6 +2687,7 @@
* @param view view for which to get the rotation.
* @deprecated Use {@link View#getRotation()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getRotation()")
@Deprecated
public static float getRotation(View view) {
return view.getRotation();
@@ -2643,6 +2697,7 @@
* @param view view for which to get the rotation.
* @deprecated Use {@link View#getRotationX()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getRotationX()")
@Deprecated
public static float getRotationX(View view) {
return view.getRotationX();
@@ -2652,6 +2707,7 @@
* @param view view for which to get the rotation.
* @deprecated Use {@link View#getRotationY()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getRotationY()")
@Deprecated
public static float getRotationY(View view) {
return view.getRotationY();
@@ -2661,6 +2717,7 @@
* @param view view for which to get the scale.
* @deprecated Use {@link View#getScaleX()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getScaleX()")
@Deprecated
public static float getScaleX(View view) {
return view.getScaleX();
@@ -2670,6 +2727,7 @@
* @param view view for which to get the scale.
* @deprecated Use {@link View#getScaleY()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getScaleY()")
@Deprecated
public static float getScaleY(View view) {
return view.getScaleY();
@@ -2679,6 +2737,7 @@
* @param view view for which to get the X.
* @deprecated Use {@link View#getX()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getX()")
@Deprecated
public static float getX(View view) {
return view.getX();
@@ -2688,6 +2747,7 @@
* @param view view for which to get the Y.
* @deprecated Use {@link View#getY()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getY()")
@Deprecated
public static float getY(View view) {
return view.getY();
@@ -2788,6 +2848,7 @@
* @deprecated SystemUiVisibility flags are deprecated. Use
* {@link WindowInsetsController} instead.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getWindowSystemUiVisibility()")
@Deprecated
public static int getWindowSystemUiVisibility(@NonNull View view) {
return view.getWindowSystemUiVisibility();
@@ -2849,6 +2910,7 @@
* @param view view for which to get the state.
* @deprecated Call {@link View#getFitsSystemWindows()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getFitsSystemWindows()")
@Deprecated
public static boolean getFitsSystemWindows(@NonNull View view) {
return view.getFitsSystemWindows();
@@ -2866,6 +2928,7 @@
*
* @deprecated Use {@link View#setFitsSystemWindows(boolean)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setFitsSystemWindows(fitSystemWindows)")
@Deprecated
public static void setFitsSystemWindows(View view, boolean fitSystemWindows) {
view.setFitsSystemWindows(fitSystemWindows);
@@ -2881,6 +2944,7 @@
* @param view view for which to jump the drawable state.
* @deprecated Use {@link View#jumpDrawablesToCurrentState()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.jumpDrawablesToCurrentState()")
@Deprecated
public static void jumpDrawablesToCurrentState(View view) {
view.jumpDrawablesToCurrentState();
@@ -3300,6 +3364,7 @@
*
* @deprecated Use {@link View#setSaveFromParentEnabled(boolean)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setSaveFromParentEnabled(enabled)")
@Deprecated
public static void setSaveFromParentEnabled(View view, boolean enabled) {
view.setSaveFromParentEnabled(enabled);
@@ -3317,6 +3382,7 @@
*
* @deprecated Use {@link View#setActivated(boolean)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setActivated(activated)")
@Deprecated
public static void setActivated(View view, boolean activated) {
view.setActivated(activated);
@@ -3338,6 +3404,7 @@
* @return true if the content in this view might overlap, false otherwise.
* @deprecated Call {@link View#hasOverlappingRendering()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.hasOverlappingRendering()")
@Deprecated
public static boolean hasOverlappingRendering(@NonNull View view) {
return view.hasOverlappingRendering();
@@ -3351,6 +3418,7 @@
* @return true if the padding is relative or false if it is not.
* @deprecated Call {@link View#isPaddingRelative()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.isPaddingRelative()")
@Deprecated
public static boolean isPaddingRelative(@NonNull View view) {
return view.isPaddingRelative();
@@ -3365,6 +3433,7 @@
* @param background the drawable to use as view background.
* @deprecated Call {@link View#setBackground(Drawable)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setBackground(background)")
@Deprecated
public static void setBackground(@NonNull View view, @Nullable Drawable background) {
view.setBackground(background);
@@ -3926,6 +3995,7 @@
* @return whether the view hierarchy is currently undergoing a layout pass
* @deprecated Call {@link View#isInLayout()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.isInLayout()")
@Deprecated
public static boolean isInLayout(@NonNull View view) {
return view.isInLayout();
@@ -3936,6 +4006,7 @@
* was last attached to or detached from a window.
* @deprecated Call {@link View#isLaidOut()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.isLaidOut()")
@Deprecated
public static boolean isLaidOut(@NonNull View view) {
return view.isLaidOut();
@@ -3952,6 +4023,7 @@
* @return true if layout direction has been resolved.
* @deprecated Call {@link View#isLayoutDirectionResolved()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.isLayoutDirectionResolved()")
@Deprecated
public static boolean isLayoutDirectionResolved(@NonNull View view) {
return view.isLayoutDirectionResolved();
@@ -4108,6 +4180,7 @@
* this view, to which future drawing operations will be clipped.
* @deprecated Call {@link View#setClipBounds(Rect)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.setClipBounds(clipBounds)")
@Deprecated
public static void setClipBounds(@NonNull View view, @Nullable Rect clipBounds) {
view.setClipBounds(clipBounds);
@@ -4122,6 +4195,7 @@
* otherwise null.
* @deprecated Call {@link View#getClipBounds()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getClipBounds()")
@Deprecated
@Nullable
public static Rect getClipBounds(@NonNull View view) {
@@ -4132,6 +4206,7 @@
* Returns true if the provided view is currently attached to a window.
* @deprecated Call {@link View#isAttachedToWindow()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.isAttachedToWindow()")
@Deprecated
public static boolean isAttachedToWindow(@NonNull View view) {
return view.isAttachedToWindow();
@@ -4143,6 +4218,7 @@
* @return true if there is a listener, false if there is none.
* @deprecated Call {@link View#hasOnClickListeners()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.hasOnClickListeners()")
@Deprecated
public static boolean hasOnClickListeners(@NonNull View view) {
return view.hasOnClickListeners();
@@ -4246,6 +4322,7 @@
* @return The logical display, or null if the view is not currently attached to a window.
* @deprecated Call {@link View#getDisplay()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "view.getDisplay()")
@Deprecated
@Nullable
public static Display getDisplay(@NonNull View view) {
diff --git a/core/core/src/main/java/androidx/core/view/ViewConfigurationCompat.java b/core/core/src/main/java/androidx/core/view/ViewConfigurationCompat.java
index 8e15fb0..157aa0c 100644
--- a/core/core/src/main/java/androidx/core/view/ViewConfigurationCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewConfigurationCompat.java
@@ -69,6 +69,7 @@
* @deprecated Call {@link ViewConfiguration#getScaledPagingTouchSlop()} directly.
* This method will be removed in a future release.
*/
+ @androidx.annotation.ReplaceWith(expression = "config.getScaledPagingTouchSlop()")
@Deprecated
public static int getScaledPagingTouchSlop(ViewConfiguration config) {
return config.getScaledPagingTouchSlop();
@@ -80,6 +81,7 @@
*
* @deprecated Use {@link ViewConfiguration#hasPermanentMenuKey()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "config.hasPermanentMenuKey()")
@Deprecated
public static boolean hasPermanentMenuKey(ViewConfiguration config) {
return config.hasPermanentMenuKey();
diff --git a/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java b/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java
index 3034440..e35464f 100644
--- a/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewGroupCompat.java
@@ -71,6 +71,7 @@
* @deprecated Use {@link ViewGroup#onRequestSendAccessibilityEvent(View, AccessibilityEvent)}
* directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "group.onRequestSendAccessibilityEvent(child, event)")
@Deprecated
public static boolean onRequestSendAccessibilityEvent(ViewGroup group, View child,
AccessibilityEvent event) {
@@ -95,6 +96,7 @@
*
* @deprecated Use {@link ViewGroup#setMotionEventSplittingEnabled(boolean)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "group.setMotionEventSplittingEnabled(split)")
@Deprecated
public static void setMotionEventSplittingEnabled(ViewGroup group, boolean split) {
group.setMotionEventSplittingEnabled(split);
@@ -111,7 +113,10 @@
* @return the layout mode to use during layout operations
*
* @see #setLayoutMode(ViewGroup, int)
+ * @deprecated Call {@link ViewGroup#getLayoutMode()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "group.getLayoutMode()")
public static int getLayoutMode(@NonNull ViewGroup group) {
return group.getLayoutMode();
}
@@ -125,7 +130,10 @@
* @param mode the layout mode to use during layout operations
*
* @see #getLayoutMode(ViewGroup)
+ * @deprecated Call {@link ViewGroup#setLayoutMode()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "group.setLayoutMode(mode)")
public static void setLayoutMode(@NonNull ViewGroup group, int mode) {
group.setLayoutMode(mode);
}
diff --git a/core/core/src/main/java/androidx/core/view/ViewParentCompat.java b/core/core/src/main/java/androidx/core/view/ViewParentCompat.java
index 1e3cdef..2a28972 100644
--- a/core/core/src/main/java/androidx/core/view/ViewParentCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewParentCompat.java
@@ -62,6 +62,7 @@
* @deprecated Use {@link ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)}
* directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "parent.requestSendAccessibilityEvent(child, event)")
@Deprecated
public static boolean requestSendAccessibilityEvent(
ViewParent parent, View child, AccessibilityEvent event) {
@@ -504,7 +505,10 @@
* <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT}
* <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED}
* </ul>
+ * @deprecated Call {@link ViewParent#notifySubtreeAccessibilityStateChanged()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "parent.notifySubtreeAccessibilityStateChanged(child, source, changeType)")
public static void notifySubtreeAccessibilityStateChanged(@NonNull ViewParent parent,
@NonNull View child, @NonNull View source, int changeType) {
parent.notifySubtreeAccessibilityStateChanged(child, source, changeType);
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 30470be..b5002e8 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
@@ -363,6 +363,7 @@
*
* @deprecated Use {@link AccessibilityEvent#getRecordCount()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "event.getRecordCount()")
@Deprecated
public static int getRecordCount(AccessibilityEvent event) {
return event.getRecordCount();
@@ -379,6 +380,7 @@
*
* @deprecated Use {@link AccessibilityEvent#appendRecord(AccessibilityRecord)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "event.appendRecord(record)")
@SuppressWarnings("deprecation")
@Deprecated
public static void appendRecord(AccessibilityEvent event, AccessibilityRecordCompat record) {
@@ -429,7 +431,10 @@
* @param changeTypes The bit mask of change types.
* @throws IllegalStateException If called from an AccessibilityService.
* @see #getContentChangeTypes(AccessibilityEvent)
+ * @deprecated Call {@link AccessibilityEvent#setContentChangeTypes()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "event.setContentChangeTypes(changeTypes)")
public static void setContentChangeTypes(@NonNull AccessibilityEvent event,
@ContentChangeType int changeTypes) {
event.setContentChangeTypes(changeTypes);
@@ -448,7 +453,10 @@
* <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT}
* <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED}
* </ul>
+ * @deprecated Call {@link AccessibilityEvent#getContentChangeTypes()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "event.getContentChangeTypes()")
@SuppressLint("WrongConstant")
@ContentChangeType
public static int getContentChangeTypes(@NonNull AccessibilityEvent event) {
@@ -462,7 +470,10 @@
* @param granularity The granularity.
*
* @throws IllegalStateException If called from an AccessibilityService.
+ * @deprecated Call {@link AccessibilityEvent#setMovementGranularity()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "event.setMovementGranularity(granularity)")
public static void setMovementGranularity(@NonNull AccessibilityEvent event, int granularity) {
event.setMovementGranularity(granularity);
}
@@ -471,7 +482,10 @@
* Gets the movement granularity that was traversed.
*
* @return The granularity.
+ * @deprecated Call {@link AccessibilityEvent#getMovementGranularity()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "event.getMovementGranularity()")
public static int getMovementGranularity(@NonNull AccessibilityEvent event) {
return event.getMovementGranularity();
}
@@ -493,7 +507,10 @@
* @param action The action.
* @throws IllegalStateException If called from an AccessibilityService.
* @see AccessibilityNodeInfoCompat#performAction(int)
+ * @deprecated Call {@link AccessibilityEvent#setAction()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "event.setAction(action)")
public static void setAction(@NonNull AccessibilityEvent event, int action) {
event.setAction(action);
}
@@ -502,7 +519,10 @@
* Gets the performed action that triggered this event.
*
* @return The action.
+ * @deprecated Call {@link AccessibilityEvent#getAction()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "event.getAction()")
public static int getAction(@NonNull AccessibilityEvent event) {
return event.getAction();
}
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityManagerCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityManagerCompat.java
index 4cba014..db071eb 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityManagerCompat.java
@@ -115,6 +115,7 @@
*
* @deprecated Use {@link AccessibilityManager#getInstalledAccessibilityServiceList()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "manager.getInstalledAccessibilityServiceList()")
@Deprecated
public static List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(
AccessibilityManager manager) {
@@ -138,6 +139,7 @@
* @deprecated Use {@link AccessibilityManager#getEnabledAccessibilityServiceList(int)}
* directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "manager.getEnabledAccessibilityServiceList(feedbackTypeFlags)")
@Deprecated
public static List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
AccessibilityManager manager, int feedbackTypeFlags) {
@@ -152,6 +154,7 @@
*
* @deprecated Use {@link AccessibilityManager#isTouchExplorationEnabled()} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "manager.isTouchExplorationEnabled()")
@Deprecated
public static boolean isTouchExplorationEnabled(AccessibilityManager manager) {
return manager.isTouchExplorationEnabled();
@@ -164,7 +167,10 @@
* @param manager AccessibilityManager for which to add the listener.
* @param listener The listener.
* @return True if successfully registered.
+ * @deprecated Call {@link AccessibilityManager#addTouchExplorationStateChangeListener(AccessibilityManager.TouchExplorationStateChangeListener)} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "manager.addTouchExplorationStateChangeListener(listener)")
public static boolean addTouchExplorationStateChangeListener(
@NonNull AccessibilityManager manager,
@NonNull TouchExplorationStateChangeListener listener) {
@@ -178,7 +184,10 @@
* @param manager AccessibilityManager for which to remove the listener.
* @param listener The listener.
* @return True if successfully unregistered.
+ * @deprecated Call {@link AccessibilityManager#removeTouchExplorationStateChangeListener(AccessibilityManager.TouchExplorationStateChangeListener)} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "manager.removeTouchExplorationStateChangeListener(listener)")
public static boolean removeTouchExplorationStateChangeListener(
@NonNull AccessibilityManager manager,
@NonNull TouchExplorationStateChangeListener listener) {
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityRecordCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityRecordCompat.java
index 4cdb3ea..cf9d33f 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityRecordCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityRecordCompat.java
@@ -136,7 +136,10 @@
* @param record The {@link AccessibilityRecord} instance to use.
* @param root The root of the virtual subtree.
* @param virtualDescendantId The id of the virtual descendant.
+ * @deprecated Call {@link AccessibilityRecord#setSource()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "record.setSource(root, virtualDescendantId)")
public static void setSource(@NonNull AccessibilityRecord record, @Nullable View root,
int virtualDescendantId) {
record.setSource(root, virtualDescendantId);
@@ -479,7 +482,10 @@
*
* @param record The {@link AccessibilityRecord} instance to use.
* @return The max scroll.
+ * @deprecated Call {@link AccessibilityRecord#getMaxScrollX()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "record.getMaxScrollX()")
public static int getMaxScrollX(@NonNull AccessibilityRecord record) {
return record.getMaxScrollX();
}
@@ -501,7 +507,10 @@
*
* @param record The {@link AccessibilityRecord} instance to use.
* @param maxScrollX The max scroll.
+ * @deprecated Call {@link AccessibilityRecord#setMaxScrollX()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "record.setMaxScrollX(maxScrollX)")
public static void setMaxScrollX(@NonNull AccessibilityRecord record, int maxScrollX) {
record.setMaxScrollX(maxScrollX);
}
@@ -523,7 +532,10 @@
*
* @param record The {@link AccessibilityRecord} instance to use.
* @return The max scroll.
+ * @deprecated Call {@link AccessibilityRecord#getMaxScrollY()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "record.getMaxScrollY()")
public static int getMaxScrollY(@NonNull AccessibilityRecord record) {
return record.getMaxScrollY();
}
@@ -545,7 +557,10 @@
*
* @param record The {@link AccessibilityRecord} instance to use.
* @param maxScrollY The max scroll.
+ * @deprecated Call {@link AccessibilityRecord#setMaxScrollY()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "record.setMaxScrollY(maxScrollY)")
public static void setMaxScrollY(@NonNull AccessibilityRecord record, int maxScrollY) {
record.setMaxScrollY(maxScrollY);
}
diff --git a/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java b/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java
index 35078e0..fbdd5249 100644
--- a/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java
@@ -113,7 +113,10 @@
* Returns the drawable used as the check mark image
*
* @see CheckedTextView#setCheckMarkDrawable(Drawable)
+ * @deprecated Call {@link CheckedTextView#getCheckMarkDrawable()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "textView.getCheckMarkDrawable()")
@Nullable
public static Drawable getCheckMarkDrawable(@NonNull CheckedTextView textView) {
return textView.getCheckMarkDrawable();
diff --git a/core/core/src/main/java/androidx/core/widget/ListPopupWindowCompat.java b/core/core/src/main/java/androidx/core/widget/ListPopupWindowCompat.java
index 4b8c16c..5818729 100644
--- a/core/core/src/main/java/androidx/core/widget/ListPopupWindowCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/ListPopupWindowCompat.java
@@ -90,7 +90,10 @@
* @param src the view on which the resulting listener will be set
* @return a touch listener that controls drag-to-open behavior, or {@code null} on
* unsupported APIs
+ * @deprecated Call {@link ListPopupWindow#createDragToOpenListener()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "listPopupWindow.createDragToOpenListener(src)")
@Nullable
public static OnTouchListener createDragToOpenListener(
@NonNull ListPopupWindow listPopupWindow, @NonNull View src) {
diff --git a/core/core/src/main/java/androidx/core/widget/ListViewCompat.java b/core/core/src/main/java/androidx/core/widget/ListViewCompat.java
index 894c7cb..328fcb68a 100644
--- a/core/core/src/main/java/androidx/core/widget/ListViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/ListViewCompat.java
@@ -35,6 +35,7 @@
* @param y the amount of pixels to scroll by vertically
* @deprecated Use {@link ListView#scrollListBy(int)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "listView.scrollListBy(y)")
@Deprecated
public static void scrollListBy(@NonNull ListView listView, int y) {
// Call the framework version directly
@@ -52,6 +53,7 @@
* @see #scrollListBy(ListView, int)
* @deprecated Use {@link ListView#canScrollList(int)} directly.
*/
+ @androidx.annotation.ReplaceWith(expression = "listView.canScrollList(direction)")
@Deprecated
public static boolean canScrollList(@NonNull ListView listView, int direction) {
// Call the framework version directly
diff --git a/core/core/src/main/java/androidx/core/widget/PopupWindowCompat.java b/core/core/src/main/java/androidx/core/widget/PopupWindowCompat.java
index b87351b..93e4bf0 100644
--- a/core/core/src/main/java/androidx/core/widget/PopupWindowCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/PopupWindowCompat.java
@@ -61,7 +61,10 @@
* @param xoff A horizontal offset from the anchor in pixels
* @param yoff A vertical offset from the anchor in pixels
* @param gravity Alignment of the popup relative to the anchor
+ * @deprecated Call {@link PopupWindow#showAsDropDown()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "popup.showAsDropDown(anchor, xoff, yoff, gravity)")
public static void showAsDropDown(@NonNull PopupWindow popup, @NonNull View anchor,
int xoff, int yoff, int gravity) {
popup.showAsDropDown(anchor, xoff, yoff, gravity);
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 c205dc4..b02cad1 100644
--- a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
@@ -116,7 +116,10 @@
* @attr name android:drawableTop
* @attr name android:drawableEnd
* @attr name android:drawableBottom
+ * @deprecated Call {@link TextView#setCompoundDrawablesRelative()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "textView.setCompoundDrawablesRelative(start, top, end, bottom)")
public static void setCompoundDrawablesRelative(@NonNull TextView textView,
@Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
@Nullable Drawable bottom) {
@@ -141,7 +144,10 @@
* @attr name android:drawableTop
* @attr name android:drawableEnd
* @attr name android:drawableBottom
+ * @deprecated Call {@link TextView#setCompoundDrawablesRelativeWithIntrinsicBounds()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom)")
public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
@Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
@Nullable Drawable bottom) {
@@ -165,7 +171,10 @@
* @attr name android:drawableTop
* @attr name android:drawableEnd
* @attr name android:drawableBottom
+ * @deprecated Call {@link TextView#setCompoundDrawablesRelativeWithIntrinsicBounds()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom)")
public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
@DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
@DrawableRes int bottom) {
@@ -175,7 +184,10 @@
/**
* Returns the maximum number of lines displayed in the given TextView, or -1 if the maximum
* height was set in pixels instead.
+ * @deprecated Call {@link TextView#getMaxLines()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "textView.getMaxLines()")
public static int getMaxLines(@NonNull TextView textView) {
return textView.getMaxLines();
}
@@ -183,7 +195,10 @@
/**
* Returns the minimum number of lines displayed in the given TextView, or -1 if the minimum
* height was set in pixels instead.
+ * @deprecated Call {@link TextView#getMinLines()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "textView.getMinLines()")
public static int getMinLines(@NonNull TextView textView) {
return textView.getMinLines();
}
@@ -207,7 +222,10 @@
/**
* Returns drawables for the start, top, end, and bottom borders from the given text view.
+ * @deprecated Call {@link TextView#getCompoundDrawablesRelative()} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "textView.getCompoundDrawablesRelative()")
@NonNull
public static Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
return textView.getCompoundDrawablesRelative();
@@ -406,7 +424,10 @@
*
* @param textView The TextView to set the action selection mode callback on.
* @param callback The action selection mode callback to set on textView.
+ * @deprecated Call {@link TextView#setCustomSelectionActionModeCallback(ActionMode.Callback)} directly.
*/
+ @Deprecated
+ @androidx.annotation.ReplaceWith(expression = "textView.setCustomSelectionActionModeCallback(callback)")
public static void setCustomSelectionActionModeCallback(@NonNull final TextView textView,
@NonNull final ActionMode.Callback callback) {
textView.setCustomSelectionActionModeCallback(
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt
index c3351b3..0da1391 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt
+++ b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt
@@ -470,4 +470,118 @@
"remoteValue"
)
}
+
+ @Test
+ fun testConcurrentUpdateNoDeadlock_file() = testConcurrentUpdateNoDeadlock(StorageVariant.FILE)
+
+ @Test
+ fun testConcurrentUpdateNoDeadlock_okio() = testConcurrentUpdateNoDeadlock(StorageVariant.OKIO)
+
+ /**
+ * Reproduce the false alarm on deadlock by Linux. It happens in the case where:<br>
+ * 1. process A holds file lock 1;<br>
+ * 2. process B holds file lock 2;<br>
+ * 3. process B (could be another thread than 2.) waits to hold file lock 1 (still held by A);<br>
+ * 4. process A (could be another thread than 1.) waits to hold file lock 2 (still held by B) -
+ * exception "Resource deadlock would occur" is thrown - caught and retried with exponential
+ * backoff.
+ */
+ private fun testConcurrentUpdateNoDeadlock(
+ storageVariant: StorageVariant
+ ) = multiProcessRule.runTest {
+ val connection = multiProcessRule.createConnection()
+ val subject1 = connection.createSubject(
+ multiProcessRule.datastoreScope
+ )
+ val subject2 = connection.createSubject(
+ multiProcessRule.datastoreScope
+ )
+
+ val file1 = tmpFolder.newFile()
+ val file2 = tmpFolder.newFile()
+ val datastore1 = createMultiProcessTestDatastore(
+ filePath = file1.canonicalPath,
+ storageVariant = storageVariant,
+ hostDatastoreScope = multiProcessRule.datastoreScope,
+ subjects = arrayOf(subject1)
+ )
+ val datastore2 = createMultiProcessTestDatastore(
+ filePath = file2.canonicalPath,
+ storageVariant = storageVariant,
+ hostDatastoreScope = multiProcessRule.datastoreScope,
+ subjects = arrayOf(subject2)
+ )
+
+ // setup real data and lock file
+ datastore1.updateData {
+ it.toBuilder().setText("hostData").build()
+ }
+ datastore2.updateData {
+ it.toBuilder().setText("hostData").build()
+ }
+
+ // process A holds file lock 1
+ val blockWrite = CompletableDeferred<Unit>()
+ val startedWrite = CompletableDeferred<Unit>()
+
+ val localUpdate1 = async {
+ datastore1.updateData {
+ startedWrite.complete(Unit)
+ blockWrite.await()
+ it.toBuilder().setInteger(3).build()
+ }
+ }
+ startedWrite.await()
+
+ // process B holds file lock 2
+ val commitWriteLatch1 = InterProcessCompletable<IpcUnit>()
+ val writeStartedLatch1 = InterProcessCompletable<IpcUnit>()
+ val setTextAction1 = async {
+ subject2.invokeInRemoteProcess(
+ SetTextAction(
+ value = "remoteValue",
+ commitTransactionLatch = commitWriteLatch1,
+ transactionStartedLatch = writeStartedLatch1
+ )
+ )
+ }
+ writeStartedLatch1.await(subject2)
+
+ // process B (could be another thread than 2.) waits to hold file lock 1
+ val commitWriteLatch2 = InterProcessCompletable<IpcUnit>()
+ val actionStartedLatch = InterProcessCompletable<IpcUnit>()
+ val setTextAction2 = async {
+ subject1.invokeInRemoteProcess(
+ SetTextAction(
+ value = "remoteValue",
+ commitTransactionLatch = commitWriteLatch2,
+ actionStartedLatch = actionStartedLatch
+ )
+ )
+ }
+ actionStartedLatch.await(subject1)
+ // wait a bit to let the other process get into updateData, might be flaky
+ delay(100)
+
+ // process A (could be another thread than 1.) waits to hold file lock 2 (still held by B)
+ val localUpdate2 = async {
+ datastore2.updateData {
+ it.toBuilder().setInteger(4).build()
+ }
+ }
+
+ blockWrite.complete(Unit)
+ commitWriteLatch1.complete(subject2, IpcUnit)
+ commitWriteLatch2.complete(subject1, IpcUnit)
+
+ setTextAction1.await()
+ setTextAction2.await()
+ localUpdate1.await()
+ localUpdate2.await()
+
+ assertThat(datastore1.data.first().text).isEqualTo("remoteValue")
+ assertThat(datastore1.data.first().integer).isEqualTo(3)
+ assertThat(datastore2.data.first().text).isEqualTo("remoteValue")
+ assertThat(datastore2.data.first().integer).isEqualTo(4)
+ }
}
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/SetTextAction.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/SetTextAction.kt
index f442d6ca..54440d2 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/SetTextAction.kt
+++ b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/ipcActions/SetTextAction.kt
@@ -28,10 +28,12 @@
private val value: String,
private val transactionStartedLatch: InterProcessCompletable<IpcUnit>? = null,
private val commitTransactionLatch: InterProcessCompletable<IpcUnit>? = null,
+ private val actionStartedLatch: InterProcessCompletable<IpcUnit>? = null,
) : IpcAction<IpcUnit>(), Parcelable {
override suspend fun invokeInRemoteProcess(
subject: TwoWayIpcSubject
): IpcUnit {
+ actionStartedLatch?.complete(subject, IpcUnit)
subject.datastore.updateData {
transactionStartedLatch?.complete(subject, IpcUnit)
commitTransactionLatch?.await(subject)
diff --git a/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt
index 3d3edff..e596d66 100644
--- a/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt
+++ b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt
@@ -25,6 +25,7 @@
import java.nio.channels.FileLock
import kotlin.contracts.ExperimentalContracts
import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -43,7 +44,7 @@
FileOutputStream(lockFile).use { lockFileStream ->
var lock: FileLock? = null
try {
- lock = lockFileStream.getChannel().lock(0L, Long.MAX_VALUE, /* shared= */ false)
+ lock = getExclusiveFileLockWithRetryIfDeadlock(lockFileStream)
return block()
} finally {
lock?.release()
@@ -78,7 +79,8 @@
// will throw an IOException with EAGAIN error, instead of returning null as
// specified in {@link FileChannel#tryLock}. We only continue if the error
// message is EAGAIN, otherwise just throw it.
- if (ex.message?.startsWith(LOCK_ERROR_MESSAGE) != true) {
+ if ((ex.message?.startsWith(LOCK_ERROR_MESSAGE) != true) &&
+ (ex.message?.startsWith(DEADLOCK_ERROR_MESSAGE) != true)) {
throw ex
}
}
@@ -162,6 +164,32 @@
}
}
}
+
+ companion object {
+ // Retry with exponential backoff to get file lock if it hits "Resource deadlock would
+ // occur" error until the backoff reaches [MAX_WAIT_MILLIS].
+ private suspend fun getExclusiveFileLockWithRetryIfDeadlock(
+ lockFileStream: FileOutputStream
+ ): FileLock {
+ var backoff = INITIAL_WAIT_MILLIS
+ while (backoff <= MAX_WAIT_MILLIS) {
+ try {
+ return lockFileStream.getChannel().lock(0L, Long.MAX_VALUE, /* shared= */ false)
+ } catch (ex: IOException) {
+ if (ex.message?.contains(DEADLOCK_ERROR_MESSAGE) != true) {
+ throw ex
+ }
+ delay(backoff)
+ backoff *= 2
+ }
+ }
+ return lockFileStream.getChannel().lock(0L, Long.MAX_VALUE, /* shared= */ false)
+ }
+
+ private val DEADLOCK_ERROR_MESSAGE = "Resource deadlock would occur"
+ private val INITIAL_WAIT_MILLIS: Long = 10
+ private val MAX_WAIT_MILLIS: Long = 60000
+ }
}
/**
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 8de75841..af84034 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -98,10 +98,11 @@
kmpDocs(project(":compose:material3:adaptive:adaptive-navigation"))
samples(project(":compose:material3:adaptive:adaptive-samples"))
kmpDocs(project(":compose:material3:material3"))
+ samples(project(":compose:material3:material3:material3-samples"))
kmpDocs(project(":compose:material3:material3-adaptive-navigation-suite"))
samples(project(":compose:material3:material3-adaptive-navigation-suite:material3-adaptive-navigation-suite-samples"))
kmpDocs(project(":compose:material3:material3-common"))
- samples(project(":compose:material3:material3:material3-samples"))
+ samples(project(":compose:material3:material3-common:material3-common-samples"))
kmpDocs(project(":compose:material3:material3-window-size-class"))
samples(project(":compose:material3:material3-window-size-class:material3-window-size-class-samples"))
kmpDocs(project(":compose:material:material"))
@@ -255,7 +256,7 @@
docs(project(":lifecycle:lifecycle-reactivestreams"))
docs(project(":lifecycle:lifecycle-reactivestreams-ktx"))
kmpDocs(project(":lifecycle:lifecycle-runtime"))
- docs(project(":lifecycle:lifecycle-runtime-compose"))
+ kmpDocs(project(":lifecycle:lifecycle-runtime-compose"))
samples(project(":lifecycle:lifecycle-runtime-compose:lifecycle-runtime-compose-samples"))
kmpDocs(project(":lifecycle:lifecycle-runtime-ktx"))
docs(project(":lifecycle:lifecycle-runtime-testing"))
@@ -418,6 +419,7 @@
samples(project(":wear:watchface:watchface-samples"))
docs(project(":wear:watchface:watchface-style"))
docs(project(":wear:wear"))
+ docs(project(":wear:wear-core"))
stubs(fileTree(dir: "../wear/wear_stubs/", include: ["com.google.android.wearable-stubs.jar"]))
docs(project(":wear:wear-input"))
samples(project(":wear:wear-input-samples"))
diff --git a/docs/lint_guide.md b/docs/lint_guide.md
index 4324d53..e3f78a1b 100644
--- a/docs/lint_guide.md
+++ b/docs/lint_guide.md
@@ -628,6 +628,28 @@
It is often necessary to process the sources more than once. This can be done by
using `context.driver.requestRepeat(detector, scope)`.
+### Debugging custom lint checks
+
+Using Android Studio, there are a few ways to debug custom lint checks:
+
+#### Debug against all lint check tests
+
+1. Set breakpoint(s) in the desired lint detector sources
+1. Click the `Gradle` icon on the right menu bar
+1. Run the `lintDebug` Gradle task and then hit the `Stop` icon in the top menu
+ bar. This creates a Run configuration.
+1. Click the `Debug` icon in the top menu bar for the newly-selected Run
+ configuration
+1. Breakpoint will get hit
+
+#### Debug against a single lint check test
+
+1. Set breakpoint(s) in the desired lint detector sources
+1. Open a lint check test, such as
+ [`AnnotationRetentionDetectorTest`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/AnnotationRetentionDetectorTest.kt)
+1. Right-click on a test method and select `Debug`
+1. Breakpoint will get hit
+
## Helpful tips {#tips}
### Useful classes/packages
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml
index e4840e4..2f2764a 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Geen emosiekone beskikbaar nie"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Jy het nog geen emosiekone gebruik nie"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"emosiekoon-tweerigtingoorskakelaar"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emosiekoonvariantkieser"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s en %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"skadu"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml
index 6f838f3..5be491c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ምንም ስሜት ገላጭ ምስሎች አይገኙም"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"ምንም ስሜት ገላጭ ምስሎችን እስካሁን አልተጠቀሙም"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"የስሜት ገላጭ ምስል ባለሁለት አቅጣጫ መቀያየሪያ"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"የስሜት ገላጭ ምስል አቅጣጫ ተቀይሯል"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"የስሜት ገላጭ ምስል ተለዋዋጭ መራጭ"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s እና %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ጥላ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
index d345acd..feecf8b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"لا تتوفر أي رموز تعبيرية."</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"لم تستخدم أي رموز تعبيرية حتى الآن."</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"مفتاح ثنائي الاتجاه للرموز التعبيرية"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"أداة اختيار الرموز التعبيرية"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s و%2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"الظل"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml
index dfdb76c..6ca5f5b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"কোনো ইম’জি উপলব্ধ নহয়"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"আপুনি এতিয়ালৈকে কোনো ইম’জি ব্যৱহাৰ কৰা নাই"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"ইম’জি বাইডাইৰেকশ্বনেল ছুইচ্চাৰ"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"দিক্-নিৰ্দেশনা প্ৰদৰ্শন কৰা ইম’জি সলনি কৰা হৈছে"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"ইম’জিৰ প্ৰকাৰ বাছনি কৰোঁতা"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s আৰু %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ছাঁ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml
index 3816c2a..02385b3 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Əlçatan emoji yoxdur"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Hələ heç bir emojidən istifadə etməməsiniz"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"ikitərəfli emoji dəyişdirici"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emoji variant seçicisi"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s və %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"kölgə"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml
index 9325eeb..8feb296 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Emodžiji nisu dostupni"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Još niste koristili emodžije"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"dvosmerni prebacivač emodžija"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"smer emodžija je promenjen"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"birač varijanti emodžija"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s i %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"senka"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml
index 8c1d50a..6b75a0a 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Няма даступных эмодзі"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Вы пакуль не выкарыстоўвалі эмодзі"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"пераключальнік кірунку для эмодзі"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"інструмент выбару варыянтаў эмодзі"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s і %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"цень"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml
index c1c4050..f6dc8b4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Няма налични емоджи"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Все още не сте използвали емоджита"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"двупосочен превключвател на емоджи"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"инструмент за избор на варианти за емоджи"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s и %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"сянка"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml
index ca8f374..fbfa46f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"কোনও ইমোজি উপলভ্য নেই"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"আপনি এখনও কোনও ইমোজি ব্যবহার করেননি"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"ইমোজি দ্বিমুখী সুইচার"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"ইমোজি ভেরিয়েন্ট বাছাইকারী"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s এবং %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ছায়া"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml
index b619963..9999766 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Emoji sličice nisu dostupne"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Još niste koristili nijednu emoji sličicu"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"dvosmjerni prebacivač emodžija"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"birač varijanti emodžija"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s i %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"sjenka"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
index 31d1289..74620de 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No hi ha cap emoji disponible"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Encara no has fet servir cap emoji"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"selector bidireccional d\'emojis"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"selector de variants d\'emojis"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s i %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ombra"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml
index 92e63e5..322f1eb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nejsou k dispozici žádné smajlíky"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Zatím jste žádná emodži nepoužili"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"dvousměrný přepínač smajlíků"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"výběr variant emodži"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s a %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"stín"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml
index b2146f9..10003a8 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Der er ingen tilgængelige emojis"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du har ikke brugt nogen emojis endnu"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"tovejsskifter til emojis"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"vælger for emojivariant"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s og %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"skygge"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml
index 95fd48a..af1afa6 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Keine Emojis verfügbar"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du hast noch keine Emojis verwendet"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"Bidirektionale Emoji-Auswahl"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"Emojivarianten-Auswahl"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s und %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"Hautton"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml
index 48d189f..b2f231a 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Δεν υπάρχουν διαθέσιμα emoji"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Δεν έχετε χρησιμοποιήσει κανένα emoji ακόμα"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"αμφίδρομο στοιχείο εναλλαγής emoji"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"επιλογέας παραλλαγής emoji"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s και %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"σκιά"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml
index 64aafcd..8489a35 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No emojis available"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"You haven\'t used any emoji yet"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"emoji bidirectional switcher"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emoji variant selector"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s and %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"shadow"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml
index 0aacec5..d056590 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No emojis available"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"You haven\'t used any emojis yet"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"emoji bidirectional switcher"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"emoji facing direction switched"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emoji variant selector"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s and %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"shadow"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml
index 64aafcd..8489a35 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No emojis available"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"You haven\'t used any emoji yet"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"emoji bidirectional switcher"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emoji variant selector"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s and %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"shadow"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml
index 64aafcd..8489a35 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No emojis available"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"You haven\'t used any emoji yet"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"emoji bidirectional switcher"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emoji variant selector"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s and %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"shadow"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml
index 3cda27a..3e02185 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No emojis available"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"You haven\'t used any emojis yet"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"emoji bidirectional switcher"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"emoji facing direction switched"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emoji variant selector"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s and %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"shadow"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml
index 852b61a..fe5c953 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No hay ningún emoji disponible"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Todavía no usaste ningún emoji"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"selector bidireccional de emojis"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"selector de variantes de emojis"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s y %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"sombra"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
index ec695ad..001b0616 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No hay emojis disponibles"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Aún no has usado ningún emoji"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"cambio bidireccional de emojis"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"selector de variantes de emojis"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s y %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"sombra"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml
index c7c7d04..2373a5f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ühtegi emotikoni pole saadaval"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Te pole veel ühtegi emotikoni kasutanud"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"emotikoni kahesuunaline lüliti"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emotikoni variandi valija"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s ja %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"vari"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml
index 6edea1d..ce763b5 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ez dago emotikonorik erabilgarri"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Ez duzu erabili emojirik oraingoz"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"noranzko biko emoji-aldatzailea"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emojien aldaeren hautatzailea"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s eta %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"itzala"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml
index d917197..ddd1f3e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"اموجی دردسترس نیست"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"هنوز از هیچ اموجیای استفاده نکردهاید"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"تغییردهنده دوسویه اموجی"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"گزینشگر متغیر اموجی"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s و %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"سایه"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml
index 4d68349..e184b24 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ei emojeita saatavilla"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Et ole vielä käyttänyt emojeita"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"emoji kaksisuuntainen vaihtaja"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emojivalitsin"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s ja %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"varjostus"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml
index c8fc499..4afaf46 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Aucun émoji proposé"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Vous n\'avez encore utilisé aucun émoji"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"sélecteur bidirectionnel d\'émoji"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"sélecteur de variantes d\'émoji"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s et %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ombre"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml
index fad386d..006beb5 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Aucun emoji disponible"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Vous n\'avez pas encore utilisé d\'emoji"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"sélecteur d\'emoji bidirectionnel"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"sélecteur de variante d\'emoji"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s et %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ombre"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml
index 91b3073..74211e6 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Non hai ningún emoji dispoñible"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Aínda non utilizaches ningún emoji"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"selector bidireccional de emojis"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"selector de variantes de emojis"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s e %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"sombra"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml
index 0960d6e..27f28f7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"કોઈ ઇમોજી ઉપલબ્ધ નથી"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"તમે હજી સુધી કોઈ ઇમોજીનો ઉપયોગ કર્યો નથી"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"બે દિશામાં સ્વિચ થઈ શકતું ઇમોજી સ્વિચર"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"ઇમોજીનો પ્રકાર પસંદગીકર્તા"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s અને %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"શૅડો"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml
index ab9a466..8954985 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"कोई इमोजी उपलब्ध नहीं है"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"आपने अब तक किसी भी इमोजी का इस्तेमाल नहीं किया है"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"दोनों तरफ़ ले जा सकने वाले स्विचर का इमोजी"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"इमोजी के वैरिएंट चुनने का टूल"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s और %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"शैडो"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml
index 00be2dc..9deac2a 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nije dostupan nijedan emoji"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Još niste upotrijebili emojije"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"dvosmjerni izmjenjivač emojija"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"alat za odabir varijante emojija"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s i %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"sjena"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml
index 727f33c..ceddf96 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nincsenek rendelkezésre álló emojik"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Még nem használt emojikat"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"kétirányú emojiváltó"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emojiváltozat-választó"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s és %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"árnyék"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml
index 414e304..135c7cf 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Հասանելի էմոջիներ չկան"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Դուք դեռ չեք օգտագործել էմոջիներ"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"էմոջիների երկկողմանի փոխանջատիչ"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"էմոջիների տարբերակի ընտրիչ"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s և %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ստվեր"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml
index 84d9bd5..0c6e8e0 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Tidak ada emoji yang tersedia"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Anda belum menggunakan emoji apa pun"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"pengalih dua arah emoji"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"pemilih varian emoji"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s dan %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"bayangan"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml
index 60e2844..a8eafdb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Engin emoji-tákn í boði"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Þú hefur ekki notað nein emoji enn"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"emoji-val í báðar áttir"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"val emoji-afbrigðis"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s og %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"skuggi"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml
index a2fee10..6ff2bf9 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nessuna emoji disponibile"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Non hai ancora usato alcuna emoji"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"selettore bidirezionale di emoji"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"emoji sottosopra"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"selettore variante emoji"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s e %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ombra"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml
index c8c10cd..7e5fbef 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"אין סמלי אמוג\'י זמינים"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"עדיין לא השתמשת באף אמוג\'י"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"לחצן דו-כיווני למעבר לאמוג\'י"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"בורר של סוגי אמוג\'י"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s ו-%2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"צל"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml
index 128f95b..395ee6d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"使用できる絵文字がありません"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"まだ絵文字を使用していません"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"絵文字の双方向切り替え"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"絵文字の向きを切り替えました"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"絵文字バリエーション セレクタ"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s、%2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"シャドウ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml
index 4434f10..5d23faa 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Emoji-ები მიუწვდომელია"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Emoji-ებით ჯერ არ გისარგებლიათ"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"emoji-ს ორმიმართულებიანი გადამრთველი"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"emoji-ის მიმართულება შეცვლილია"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emoji-ს ვარიანტის ამომრჩევი"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s და %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ჩრდილი"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml
index 173b655..cd6a8c5 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Эмоджи жоқ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Әлі ешқандай эмоджи пайдаланылған жоқ."</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"екіжақты эмоджи ауыстырғыш"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"эмоджи бағыты ауыстырылды"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"эмоджи нұсқаларын таңдау құралы"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s және %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"көлеңке"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml
index f202d60..0b4dffc 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"មិនមានរូបអារម្មណ៍ទេ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"អ្នកមិនទាន់បានប្រើរូបអារម្មណ៍ណាមួយនៅឡើយទេ"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"មុខងារប្ដូរទ្វេទិសនៃរូបអារម្មណ៍"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"បានប្ដូរទិសដៅបែររបស់រូបអារម្មណ៍"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"ផ្ទាំងជ្រើសរើសជម្រើសរូបអារម្មណ៍"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s និង %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ស្រមោល"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml
index 3e6397e..6b1a4ec 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ಯಾವುದೇ ಎಮೊಜಿಗಳು ಲಭ್ಯವಿಲ್ಲ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"ನೀವು ಇನ್ನೂ ಯಾವುದೇ ಎಮೋಜಿಗಳನ್ನು ಬಳಸಿಲ್ಲ"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"ಎಮೋಜಿ ಬೈಡೈರೆಕ್ಷನಲ್ ಸ್ವಿಚರ್"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"ಎಮೋಜಿ ವೇರಿಯಂಟ್ ಸೆಲೆಕ್ಟರ್"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s ಮತ್ತು %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ನೆರಳು"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml
index b0fd567..22cace8 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"사용 가능한 그림 이모티콘 없음"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"아직 사용한 이모티콘이 없습니다."</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"그림 이모티콘 양방향 전환기"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"이모티콘 방향 전환됨"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"그림 이모티콘 옵션 선택기"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s 및 %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"그림자"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml
index e5e69cc1..7a611da 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Жеткиликтүү быйтыкчалар жок"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Бир да быйтыкча колдоно элексиз"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"эки тараптуу быйтыкча которгуч"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"быйтыкча тандагыч"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s жана %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"көлөкө"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml
index 7203809..174400e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ບໍ່ມີອີໂມຈິໃຫ້ນຳໃຊ້"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"ທ່ານຍັງບໍ່ໄດ້ໃຊ້ອີໂມຈິໃດເທື່ອ"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"ຕົວສະຫຼັບອີໂມຈິແບບ 2 ທິດທາງ"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"ປ່ຽນທິດທາງການຫັນໜ້າຂອງອີໂມຈິແລ້ວ"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"ຕົວເລືອກຕົວແປອີໂມຈິ"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s ແລະ %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ເງົາ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml
index 6c7ed80..dc59457 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nėra jokių pasiekiamų jaustukų"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Dar nenaudojote jokių jaustukų"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"dvikryptis jaustukų perjungikli"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"jaustuko varianto parinkiklis"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s ir %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"šešėlis"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml
index 43ed60e..a5e643a 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nav pieejamu emocijzīmju"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Jūs vēl neesat izmantojis nevienu emocijzīmi"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"emocijzīmju divvirzienu pārslēdzējs"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emocijzīmes varianta atlasītājs"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s un %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ēna"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml
index 756205f..7695add 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Нема достапни емоџија"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Сѐ уште не сте користеле емоџија"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"двонасочен менувач на емоџија"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"избирач на варијанти на емоџија"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s и %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"сенка"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml
index 5eaa499..60b9857 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ഇമോജികളൊന്നും ലഭ്യമല്ല"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"നിങ്ങൾ ഇതുവരെ ഇമോജികളൊന്നും ഉപയോഗിച്ചിട്ടില്ല"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"ഇമോജി ദ്വിദിശ സ്വിച്ചർ"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"ഇമോജിയുടെ ദിശ മാറ്റി"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"ഇമോജി വേരിയന്റ് സെലക്ടർ"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s, %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ഷാഡോ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml
index 8a10449..cc43f11 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Боломжтой эможи алга"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Та ямар нэгэн эможи ашиглаагүй байна"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"эможигийн хоёр чиглэлтэй сэлгүүр"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"эможигийн хувилбар сонгогч"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s болон %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"сүүдэр"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml
index ecebaed..8a676b6 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"कोणतेही इमोजी उपलब्ध नाहीत"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"तुम्ही अद्याप कोणतेही इमोजी वापरलेले नाहीत"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"इमोजीचा द्विदिश स्विचर"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"इमोजी व्हेरीयंट सिलेक्टर"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s आणि %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"शॅडो"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml
index 616b9ce..d5ecef2 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Tiada emoji tersedia"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Anda belum menggunakan mana-mana emoji lagi"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"penukar dwiarah emoji"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"emoji menghadap arah ditukar"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"pemilih varian emoji"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s dan %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"bebayang"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml
index 3a8a920..99fb7ff 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"အီမိုဂျီ မရနိုင်ပါ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"အီမိုဂျီ အသုံးမပြုသေးပါ"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"အီမိုဂျီ လမ်းကြောင်းနှစ်ခုပြောင်းစနစ်"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"အီမိုဂျီလှည့်သောဘက်ကို ပြောင်းထားသည်"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"အီမိုဂျီမူကွဲ ရွေးချယ်စနစ်"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s နှင့် %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"အရိပ်"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml
index 7283de8..eca673e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ingen emojier er tilgjengelige"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du har ikke brukt noen emojier ennå"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"toveisvelger for emoji"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"velger for emojivariant"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s og %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"skygge"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml
index 63a95e5..94c2101 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"कुनै पनि इमोजी उपलब्ध छैन"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"तपाईंले हालसम्म कुनै पनि इमोजी प्रयोग गर्नुभएको छैन"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"दुवै दिशामा लैजान सकिने स्विचरको इमोजी"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"इमोजी भेरियन्ट सेलेक्टर"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s र %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"छाया"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml
index 1e9aaff..6b7ce3e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Geen emoji\'s beschikbaar"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Je hebt nog geen emoji\'s gebruikt"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"bidirectionele emoji-schakelaar"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emoji-variantkiezer"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s en %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"schaduw"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml
index fe11974..d692666 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"କୌଣସି ଇମୋଜି ଉପଲବ୍ଧ ନାହିଁ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"ଆପଣ ଏପର୍ଯ୍ୟନ୍ତ କୌଣସି ଇମୋଜି ବ୍ୟବହାର କରିନାହାଁନ୍ତି"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"ଇମୋଜିର ବାଇଡାଇରେକ୍ସନାଲ ସୁଇଚର"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"ଇମୋଜି ଭାରିଏଣ୍ଟ ଚୟନକାରୀ"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s ଏବଂ %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ସେଡୋ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml
index 2f6c890..65ba6eb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ਕੋਈ ਇਮੋਜੀ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"ਤੁਸੀਂ ਹਾਲੇ ਤੱਕ ਕਿਸੇ ਵੀ ਇਮੋਜੀ ਦੀ ਵਰਤੋਂ ਨਹੀਂ ਕੀਤੀ ਹੈ"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"ਇਮੋਜੀ ਬਾਇਡਾਇਰੈਕਸ਼ਨਲ ਸਵਿੱਚਰ"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"ਇਮੋਜੀ ਕਿਸਮ ਚੋਣਕਾਰ"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s ਅਤੇ %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"ਸ਼ੈਡੋ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml
index 5505626..30fc600 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Brak dostępnych emotikonów"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Żadne emotikony nie zostały jeszcze użyte"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"dwukierunkowy przełącznik emotikonów"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"selektor wariantu emotikona"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s i %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"cień"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml
index caa37c5..687fdf3 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Não há emojis disponíveis"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Você ainda não usou emojis"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"seletor bidirecional de emojis"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"seletor de variante do emoji"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s e %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"sombra"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml
index ad29a54..2ef06ac 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nenhum emoji disponível"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Ainda não utilizou emojis"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"comutador bidirecional de emojis"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"direção voltada para o emoji alterada"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"seletor de variantes de emojis"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s e %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"sombra"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml
index caa37c5..687fdf3 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Não há emojis disponíveis"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Você ainda não usou emojis"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"seletor bidirecional de emojis"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"seletor de variante do emoji"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s e %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"sombra"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml
index c06f239..621d664 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nu sunt disponibile emoji-uri"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Încă nu ai folosit emoji"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"comutator bidirecțional de emojiuri"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"selector de variante de emoji"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s și %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"umbră"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml
index e2fea1f..79e8f91 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Нет доступных эмодзи"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Вы ещё не использовали эмодзи"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"Двухсторонний переключатель эмодзи"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"выбор вариантов эмодзи"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s и %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"теневой"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml
index b906785..85d2c99 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ඉමොජි කිසිවක් නොලැබේ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"ඔබ තවමත් කිසිදු ඉමෝජියක් භාවිතා කර නැත"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"ද්විත්ව දිශා ඉමොජි මාරුකරණය"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"ඉමොජි ප්රභේද තෝරකය"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s සහ %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"සෙවනැල්ල"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml
index 96fa6ca..11ed0ee 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nie sú k dispozícii žiadne emodži"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Zatiaľ ste nepoužili žiadne emodži"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"obojsmerný prepínač emodži"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"smer otočenia emodži bol prepnutý"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"selektor variantu emodži"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s a %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"tieň"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml
index 02f0e10..cdf2ba4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ni emodžijev"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Uporabili niste še nobenega emodžija."</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"dvosmerni preklopnik emodžijev"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"Izbirnik različice emodžija"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s in %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"senčenje"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml
index 68d70e5..3948314 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nuk ofrohen emoji"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Nuk ke përdorur ende asnjë emoji"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"ndërruesi me dy drejtime për emoji-t"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"përzgjedhësi i variantit të emoji-t"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s dhe %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"hije"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml
index 712a714..67d3619 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Емоџији нису доступни"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Још нисте користили емоџије"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"двосмерни пребацивач емоџија"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"смер емоџија је промењен"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"бирач варијанти емоџија"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s и %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"сенка"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml
index 2ce0e0f..48c1766 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Inga emojier tillgängliga"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du har ännu inte använt emojis"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"dubbelriktad emojiväxlare"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"Väljare av emoji-varianter"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s och %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"skugga"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml
index 1f95731..3f7e24b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Hakuna emoji zinazopatikana"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Bado hujatumia emoji zozote"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"kibadilishaji cha emoji cha pande mbili"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"imebadilisha upande ambao emoji inaangalia"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"kiteuzi cha kibadala cha emoji"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s na %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"kivuli"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml
index dff24f7..5f54f35 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ஈமோஜிகள் எதுவுமில்லை"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"இதுவரை ஈமோஜி எதையும் நீங்கள் பயன்படுத்தவில்லை"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"ஈமோஜி இருபக்க மாற்றி"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"ஈமோஜி வகைத் தேர்வி"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s மற்றும் %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"நிழல்"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml
index 0d14b90..3ad93db 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ఎమోజీలు ఏవీ అందుబాటులో లేవు"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"మీరు ఇంకా ఎమోజీలు ఏవీ ఉపయోగించలేదు"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"ఎమోజీ ద్విదిశాత్మక స్విచ్చర్"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"ఎమోజి రకాన్ని ఎంపిక చేసే సాధనం"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s, %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"షాడో"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml
index 432627d..7b6e8f3 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ไม่มีอีโมจิ"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"คุณยังไม่ได้ใช้อีโมจิเลย"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"ตัวสลับอีโมจิแบบ 2 ทาง"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"ตัวเลือกตัวแปรอีโมจิ"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s และ %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"เงา"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml
index 611e2ce..2a14fba 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Walang available na emoji"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Hindi ka pa gumamit ng anumang emoji"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"bidirectional na switcher ng emoji"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"pinalitan ang direksyon kung saan nakaharap ang emoji"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"selector ng variant ng emoji"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s at %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"shadow"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml
index 57960f3..f0d13cd 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Kullanılabilir emoji yok"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Henüz emoji kullanmadınız"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"çift yönlü emoji değiştirici"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"emoji yönü değiştirildi"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emoji varyant seçici"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s ve %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"gölge"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml
index 1d599ff..d611feb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Немає смайлів"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Ви ще не використовували смайли"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"двосторонній перемикач смайлів"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"засіб вибору варіанта смайла"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s і %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"тінь"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml
index 7ed0acd..68fa937 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"کوئی بھی ایموجی دستیاب نہیں ہے"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"آپ نے ابھی تک کوئی بھی ایموجی استعمال نہیں کی ہے"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"دو طرفہ سوئچر ایموجی"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"ایموجی کا سمتِ رخ سوئچ کر دیا گیا"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"ایموجی کی قسم کا منتخب کنندہ"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s اور %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"پرچھائیں"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml
index f4ff0d5..6610ffd5 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Hech qanday emoji mavjud emas"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Hanuz birorta emoji ishlatmagansiz"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"ikki tomonlama emoji almashtirgich"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"emoji yuzlanish tomoni almashdi"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"emoji variant tanlagich"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s va %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"soya"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml
index f765385..9e1eac8 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Không có biểu tượng cảm xúc nào"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Bạn chưa sử dụng biểu tượng cảm xúc nào"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"trình chuyển đổi hai chiều biểu tượng cảm xúc"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"đã chuyển hướng mặt của biểu tượng cảm xúc"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"bộ chọn biến thể biểu tượng cảm xúc"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s và %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"bóng"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml
index 9774a91..e3e7e19 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"没有可用的表情符号"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"您尚未使用过任何表情符号"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"表情符号双向切换器"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"表情符号变体选择器"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s和%2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"阴影"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml
index 4db6d89..dd58b5b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"沒有可用的 Emoji"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"你尚未使用任何 Emoji"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"Emoji 雙向切換工具"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"Emoji 變化版本選取器"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s和%2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"陰影"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml
index bc57f72..04ecc98 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml
@@ -30,6 +30,8 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"沒有可用的表情符號"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"你尚未使用任何表情符號"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"表情符號雙向切換器"</string>
+ <!-- no translation found for emoji_bidirectional_switcher_clicked_desc (5055290162204827523) -->
+ <skip />
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"表情符號變化版本選取器"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"%1$s和%2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"陰影"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml
index adf8cba..3918f29 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml
@@ -30,6 +30,7 @@
<string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Awekho ama-emoji atholakalayo"</string>
<string name="emoji_empty_recent_category" msgid="7863877827879290200">"Awukasebenzisi noma yimaphi ama-emoji okwamanje"</string>
<string name="emoji_bidirectional_switcher_content_desc" msgid="5084600168354220605">"isishintshi se-emoji ye-bidirectional"</string>
+ <string name="emoji_bidirectional_switcher_clicked_desc" msgid="5055290162204827523">"isikhombisi-ndlela esibheke ku-emoji sishintshiwe"</string>
<string name="emoji_variant_selector_content_desc" msgid="2898934883418401376">"isikhethi esihlukile se-emoji"</string>
<string name="emoji_variant_content_desc_template" msgid="6381933050671041489">"Okuthi %1$s nokuthi %2$s"</string>
<string name="emoji_skin_tone_shadow_content_desc" msgid="1759906883307507376">"isithunzi"</string>
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 b11fbda..f3f56e8 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -82,6 +82,18 @@
private static final double DIFFERENCE_TOLERANCE = .001;
private static final boolean ENABLE_STRICT_MODE_FOR_UNBUFFERED_IO = true;
+ /** Test XMP value that is different to all the XMP values embedded in the test images. */
+ private static final String TEST_XMP =
+ "<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>"
+ + "<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 10.73'>"
+ + "<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>"
+ + "<rdf:Description rdf:about='' xmlns:photoshop='http://ns.adobe.com/photoshop/1.0/'>"
+ + "<photoshop:DateCreated>2024-03-15T17:44:18</photoshop:DateCreated>"
+ + "</rdf:Description>"
+ + "</rdf:RDF>"
+ + "</x:xmpmeta>"
+ + "<?xpacket end='w'?>";
+
@Rule
public GrantPermissionRule mRuntimePermissionRule =
GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE);
@@ -162,6 +174,54 @@
writeToFilesWithExif(imageFile, ExpectedAttributes.JPEG_WITH_EXIF_WITH_XMP);
}
+ // https://issuetracker.google.com/309843390
+ @Test
+ @LargeTest
+ public void testJpegWithExifAndXmp_doesntDuplicateXmp() throws Throwable {
+ File imageFile =
+ copyFromResourceToFile(
+ R.raw.jpeg_with_exif_with_xmp, "jpeg_with_exif_with_xmp.jpg");
+ ExifInterface exifInterface = new ExifInterface(imageFile.getAbsolutePath());
+
+ exifInterface.setAttribute(ExifInterface.TAG_XMP, TEST_XMP);
+
+ exifInterface.saveAttributes();
+
+ byte[] imageBytes = Files.toByteArray(imageFile);
+ assertThat(countOccurrences(imageBytes, "<?xpacket begin=".getBytes(Charsets.UTF_8)))
+ .isEqualTo(1);
+ }
+
+ /**
+ * Returns the number of times {@code pattern} appears in {@code source}.
+ *
+ * <p>Overlapping occurrences are counted multiple times, e.g. {@code countOccurrences([0, 1, 0,
+ * 1, 0], [0, 1, 0])} will return 2.
+ */
+ private static int countOccurrences(byte[] source, byte[] pattern) {
+ int count = 0;
+ for (int i = 0; i < source.length - pattern.length; i++) {
+ if (containsAtIndex(source, i, pattern)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Returns {@code true} if {@code source} contains {@code pattern} starting at {@code index}.
+ *
+ * @throws IndexOutOfBoundsException if {@code source.length < index + pattern.length}.
+ */
+ private static boolean containsAtIndex(byte[] source, int index, byte[] pattern) {
+ for (int i = 0; i < pattern.length; i++) {
+ if (pattern[i] != source[index + i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
// https://issuetracker.google.com/264729367
@Test
@LargeTest
@@ -1431,9 +1491,11 @@
ExifInterface exifInterface = exifInterfaceFactory.create(imageFile);
exifInterface.setAttribute(ExifInterface.TAG_MAKE, "abc");
+ exifInterface.setAttribute(ExifInterface.TAG_XMP, TEST_XMP);
exifInterface.saveAttributes();
- expectedAttributes = expectedAttributes.buildUpon().setMake("abc").build();
+ expectedAttributes =
+ expectedAttributes.buildUpon().setMake("abc").clearXmp().setXmp(TEST_XMP).build();
// Check expected modifications are visible without re-parsing the file.
compareWithExpectedAttributes(exifInterface, expectedAttributes, verboseTag);
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExpectedAttributes.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExpectedAttributes.java
index c53290a..58890d1 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExpectedAttributes.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExpectedAttributes.java
@@ -23,6 +23,7 @@
import androidx.exifinterface.test.R;
import com.google.common.base.Charsets;
+import com.google.common.base.Preconditions;
import com.google.common.io.CharStreams;
import java.io.IOException;
@@ -238,6 +239,7 @@
// XMP information.
private boolean mHasXmp;
+ @Nullable private String mXmp;
@Nullable private Integer mXmpResourceId;
private long mXmpOffset;
private long mXmpLength;
@@ -280,6 +282,7 @@
mIso = attributes.iso;
mOrientation = attributes.orientation;
mHasXmp = attributes.hasXmp;
+ mXmp = attributes.mXmp;
mXmpResourceId = attributes.mXmpResourceId;
mXmpOffset = attributes.xmpOffset;
mXmpLength = attributes.xmpLength;
@@ -488,9 +491,26 @@
return this;
}
- /** Sets the resource ID of the expected XMP data. */
+ /**
+ * Sets the expected XMP data.
+ *
+ * <p>Clears any value set by {@link #setXmpResourceId}.
+ */
+ public Builder setXmp(String xmp) {
+ mHasXmp = true;
+ mXmp = xmp;
+ mXmpResourceId = null;
+ return this;
+ }
+
+ /**
+ * Sets the resource ID of the expected XMP data.
+ *
+ * <p>Clears any value set by {@link #setXmp}.
+ */
public Builder setXmpResourceId(@RawRes int xmpResourceId) {
mHasXmp = true;
+ mXmp = null;
mXmpResourceId = xmpResourceId;
return this;
}
@@ -514,6 +534,7 @@
public Builder clearXmp() {
mHasXmp = false;
+ mXmp = null;
mXmpResourceId = null;
mXmpOffset = 0;
mXmpLength = 0;
@@ -578,6 +599,7 @@
public final int orientation;
// XMP information.
+ @Nullable private final String mXmp;
@Nullable private final Integer mXmpResourceId;
@Nullable private String mMemoizedXmp;
public final boolean hasXmp;
@@ -621,14 +643,19 @@
iso = builder.mIso;
orientation = builder.mOrientation;
hasXmp = builder.mHasXmp;
+ mXmp = builder.mXmp;
mXmpResourceId = builder.mXmpResourceId;
+ Preconditions.checkArgument(
+ mXmp == null || mXmpResourceId == null,
+ "At most one of mXmp or mXmpResourceId may be set");
+ mMemoizedXmp = mXmp;
xmpOffset = builder.mXmpOffset;
xmpLength = builder.mXmpLength;
}
/**
- * Returns the expected XMP data read from {@code resources} using {@link
- * Builder#setXmpResourceId}.
+ * Returns the expected XMP data set directly with {@link Builder#setXmp} or read from {@code
+ * resources} using {@link Builder#setXmpResourceId}.
*
* <p>Returns null if no expected XMP data was set.
*/
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 6632acb..797692e 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -4260,6 +4260,13 @@
if (i == IFD_TYPE_THUMBNAIL && !mHasThumbnail) {
continue;
}
+ if (tag.equals(TAG_XMP) && i == IFD_TYPE_PREVIEW && mXmpIsFromSeparateMarker) {
+ // XMP was read from a standalone XMP APP1 segment in the source file, and only
+ // stored in sExifTagMapsForWriting[IFD_TYPE_PRIMARY], so we shouldn't store the
+ // updated value in sExifTagMapsForWriting[IFD_TYPE_PREVIEW] here, otherwise we risk
+ // incorrectly writing the updated value twice in the resulting file.
+ continue;
+ }
final ExifTag exifTag = sExifTagMapsForWriting[i].get(tag);
if (exifTag != null) {
if (value == null) {
@@ -6294,6 +6301,17 @@
dataOutputStream.writeByte(MARKER_APP1);
writeExifSegment(dataOutputStream);
+ if (xmpAttribute != null && mXmpIsFromSeparateMarker) {
+ // Write XMP APP1 segment. The XMP spec (part 3, section 1.1.3) recommends for this to
+ // directly follow the Exif APP1 segment.
+ dataOutputStream.write(MARKER);
+ dataOutputStream.writeByte(MARKER_APP1);
+ int length = 2 + IDENTIFIER_XMP_APP1.length + xmpAttribute.bytes.length;
+ dataOutputStream.writeUnsignedShort(length);
+ dataOutputStream.write(IDENTIFIER_XMP_APP1);
+ dataOutputStream.write(xmpAttribute.bytes);
+ }
+
// Re-add previously removed XMP data.
if (xmpAttribute != null) {
mAttributes[IFD_TYPE_PRIMARY].put(TAG_XMP, xmpAttribute);
@@ -6313,12 +6331,22 @@
if (length < 0) {
throw new IOException("Invalid length");
}
- byte[] identifier = new byte[6];
- if (length >= 6) {
+ // If the length is long enough, we read enough bytes for the XMP identifier,
+ // because it's longer than the EXIF one.
+ @Nullable byte[] identifier;
+ if (length >= IDENTIFIER_XMP_APP1.length) {
+ identifier = new byte[IDENTIFIER_XMP_APP1.length];
+ } else if (length >= IDENTIFIER_EXIF_APP1.length) {
+ identifier = new byte[IDENTIFIER_EXIF_APP1.length];
+ } else {
+ identifier = null;
+ }
+ if (identifier != null) {
dataInputStream.readFully(identifier);
- if (Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) {
- // Skip the original EXIF APP1 segment.
- dataInputStream.skipFully(length - 6);
+ if (startsWith(identifier, IDENTIFIER_EXIF_APP1)
+ || startsWith(identifier, IDENTIFIER_XMP_APP1)) {
+ // Skip the original EXIF or XMP APP1 segment.
+ dataInputStream.skipFully(length - identifier.length);
break;
}
}
@@ -6326,8 +6354,8 @@
dataOutputStream.writeByte(MARKER);
dataOutputStream.writeByte(marker);
dataOutputStream.writeUnsignedShort(length + 2);
- if (length >= 6) {
- length -= 6;
+ if (identifier != null) {
+ length -= identifier.length;
dataOutputStream.write(identifier);
}
int read;
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
index 77c93de..9dd1719 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
@@ -338,6 +338,13 @@
if (sharedElementNameMapping.isEmpty()) {
// We couldn't find any valid shared element mappings, so clear out
// the shared element transition information entirely
+ Log.i(FragmentManager.TAG,
+ "Ignoring shared elements transition $sharedElementTransition between " +
+ "$firstOut and $lastIn as there are no matching elements " +
+ "in both the entering and exiting fragment. In order to run a " +
+ "SharedElementTransition, both fragments involved must have the " +
+ "element."
+ )
sharedElementTransition = null
sharedElementFirstOutViews.clear()
sharedElementLastInViews.clear()
@@ -716,7 +723,9 @@
Build.VERSION.SDK_INT >= 34 &&
it.transition != null &&
transitionImpl.isSeekingSupported(it.transition)
- }
+ } &&
+ (sharedElementTransition == null ||
+ transitionImpl.isSeekingSupported(sharedElementTransition))
val transitioning: Boolean
get() = transitionInfos.all {
@@ -737,6 +746,16 @@
}
return
}
+ if (transitioning && sharedElementTransition != null && !isSeekingSupported) {
+ Log.i(FragmentManager.TAG,
+ "Ignoring shared elements transition $sharedElementTransition between " +
+ "$firstOut and $lastIn as neither fragment has set a Transition. In " +
+ "order to run a SharedElementTransition, you must also set either an " +
+ "enter or exit transition on a fragment involved in the transaction. The " +
+ "sharedElementTransition will run after the back gesture has been " +
+ "committed."
+ )
+ }
if (isSeekingSupported && transitioning) {
// We need to set the listener before we create the controller, but we need the
// controller to do the desired cancel behavior (animateToStart). So we use this
diff --git a/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt
index 457cc70..e71b14a 100644
--- a/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt
+++ b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt
@@ -27,6 +27,7 @@
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.preferencesOf
+import androidx.glance.Button
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.ImageProvider
@@ -205,7 +206,7 @@
Text("text-row")
}
Spacer()
- Text("text-in-column")
+ Button("text-in-column", onClick = {})
}
}
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/components/Scaffold.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/components/Scaffold.kt
index 4ea2755..4c136aa 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/components/Scaffold.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/components/Scaffold.kt
@@ -42,7 +42,7 @@
fun Scaffold(
titleBar: @Composable () -> Unit,
modifier: GlanceModifier = GlanceModifier,
- backgroundColor: ColorProvider = GlanceTheme.colors.surface,
+ backgroundColor: ColorProvider = GlanceTheme.colors.widgetBackground,
content: @Composable () -> Unit,
) {
Box(modifier
diff --git a/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/UnitTestFiltersTest.kt b/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/UnitTestFiltersTest.kt
index 69aa1a9..02f1d26 100644
--- a/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/UnitTestFiltersTest.kt
+++ b/glance/glance-testing/src/test/kotlin/androidx/glance/testing/unit/UnitTestFiltersTest.kt
@@ -16,6 +16,7 @@
package androidx.glance.testing.unit
+import androidx.glance.EmittableButton
import androidx.glance.GlanceModifier
import androidx.glance.layout.EmittableColumn
import androidx.glance.semantics.semantics
@@ -68,6 +69,19 @@
}
@Test
+ fun hasTextOnButton_match_returnsTrue() {
+ val testSingleNode = GlanceMappedNode(
+ EmittableButton().apply {
+ text = "existing text"
+ }
+ )
+
+ val result = hasTextEqualTo("existing text").matches(testSingleNode)
+
+ assertThat(result).isTrue()
+ }
+
+ @Test
fun hasTextEqualTo_noMatch_returnsFalse() {
val testSingleNode = GlanceMappedNode(
EmittableText().apply {
diff --git a/glance/glance/src/main/java/androidx/glance/Button.kt b/glance/glance/src/main/java/androidx/glance/Button.kt
index c40d42c..56592f7 100644
--- a/glance/glance/src/main/java/androidx/glance/Button.kt
+++ b/glance/glance/src/main/java/androidx/glance/Button.kt
@@ -129,13 +129,10 @@
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class EmittableButton : Emittable {
+class EmittableButton : EmittableWithText() {
override var modifier: GlanceModifier = GlanceModifier
- var text: String = ""
- var style: TextStyle? = null
var colors: ButtonColors? = null
var enabled: Boolean = true
- var maxLines: Int = Int.MAX_VALUE
override fun copy(): Emittable = EmittableButton().also {
it.modifier = modifier
diff --git a/gradle.properties b/gradle.properties
index 08f5815..241307e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -48,7 +48,7 @@
androidx.includeOptionalProjects=false
# Keep ComposeCompiler pinned unless performing Kotlin upgrade & ComposeCompiler release
-androidx.unpinComposeCompiler=false
+androidx.unpinComposeCompiler=true
# Disable features we do not use
android.defaults.buildfeatures.aidl=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f5c9dc5..7745cc6 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -108,7 +108,7 @@
checkerframework = { module = "org.checkerframework:checker-qual", version = "2.5.3" }
checkmark = { module = "net.saff.checkmark:checkmark", version = "0.1.6" }
constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.0.1"}
-dackka = { module = "com.google.devsite:dackka", version = "1.5.1" }
+dackka = { module = "com.google.devsite:dackka", version = "1.5.2" }
dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
daggerCompiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
desugarJdkLibs = { module = "com.android.tools:desugar_jdk_libs", version = "2.0.3" }
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 962b2ad..a5b9900 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
@@ -134,6 +134,25 @@
}
@Test
+ fun testExecuteHasEGLContext() {
+ val glRenderer = GLRenderer()
+ glRenderer.start()
+ try {
+ var hasContext = false
+ val contextLatch = CountDownLatch(1)
+ glRenderer.execute {
+ val eglContext = EGL14.eglGetCurrentContext()
+ hasContext = eglContext != null && eglContext != EGL14.EGL_NO_CONTEXT
+ contextLatch.countDown()
+ }
+ assertTrue(contextLatch.await(3000, TimeUnit.MILLISECONDS))
+ assertTrue(hasContext)
+ } finally {
+ glRenderer.stop(true)
+ }
+ }
+
+ @Test
fun testDetachExecutesPendingRequests() {
val latch = CountDownLatch(1)
val renderer = object : GLRenderer.RenderCallback {
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
index b4354a1..86ab2af 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLRenderer.kt
@@ -225,7 +225,8 @@
/**
* Queue a [Runnable] to be executed on the GL rendering thread. Note it is important that this
- * [Runnable] does not block otherwise it can stall the GL thread.
+ * [Runnable] does not block otherwise it can stall the GL thread. The EGLContext will
+ * be created after [start] is invoked and before the runnable is executed.
*
* @param runnable Runnable to be executed
*/
@@ -304,8 +305,8 @@
/**
* Callback invoked on the backing thread after EGL dependencies are initialized.
* This is guaranteed to be invoked before any instance of
- * [RenderCallback.onSurfaceCreated] is called.
- * This will be invoked lazily before the first request to [GLRenderer.requestRender]
+ * [RenderCallback.onSurfaceCreated] is called. This will be invoked after
+ * [GLRenderer.start].
*/
// Suppressing CallbackMethodName due to b/238939160
@Suppress("AcronymName", "CallbackMethodName")
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt
index afd433b..26d395e 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/GLThread.kt
@@ -51,7 +51,11 @@
override fun start() {
super.start()
- mHandler = Handler(looper)
+ mHandler = Handler(looper).apply {
+ // Create an EGLContext right after starting in order to have one ready on a call to
+ // GLRenderer#execute
+ post { obtainEGLManager() }
+ }
}
/**
diff --git a/graphics/graphics-shapes/build.gradle b/graphics/graphics-shapes/build.gradle
index 918e133..3fcc603 100644
--- a/graphics/graphics-shapes/build.gradle
+++ b/graphics/graphics-shapes/build.gradle
@@ -135,5 +135,4 @@
mavenVersion = LibraryVersions.GRAPHICS_SHAPES
inceptionYear = "2022"
description = "create and render rounded polygonal shapes"
- metalavaK2UastEnabled = true
}
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 bf9b12f..86b2761 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
@@ -285,7 +285,7 @@
override fun toString(): String =
"ExerciseUpdate(" +
- "state=$exerciseStateInfo.state, " +
+ "state=${exerciseStateInfo.state}, " +
"startTime=$startTime, " +
"updateDurationFromBoot=$updateDurationFromBoot, " +
"latestMetrics=$latestMetrics, " +
diff --git a/inspection/inspection-gradle-plugin/lint-baseline.xml b/inspection/inspection-gradle-plugin/lint-baseline.xml
index b356138..cae3d85 100644
--- a/inspection/inspection-gradle-plugin/lint-baseline.xml
+++ b/inspection/inspection-gradle-plugin/lint-baseline.xml
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.4.0-alpha09" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha09)" variant="all" version="8.4.0-alpha09">
+<issues format="6" by="lint 8.4.0-alpha12" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha12)" variant="all" version="8.4.0-alpha12">
<issue
id="EagerGradleConfiguration"
- message="Avoid using eager method get"
+ message="Avoid using method get"
errorLine1=" it.jars.from(jar.get().archiveFile)"
errorLine2=" ~~~">
<location
@@ -12,7 +12,7 @@
<issue
id="EagerGradleConfiguration"
- message="Avoid using eager method get"
+ message="Avoid using method get"
errorLine1=" val fileTree = project.fileTree(zipTask.get().destinationDir)"
errorLine2=" ~~~">
<location
@@ -21,7 +21,7 @@
<issue
id="EagerGradleConfiguration"
- message="Avoid using eager method get"
+ message="Avoid using method get"
errorLine1=" it.from(versionTask.get().outputDir)"
errorLine2=" ~~~">
<location
@@ -30,7 +30,7 @@
<issue
id="EagerGradleConfiguration"
- message="Avoid using eager method get"
+ message="Avoid using method get"
errorLine1=" it.from(versionTask.get().outputDir)"
errorLine2=" ~~~">
<location
diff --git a/libraryversions.toml b/libraryversions.toml
index 0b0bfe8..6880e60 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -14,10 +14,10 @@
BLUETOOTH = "1.0.0-alpha02"
BROWSER = "1.9.0-alpha01"
BUILDSRC_TESTS = "1.0.0-alpha01"
-CAMERA = "1.4.0-alpha04"
+CAMERA = "1.4.0-alpha05"
CAMERA_PIPE = "1.0.0-alpha01"
CAMERA_TESTING = "1.0.0-alpha01"
-CAMERA_VIEWFINDER = "1.4.0-alpha04"
+CAMERA_VIEWFINDER = "1.4.0-alpha05"
CAMERA_VIEWFINDER_COMPOSE = "1.0.0-alpha01"
CARDVIEW = "1.1.0-alpha01"
CAR_APP = "1.7.0-alpha01"
@@ -34,7 +34,7 @@
CONSTRAINTLAYOUT_CORE = "1.1.0-alpha13"
CONTENTPAGER = "1.1.0-alpha01"
COORDINATORLAYOUT = "1.3.0-alpha02"
-CORE = "1.13.0-rc01"
+CORE = "1.14.0-alpha01"
CORE_ANIMATION = "1.0.0-rc01"
CORE_ANIMATION_TESTING = "1.0.0-rc01"
CORE_APPDIGEST = "1.0.0-alpha01"
@@ -158,6 +158,7 @@
WEAR = "1.4.0-alpha01"
WEAR_COMPOSE = "1.4.0-alpha06"
WEAR_COMPOSE_MATERIAL3 = "1.0.0-alpha20"
+WEAR_CORE = "1.0.0-alpha01"
WEAR_INPUT = "1.2.0-alpha03"
WEAR_INPUT_TESTING = "1.2.0-alpha03"
WEAR_ONGOING = "1.1.0-alpha02"
diff --git a/lifecycle/lifecycle-common/build.gradle b/lifecycle/lifecycle-common/build.gradle
index 322c633..c59b4ba 100644
--- a/lifecycle/lifecycle-common/build.gradle
+++ b/lifecycle/lifecycle-common/build.gradle
@@ -84,5 +84,4 @@
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Lifecycle-Common"
- metalavaK2UastEnabled = true
}
diff --git a/lifecycle/lifecycle-livedata-core-lint/build.gradle b/lifecycle/lifecycle-livedata-core-lint/build.gradle
index 9699395..85b76e5 100644
--- a/lifecycle/lifecycle-livedata-core-lint/build.gradle
+++ b/lifecycle/lifecycle-livedata-core-lint/build.gradle
@@ -29,8 +29,7 @@
}
dependencies {
- compileOnly(libs.androidLintMinApi)
- compileOnly(libs.androidLintMin)
+ compileOnly(libs.androidLintApi)
compileOnly(libs.kotlinStdlib)
testImplementation(libs.kotlinStdlib)
diff --git a/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/LiveDataCoreIssueRegistry.kt b/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/LiveDataCoreIssueRegistry.kt
index 0b251e7..d328622 100644
--- a/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/LiveDataCoreIssueRegistry.kt
+++ b/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/LiveDataCoreIssueRegistry.kt
@@ -18,11 +18,10 @@
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.client.api.Vendor
-import com.android.tools.lint.detector.api.CURRENT_API
@Suppress("UnstableApiUsage")
class LiveDataCoreIssueRegistry : IssueRegistry() {
- override val minApi = CURRENT_API
+ override val minApi = 10 // Only compatible with the latest lint
override val api = 14
override val issues get() = listOf(NonNullableMutableLiveDataDetector.ISSUE)
override val vendor = Vendor(
diff --git a/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt b/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
index cf14cdc..27ff2c6 100644
--- a/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
+++ b/lifecycle/lifecycle-livedata-core-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
@@ -33,8 +33,15 @@
import com.intellij.psi.PsiVariable
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.impl.source.PsiImmediateClassType
+import org.jetbrains.kotlin.analysis.api.analyze
+import org.jetbrains.kotlin.analysis.api.calls.KtCall
+import org.jetbrains.kotlin.analysis.api.calls.KtCallableMemberCall
+import org.jetbrains.kotlin.analysis.api.calls.singleCallOrNull
+import org.jetbrains.kotlin.analysis.api.types.KtNonErrorClassType
+import org.jetbrains.kotlin.analysis.api.types.KtTypeParameterType
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtCallableDeclaration
+import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtNullableType
import org.jetbrains.kotlin.psi.KtTypeReference
import org.jetbrains.uast.UAnnotated
@@ -119,6 +126,24 @@
}
override fun visitCallExpression(node: UCallExpression) {
+ var isGeneric = false
+ val ktCallExpression = node.sourcePsi as? KtCallExpression
+ ?: node.sourcePsi as? KtNameReferenceExpression ?: return
+ analyze(ktCallExpression) {
+ val ktCall = ktCallExpression.resolveCall()?.singleCallOrNull<KtCall>()
+ val callee = (ktCall as? KtCallableMemberCall<*, *>)?.partiallyAppliedSymbol
+ val receiver = callee?.extensionReceiver ?: callee?.dispatchReceiver
+ var receiverType = receiver?.type as? KtNonErrorClassType
+ while (!isGeneric && receiverType != null) {
+ val typeArgument = receiverType.ownTypeArguments.singleOrNull()?.type
+ if (typeArgument is KtTypeParameterType) {
+ isGeneric = true
+ }
+ receiverType = typeArgument as? KtNonErrorClassType
+ }
+ }
+ if (isGeneric) return
+
if (!isKotlin(node.lang) || !methods.contains(node.methodName) ||
!context.evaluator.isMemberInSubClassOf(
node.resolve()!!, "androidx.lifecycle.LiveData", false
diff --git a/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/NonNullableMutableLiveDataDetectorTest.kt b/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/NonNullableMutableLiveDataDetectorTest.kt
index 03173d3..e6be209 100644
--- a/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/NonNullableMutableLiveDataDetectorTest.kt
+++ b/lifecycle/lifecycle-livedata-core-lint/src/test/java/androidx/lifecycle/livedata/core/lint/NonNullableMutableLiveDataDetectorTest.kt
@@ -26,7 +26,6 @@
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.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -785,7 +784,6 @@
).expectClean()
}
- @Ignore("b/187536061")
@Test
fun genericParameterDefinition() {
check(
diff --git a/lifecycle/lifecycle-runtime-compose/build.gradle b/lifecycle/lifecycle-runtime-compose/build.gradle
index 5acc0e3..027cee9 100644
--- a/lifecycle/lifecycle-runtime-compose/build.gradle
+++ b/lifecycle/lifecycle-runtime-compose/build.gradle
@@ -22,28 +22,52 @@
* modifying its settings.
*/
import androidx.build.LibraryType
+import androidx.build.PlatformIdentifier
plugins {
id("AndroidXPlugin")
id("com.android.library")
id("AndroidXComposePlugin")
- id("org.jetbrains.kotlin.android")
}
-dependencies {
- api projectOrArtifact(":lifecycle:lifecycle-runtime-ktx")
- api("androidx.annotation:annotation-experimental:1.4.0")
- api("androidx.compose.runtime:runtime:1.0.1")
+androidXMultiplatform {
+ android()
+ desktop()
- implementation(libs.kotlinStdlib)
+ defaultPlatform(PlatformIdentifier.ANDROID)
- androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
- androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
- androidTestImplementation project(":compose:test-utils")
- androidTestImplementation(libs.testRules)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.junit)
- androidTestImplementation(libs.truth)
+ sourceSets {
+ commonMain {
+ dependencies {
+ api(projectOrArtifact(":lifecycle:lifecycle-runtime"))
+ api(project(":annotation:annotation"))
+ api(project(":compose:runtime:runtime"))
+ }
+ }
+
+ androidMain {
+ dependsOn(commonMain)
+ dependencies {
+ // Although this artifact is empty, it ensures that upgrading
+ // `lifecycle-runtime-compose` also updates `lifecycle-runtime-ktx`
+ // in cases where our constraints fail (e.g., internally in AndroidX
+ // when using project dependencies).
+ api(projectOrArtifact(":lifecycle:lifecycle-runtime-ktx"))
+ }
+ }
+
+ androidInstrumentedTest {
+ dependencies {
+ implementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
+ implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:test-utils"))
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ implementation(libs.truth)
+ }
+ }
+ }
}
androidx {
@@ -51,8 +75,7 @@
type = LibraryType.PUBLISHED_KOTLIN_ONLY_LIBRARY
inceptionYear = "2021"
description = "Compose integration with Lifecycle"
- metalavaK2UastEnabled = true
- samples(projectOrArtifact(":lifecycle:lifecycle-runtime-compose:lifecycle-runtime-compose-samples"))
+ samples(project(":lifecycle:lifecycle-runtime-compose:lifecycle-runtime-compose-samples"))
}
android {
diff --git a/lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/CollectAsStateWithLifecycleTests.kt b/lifecycle/lifecycle-runtime-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/compose/CollectAsStateWithLifecycleTests.kt
similarity index 100%
rename from lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/CollectAsStateWithLifecycleTests.kt
rename to lifecycle/lifecycle-runtime-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/compose/CollectAsStateWithLifecycleTests.kt
diff --git a/lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/DropUnlessLifecycleTest.kt b/lifecycle/lifecycle-runtime-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/compose/DropUnlessLifecycleTest.kt
similarity index 100%
rename from lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/DropUnlessLifecycleTest.kt
rename to lifecycle/lifecycle-runtime-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/compose/DropUnlessLifecycleTest.kt
diff --git a/lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/LifecycleEffectTest.kt b/lifecycle/lifecycle-runtime-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/compose/LifecycleEffectTest.kt
similarity index 100%
rename from lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/LifecycleEffectTest.kt
rename to lifecycle/lifecycle-runtime-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/compose/LifecycleEffectTest.kt
diff --git a/lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/LifecycleExtTest.kt b/lifecycle/lifecycle-runtime-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/compose/LifecycleExtTest.kt
similarity index 100%
rename from lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/LifecycleExtTest.kt
rename to lifecycle/lifecycle-runtime-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/compose/LifecycleExtTest.kt
diff --git a/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/DropUnlessLifecycle.kt b/lifecycle/lifecycle-runtime-compose/src/commonMain/kotlin/androidx/lifecycle/compose/DropUnlessLifecycle.kt
similarity index 100%
rename from lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/DropUnlessLifecycle.kt
rename to lifecycle/lifecycle-runtime-compose/src/commonMain/kotlin/androidx/lifecycle/compose/DropUnlessLifecycle.kt
diff --git a/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/FlowExt.kt b/lifecycle/lifecycle-runtime-compose/src/commonMain/kotlin/androidx/lifecycle/compose/FlowExt.kt
similarity index 97%
rename from lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/FlowExt.kt
rename to lifecycle/lifecycle-runtime-compose/src/commonMain/kotlin/androidx/lifecycle/compose/FlowExt.kt
index d5cc18c..3aaed3b 100644
--- a/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/FlowExt.kt
+++ b/lifecycle/lifecycle-runtime-compose/src/commonMain/kotlin/androidx/lifecycle/compose/FlowExt.kt
@@ -53,6 +53,7 @@
* @param context [CoroutineContext] to use for collecting.
*/
@Composable
+@Suppress("StateFlowValueCalledInComposition") // Initial value for an ongoing collect.
fun <T> StateFlow<T>.collectAsStateWithLifecycle(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
@@ -87,6 +88,7 @@
* @param context [CoroutineContext] to use for collecting.
*/
@Composable
+@Suppress("StateFlowValueCalledInComposition") // Initial value for an ongoing collect.
fun <T> StateFlow<T>.collectAsStateWithLifecycle(
lifecycle: Lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
diff --git a/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LifecycleEffect.kt b/lifecycle/lifecycle-runtime-compose/src/commonMain/kotlin/androidx/lifecycle/compose/LifecycleEffect.kt
similarity index 100%
rename from lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LifecycleEffect.kt
rename to lifecycle/lifecycle-runtime-compose/src/commonMain/kotlin/androidx/lifecycle/compose/LifecycleEffect.kt
diff --git a/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LifecycleExt.kt b/lifecycle/lifecycle-runtime-compose/src/commonMain/kotlin/androidx/lifecycle/compose/LifecycleExt.kt
similarity index 100%
rename from lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LifecycleExt.kt
rename to lifecycle/lifecycle-runtime-compose/src/commonMain/kotlin/androidx/lifecycle/compose/LifecycleExt.kt
diff --git a/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LocalLifecycleOwner.kt b/lifecycle/lifecycle-runtime-compose/src/commonMain/kotlin/androidx/lifecycle/compose/LocalLifecycleOwner.kt
similarity index 100%
rename from lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LocalLifecycleOwner.kt
rename to lifecycle/lifecycle-runtime-compose/src/commonMain/kotlin/androidx/lifecycle/compose/LocalLifecycleOwner.kt
diff --git a/lifecycle/lifecycle-viewmodel-compose/build.gradle b/lifecycle/lifecycle-viewmodel-compose/build.gradle
index 8e107a8..b3f68562 100644
--- a/lifecycle/lifecycle-viewmodel-compose/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/build.gradle
@@ -98,7 +98,6 @@
inceptionYear = "2021"
description = "Compose integration with Lifecycle ViewModel"
runApiTasks = new RunApiTasks.Yes()
- metalavaK2UastEnabled = true
samples(projectOrArtifact(":lifecycle:lifecycle-viewmodel-compose:lifecycle-viewmodel-compose-samples"))
}
diff --git a/lifecycle/lifecycle-viewmodel/build.gradle b/lifecycle/lifecycle-viewmodel/build.gradle
index 7a72a8e..82353a3 100644
--- a/lifecycle/lifecycle-viewmodel/build.gradle
+++ b/lifecycle/lifecycle-viewmodel/build.gradle
@@ -47,6 +47,10 @@
defaultPlatform(PlatformIdentifier.ANDROID)
sourceSets {
+ configureEach {
+ languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
+ }
+
commonMain {
dependencies {
api(project(":annotation:annotation"))
@@ -133,5 +137,4 @@
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Lifecycle ViewModel"
- metalavaK2UastEnabled = true
}
diff --git a/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/ViewModel.kt b/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/ViewModel.kt
index 9328eab..cb1bb1a 100644
--- a/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/ViewModel.kt
+++ b/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/ViewModel.kt
@@ -18,9 +18,10 @@
package androidx.lifecycle
import androidx.annotation.MainThread
-import androidx.lifecycle.viewmodel.internal.Lock
+import androidx.lifecycle.viewmodel.internal.SynchronizedObject
import androidx.lifecycle.viewmodel.internal.VIEW_MODEL_SCOPE_KEY
import androidx.lifecycle.viewmodel.internal.createViewModelScope
+import androidx.lifecycle.viewmodel.internal.synchronized
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -220,9 +221,9 @@
* @see ViewModel.onCleared
*/
public val ViewModel.viewModelScope: CoroutineScope
- get() = viewModelScopeLock.withLock {
+ get() = synchronized(VIEW_MODEL_SCOPE_LOCK) {
getCloseable(VIEW_MODEL_SCOPE_KEY)
?: createViewModelScope().also { scope -> addCloseable(VIEW_MODEL_SCOPE_KEY, scope) }
}
-private val viewModelScopeLock = Lock()
+private val VIEW_MODEL_SCOPE_LOCK = SynchronizedObject()
diff --git a/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/InitializerViewModelFactory.kt b/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/InitializerViewModelFactory.kt
index a38409b..4e111db 100644
--- a/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/InitializerViewModelFactory.kt
+++ b/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/InitializerViewModelFactory.kt
@@ -20,6 +20,7 @@
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.internal.ViewModelProviders
+import androidx.lifecycle.viewmodel.internal.canonicalName
import kotlin.jvm.JvmName
import kotlin.reflect.KClass
@@ -54,7 +55,7 @@
initializer: CreationExtras.() -> T,
) {
require(clazz !in initializers) {
- "A `initializer` with the same `clazz` has already been added: ${clazz.qualifiedName}."
+ "A `initializer` with the same `clazz` has already been added: ${clazz.canonicalName}."
}
initializers[clazz] = ViewModelInitializer(clazz, initializer)
}
diff --git a/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/internal/Lock.kt b/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/internal/Lock.kt
deleted file mode 100644
index b342a19..0000000
--- a/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/internal/Lock.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.lifecycle.viewmodel.internal
-
-/**
- * Provides a custom multiplatform locking mechanism for controlling access to a shared resource by
- * multiple threads.
- *
- * The implementation depends on the platform:
- * - On JVM/ART: uses JDK's synchronization.
- * - On Native: uses posix.
- */
-internal expect class Lock() {
-
- /**
- * Executes the given function [block] while holding the monitor of the current [Lock].
- */
- inline fun <T> withLock(crossinline block: () -> T): T
-}
diff --git a/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.kt b/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.kt
new file mode 100644
index 0000000..d7af3ef
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle.viewmodel.internal
+
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+
+/**
+ * A [SynchronizedObject] provides a mechanism for thread coordination. Instances of this class
+ * are used within [synchronized] functions to establish mutual exclusion, guaranteeing that only
+ * one thread accesses a protected resource or code block at a time.
+ */
+internal expect class SynchronizedObject()
+
+/**
+ * Executes the given function [action] while holding the monitor of the given object [lock].
+ *
+ * The implementation is platform specific:
+ * - JVM: implemented via `synchronized`, `ReentrantLock` is avoided for performance reasons.
+ * - Native: implemented via POSIX mutex with `PTHREAD_MUTEX_RECURSIVE` flag.
+ */
+internal inline fun <T> synchronized(lock: SynchronizedObject, crossinline action: () -> T): T {
+ contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
+ return synchronizedImpl(lock, action)
+}
+
+/**
+ * Executes the given function [action] while holding the monitor of the given object [lock].
+ *
+ * The implementation is platform specific:
+ * - JVM: implemented via `synchronized`, `ReentrantLock` is avoided for performance reasons.
+ * - Native: implemented via POSIX mutex with `PTHREAD_MUTEX_RECURSIVE` flag.
+ *
+ * **This is a private API and should not be used from general code.** This function exists
+ * primarily as a workaround for a Kotlin issue
+ * ([KT-29963](https://youtrack.jetbrains.com/issue/KT-29963)).
+ *
+ * You **MUST** use [synchronized] instead.
+ */
+internal expect inline fun <T> synchronizedImpl(
+ lock: SynchronizedObject,
+ crossinline action: () -> T,
+): T
diff --git a/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/internal/ViewModelImpl.kt b/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/internal/ViewModelImpl.kt
index 752ec79..dd79e0c 100644
--- a/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/internal/ViewModelImpl.kt
+++ b/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/internal/ViewModelImpl.kt
@@ -33,7 +33,7 @@
*/
internal class ViewModelImpl {
- private val lock = Lock()
+ private val lock = SynchronizedObject()
/**
* Holds a mapping between [String] keys and [AutoCloseable] resources that have been associated
@@ -47,7 +47,7 @@
* 2. [closeables][AutoCloseable.close]
* 3. [ViewModel.onCleared]
*
- * **Note:** Manually [Lock] is necessary to prevent issues on Android API 21 and 22.
+ * **Note:** Manually [SynchronizedObject] is necessary to prevent issues on Android API 21 and 22.
* This avoids potential problems found in older versions of `ConcurrentHashMap`.
*
* @see <a href="https://issuetracker.google.com/37042460">b/37042460</a>
@@ -83,7 +83,7 @@
if (isCleared) return
isCleared = true
- lock.withLock {
+ synchronized(lock) {
// 1. Closes resources added without a key.
// 2. Closes resources added with a key.
for (closeable in closeables + keyToCloseables.values) {
@@ -105,7 +105,7 @@
return
}
- val oldCloseable = lock.withLock { keyToCloseables.put(key, closeable) }
+ val oldCloseable = synchronized(lock) { keyToCloseables.put(key, closeable) }
closeWithRuntimeException(oldCloseable)
}
@@ -119,13 +119,13 @@
return
}
- lock.withLock { closeables += closeable }
+ synchronized(lock) { closeables += closeable }
}
/** @see [ViewModel.getCloseable] */
fun <T : AutoCloseable> getCloseable(key: String): T? =
@Suppress("UNCHECKED_CAST")
- lock.withLock { keyToCloseables[key] as T? }
+ synchronized(lock) { keyToCloseables[key] as T? }
private fun closeWithRuntimeException(closeable: AutoCloseable?) {
try {
diff --git a/lifecycle/lifecycle-viewmodel/src/jvmMain/kotlin/androidx/lifecycle/viewmodel/internal/Lock.jvm.kt b/lifecycle/lifecycle-viewmodel/src/jvmMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.jvm.kt
similarity index 75%
rename from lifecycle/lifecycle-viewmodel/src/jvmMain/kotlin/androidx/lifecycle/viewmodel/internal/Lock.jvm.kt
rename to lifecycle/lifecycle-viewmodel/src/jvmMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.jvm.kt
index 9d20353..6d79f87 100644
--- a/lifecycle/lifecycle-viewmodel/src/jvmMain/kotlin/androidx/lifecycle/viewmodel/internal/Lock.jvm.kt
+++ b/lifecycle/lifecycle-viewmodel/src/jvmMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.jvm.kt
@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package androidx.lifecycle.viewmodel.internal
-internal actual class Lock actual constructor() {
- actual inline fun <T> withLock(crossinline block: () -> T): T =
- synchronized(lock = this, block)
-}
+internal actual class SynchronizedObject actual constructor()
+
+internal actual inline fun <T> synchronizedImpl(
+ lock: SynchronizedObject,
+ crossinline action: () -> T
+): T = kotlin.synchronized(lock, action)
diff --git a/lifecycle/lifecycle-viewmodel/src/nativeMain/kotlin/androidx/lifecycle/viewmodel/internal/Lock.native.kt b/lifecycle/lifecycle-viewmodel/src/nativeMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.kt
similarity index 79%
rename from lifecycle/lifecycle-viewmodel/src/nativeMain/kotlin/androidx/lifecycle/viewmodel/internal/Lock.native.kt
rename to lifecycle/lifecycle-viewmodel/src/nativeMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.kt
index b694342..e20fb64 100644
--- a/lifecycle/lifecycle-viewmodel/src/nativeMain/kotlin/androidx/lifecycle/viewmodel/internal/Lock.native.kt
+++ b/lifecycle/lifecycle-viewmodel/src/nativeMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.kt
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package androidx.lifecycle.viewmodel.internal
import kotlin.native.internal.createCleaner
@@ -30,7 +29,6 @@
import platform.posix.pthread_mutexattr_init
import platform.posix.pthread_mutexattr_settype
import platform.posix.pthread_mutexattr_t
-
/**
* Wrapper for platform.posix.PTHREAD_MUTEX_RECURSIVE which
* is represented as kotlin.Int on darwin platforms and kotlin.UInt on linuxX64
@@ -38,26 +36,24 @@
*/
internal expect val PTHREAD_MUTEX_RECURSIVE: Int
-internal actual class Lock actual constructor() {
+internal actual class SynchronizedObject actual constructor() {
private val resource = Resource()
@Suppress("unused") // The returned Cleaner must be assigned to a property
@OptIn(ExperimentalStdlibApi::class)
- private val cleaner = createCleaner(resource, Resource::destroy)
+ private val cleaner = createCleaner(resource, Resource::dispose)
- actual inline fun <T> withLock(crossinline block: () -> T): T {
+ fun lock() {
resource.lock()
- return try {
- block()
- } finally {
- resource.unlock()
- }
+ }
+
+ fun unlock() {
+ resource.unlock()
}
@OptIn(ExperimentalForeignApi::class)
- @PublishedApi
- internal class Resource {
+ private class Resource {
private val arena: Arena = Arena()
private val attr: pthread_mutexattr_t = arena.alloc()
private val mutex: pthread_mutex_t = arena.alloc()
@@ -68,17 +64,26 @@
pthread_mutex_init(mutex.ptr, attr.ptr)
}
- @PublishedApi
- internal fun lock(): Int = pthread_mutex_lock(mutex.ptr)
+ fun lock(): Int = pthread_mutex_lock(mutex.ptr)
- @PublishedApi
- internal fun unlock(): Int = pthread_mutex_unlock(mutex.ptr)
+ fun unlock(): Int = pthread_mutex_unlock(mutex.ptr)
- @PublishedApi
- internal fun destroy() {
+ fun dispose() {
pthread_mutex_destroy(mutex.ptr)
pthread_mutexattr_destroy(attr.ptr)
arena.clear()
}
}
}
+
+internal actual inline fun <T> synchronizedImpl(
+ lock: SynchronizedObject,
+ crossinline action: () -> T
+): T {
+ lock.lock()
+ return try {
+ action()
+ } finally {
+ lock.unlock()
+ }
+}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/ObsoleteCompatMethodMissingMultiLineReplaceWith.java b/lint-checks/integration-tests/src/main/java/androidx/ObsoleteCompatMethodMissingMultiLineReplaceWith.java
new file mode 100644
index 0000000..1af72c9
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/androidx/ObsoleteCompatMethodMissingMultiLineReplaceWith.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx;
+
+public class ObsoleteCompatMethodMissingMultiLineReplaceWith {
+ private ObsoleteCompatMethodMissingMultiLineReplaceWith() {
+ // This class is non-instantiable.
+ }
+
+ /**
+ * Return the object's hash code.
+ *
+ * @param obj the object
+ * @return the hash code
+ * @deprecated Call {@link Object#hashCode()} directly.
+ */
+ @Deprecated
+ public static long hashCode(Object obj) {
+ return obj.hashCode(
+
+ );
+ }
+}
diff --git a/lint-checks/src/main/java/androidx/build/lint/ObsoleteCompatDetector.kt b/lint-checks/src/main/java/androidx/build/lint/ObsoleteCompatDetector.kt
index ef7fbcf..39652e2 100644
--- a/lint-checks/src/main/java/androidx/build/lint/ObsoleteCompatDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/ObsoleteCompatDetector.kt
@@ -131,7 +131,9 @@
}
if (!hasReplaceWith) {
- val replacement = expression.javaPsi!!.text!!.replace("\"", "\\\"")
+ val replacement = expression.javaPsi!!.text!!
+ .replace("\"", "\\\"")
+ .replace(Regex("\n\\s*"), "")
lintFix.add(
LintFix.create()
.name("Annotate with @ReplaceWith")
diff --git a/lint-checks/src/main/java/androidx/build/lint/PrereleaseSdkCoreDependencyDetector.kt b/lint-checks/src/main/java/androidx/build/lint/PrereleaseSdkCoreDependencyDetector.kt
index 470dce6..42e64c2 100644
--- a/lint-checks/src/main/java/androidx/build/lint/PrereleaseSdkCoreDependencyDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/PrereleaseSdkCoreDependencyDetector.kt
@@ -69,7 +69,11 @@
val coordinates = library.resolvedCoordinates
return coordinates.artifactId == "core" &&
coordinates.groupId == "androidx.core" &&
- coordinates.version != "unspecified"
+ // The dependency is invalid if it was listed using a versioned instead of project
+ // dependency. The coordinates of a project dependency may have been resolved to the
+ // current version in the coordinates, but the identifier describing this dependency
+ // won't contain the version (it will be something like ":@@:core:core::debug").
+ (coordinates.version != "unspecified" && coordinates.version in identifier)
}
}
diff --git a/lint-checks/src/test/java/androidx/build/lint/ObsoleteCompatDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/ObsoleteCompatDetectorTest.kt
index 2143e46b..c71313d 100644
--- a/lint-checks/src/test/java/androidx/build/lint/ObsoleteCompatDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/ObsoleteCompatDetectorTest.kt
@@ -108,6 +108,32 @@
}
@Test
+ fun `Obsolete compat method missing multi-line @ReplaceWith`() {
+ val input = arrayOf(
+ javaSample("androidx.ObsoleteCompatMethodMissingMultiLineReplaceWith"),
+ )
+
+ /* ktlint-disable max-line-length */
+ val expected = """
+src/androidx/ObsoleteCompatMethodMissingMultiLineReplaceWith.java:32: Error: Obsolete compat method should provide replacement [ObsoleteCompatMethod]
+ public static long hashCode(Object obj) {
+ ~~~~~~~~
+1 errors, 0 warnings
+ """.trimIndent()
+
+ val expectedAutoFix = """
+Autofix for src/androidx/ObsoleteCompatMethodMissingMultiLineReplaceWith.java line 32: Replace obsolete compat method:
+@@ -18 +18
++ import androidx.annotation.ReplaceWith;
+@@ -31 +32
++ @ReplaceWith(expression = "obj.hashCode()")
+ """.trimIndent()
+ /* ktlint-enable max-line-length */
+
+ check(*input).expect(expected).expectFixDiffs(expectedAutoFix)
+ }
+
+ @Test
fun `Obsolete compat methods missing @Deprecated`() {
val input = arrayOf(
javaSample("androidx.ObsoleteCompatMethodMissingDeprecated"),
diff --git a/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
new file mode 100644
index 0000000..a8f7b186
--- /dev/null
+++ b/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.lint.gradle
+
+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.Incident
+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.intellij.psi.PsiClass
+import com.intellij.psi.PsiClassType
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+
+/**
+ * Checks for usages of [eager APIs](https://docs.gradle.org/current/userguide/task_configuration_avoidance.html)
+ * and [project isolation unsafe APIs](https://docs.gradle.org/nightly/userguide/isolated_projects.html)
+ */
+class DiscouragedGradleMethodDetector : Detector(), Detector.UastScanner {
+
+ override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(
+ UCallExpression::class.java
+ )
+
+ override fun createUastHandler(context: JavaContext): UElementHandler = object :
+ UElementHandler() {
+ override fun visitCallExpression(node: UCallExpression) {
+ val methodName = node.methodName
+ val (containingClassName, replacementMethod, issue) = REPLACEMENTS[methodName] ?: return
+ val containingClass = (node.receiverType as? PsiClassType)?.resolve() ?: return
+ // Check that the called method is from the expected class (or a child class) and not an
+ // unrelated method with the same name).
+ if (!containingClass.isInstanceOf(containingClassName)) return
+
+ val fix = replacementMethod?.let {
+ fix()
+ .replace()
+ .with(it)
+ .reformat(true)
+ // Don't auto-fix from the command line because the replacement methods don't
+ // have the same return types, so the fixed code likely won't compile.
+ .autoFix(robot = false, independent = false)
+ .build()
+ }
+ val message = replacementMethod?.let { "Use $it instead of $methodName" }
+ ?: "Avoid using method $methodName"
+
+ val incident = Incident(context)
+ .issue(issue)
+ .location(context.getNameLocation(node))
+ .message(message)
+ .fix(fix)
+ .scope(node)
+ context.report(incident)
+ }
+ }
+
+ /** Checks if the class is [qualifiedName] or has [qualifiedName] as a super type. */
+ fun PsiClass.isInstanceOf(qualifiedName: String): Boolean =
+ // Recursion will stop when this hits Object, which has no [supers]
+ qualifiedName == this.qualifiedName || supers.any { it.isInstanceOf(qualifiedName) }
+
+ companion object {
+ private const val PROJECT = "org.gradle.api.Project"
+ private const val TASK_CONTAINER = "org.gradle.api.tasks.TaskContainer"
+ private const val TASK_PROVIDER = "org.gradle.api.tasks.TaskProvider"
+ private const val DOMAIN_OBJECT_COLLECTION = "org.gradle.api.DomainObjectCollection"
+ private const val TASK_COLLECTION = "org.gradle.api.tasks.TaskCollection"
+ private const val NAMED_DOMAIN_OBJECT_COLLECTION =
+ "org.gradle.api.NamedDomainObjectCollection"
+
+ val EAGER_CONFIGURATION_ISSUE = Issue.create(
+ "EagerGradleConfiguration",
+ "Avoid using eager task APIs",
+ """
+ Lazy APIs defer creating and configuring objects until they are needed instead of
+ doing unnecessary work in the configuration phase.
+ See https://docs.gradle.org/current/userguide/task_configuration_avoidance.html for
+ more details.
+ """,
+ Category.CORRECTNESS, 5, Severity.ERROR,
+ Implementation(
+ DiscouragedGradleMethodDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+
+ val PROJECT_ISOLATION_ISSUE = Issue.create(
+ "GradleProjectIsolation",
+ "Avoid using APIs that are not project isolation safe",
+ """
+ Using APIs that reach out cross projects makes it not safe for Gradle project
+ isolation.
+ See https://docs.gradle.org/nightly/userguide/isolated_projects.html for
+ more details.
+ """,
+ Category.CORRECTNESS, 5, Severity.ERROR,
+ Implementation(
+ DiscouragedGradleMethodDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+
+ // A map from eager method name to the containing class of the method and the name of the
+ // replacement method, if there is a direct equivalent.
+ private val REPLACEMENTS = mapOf(
+ "all" to
+ Replacement(DOMAIN_OBJECT_COLLECTION, "configureEach", EAGER_CONFIGURATION_ISSUE),
+ "create" to Replacement(TASK_CONTAINER, "register", EAGER_CONFIGURATION_ISSUE),
+ "findAll" to
+ Replacement(NAMED_DOMAIN_OBJECT_COLLECTION, null, EAGER_CONFIGURATION_ISSUE),
+ "findByName" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
+ "findByPath" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
+ "findProperty" to
+ Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
+ "iterator" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
+ "get" to Replacement(TASK_PROVIDER, null, EAGER_CONFIGURATION_ISSUE),
+ "getAt" to Replacement(TASK_COLLECTION, "named", EAGER_CONFIGURATION_ISSUE),
+ "getByPath" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
+ "getByName" to Replacement(TASK_CONTAINER, "named", EAGER_CONFIGURATION_ISSUE),
+ "matching" to Replacement(TASK_COLLECTION, null, EAGER_CONFIGURATION_ISSUE),
+ "replace" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
+ "remove" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
+ "whenTaskAdded" to
+ Replacement(TASK_CONTAINER, "configureEach", EAGER_CONFIGURATION_ISSUE),
+ "whenObjectAdded" to
+ Replacement(DOMAIN_OBJECT_COLLECTION, "configureEach", EAGER_CONFIGURATION_ISSUE),
+ )
+ }
+}
+
+private data class Replacement(
+ val qualifiedName: String,
+ val recommendedReplacement: String?,
+ val issue: Issue
+)
diff --git a/lint/lint-gradle/src/main/java/androidx/lint/gradle/EagerConfigurationDetector.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/EagerConfigurationDetector.kt
deleted file mode 100644
index 603d470..0000000
--- a/lint/lint-gradle/src/main/java/androidx/lint/gradle/EagerConfigurationDetector.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.lint.gradle
-
-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.Incident
-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.intellij.psi.PsiClass
-import com.intellij.psi.PsiClassType
-import org.jetbrains.uast.UCallExpression
-import org.jetbrains.uast.UElement
-
-/**
- * Checks for usages of [eager APIs](https://docs.gradle.org/current/userguide/task_configuration_avoidance.html).
- */
-class EagerConfigurationDetector : Detector(), Detector.UastScanner {
-
- override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(
- UCallExpression::class.java
- )
-
- override fun createUastHandler(context: JavaContext): UElementHandler = object :
- UElementHandler() {
- override fun visitCallExpression(node: UCallExpression) {
- val methodName = node.methodName
- val (containingClassName, replacementMethod) = REPLACEMENTS[methodName] ?: return
- val containingClass = (node.receiverType as? PsiClassType)?.resolve() ?: return
- // Check that the called method is from the expected class (or a child class) and not an
- // unrelated method with the same name).
- if (!containingClass.isInstanceOf(containingClassName)) return
-
- val fix = replacementMethod?.let {
- fix()
- .replace()
- .with(it)
- .reformat(true)
- // Don't auto-fix from the command line because the replacement methods don't
- // have the same return types, so the fixed code likely won't compile.
- .autoFix(robot = false, independent = false)
- .build()
- }
- val message = replacementMethod?.let { "Use $it instead of $methodName" }
- ?: "Avoid using eager method $methodName"
-
- val incident = Incident(context)
- .issue(ISSUE)
- .location(context.getNameLocation(node))
- .message(message)
- .fix(fix)
- .scope(node)
- context.report(incident)
- }
- }
-
- /** Checks if the class is [qualifiedName] or has [qualifiedName] as a super type. */
- fun PsiClass.isInstanceOf(qualifiedName: String): Boolean =
- // Recursion will stop when this hits Object, which has no [supers]
- qualifiedName == this.qualifiedName || supers.any { it.isInstanceOf(qualifiedName) }
-
- companion object {
- private const val TASK_CONTAINER = "org.gradle.api.tasks.TaskContainer"
- private const val TASK_PROVIDER = "org.gradle.api.tasks.TaskProvider"
- private const val DOMAIN_OBJECT_COLLECTION = "org.gradle.api.DomainObjectCollection"
- private const val TASK_COLLECTION = "org.gradle.api.tasks.TaskCollection"
- private const val NAMED_DOMAIN_OBJECT_COLLECTION =
- "org.gradle.api.NamedDomainObjectCollection"
-
- // A map from eager method name to the containing class of the method and the name of the
- // replacement method, if there is a direct equivalent.
- private val REPLACEMENTS = mapOf(
- "create" to Pair(TASK_CONTAINER, "register"),
- "getByName" to Pair(TASK_CONTAINER, "named"),
- "all" to Pair(DOMAIN_OBJECT_COLLECTION, "configureEach"),
- "whenTaskAdded" to Pair(TASK_CONTAINER, "configureEach"),
- "whenObjectAdded" to Pair(DOMAIN_OBJECT_COLLECTION, "configureEach"),
- "getAt" to Pair(TASK_COLLECTION, "named"),
- "getByPath" to Pair(TASK_CONTAINER, null),
- "findByName" to Pair(TASK_CONTAINER, null),
- "findByPath" to Pair(TASK_CONTAINER, null),
- "replace" to Pair(TASK_CONTAINER, null),
- "remove" to Pair(TASK_CONTAINER, null),
- "iterator" to Pair(TASK_CONTAINER, null),
- "findAll" to Pair(NAMED_DOMAIN_OBJECT_COLLECTION, null),
- "matching" to Pair(TASK_COLLECTION, null),
- "get" to Pair(TASK_PROVIDER, null),
- )
-
- val ISSUE = Issue.create(
- "EagerGradleConfiguration",
- "Avoid using eager task APIs",
- """
- Lazy APIs defer creating and configuring objects until they are needed instead of
- doing unnecessary work in the configuration phase.
- See https://docs.gradle.org/current/userguide/task_configuration_avoidance.html for
- more details.
- """,
- Category.CORRECTNESS, 5, Severity.ERROR,
- Implementation(
- EagerConfigurationDetector::class.java,
- Scope.JAVA_FILE_SCOPE
- )
- )
- }
-}
diff --git a/lint/lint-gradle/src/main/java/androidx/lint/gradle/GradleIssueRegistry.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/GradleIssueRegistry.kt
index b638571..1a5ca3e 100644
--- a/lint/lint-gradle/src/main/java/androidx/lint/gradle/GradleIssueRegistry.kt
+++ b/lint/lint-gradle/src/main/java/androidx/lint/gradle/GradleIssueRegistry.kt
@@ -27,10 +27,12 @@
override val api = CURRENT_API
override val issues = listOf(
- EagerConfigurationDetector.ISSUE,
+ DiscouragedGradleMethodDetector.EAGER_CONFIGURATION_ISSUE,
+ DiscouragedGradleMethodDetector.PROJECT_ISOLATION_ISSUE,
InternalApiUsageDetector.INTERNAL_GRADLE_ISSUE,
InternalApiUsageDetector.INTERNAL_AGP_ISSUE,
WithPluginClasspathUsageDetector.ISSUE,
+ WithTypeWithoutConfigureEachUsageDetector.ISSUE,
)
override val vendor = Vendor(
diff --git a/lint/lint-gradle/src/main/java/androidx/lint/gradle/WithTypeWithoutConfigureEachUsageDetector.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/WithTypeWithoutConfigureEachUsageDetector.kt
new file mode 100644
index 0000000..614b012
--- /dev/null
+++ b/lint/lint-gradle/src/main/java/androidx/lint/gradle/WithTypeWithoutConfigureEachUsageDetector.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.lint.gradle
+
+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.Incident
+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.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+
+class WithTypeWithoutConfigureEachUsageDetector : Detector(), Detector.UastScanner {
+ override fun getApplicableMethodNames(): List<String> = listOf("withType")
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ val evaluator = context.evaluator
+ val message = "Avoid passing a closure to withType, use withType().configureEach instead"
+ val incident = Incident(context)
+ .issue(ISSUE)
+ .location(context.getNameLocation(node))
+ .message(message)
+ .scope(node)
+
+ if (evaluator.isMemberInClass(node.resolve(), DOMAIN_OBJECT_COLLECTION) &&
+ node.valueArgumentCount != 1) {
+ context.report(incident)
+ }
+ }
+
+ companion object {
+ private const val DOMAIN_OBJECT_COLLECTION = "org.gradle.api.DomainObjectCollection"
+
+ val ISSUE = Issue.create(
+ id = "WithTypeWithoutConfigureEach",
+ briefDescription = "Flags usage of withType with a closure instead of configureEach",
+ explanation = """
+ Using withType with a closure directly eagerly creates task.
+ Using configureEach defers the creation of tasks.
+ """,
+ category = Category.CORRECTNESS,
+ priority = 5,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ WithTypeWithoutConfigureEachUsageDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+ }
+}
diff --git a/lint/lint-gradle/src/test/java/androidx/lint/gradle/EagerConfigurationDetectorTest.kt b/lint/lint-gradle/src/test/java/androidx/lint/gradle/EagerConfigurationIssueTest.kt
similarity index 92%
rename from lint/lint-gradle/src/test/java/androidx/lint/gradle/EagerConfigurationDetectorTest.kt
rename to lint/lint-gradle/src/test/java/androidx/lint/gradle/EagerConfigurationIssueTest.kt
index 8e8e434..1d2864f 100644
--- a/lint/lint-gradle/src/test/java/androidx/lint/gradle/EagerConfigurationDetectorTest.kt
+++ b/lint/lint-gradle/src/test/java/androidx/lint/gradle/EagerConfigurationIssueTest.kt
@@ -21,9 +21,9 @@
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
-class EagerConfigurationDetectorTest : GradleLintDetectorTest(
- detector = EagerConfigurationDetector(),
- issues = listOf(EagerConfigurationDetector.ISSUE)
+class EagerConfigurationIssueTest : GradleLintDetectorTest(
+ detector = DiscouragedGradleMethodDetector(),
+ issues = listOf(DiscouragedGradleMethodDetector.EAGER_CONFIGURATION_ISSUE)
) {
@Test
fun `Test usage of TaskContainer#create`() {
@@ -228,7 +228,7 @@
)
val expected = """
- src/test.kt:4: Error: Avoid using eager method getByPath [EagerGradleConfiguration]
+ src/test.kt:4: Error: Avoid using method getByPath [EagerGradleConfiguration]
project.tasks.getByPath("example")
~~~~~~~~~
1 errors, 0 warnings
@@ -250,7 +250,7 @@
)
val expected = """
- src/test.kt:4: Error: Avoid using eager method findByName [EagerGradleConfiguration]
+ src/test.kt:4: Error: Avoid using method findByName [EagerGradleConfiguration]
project.tasks.findByName("example")
~~~~~~~~~~
1 errors, 0 warnings
@@ -272,7 +272,7 @@
)
val expected = """
- src/test.kt:4: Error: Avoid using eager method findByPath [EagerGradleConfiguration]
+ src/test.kt:4: Error: Avoid using method findByPath [EagerGradleConfiguration]
project.tasks.findByPath("example")
~~~~~~~~~~
1 errors, 0 warnings
@@ -294,7 +294,7 @@
)
val expected = """
- src/test.kt:4: Error: Avoid using eager method replace [EagerGradleConfiguration]
+ src/test.kt:4: Error: Avoid using method replace [EagerGradleConfiguration]
project.tasks.replace("example")
~~~~~~~
1 errors, 0 warnings
@@ -316,7 +316,7 @@
)
val expected = """
- src/test.kt:4: Error: Avoid using eager method remove [EagerGradleConfiguration]
+ src/test.kt:4: Error: Avoid using method remove [EagerGradleConfiguration]
project.tasks.remove(task)
~~~~~~
1 errors, 0 warnings
@@ -338,7 +338,7 @@
)
val expected = """
- src/test.kt:4: Error: Avoid using eager method findByPath [EagerGradleConfiguration]
+ src/test.kt:4: Error: Avoid using method findByPath [EagerGradleConfiguration]
project.tasks.findByPath("example")
~~~~~~~~~~
1 errors, 0 warnings
@@ -361,7 +361,7 @@
)
val expected = """
- src/test.kt:5: Error: Avoid using eager method findAll [EagerGradleConfiguration]
+ src/test.kt:5: Error: Avoid using method findAll [EagerGradleConfiguration]
project.tasks.findAll(closure)
~~~~~~~
1 errors, 0 warnings
@@ -384,7 +384,7 @@
)
val expected = """
- src/test.kt:5: Error: Avoid using eager method matching [EagerGradleConfiguration]
+ src/test.kt:5: Error: Avoid using method matching [EagerGradleConfiguration]
project.tasks.matching(closure)
~~~~~~~~
1 errors, 0 warnings
@@ -406,7 +406,7 @@
)
val expected = """
- src/test.kt:4: Error: Avoid using eager method get [EagerGradleConfiguration]
+ src/test.kt:4: Error: Avoid using method get [EagerGradleConfiguration]
project.tasks.register("example").get()
~~~
1 errors, 0 warnings
diff --git a/lint/lint-gradle/src/test/java/androidx/lint/gradle/ProjectIsolationIssueTest.kt b/lint/lint-gradle/src/test/java/androidx/lint/gradle/ProjectIsolationIssueTest.kt
new file mode 100644
index 0000000..e2f90c0
--- /dev/null
+++ b/lint/lint-gradle/src/test/java/androidx/lint/gradle/ProjectIsolationIssueTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.lint.gradle
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ProjectIsolationIssueTest : GradleLintDetectorTest(
+ detector = DiscouragedGradleMethodDetector(),
+ issues = listOf(DiscouragedGradleMethodDetector.PROJECT_ISOLATION_ISSUE)
+) {
+ @Test
+ fun `Test usage of TaskContainer#create`() {
+ val input = kotlin(
+ """
+ import org.gradle.api.Project
+
+ fun configure(project: Project) {
+ project.findProperty("example")
+ }
+ """.trimIndent()
+ )
+
+ val expected = """
+ src/test.kt:4: Error: Use providers.gradleProperty instead of findProperty [GradleProjectIsolation]
+ project.findProperty("example")
+ ~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.trimIndent()
+ val expectedFixDiffs = """
+ Fix for src/test.kt line 4: Replace with providers.gradleProperty:
+ @@ -4 +4
+ - project.findProperty("example")
+ + project.providers.gradleProperty("example")
+ """.trimIndent()
+
+ check(input).expect(expected).expectFixDiffs(expectedFixDiffs)
+ }
+}
diff --git a/lint/lint-gradle/src/test/java/androidx/lint/gradle/Stubs.kt b/lint/lint-gradle/src/test/java/androidx/lint/gradle/Stubs.kt
index fdeb2ce..18eb6cf 100644
--- a/lint/lint-gradle/src/test/java/androidx/lint/gradle/Stubs.kt
+++ b/lint/lint-gradle/src/test/java/androidx/lint/gradle/Stubs.kt
@@ -29,13 +29,14 @@
package org.gradle.api.tasks
import groovy.lang.Closure
+ import java.lang.Class
import org.gradle.api.DomainObjectCollection
import org.gradle.api.NamedDomainObjectCollection
import org.gradle.api.provider.Provider
import org.gradle.api.Task
import org.gradle.api.tasks.TaskProvider
- class TaskContainer : DomainObjectCollection<Task>, TaskCollection<Task>, NamedDomainObjectCollection<Task> {
+ class TaskContainer : DomainObjectCollection<Task>, TaskCollection<Task>, NamedDomainObjectCollection<Task>, NamedDomainObjectSet<Task> {
fun create(name: String) = Unit
fun register(name: String): TaskProvider<Task> = TODO()
fun getByName(name: String) = Unit
@@ -75,9 +76,11 @@
import groovy.lang.Closure
import org.gradle.api.tasks.TaskContainer
+ import java.lang.Class
class Project {
val tasks: TaskContainer
+ fun findProperty(propertyName: String): Object? = null
}
interface NamedDomainObjectCollection<T> : Collection<T>, DomainObjectCollection<T>, Iterable<T> {
@@ -89,8 +92,7 @@
fun all(action: Action<in T>)
fun configureEach(action: Action<in T>)
fun whenObjectAdded(action: Action<in T>)
- fun withType(cls: Class)
- fun withType(cls: Class, action: Action)
+ fun withType(type: Class<S>)
}
interface Action<T>
diff --git a/lint/lint-gradle/src/test/java/androidx/lint/gradle/WithTypeWithoutConfigureEachUsageDetectorTest.kt b/lint/lint-gradle/src/test/java/androidx/lint/gradle/WithTypeWithoutConfigureEachUsageDetectorTest.kt
new file mode 100644
index 0000000..455ad21
--- /dev/null
+++ b/lint/lint-gradle/src/test/java/androidx/lint/gradle/WithTypeWithoutConfigureEachUsageDetectorTest.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.lint.gradle
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class WithTypeWithoutConfigureEachUsageDetectorTest : GradleLintDetectorTest(
+ detector = WithTypeWithoutConfigureEachUsageDetector(),
+ issues = listOf(WithTypeWithoutConfigureEachUsageDetector.ISSUE)
+
+) {
+ @Test
+ fun `Test withType Without ConfigureEach usage`() {
+
+ val input = kotlin(
+ """
+ import org.gradle.api.Project
+
+ fun configure(project: Project) {
+ project.tasks.withType(Example::class.java) {}
+ }
+ """.trimIndent()
+ )
+
+ val message = "Avoid passing a closure to withType, use withType().configureEach instead"
+
+ val expected = """
+ src/test.kt:4: Error: $message [WithTypeWithoutConfigureEach]
+ project.tasks.withType(Example::class.java) {}
+ ~~~~~~~~
+ 1 errors, 0 warnings
+ """.trimIndent()
+
+ check(input).expect(expected)
+ }
+
+ @Test
+ fun `Test withType With ConfigureEach usage`() {
+
+ val input = kotlin(
+ """
+ import org.gradle.api.Project
+
+ fun configure(project: Project) {
+ project.tasks.withType(Example::class.java).configureEach {}
+ }
+ """.trimIndent()
+ )
+ check(input).expectClean()
+ }
+}
diff --git a/mediarouter/mediarouter/src/main/res/values-te/strings.xml b/mediarouter/mediarouter/src/main/res/values-te/strings.xml
index 290e656..85ae80a 100644
--- a/mediarouter/mediarouter/src/main/res/values-te/strings.xml
+++ b/mediarouter/mediarouter/src/main/res/values-te/strings.xml
@@ -27,7 +27,7 @@
<string name="mr_chooser_looking_for_devices" msgid="4257319068277776035">"పరికరాల కోసం వెతుకుతోంది..."</string>
<string name="mr_controller_disconnect" msgid="7812275474138309497">"డిస్కనెక్ట్ చేయి"</string>
<string name="mr_controller_stop_casting" msgid="804210341192624074">"ప్రసారాన్ని ఆపివేయి"</string>
- <string name="mr_controller_close_description" msgid="5684434439232634509">"మూసివేయి"</string>
+ <string name="mr_controller_close_description" msgid="5684434439232634509">"మూసివేయండి"</string>
<string name="mr_controller_play" msgid="1253345086594430054">"ప్లే చేయి"</string>
<string name="mr_controller_pause" msgid="747801650871398383">"పాజ్ చేయి"</string>
<string name="mr_controller_stop" msgid="5497722768305745508">"ఆపు"</string>
diff --git a/navigation/navigation-common/api/current.txt b/navigation/navigation-common/api/current.txt
index 5bffb80..91a9664 100644
--- a/navigation/navigation-common/api/current.txt
+++ b/navigation/navigation-common/api/current.txt
@@ -142,6 +142,10 @@
public static final class NavBackStackEntry.Companion {
}
+ public final class NavBackStackEntryKt {
+ method @SuppressCompatibility @androidx.navigation.ExperimentalSafeArgsApi public static inline <reified T> T toRoute(androidx.navigation.NavBackStackEntry);
+ }
+
public final class NavDeepLink {
method public String? getAction();
method public String? getMimeType();
@@ -257,14 +261,17 @@
ctor public NavDestinationBuilder(androidx.navigation.Navigator<? extends D> navigator, String? route);
ctor @SuppressCompatibility @androidx.navigation.ExperimentalSafeArgsApi public NavDestinationBuilder(androidx.navigation.Navigator<? extends D> navigator, kotlin.reflect.KClass<?>? route, java.util.Map<kotlin.reflect.KType,androidx.navigation.NavType<?>> typeMap);
method @Deprecated public final void action(int actionId, kotlin.jvm.functions.Function1<? super androidx.navigation.NavActionBuilder,kotlin.Unit> actionBuilder);
+ method public final void argument(String name, androidx.navigation.NavArgument argument);
method public final void argument(String name, kotlin.jvm.functions.Function1<? super androidx.navigation.NavArgumentBuilder,kotlin.Unit> argumentBuilder);
method public D build();
+ method public final void deepLink(androidx.navigation.NavDeepLink navDeepLink);
method public final void deepLink(String uriPattern);
method public final void deepLink(kotlin.jvm.functions.Function1<? super androidx.navigation.NavDeepLinkDslBuilder,kotlin.Unit> navDeepLink);
method public final int getId();
method public final CharSequence? getLabel();
method protected final androidx.navigation.Navigator<? extends D> getNavigator();
method public final String? getRoute();
+ method protected D instantiateDestination();
method public final void setLabel(CharSequence?);
property public final int id;
property public final CharSequence? label;
diff --git a/navigation/navigation-common/api/restricted_current.txt b/navigation/navigation-common/api/restricted_current.txt
index 5bffb80..91a9664 100644
--- a/navigation/navigation-common/api/restricted_current.txt
+++ b/navigation/navigation-common/api/restricted_current.txt
@@ -142,6 +142,10 @@
public static final class NavBackStackEntry.Companion {
}
+ public final class NavBackStackEntryKt {
+ method @SuppressCompatibility @androidx.navigation.ExperimentalSafeArgsApi public static inline <reified T> T toRoute(androidx.navigation.NavBackStackEntry);
+ }
+
public final class NavDeepLink {
method public String? getAction();
method public String? getMimeType();
@@ -257,14 +261,17 @@
ctor public NavDestinationBuilder(androidx.navigation.Navigator<? extends D> navigator, String? route);
ctor @SuppressCompatibility @androidx.navigation.ExperimentalSafeArgsApi public NavDestinationBuilder(androidx.navigation.Navigator<? extends D> navigator, kotlin.reflect.KClass<?>? route, java.util.Map<kotlin.reflect.KType,androidx.navigation.NavType<?>> typeMap);
method @Deprecated public final void action(int actionId, kotlin.jvm.functions.Function1<? super androidx.navigation.NavActionBuilder,kotlin.Unit> actionBuilder);
+ method public final void argument(String name, androidx.navigation.NavArgument argument);
method public final void argument(String name, kotlin.jvm.functions.Function1<? super androidx.navigation.NavArgumentBuilder,kotlin.Unit> argumentBuilder);
method public D build();
+ method public final void deepLink(androidx.navigation.NavDeepLink navDeepLink);
method public final void deepLink(String uriPattern);
method public final void deepLink(kotlin.jvm.functions.Function1<? super androidx.navigation.NavDeepLinkDslBuilder,kotlin.Unit> navDeepLink);
method public final int getId();
method public final CharSequence? getLabel();
method protected final androidx.navigation.Navigator<? extends D> getNavigator();
method public final String? getRoute();
+ method protected D instantiateDestination();
method public final void setLabel(CharSequence?);
property public final int id;
property public final CharSequence? label;
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
index 540df8d..860d0a9 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
@@ -115,6 +115,7 @@
argument("testArg2") {
type = NavType.StringType
}
+ argument("testArg3", NavArgument.Builder().setDefaultValue("123").build())
}
assertWithMessage("NavDestination should have default arguments set")
.that(destination.arguments.get("testArg")?.defaultValue)
@@ -122,6 +123,9 @@
assertWithMessage("NavArgument shouldn't have a default value")
.that(destination.arguments.get("testArg2")?.isDefaultValuePresent)
.isFalse()
+ assertWithMessage("NavDestination should have implicit default arguments set")
+ .that(destination.arguments.get("testArg3")?.defaultValue)
+ .isEqualTo("123")
}
@Test
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/serialization/RouteDecoderTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/serialization/RouteDecoderTest.kt
new file mode 100644
index 0000000..7ac7dbc
--- /dev/null
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/serialization/RouteDecoderTest.kt
@@ -0,0 +1,313 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.navigation.serialization
+
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+import androidx.core.os.bundleOf
+import androidx.navigation.NamedNavArgument
+import androidx.navigation.NavType
+import androidx.navigation.common.test.R
+import androidx.navigation.navArgument
+import com.google.common.truth.Truth.assertThat
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.serializer
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class RouteDecoderTest {
+
+ @Test
+ fun decodeString() {
+ @Serializable
+ data class TestClass(val arg: String)
+
+ val bundle = bundleOf("arg" to "theArg")
+ val result = decode<TestClass>(bundle, listOf(stringArgument("arg")))
+ assertThat(result.arg).isEqualTo("theArg")
+ }
+
+ @Test
+ fun decodeInt() {
+ @Serializable
+ class TestClass(val arg: Int)
+
+ val bundle = bundleOf("arg" to 15)
+ val result = decode<TestClass>(bundle, listOf(intArgument("arg")))
+ assertThat(result.arg).isEqualTo(15)
+ }
+
+ @Test
+ fun decodeBoolean() {
+ @Serializable
+ class TestClass(val arg: Boolean)
+
+ val bundle = bundleOf("arg" to false)
+ val result = decode<TestClass>(bundle, listOf(
+ navArgument("arg") {
+ type = NavType.BoolType
+ }
+ ))
+ assertThat(result.arg).isEqualTo(false)
+ }
+
+ @Test
+ fun decodeLong() {
+ @Serializable
+ class TestClass(val arg: Long)
+
+ val bundle = bundleOf("arg" to 1L)
+ val result = decode<TestClass>(bundle, listOf(
+ navArgument("arg") {
+ type = NavType.LongType
+ }
+ ))
+ assertThat(result.arg).isEqualTo(1L)
+ }
+
+ @Test
+ fun decodeFloat() {
+ @Serializable
+ class TestClass(val arg: Float)
+
+ val bundle = bundleOf("arg" to 1.0F)
+ val result = decode<TestClass>(bundle, listOf(
+ navArgument("arg") {
+ type = NavType.FloatType
+ }
+ ))
+ assertThat(result.arg).isEqualTo(1.0F)
+ }
+
+ @Test
+ fun decodeReference() {
+ @Serializable
+ class TestClass(val arg: Int)
+ val bundle = bundleOf("arg" to R.id.nav_id_reference)
+ val result = decode<TestClass>(bundle, listOf(
+ navArgument("arg") {
+ type = NavType.ReferenceType
+ }
+ ))
+ assertThat(result.arg).isEqualTo(R.id.nav_id_reference)
+ }
+
+ @Test
+ fun decodeStringArray() {
+ @Serializable
+ data class TestClass(val arg: Array<String>)
+
+ val expected = arrayOf("arg1", "arg")
+ val bundle = bundleOf("arg" to expected)
+ val result = decode<TestClass>(bundle, listOf(
+ navArgument("arg") {
+ type = NavType.StringArrayType
+ }
+ ))
+ assertThat(result.arg).isEqualTo(expected)
+ }
+
+ @Test
+ fun decodeIntArray() {
+ @Serializable
+ class TestClass(val arg: IntArray)
+
+ val bundle = bundleOf("arg" to intArrayOf(0, 1, 2, 3))
+ val result = serializer<TestClass>().decodeArguments(
+ bundle,
+ mapOf("arg" to NavType.IntArrayType as NavType<*>,)
+ )
+ assertThat(result.arg).isEqualTo(intArrayOf(0, 1, 2, 3))
+ }
+
+ @Test
+ fun decodeBooleanArray() {
+ @Serializable
+ class TestClass(val arg: BooleanArray)
+
+ val bundle = bundleOf("arg" to booleanArrayOf(false, true))
+ val result = decode<TestClass>(bundle, listOf(
+ navArgument("arg") {
+ type = NavType.BoolArrayType
+ }
+ ))
+ assertThat(result.arg).isEqualTo(booleanArrayOf(false, true))
+ }
+
+ @Test
+ fun decodeLongArray() {
+ @Serializable
+ class TestClass(val arg: LongArray)
+
+ val bundle = bundleOf("arg" to longArrayOf(1L, 2L))
+ val result = decode<TestClass>(bundle, listOf(
+ navArgument("arg") {
+ type = NavType.LongArrayType
+ }
+ ))
+ assertThat(result.arg).isEqualTo(longArrayOf(1L, 2L))
+ }
+
+ @Test
+ fun decodeFloatArray() {
+ @Serializable
+ class TestClass(val arg: FloatArray)
+
+ val bundle = bundleOf("arg" to floatArrayOf(1.0F, 1.5F))
+ val result = decode<TestClass>(bundle, listOf(
+ navArgument("arg") {
+ type = NavType.FloatArrayType
+ }
+ ))
+ assertThat(result.arg).isEqualTo(floatArrayOf(1.0F, 1.5F))
+ }
+
+ @Test
+ fun decodeIntString() {
+ @Serializable
+ @SerialName(PATH_SERIAL_NAME)
+ class TestClass(val arg: Int, val arg2: String)
+
+ val bundle = bundleOf("arg" to 15, "arg2" to "theArg")
+ val result = decode<TestClass>(
+ bundle,
+ listOf(stringArgument("arg2"), intArgument("arg"))
+ )
+ assertThat(result.arg).isEqualTo(15)
+ assertThat(result.arg2).isEqualTo("theArg")
+ }
+
+ @Test
+ fun decodeCustomType() {
+ @Serializable
+ class CustomType(val nestedArg: Int) : Parcelable {
+ override fun describeContents() = 0
+ override fun writeToParcel(dest: Parcel, flags: Int) {}
+ }
+
+ @Serializable
+ class TestClass(val custom: CustomType)
+
+ @Suppress("DEPRECATION")
+ val customArg = navArgument("custom") {
+ type = object : NavType<CustomType>(false) {
+ override fun put(bundle: Bundle, key: String, value: CustomType) { }
+ override fun get(bundle: Bundle, key: String): CustomType =
+ bundle[key] as CustomType
+ override fun parseValue(value: String): CustomType = CustomType(15)
+ override fun serializeAsValue(value: CustomType) = ""
+ }
+ }
+ val bundle = bundleOf("custom" to CustomType(1))
+ val result = decode<TestClass>(
+ bundle,
+ listOf(customArg)
+ )
+ assertThat(result.custom.nestedArg).isEqualTo(1)
+ }
+
+ @Test
+ fun decodeNullLiteral() {
+ @Serializable
+ data class TestClass(val arg: String)
+
+ val bundle = bundleOf("arg" to "null")
+ val result = decode<TestClass>(bundle, listOf(stringArgument("arg")))
+ assertThat(result.arg).isEqualTo("null")
+ }
+
+ @Test
+ fun decodeNullPrimitive() {
+ @Serializable
+ data class TestClass(val arg: String?)
+
+ val bundle = bundleOf("arg" to null)
+ val result = decode<TestClass>(bundle, listOf(nullableStringArgument("arg")))
+ assertThat(result.arg).isEqualTo(null)
+ }
+
+ @Test
+ fun decodeNullCustom() {
+ @Serializable
+ class CustomType : Parcelable {
+ override fun describeContents() = 0
+ override fun writeToParcel(dest: Parcel, flags: Int) {}
+ }
+
+ @Serializable
+ class TestClass(val custom: CustomType?)
+
+ @Suppress("DEPRECATION")
+ val customArg = navArgument("custom") {
+ type = object : NavType<CustomType>(true) {
+ override fun put(bundle: Bundle, key: String, value: CustomType) { }
+ override fun get(bundle: Bundle, key: String): CustomType? =
+ bundle[key] as? CustomType
+ override fun parseValue(value: String): CustomType = CustomType()
+ override fun serializeAsValue(value: CustomType) = ""
+ }
+ }
+ val bundle = bundleOf("custom" to null)
+ val result = decode<TestClass>(
+ bundle,
+ listOf(customArg)
+ )
+ assertThat(result.custom).isNull()
+ }
+
+ @Test
+ fun decodeNestedNull() {
+ @Serializable
+ class CustomType(val arg: Int?) : Parcelable {
+ override fun describeContents() = 0
+ override fun writeToParcel(dest: Parcel, flags: Int) {}
+ }
+
+ @Serializable
+ class TestClass(val custom: CustomType)
+
+ @Suppress("DEPRECATION")
+ val customArg = navArgument("custom") {
+ type = object : NavType<CustomType>(false) {
+ override fun put(bundle: Bundle, key: String, value: CustomType) { }
+ override fun get(bundle: Bundle, key: String): CustomType? =
+ bundle[key] as? CustomType
+ override fun parseValue(value: String): CustomType = CustomType(0)
+ override fun serializeAsValue(value: CustomType) = ""
+ }
+ }
+ val bundle = bundleOf("custom" to CustomType(null))
+ val result = decode<TestClass>(
+ bundle,
+ listOf(customArg)
+ )
+ assertThat(result.custom.arg).isNull()
+ }
+
+ private inline fun <reified T : Any> decode(
+ bundle: Bundle,
+ args: List<NamedNavArgument> = emptyList()
+ ): T {
+ val typeMap = mutableMapOf<String, NavType<Any?>>()
+ args.forEach { typeMap[it.name] = it.argument.type }
+ return serializer<T>().decodeArguments(bundle, typeMap)
+ }
+}
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/serialization/RouteFilledTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/serialization/RouteFilledTest.kt
index 553e1a0..7f1ac4a 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/serialization/RouteFilledTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/serialization/RouteFilledTest.kt
@@ -750,7 +750,7 @@
private interface TestInterface
-private fun stringArgument(
+internal fun stringArgument(
name: String,
hasDefaultValue: Boolean = false
) = navArgument(name) {
@@ -759,7 +759,7 @@
unknownDefaultValuePresent = hasDefaultValue
}
-private fun nullableStringArgument(
+internal fun nullableStringArgument(
name: String,
hasDefaultValue: Boolean = false
) = navArgument(name) {
@@ -768,7 +768,7 @@
unknownDefaultValuePresent = hasDefaultValue
}
-private fun intArgument(
+internal fun intArgument(
name: String,
hasDefaultValue: Boolean = false
) = navArgument(name) {
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
index c963932..c474d2d 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
@@ -37,10 +37,12 @@
import androidx.lifecycle.enableSavedStateHandles
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.MutableCreationExtras
+import androidx.navigation.serialization.decodeArguments
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import java.util.UUID
+import kotlinx.serialization.serializer
/**
* Representation of an entry in the back stack of a [androidx.navigation.NavController]. The
@@ -289,3 +291,21 @@
private class SavedStateViewModel(val handle: SavedStateHandle) : ViewModel()
}
+
+/**
+ * Returns route as an object of type [T]
+ *
+ * Extrapolates arguments from [NavBackStackEntry.arguments] and recreates object [T]
+ *
+ * @param [T] the entry's [NavDestination.route] as a [KClass]
+ *
+ * @return A new instance of this entry's [NavDestination.route] as an object of type [T]
+ */
+@ExperimentalSafeArgsApi
+public inline fun <reified T> NavBackStackEntry.toRoute(): T {
+ val bundle = arguments ?: Bundle()
+ val typeMap = destination.arguments.mapValues {
+ it.value.type
+ }
+ return serializer<T>().decodeArguments(bundle, typeMap)
+}
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt
index ab06664..0ef8b28 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt
@@ -34,7 +34,8 @@
@NavDestinationDsl
public open class NavDestinationBuilder<out D : NavDestination> internal constructor(
/**
- * The navigator the destination was created from
+ * The navigator the destination that will be used in [instantiateDestination]
+ * to create the destination.
*/
protected val navigator: Navigator<out D>,
/**
@@ -121,6 +122,14 @@
arguments[name] = NavArgumentBuilder().apply(argumentBuilder).build()
}
+ /**
+ * Add a [NavArgument] to this destination.
+ */
+ @Suppress("BuilderSetStyle")
+ public fun argument(name: String, argument: NavArgument) {
+ arguments[name] = argument
+ }
+
private var deepLinks = mutableListOf<NavDeepLink>()
/**
@@ -166,6 +175,28 @@
deepLinks.add(NavDeepLinkDslBuilder().apply(navDeepLink).build())
}
+ /**
+ * Add a deep link to this destination.
+ *
+ * In addition to a direct Uri match, the following features are supported:
+ *
+ * * Uris without a scheme are assumed as http and https. For example,
+ * `www.example.com` will match `http://www.example.com` and
+ * `https://www.example.com`.
+ * * Placeholders in the form of `{placeholder_name}` matches 1 or more
+ * characters. The String value of the placeholder will be available in the arguments
+ * [Bundle] with a key of the same name. For example,
+ * `http://www.example.com/users/{id}` will match
+ * `http://www.example.com/users/4`.
+ * * The `.*` wildcard can be used to match 0 or more characters.
+ *
+ * @param navDeepLink the NavDeepLink to be added to this destination
+ */
+ @Suppress("BuilderSetStyle")
+ public fun deepLink(navDeepLink: NavDeepLink) {
+ deepLinks.add(navDeepLink)
+ }
+
private var actions = mutableMapOf<Int, NavAction>()
/**
@@ -180,10 +211,19 @@
}
/**
+ * Instantiate a new instance of [D] that will be passed to [build].
+ *
+ * By default, this calls [Navigator.createDestination] on [navigator], but can
+ * be overridden to call a custom constructor, etc.
+ */
+ @Suppress("BuilderSetStyle")
+ protected open fun instantiateDestination(): D = navigator.createDestination()
+
+ /**
* Build the NavDestination by calling [Navigator.createDestination].
*/
public open fun build(): D {
- return navigator.createDestination().also { destination ->
+ return instantiateDestination().also { destination ->
destination.label = label
arguments.forEach { (name, argument) ->
destination.addArgument(name, argument)
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/serialization/RouteDecoder.kt b/navigation/navigation-common/src/main/java/androidx/navigation/serialization/RouteDecoder.kt
new file mode 100644
index 0000000..3eb4a47
--- /dev/null
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/serialization/RouteDecoder.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.navigation.serialization
+
+import android.os.Bundle
+import androidx.navigation.NavType
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.AbstractDecoder
+import kotlinx.serialization.encoding.CompositeDecoder
+import kotlinx.serialization.modules.EmptySerializersModule
+import kotlinx.serialization.modules.SerializersModule
+
+@OptIn(ExperimentalSerializationApi::class)
+internal class RouteDecoder(
+ bundle: Bundle,
+ typeMap: Map<String, NavType<*>>
+) : AbstractDecoder() {
+
+ private val decoder = Decoder(bundle, typeMap)
+
+ @Suppress("DEPRECATION") // deprecated in 1.6.3
+ override val serializersModule: SerializersModule = EmptySerializersModule
+
+ override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
+ return decoder.incrementElement(descriptor)
+ }
+
+ override fun decodeValue(): Any = decoder.decodeValue()
+
+ override fun decodeNull(): Nothing? = null
+
+ // we want to know if it is not null, so its !isNull
+ override fun decodeNotNullMark(): Boolean = !decoder.isCurrentElementNull()
+
+ // value from decodeValue() rather than decodeInt, decodeBoolean etc.. needs to be casted
+ @Suppress("UNCHECKED_CAST")
+ override fun <T> decodeSerializableElement(
+ descriptor: SerialDescriptor,
+ index: Int,
+ deserializer: DeserializationStrategy<T>,
+ previousValue: T?
+ ): T = decoder.decodeValue() as T
+}
+
+private class Decoder(
+ private val bundle: Bundle,
+ private val typeMap: Map<String, NavType<*>>
+) {
+ private var elementIndex: Int = -1
+ private var elementName: String = ""
+
+ @OptIn(ExperimentalSerializationApi::class)
+ fun incrementElement(descriptor: SerialDescriptor): Int {
+ if (++elementIndex >= descriptor.elementsCount) return CompositeDecoder.DECODE_DONE
+ elementName = descriptor.getElementName(elementIndex)
+ return elementIndex
+ }
+
+ fun decodeValue(): Any {
+ val navType = typeMap[elementName]
+ val arg = navType?.get(bundle, elementName)
+ checkNotNull(arg) {
+ "Unexpected null value for non-nullable argument $elementName"
+ }
+ return arg
+ }
+
+ fun isCurrentElementNull(): Boolean {
+ val navType = typeMap[elementName]
+ return navType?.isNullableAllowed == true && navType[bundle, elementName] == null
+ }
+}
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/serialization/RouteDeserializer.kt b/navigation/navigation-common/src/main/java/androidx/navigation/serialization/RouteDeserializer.kt
new file mode 100644
index 0000000..96b8b88
--- /dev/null
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/serialization/RouteDeserializer.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.navigation.serialization
+
+import android.os.Bundle
+import androidx.annotation.RestrictTo
+import androidx.navigation.NavType
+import kotlinx.serialization.KSerializer
+
+// public due to reified toRoute()
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public fun <T> KSerializer<T>.decodeArguments(
+ bundle: Bundle,
+ typeMap: Map<String, NavType<*>>
+): T = RouteDecoder(bundle, typeMap).decodeSerializableValue(this)
diff --git a/navigation/navigation-compose/api/current.txt b/navigation/navigation-compose/api/current.txt
index fb10176..2fcf879 100644
--- a/navigation/navigation-compose/api/current.txt
+++ b/navigation/navigation-compose/api/current.txt
@@ -15,6 +15,28 @@
ctor public ComposeNavigator.Destination(androidx.navigation.compose.ComposeNavigator navigator, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
}
+ @androidx.navigation.NavDestinationDsl public final class ComposeNavigatorDestinationBuilder extends androidx.navigation.NavDestinationBuilder<androidx.navigation.compose.ComposeNavigator.Destination> {
+ ctor public ComposeNavigatorDestinationBuilder(androidx.navigation.compose.ComposeNavigator navigator, String route, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
+ ctor @SuppressCompatibility @androidx.navigation.ExperimentalSafeArgsApi public ComposeNavigatorDestinationBuilder(androidx.navigation.compose.ComposeNavigator navigator, kotlin.reflect.KClass<?> route, java.util.Map<kotlin.reflect.KType,androidx.navigation.NavType<?>> typeMap, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
+ method public androidx.navigation.compose.ComposeNavigator.Destination build();
+ method public kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? getEnterTransition();
+ method public kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? getExitTransition();
+ method public kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? getPopEnterTransition();
+ method public kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? getPopExitTransition();
+ method public kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.SizeTransform?>? getSizeTransform();
+ method protected androidx.navigation.compose.ComposeNavigator.Destination instantiateDestination();
+ method public void setEnterTransition(kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>?);
+ method public void setExitTransition(kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>?);
+ method public void setPopEnterTransition(kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>?);
+ method public void setPopExitTransition(kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>?);
+ method public void setSizeTransform(kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.SizeTransform?>?);
+ property public final kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? enterTransition;
+ property public final kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? exitTransition;
+ property public final kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? popEnterTransition;
+ property public final kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? popExitTransition;
+ property public final kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.SizeTransform?>? sizeTransform;
+ }
+
public final class DialogHostKt {
method @androidx.compose.runtime.Composable public static void DialogHost(androidx.navigation.compose.DialogNavigator dialogNavigator);
}
@@ -28,6 +50,11 @@
ctor public DialogNavigator.Destination(androidx.navigation.compose.DialogNavigator navigator, optional androidx.compose.ui.window.DialogProperties dialogProperties, kotlin.jvm.functions.Function1<? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
}
+ @androidx.navigation.NavDestinationDsl public final class DialogNavigatorDestinationBuilder extends androidx.navigation.NavDestinationBuilder<androidx.navigation.compose.DialogNavigator.Destination> {
+ ctor public DialogNavigatorDestinationBuilder(androidx.navigation.compose.DialogNavigator navigator, String route, androidx.compose.ui.window.DialogProperties dialogProperties, kotlin.jvm.functions.Function1<? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
+ method protected androidx.navigation.compose.DialogNavigator.Destination instantiateDestination();
+ }
+
public final class NavBackStackEntryProviderKt {
method @androidx.compose.runtime.Composable public static void LocalOwnersProvider(androidx.navigation.NavBackStackEntry, androidx.compose.runtime.saveable.SaveableStateHolder saveableStateHolder, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
@@ -36,6 +63,7 @@
method @Deprecated public static void composable(androidx.navigation.NavGraphBuilder, String route, optional java.util.List<androidx.navigation.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.EnterTransition?>? enterTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.ExitTransition?>? exitTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.EnterTransition?>? popEnterTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.ExitTransition?>? popExitTransition, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
method @Deprecated public static void composable(androidx.navigation.NavGraphBuilder, String route, optional java.util.List<androidx.navigation.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, kotlin.jvm.functions.Function1<? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
method public static void composable(androidx.navigation.NavGraphBuilder, String route, optional java.util.List<androidx.navigation.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, optional kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? enterTransition, optional kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? exitTransition, optional kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? popEnterTransition, optional kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? popExitTransition, optional kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.SizeTransform?>? sizeTransform, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.navigation.ExperimentalSafeArgsApi public static inline <reified T> void composable(androidx.navigation.NavGraphBuilder, optional java.util.Map<kotlin.reflect.KType,androidx.navigation.NavType<?>> typeMap, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? enterTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? exitTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? popEnterTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? popExitTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.SizeTransform?>? sizeTransform, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
method public static void dialog(androidx.navigation.NavGraphBuilder, String route, optional java.util.List<androidx.navigation.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, optional androidx.compose.ui.window.DialogProperties dialogProperties, kotlin.jvm.functions.Function1<? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
method @Deprecated public static void navigation(androidx.navigation.NavGraphBuilder, String startDestination, String route, optional java.util.List<androidx.navigation.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.EnterTransition?>? enterTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.ExitTransition?>? exitTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.EnterTransition?>? popEnterTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.ExitTransition?>? popExitTransition, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
method @Deprecated public static void navigation(androidx.navigation.NavGraphBuilder, String startDestination, String route, optional java.util.List<androidx.navigation.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
diff --git a/navigation/navigation-compose/api/restricted_current.txt b/navigation/navigation-compose/api/restricted_current.txt
index fb10176..2fcf879 100644
--- a/navigation/navigation-compose/api/restricted_current.txt
+++ b/navigation/navigation-compose/api/restricted_current.txt
@@ -15,6 +15,28 @@
ctor public ComposeNavigator.Destination(androidx.navigation.compose.ComposeNavigator navigator, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
}
+ @androidx.navigation.NavDestinationDsl public final class ComposeNavigatorDestinationBuilder extends androidx.navigation.NavDestinationBuilder<androidx.navigation.compose.ComposeNavigator.Destination> {
+ ctor public ComposeNavigatorDestinationBuilder(androidx.navigation.compose.ComposeNavigator navigator, String route, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
+ ctor @SuppressCompatibility @androidx.navigation.ExperimentalSafeArgsApi public ComposeNavigatorDestinationBuilder(androidx.navigation.compose.ComposeNavigator navigator, kotlin.reflect.KClass<?> route, java.util.Map<kotlin.reflect.KType,androidx.navigation.NavType<?>> typeMap, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
+ method public androidx.navigation.compose.ComposeNavigator.Destination build();
+ method public kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? getEnterTransition();
+ method public kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? getExitTransition();
+ method public kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? getPopEnterTransition();
+ method public kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? getPopExitTransition();
+ method public kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.SizeTransform?>? getSizeTransform();
+ method protected androidx.navigation.compose.ComposeNavigator.Destination instantiateDestination();
+ method public void setEnterTransition(kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>?);
+ method public void setExitTransition(kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>?);
+ method public void setPopEnterTransition(kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>?);
+ method public void setPopExitTransition(kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>?);
+ method public void setSizeTransform(kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.SizeTransform?>?);
+ property public final kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? enterTransition;
+ property public final kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? exitTransition;
+ property public final kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? popEnterTransition;
+ property public final kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? popExitTransition;
+ property public final kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.SizeTransform?>? sizeTransform;
+ }
+
public final class DialogHostKt {
method @androidx.compose.runtime.Composable public static void DialogHost(androidx.navigation.compose.DialogNavigator dialogNavigator);
}
@@ -28,6 +50,11 @@
ctor public DialogNavigator.Destination(androidx.navigation.compose.DialogNavigator navigator, optional androidx.compose.ui.window.DialogProperties dialogProperties, kotlin.jvm.functions.Function1<? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
}
+ @androidx.navigation.NavDestinationDsl public final class DialogNavigatorDestinationBuilder extends androidx.navigation.NavDestinationBuilder<androidx.navigation.compose.DialogNavigator.Destination> {
+ ctor public DialogNavigatorDestinationBuilder(androidx.navigation.compose.DialogNavigator navigator, String route, androidx.compose.ui.window.DialogProperties dialogProperties, kotlin.jvm.functions.Function1<? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
+ method protected androidx.navigation.compose.DialogNavigator.Destination instantiateDestination();
+ }
+
public final class NavBackStackEntryProviderKt {
method @androidx.compose.runtime.Composable public static void LocalOwnersProvider(androidx.navigation.NavBackStackEntry, androidx.compose.runtime.saveable.SaveableStateHolder saveableStateHolder, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
@@ -36,6 +63,7 @@
method @Deprecated public static void composable(androidx.navigation.NavGraphBuilder, String route, optional java.util.List<androidx.navigation.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.EnterTransition?>? enterTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.ExitTransition?>? exitTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.EnterTransition?>? popEnterTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.ExitTransition?>? popExitTransition, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
method @Deprecated public static void composable(androidx.navigation.NavGraphBuilder, String route, optional java.util.List<androidx.navigation.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, kotlin.jvm.functions.Function1<? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
method public static void composable(androidx.navigation.NavGraphBuilder, String route, optional java.util.List<androidx.navigation.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, optional kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? enterTransition, optional kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? exitTransition, optional kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? popEnterTransition, optional kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? popExitTransition, optional kotlin.jvm.functions.Function1<androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.SizeTransform?>? sizeTransform, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.navigation.ExperimentalSafeArgsApi public static inline <reified T> void composable(androidx.navigation.NavGraphBuilder, optional java.util.Map<kotlin.reflect.KType,androidx.navigation.NavType<?>> typeMap, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? enterTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? exitTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.EnterTransition?>? popEnterTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.ExitTransition?>? popExitTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,androidx.compose.animation.SizeTransform?>? sizeTransform, kotlin.jvm.functions.Function2<? super androidx.compose.animation.AnimatedContentScope,? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
method public static void dialog(androidx.navigation.NavGraphBuilder, String route, optional java.util.List<androidx.navigation.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, optional androidx.compose.ui.window.DialogProperties dialogProperties, kotlin.jvm.functions.Function1<? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
method @Deprecated public static void navigation(androidx.navigation.NavGraphBuilder, String startDestination, String route, optional java.util.List<androidx.navigation.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.EnterTransition?>? enterTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.ExitTransition?>? exitTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.EnterTransition?>? popEnterTransition, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<androidx.navigation.NavBackStackEntry>,? extends androidx.compose.animation.ExitTransition?>? popExitTransition, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
method @Deprecated public static void navigation(androidx.navigation.NavGraphBuilder, String startDestination, String route, optional java.util.List<androidx.navigation.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index fb1c6ba..248cbbf 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -21,6 +21,7 @@
id("com.android.library")
id("AndroidXComposePlugin")
id("org.jetbrains.kotlin.android")
+ alias(libs.plugins.kotlinSerialization)
}
dependencies {
@@ -34,6 +35,7 @@
api(project(":compose:ui:ui"))
api("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
api(projectOrArtifact(":navigation:navigation-runtime-ktx"))
+ implementation(libs.kotlinSerializationCore)
androidTestImplementation(projectOrArtifact(":compose:material:material"))
androidTestImplementation project(":compose:test-utils")
diff --git a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavGraphBuilderTest.kt b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavGraphBuilderTest.kt
index 2f5ba2f..e151bc3 100644
--- a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavGraphBuilderTest.kt
+++ b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavGraphBuilderTest.kt
@@ -17,11 +17,16 @@
package androidx.navigation.compose
import android.net.Uri
+import android.os.Bundle
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.core.net.toUri
+import androidx.navigation.ExperimentalSafeArgsApi
import androidx.navigation.NavDeepLinkRequest
+import androidx.navigation.NavGraph
+import androidx.navigation.NavType
import androidx.navigation.contains
+import androidx.navigation.get
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink
import androidx.navigation.navigation
@@ -30,7 +35,9 @@
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import kotlin.reflect.typeOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.serialization.Serializable
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -212,8 +219,142 @@
).isTrue()
}
}
+
+ @OptIn(ExperimentalSafeArgsApi::class)
+ @Test
+ fun testComposableKClass() {
+ lateinit var navController: TestNavHostController
+ composeTestRule.setContent {
+ navController = TestNavHostController(LocalContext.current)
+ navController.navigatorProvider.addNavigator(ComposeNavigator())
+
+ NavHost(navController, startDestination = firstRoute) {
+ composable(firstRoute) { }
+ composable<TestClass> { }
+ }
+ }
+ composeTestRule.runOnIdle {
+ assertThat(firstRoute in navController.graph).isTrue()
+ assertThat(TestClass::class in navController.graph).isTrue()
+ assertThat(navController.graph[TestClass::class].route).isEqualTo(TEST_CLASS_ROUTE)
+ }
+ }
+
+ @OptIn(ExperimentalSafeArgsApi::class)
+ @Test
+ fun testComposableKClassArgs() {
+ lateinit var navController: TestNavHostController
+ composeTestRule.setContent {
+ navController = TestNavHostController(LocalContext.current)
+ navController.navigatorProvider.addNavigator(ComposeNavigator())
+
+ NavHost(navController, startDestination = firstRoute) {
+ composable(firstRoute) { }
+ composable<TestClassArg> { }
+ }
+ }
+ composeTestRule.runOnIdle {
+ assertThat(TestClassArg::class in navController.graph).isTrue()
+ val dest = navController.graph[TestClassArg::class]
+ assertThat(dest.route).isEqualTo(TEST_CLASS_ARG_ROUTE)
+ assertThat(dest.arguments["arg"]).isNotNull()
+ }
+ }
+
+ @OptIn(ExperimentalSafeArgsApi::class)
+ @Test
+ fun testComposableKClassArgsCustomType() {
+ @Serializable
+ class TestClass(val arg: CustomType)
+
+ lateinit var navController: TestNavHostController
+ composeTestRule.setContent {
+ navController = TestNavHostController(LocalContext.current)
+ navController.navigatorProvider.addNavigator(ComposeNavigator())
+
+ NavHost(navController, startDestination = firstRoute) {
+ composable(firstRoute) { }
+ composable<TestClass>(typeMap = mapOf(typeOf<CustomType>() to customNavType)) { }
+ }
+ }
+ composeTestRule.runOnIdle {
+ val dest = navController.graph[TestClass::class]
+ assertThat(dest.arguments["arg"]).isNotNull()
+ assertThat(dest.arguments["arg"]!!.type).isEqualTo(customNavType)
+ }
+ }
+
+ @OptIn(ExperimentalSafeArgsApi::class)
+ @Test
+ fun testNestedComposableKClassArgs() {
+ lateinit var navController: TestNavHostController
+ composeTestRule.setContent {
+ navController = TestNavHostController(LocalContext.current)
+ navController.navigatorProvider.addNavigator(ComposeNavigator())
+
+ NavHost(navController, startDestination = firstRoute) {
+ composable(firstRoute) { }
+ navigation(startDestination = TEST_CLASS_ARG_ROUTE, route = secondRoute,
+ ) {
+ composable<TestClassArg> {}
+ }
+ }
+ }
+ composeTestRule.runOnIdle {
+ val nestedGraph = navController.graph[secondRoute] as NavGraph
+ val dest = nestedGraph.findNode<TestClassArg>()
+ assertThat(dest).isNotNull()
+ assertThat(dest!!.route).isEqualTo(TEST_CLASS_ARG_ROUTE)
+ assertThat(dest.arguments["arg"]).isNotNull()
+ }
+ }
+
+ @OptIn(ExperimentalSafeArgsApi::class)
+ @Test
+ fun testComposableKClassArgsMissingCustomType() {
+ @Serializable
+ class TestClass(val arg: CustomType)
+
+ lateinit var exception: String
+ lateinit var navController: TestNavHostController
+ try {
+ composeTestRule.setContent {
+ navController = TestNavHostController(LocalContext.current)
+ navController.navigatorProvider.addNavigator(ComposeNavigator())
+
+ NavHost(navController, startDestination = firstRoute) {
+ composable(firstRoute) { }
+ composable<TestClass> { }
+ }
+ }
+ } catch (e: IllegalArgumentException) {
+ exception = e.message!!
+ }
+ assertThat(exception).isEqualTo(
+ "Cannot cast arg of type androidx.navigation.compose.CustomType to a " +
+ "NavType. Make sure to provide custom NavType for this argument."
+ )
+ }
}
private const val firstRoute = "first"
private const val secondRoute = "second"
private const val thirdRoute = "third"
+internal const val TEST_CLASS_ROUTE = "androidx.navigation.compose.TestClass"
+internal const val TEST_CLASS_ARG_ROUTE = "androidx.navigation.compose.TestClassArg/{arg}"
+
+@Serializable
+internal class TestClass
+
+@Serializable
+internal class TestClassArg(val arg: Int)
+
+@Serializable
+internal class CustomType
+
+internal val customNavType = object : NavType<CustomType>(false) {
+ override fun put(bundle: Bundle, key: String, value: CustomType) { }
+ override fun get(bundle: Bundle, key: String): CustomType? = null
+ override fun parseValue(value: String): CustomType = CustomType()
+ override fun serializeAsValue(value: CustomType) = "customValue"
+}
diff --git a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostControllerTest.kt b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostControllerTest.kt
index e1418cf..edee3fc 100644
--- a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostControllerTest.kt
+++ b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostControllerTest.kt
@@ -21,18 +21,21 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.navigation.ExperimentalSafeArgsApi
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.NoOpNavigator
import androidx.navigation.createGraph
import androidx.navigation.get
+import androidx.navigation.toRoute
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.testutils.TestNavigator
import androidx.testutils.test
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.serialization.Serializable
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -281,6 +284,112 @@
}
}
+ @OptIn(ExperimentalSafeArgsApi::class)
+ @Test
+ fun testNavigateKClass() {
+ lateinit var navController: NavHostController
+ composeTestRule.setContent {
+ navController = rememberNavController()
+
+ NavHost(navController, startDestination = "first") {
+ composable("first") { }
+ composable<TestClass> { }
+ }
+ }
+
+ composeTestRule.runOnUiThread {
+ navController.navigate(TestClass()) {}
+ }
+ composeTestRule.runOnIdle {
+ assertThat(navController.currentDestination?.route).isEqualTo(TEST_CLASS_ROUTE)
+ }
+ }
+
+ @OptIn(ExperimentalSafeArgsApi::class)
+ @Test
+ fun testNavigateKClassArgs() {
+ lateinit var args: TestClassArg
+ lateinit var navController: NavHostController
+ composeTestRule.setContent {
+ navController = rememberNavController()
+
+ NavHost(navController, startDestination = "first") {
+ composable("first") { }
+ composable<TestClassArg> {
+ args = it.toRoute<TestClassArg>()
+ }
+ }
+ }
+ composeTestRule.runOnUiThread {
+ navController.navigate(TestClassArg(0)) {}
+ }
+ composeTestRule.runOnIdle {
+ assertThat(navController.currentDestination?.route).isEqualTo(TEST_CLASS_ARG_ROUTE)
+ assertThat(args.arg).isEqualTo(0)
+ }
+ }
+
+ @OptIn(ExperimentalSafeArgsApi::class)
+ @Test
+ fun testNavigateKClassMultipleArgs() {
+ @Serializable
+ class TestClass(val arg: Int, val arg2: Boolean)
+
+ lateinit var args: TestClass
+ lateinit var navController: NavHostController
+ composeTestRule.setContent {
+ navController = rememberNavController()
+
+ NavHost(navController, startDestination = "first") {
+ composable("first") { }
+ composable<TestClass> {
+ args = it.toRoute<TestClass>()
+ }
+ }
+ }
+ composeTestRule.runOnUiThread {
+ navController.navigate(TestClass(0, false)) {}
+ }
+ composeTestRule.runOnIdle {
+ assertThat(navController.currentDestination?.route).isEqualTo(
+ "androidx.navigation.compose.NavHostControllerTest." +
+ "testNavigateKClassMultipleArgs.TestClass/{arg}/{arg2}"
+ )
+ assertThat(args.arg).isEqualTo(0)
+ assertThat(args.arg2).isEqualTo(false)
+ }
+ }
+
+ @OptIn(ExperimentalSafeArgsApi::class)
+ @Test
+ fun testNavigateKClassArgsNullValue() {
+ @Serializable
+ class TestClass(val arg: String?)
+
+ lateinit var args: TestClass
+ lateinit var navController: NavHostController
+ composeTestRule.setContent {
+ navController = rememberNavController()
+
+ NavHost(navController, startDestination = "first") {
+ composable("first") { }
+ composable<TestClass> {
+ args = it.toRoute<TestClass>()
+ }
+ }
+ }
+ composeTestRule.runOnUiThread {
+ navController.navigate(TestClass(null)) {}
+ }
+ composeTestRule.runOnIdle {
+ assertThat(navController.currentDestination?.route).isEqualTo(
+ "androidx.navigation.compose.NavHostControllerTest." +
+ "testNavigateKClassArgsNullValue.TestClass/{arg}"
+ )
+ assertThat(args.arg).isNull()
+ }
+ }
+
@Test
fun testGetBackStackEntry() {
lateinit var navController: NavController
@@ -337,6 +446,27 @@
)
}
}
+
+ @OptIn(ExperimentalSafeArgsApi::class)
+ @Test
+ fun testGetBackStackEntryKClass() {
+ lateinit var navController: NavHostController
+ composeTestRule.setContent {
+ navController = rememberNavController()
+
+ NavHost(navController, startDestination = "first") {
+ composable("first") { }
+ composable<TestClass> { }
+ }
+ }
+ composeTestRule.runOnUiThread {
+ navController.navigate(TestClass()) {}
+ }
+ composeTestRule.runOnIdle {
+ assertThat(navController.getBackStackEntry<TestClass>().destination.route)
+ .isEqualTo(TEST_CLASS_ROUTE)
+ }
+ }
}
private const val FIRST_DESTINATION = "first"
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigatorDestinationBuilder.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigatorDestinationBuilder.kt
new file mode 100644
index 0000000..e193190d
--- /dev/null
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigatorDestinationBuilder.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:SuppressLint("NullAnnotationGroup") // b/331484152
+
+package androidx.navigation.compose
+
+import android.annotation.SuppressLint
+import androidx.compose.animation.AnimatedContentScope
+import androidx.compose.animation.AnimatedContentTransitionScope
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.SizeTransform
+import androidx.compose.runtime.Composable
+import androidx.navigation.ExperimentalSafeArgsApi
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavDestinationBuilder
+import androidx.navigation.NavDestinationDsl
+import androidx.navigation.NavType
+import kotlin.reflect.KClass
+import kotlin.reflect.KType
+
+/**
+ * DSL for constructing a new [ComposeNavigator.Destination]
+ */
+@NavDestinationDsl
+public class ComposeNavigatorDestinationBuilder :
+ NavDestinationBuilder<ComposeNavigator.Destination> {
+
+ private val composeNavigator: ComposeNavigator
+ private val content: @Composable (AnimatedContentScope.(NavBackStackEntry) -> Unit)
+
+ var enterTransition: (@JvmSuppressWildcards
+ AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = null
+
+ var exitTransition: (@JvmSuppressWildcards
+ AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = null
+
+ var popEnterTransition: (@JvmSuppressWildcards
+ AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = null
+
+ var popExitTransition: (@JvmSuppressWildcards
+ AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = null
+
+ var sizeTransform: (@JvmSuppressWildcards
+ AnimatedContentTransitionScope<NavBackStackEntry>.() -> SizeTransform?)? = null
+
+ /**
+ * DSL for constructing a new [ComposeNavigator.Destination]
+ *
+ * @param navigator navigator used to create the destination
+ * @param route the destination's unique route
+ * @param content composable for the destination
+ */
+ public constructor(
+ navigator: ComposeNavigator,
+ route: String,
+ content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
+ ) : super(navigator, route) {
+ this.composeNavigator = navigator
+ this.content = content
+ }
+
+ /**
+ * DSL for constructing a new [ComposeNavigator.Destination]
+ *
+ * @param navigator navigator used to create the destination
+ * @param route the destination's unique route from a [KClass]
+ * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom
+ * [NavType]. Required only when destination contains custom NavTypes.
+ * @param content composable for the destination
+ */
+ @ExperimentalSafeArgsApi
+ public constructor(
+ navigator: ComposeNavigator,
+ route: KClass<*>,
+ typeMap: Map<KType, @JvmSuppressWildcards NavType<*>>,
+ content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
+ ) : super(navigator, route, typeMap) {
+ this.composeNavigator = navigator
+ this.content = content
+ }
+
+ override fun instantiateDestination(): ComposeNavigator.Destination {
+ return ComposeNavigator.Destination(composeNavigator, content)
+ }
+
+ override fun build(): ComposeNavigator.Destination {
+ return super.build().also { destination ->
+ destination.enterTransition = enterTransition
+ destination.exitTransition = exitTransition
+ destination.popEnterTransition = popEnterTransition
+ destination.popExitTransition = popExitTransition
+ destination.sizeTransform = sizeTransform
+ }
+ }
+}
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigatorDestinationBuilder.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigatorDestinationBuilder.kt
new file mode 100644
index 0000000..d98e377
--- /dev/null
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigatorDestinationBuilder.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.navigation.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.window.DialogProperties
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavDestinationBuilder
+import androidx.navigation.NavDestinationDsl
+
+/**
+ * DSL for constructing a new [DialogNavigator.Destination]
+ */
+@NavDestinationDsl
+public class DialogNavigatorDestinationBuilder :
+ NavDestinationBuilder<DialogNavigator.Destination> {
+
+ private val dialogNavigator: DialogNavigator
+ private val dialogProperties: DialogProperties
+ private val content: @Composable (NavBackStackEntry) -> Unit
+
+ /**
+ * DSL for constructing a new [DialogNavigator.Destination]
+ *
+ * @param navigator navigator used to create the destination
+ * @param route the destination's unique route
+ * @param dialogProperties properties that should be passed to
+ * [androidx.compose.ui.window.Dialog].
+ * @param content composable for the destination
+ */
+ public constructor(
+ navigator: DialogNavigator,
+ route: String,
+ dialogProperties: DialogProperties,
+ content: @Composable (NavBackStackEntry) -> Unit
+ ) : super(navigator, route) {
+ this.dialogNavigator = navigator
+ this.dialogProperties = dialogProperties
+ this.content = content
+ }
+
+ override fun instantiateDestination(): DialogNavigator.Destination {
+ return DialogNavigator.Destination(dialogNavigator, dialogProperties, content)
+ }
+}
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavGraphBuilder.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavGraphBuilder.kt
index cf5a320..2effc95 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavGraphBuilder.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavGraphBuilder.kt
@@ -14,8 +14,11 @@
* limitations under the License.
*/
+@file:SuppressLint("NullAnnotationGroup") // b/331484152
+
package androidx.navigation.compose
+import android.annotation.SuppressLint
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
@@ -23,12 +26,15 @@
import androidx.compose.animation.SizeTransform
import androidx.compose.runtime.Composable
import androidx.compose.ui.window.DialogProperties
+import androidx.navigation.ExperimentalSafeArgsApi
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDeepLink
import androidx.navigation.NavGraph
import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavType
import androidx.navigation.get
+import kotlin.reflect.KType
/**
* Add the [Composable] to the [NavGraphBuilder]
@@ -96,17 +102,17 @@
exitTransition,
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
) {
- addDestination(
- ComposeNavigator.Destination(
+ destination(
+ ComposeNavigatorDestinationBuilder(
provider[ComposeNavigator::class],
+ route,
content
).apply {
- this.route = route
arguments.forEach { (argumentName, argument) ->
- addArgument(argumentName, argument)
+ argument(argumentName, argument)
}
deepLinks.forEach { deepLink ->
- addDeepLink(deepLink)
+ deepLink(deepLink)
}
this.enterTransition = enterTransition
this.exitTransition = exitTransition
@@ -147,17 +153,66 @@
AnimatedContentTransitionScope<NavBackStackEntry>.() -> SizeTransform?)? = null,
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
) {
- addDestination(
- ComposeNavigator.Destination(
+ destination(
+ ComposeNavigatorDestinationBuilder(
provider[ComposeNavigator::class],
+ route,
content
).apply {
- this.route = route
arguments.forEach { (argumentName, argument) ->
- addArgument(argumentName, argument)
+ argument(argumentName, argument)
}
deepLinks.forEach { deepLink ->
- addDeepLink(deepLink)
+ deepLink(deepLink)
+ }
+ this.enterTransition = enterTransition
+ this.exitTransition = exitTransition
+ this.popEnterTransition = popEnterTransition
+ this.popExitTransition = popExitTransition
+ this.sizeTransform = sizeTransform
+ }
+ )
+}
+
+/**
+ * Add the [Composable] to the [NavGraphBuilder]
+ *
+ * @param T route from a [KClass] for the destination
+ * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom
+ * [NavType]. Required only when [T] contains custom NavTypes.
+ * @param deepLinks list of deep links to associate with the destinations
+ * @param enterTransition callback to determine the destination's enter transition
+ * @param exitTransition callback to determine the destination's exit transition
+ * @param popEnterTransition callback to determine the destination's popEnter transition
+ * @param popExitTransition callback to determine the destination's popExit transition
+ * @param sizeTransform callback to determine the destination's sizeTransform.
+ * @param content composable for the destination
+ */
+@ExperimentalSafeArgsApi
+public inline fun <reified T : Any> NavGraphBuilder.composable(
+ typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap(),
+ deepLinks: List<NavDeepLink> = emptyList(),
+ noinline enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() ->
+ @JvmSuppressWildcards EnterTransition?)? = null,
+ noinline exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() ->
+ @JvmSuppressWildcards ExitTransition?)? = null,
+ noinline popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() ->
+ @JvmSuppressWildcards EnterTransition?)? = enterTransition,
+ noinline popExitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() ->
+ @JvmSuppressWildcards ExitTransition?)? = exitTransition,
+ noinline sizeTransform: (AnimatedContentTransitionScope<NavBackStackEntry>.() ->
+ @JvmSuppressWildcards SizeTransform?)? = null,
+ noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
+) {
+ destination(
+ ComposeNavigatorDestinationBuilder(
+ provider[ComposeNavigator::class],
+ T::class,
+ typeMap,
+ content
+ ).apply {
+ deepLinks.forEach { deepLink ->
+ deepLink(deepLink)
}
this.enterTransition = enterTransition
this.exitTransition = exitTransition
@@ -313,18 +368,18 @@
dialogProperties: DialogProperties = DialogProperties(),
content: @Composable (NavBackStackEntry) -> Unit
) {
- addDestination(
- DialogNavigator.Destination(
+ destination(
+ DialogNavigatorDestinationBuilder(
provider[DialogNavigator::class],
+ route,
dialogProperties,
content
).apply {
- this.route = route
arguments.forEach { (argumentName, argument) ->
- addArgument(argumentName, argument)
+ argument(argumentName, argument)
}
deepLinks.forEach { deepLink ->
- addDeepLink(deepLink)
+ deepLink(deepLink)
}
}
)
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
index 1cfaaf2..4d35f67 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
@@ -154,11 +154,12 @@
fragment<EmptyFragment>("first")
fragment<EmptyFragment, TestClass>()
}
- navController.navigate(TestClass())
-
val fm = supportFragmentManager.findFragmentById(R.id.nav_host)?.childFragmentManager
fm?.executePendingTransactions()
+ navController.navigate(TestClass())
+ fm?.executePendingTransactions()
+
assertThat(navController.currentBackStackEntry?.destination?.route)
.isEqualTo(TEST_CLASS_ROUTE)
assertThat(navController.visibleEntries.value).containsExactly(
@@ -172,11 +173,12 @@
fragment<EmptyFragment>("first")
fragment<EmptyFragment, TestClassArg>()
}
- navController.navigate(TestClassArg(15))
-
val fm = supportFragmentManager.findFragmentById(R.id.nav_host)?.childFragmentManager
fm?.executePendingTransactions()
+ navController.navigate(TestClassArg(15))
+ fm?.executePendingTransactions()
+
assertThat(navController.currentBackStackEntry?.destination?.route)
.isEqualTo(TEST_CLASS_ARG_ROUTE)
assertThat(navController.visibleEntries.value).containsExactly(
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
index 37f31e0..28fe0f8 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerRouteTest.kt
@@ -288,6 +288,30 @@
@UiThreadTest
@Test
+ fun testNestedStartDestinationKClass() {
+ @Serializable
+ class NestedGraph
+
+ @Serializable
+ @SerialName("test")
+ class TestClass
+
+ val navController = createNavController()
+ navController.graph = navController.createGraph(
+ startDestination = NestedGraph::class
+ ) {
+ navigation(route = NestedGraph::class, startDestination = TestClass::class) {
+ test(route = TestClass::class)
+ }
+ }
+ assertThat(navController.currentDestination?.route).isEqualTo("test")
+ assertThat(navController.currentDestination?.id).isEqualTo(
+ serializer<TestClass>().hashCode()
+ )
+ }
+
+ @UiThreadTest
+ @Test
fun testStartDestinationObject() {
@Serializable
@SerialName("test")
@@ -429,6 +453,41 @@
@UiThreadTest
@Test
+ fun testNestedStartDestinationObjectWithPathArg() {
+ @Serializable
+ @SerialName("graph")
+ class NestedGraph(val nestedArg: Int)
+
+ @Serializable
+ @SerialName("test")
+ class TestClass(val arg: Int)
+
+ val navController = createNavController()
+ navController.graph = navController.createGraph(
+ startDestination = NestedGraph(0)
+ ) {
+ navigation(route = NestedGraph::class, startDestination = TestClass(1)) {
+ test(route = TestClass::class)
+ }
+ }
+ assertThat(navController.currentDestination?.route).isEqualTo(
+ "test/{arg}"
+ )
+ assertThat(navController.currentDestination?.id).isEqualTo(
+ serializer<TestClass>().hashCode()
+ )
+
+ val nestedArg = navController.currentBackStackEntry?.arguments?.getInt("nestedArg")
+ assertThat(nestedArg).isNotNull()
+ assertThat(nestedArg).isEqualTo(0)
+
+ val arg = navController.currentBackStackEntry?.arguments?.getInt("arg")
+ assertThat(arg).isNotNull()
+ assertThat(arg).isEqualTo(1)
+ }
+
+ @UiThreadTest
+ @Test
fun testNestedStartDestinationWithPathArg() {
val navController = createNavController()
navController.graph = navController.createGraph(
diff --git a/navigation/navigation-safe-args-gradle-plugin/lint-baseline.xml b/navigation/navigation-safe-args-gradle-plugin/lint-baseline.xml
index 1cfa523..ac0c4e5 100644
--- a/navigation/navigation-safe-args-gradle-plugin/lint-baseline.xml
+++ b/navigation/navigation-safe-args-gradle-plugin/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.4.0-alpha09" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha09)" variant="all" version="8.4.0-alpha09">
+<issues format="6" by="lint 8.4.0-alpha12" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha12)" variant="all" version="8.4.0-alpha12">
<issue
id="BanThreadSleep"
@@ -48,7 +48,7 @@
<issue
id="EagerGradleConfiguration"
- message="Avoid using eager method get"
+ message="Avoid using method get"
errorLine1=" variant.registerJavaGeneratingTask(task, task.get().outputDir.asFile.get())"
errorLine2=" ~~~">
<location
@@ -56,6 +56,15 @@
</issue>
<issue
+ id="GradleProjectIsolation"
+ message="Use providers.gradleProperty instead of findProperty"
+ errorLine1=" (project.findProperty("android.useAndroidX") == "true").also {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt"/>
+ </issue>
+
+ <issue
id="WithPluginClasspathUsage"
message="Avoid usage of GradleRunner#withPluginClasspath, which is broken. Instead use something like https://github.com/autonomousapps/dependency-analysis-gradle-plugin/tree/main/testkit#gradle-testkit-support-plugin"
errorLine1=" .withProjectDir(projectRoot()).withPluginClasspath()"
diff --git a/pdf/OWNERS b/pdf/OWNERS
index cb76f69..81dd73b 100644
--- a/pdf/OWNERS
+++ b/pdf/OWNERS
@@ -5,3 +5,5 @@
riyathakur@google.com
dheekshitha@google.com
mayankkk@google.com
+pratyushpks@google.com
+anothermark@google.com
diff --git a/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/Dimensions.aidl b/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/Dimensions.aidl
new file mode 100644
index 0000000..1dfe734
--- /dev/null
+++ b/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/Dimensions.aidl
@@ -0,0 +1,21 @@
+///////////////////////////////////////////////////////////////////////////////
+// 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 androidx.pdf.aidl;
+@JavaOnlyStableParcelable @JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+parcelable Dimensions;
diff --git a/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/LinkRects.aidl b/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/LinkRects.aidl
new file mode 100644
index 0000000..028ecf0
--- /dev/null
+++ b/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/LinkRects.aidl
@@ -0,0 +1,21 @@
+///////////////////////////////////////////////////////////////////////////////
+// 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 androidx.pdf.aidl;
+@JavaOnlyStableParcelable @JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+parcelable LinkRects;
diff --git a/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/MatchRects.aidl b/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/MatchRects.aidl
new file mode 100644
index 0000000..aef58dd
--- /dev/null
+++ b/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/MatchRects.aidl
@@ -0,0 +1,21 @@
+///////////////////////////////////////////////////////////////////////////////
+// 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 androidx.pdf.aidl;
+@JavaOnlyStableParcelable @JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+parcelable MatchRects;
diff --git a/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/PageSelection.aidl b/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/PageSelection.aidl
new file mode 100644
index 0000000..7915e96
--- /dev/null
+++ b/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/PageSelection.aidl
@@ -0,0 +1,21 @@
+///////////////////////////////////////////////////////////////////////////////
+// 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 androidx.pdf.aidl;
+@JavaOnlyStableParcelable @JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+parcelable PageSelection;
diff --git a/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/PdfDocumentRemote.aidl b/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/PdfDocumentRemote.aidl
new file mode 100644
index 0000000..af61188
--- /dev/null
+++ b/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/PdfDocumentRemote.aidl
@@ -0,0 +1,36 @@
+///////////////////////////////////////////////////////////////////////////////
+// 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 androidx.pdf.aidl;
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+interface PdfDocumentRemote {
+ int create(in ParcelFileDescriptor pfd, String password);
+ int numPages();
+ androidx.pdf.aidl.Dimensions getPageDimensions(int pageNum);
+ boolean renderPage(int pageNum, in androidx.pdf.aidl.Dimensions size, boolean hideTextAnnots, in ParcelFileDescriptor output);
+ boolean renderTile(int pageNum, int pageWidth, int pageHeight, int left, int top, in androidx.pdf.aidl.Dimensions tileSize, boolean hideTextAnnots, in ParcelFileDescriptor output);
+ String getPageText(int pageNum);
+ List<String> getPageAltText(int pageNum);
+ androidx.pdf.aidl.MatchRects searchPageText(int pageNum, String query);
+ androidx.pdf.aidl.PageSelection selectPageText(int pageNum, in androidx.pdf.aidl.SelectionBoundary start, in androidx.pdf.aidl.SelectionBoundary stop);
+ androidx.pdf.aidl.LinkRects getPageLinks(int pageNum);
+ byte[] getPageGotoLinksByteArray(int pageNum);
+ boolean isPdfLinearized();
+ boolean cloneWithoutSecurity(in ParcelFileDescriptor destination);
+ boolean saveAs(in ParcelFileDescriptor destination);
+}
diff --git a/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/SelectionBoundary.aidl b/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/SelectionBoundary.aidl
new file mode 100644
index 0000000..8348dad
--- /dev/null
+++ b/pdf/pdf-viewer/api/aidlRelease/current/androidx/pdf/aidl/SelectionBoundary.aidl
@@ -0,0 +1,21 @@
+///////////////////////////////////////////////////////////////////////////////
+// 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 androidx.pdf.aidl;
+@JavaOnlyStableParcelable @JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+parcelable SelectionBoundary;
diff --git a/pdf/pdf-viewer/build.gradle b/pdf/pdf-viewer/build.gradle
index a6f0ecf..5bd95aa 100644
--- a/pdf/pdf-viewer/build.gradle
+++ b/pdf/pdf-viewer/build.gradle
@@ -19,15 +19,68 @@
plugins {
id("AndroidXPlugin")
id("com.android.library")
+ id("androidx.stableaidl")
+ id("kotlin-android")
+}
+
+// Needed for material dependency
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
}
dependencies {
- annotationProcessor(libs.nullaway)
- // Add dependencies here
+ api(libs.rxjava2)
+ api(libs.guavaAndroid)
+ api("com.google.android.material:material:1.6.0")
+
+ implementation(libs.kotlinStdlib)
+ implementation("androidx.exifinterface:exifinterface:1.3.2")
+
+ testImplementation(libs.junit)
+ testImplementation(libs.testCore)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.junit)
+ testImplementation(libs.mockitoCore4)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.truth)
}
android {
namespace "androidx.pdf"
+
+ defaultConfig {
+ minSdkVersion 30
+ }
+
+ buildFeatures {
+ aidl = true
+ }
+
+ buildTypes.all {
+ stableAidl {
+ version 1
+ }
+ }
+
+ externalNativeBuild {
+ cmake {
+ path file('src/main/native/CMakeLists.txt')
+ version libs.versions.cmake.get()
+ }
+ }
+ sourceSets {
+ test {
+ assets {
+ srcDirs += ["src/test/assets"]
+ }
+ resources {
+ srcDirs += ["src/test/res"]
+ }
+ }
+ }
}
androidx {
diff --git a/pdf/pdf-viewer/lint-baseline.xml b/pdf/pdf-viewer/lint-baseline.xml
new file mode 100644
index 0000000..2cc3dde
--- /dev/null
+++ b/pdf/pdf-viewer/lint-baseline.xml
@@ -0,0 +1,5395 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha12" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha12)" variant="all" version="8.4.0-alpha12">
+
+ <issue
+ id="WrongCall"
+ message="Suspicious method call; should probably call "`layout`" rather than "`onLayout`""
+ errorLine1=" onLayout(false, getLeft(), getTop(), getRight(), getBottom());"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginatedView.java"/>
+ </issue>
+
+ <issue
+ id="BanConcurrentHashMap"
+ message="Detected ConcurrentHashMap usage."
+ errorLine1="import java.util.concurrent.ConcurrentHashMap;"
+ errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/DiskCache.java"/>
+ </issue>
+
+ <issue
+ id="BanConcurrentHashMap"
+ message="Detected ConcurrentHashMap usage."
+ errorLine1="import java.util.concurrent.ConcurrentHashMap;"
+ errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java"/>
+ </issue>
+
+ <issue
+ id="BanConcurrentHashMap"
+ message="Detected ConcurrentHashMap usage."
+ errorLine1="import java.util.concurrent.ConcurrentHashMap;"
+ errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Timer.java"/>
+ </issue>
+
+ <issue
+ id="BanParcelableUsage"
+ message="Class implements android.os.Parcelable"
+ errorLine1="public class ChoiceOption implements Parcelable {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ChoiceOption.java"/>
+ </issue>
+
+ <issue
+ id="BanParcelableUsage"
+ message="Class implements android.os.Parcelable"
+ errorLine1="public class ContentOpenable implements Openable, Parcelable {"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/ContentOpenable.java"/>
+ </issue>
+
+ <issue
+ id="BanParcelableUsage"
+ message="Class implements android.os.Parcelable"
+ errorLine1="public class Dimensions implements Parcelable {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/Dimensions.java"/>
+ </issue>
+
+ <issue
+ id="BanParcelableUsage"
+ message="Class implements android.os.Parcelable"
+ errorLine1="public class FileOpenable implements Openable, Parcelable {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FileOpenable.java"/>
+ </issue>
+
+ <issue
+ id="BanParcelableUsage"
+ message="Class implements android.os.Parcelable"
+ errorLine1="public class LinkRects extends ListOfList<Rect> implements Parcelable {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/LinkRects.java"/>
+ </issue>
+
+ <issue
+ id="BanParcelableUsage"
+ message="Class implements android.os.Parcelable"
+ errorLine1="public class MatchRects extends ListOfList<Rect> implements Parcelable {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/MatchRects.java"/>
+ </issue>
+
+ <issue
+ id="BanParcelableUsage"
+ message="Class implements android.os.Parcelable"
+ errorLine1="public class SelectionBoundary implements Parcelable {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/SelectionBoundary.java"/>
+ </issue>
+
+ <issue
+ id="BanParcelableUsage"
+ message="Class implements android.os.Parcelable"
+ errorLine1="public class TextSelection implements Parcelable {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/TextSelection.java"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" /** Bootstrap {@link AppInfo} from any {@link Context}. Can be called multiple times. */"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/pdf/util/AppInfo.java"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" /**"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginationModel.java"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" /**"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginationModel.java"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" /**"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginationModel.java"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" private synchronized void waitForTask() {"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfTaskExecutor.java"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" /** Schedule the given task. */"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfTaskExecutor.java"/>
+ </issue>
+
+ <issue
+ id="BanSynchronizedMethods"
+ message="Use of synchronized methods is not recommended"
+ errorLine1=" @Nullable"
+ errorLine2=" ^">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfTaskExecutor.java"/>
+ </issue>
+
+ <issue
+ id="BanTargetApiAnnotation"
+ message="Use `@RequiresApi` instead of `@TargetApi`"
+ errorLine1=" @TargetApi(17) // Guarded by elapsedRealtimeNanosExists()"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/persistence/SystemClockImpl.java"/>
+ </issue>
+
+ <issue
+ id="BanTargetApiAnnotation"
+ message="Use `@RequiresApi` instead of `@TargetApi`"
+ errorLine1=" @TargetApi(Build.VERSION_CODES.Q)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/SystemGestureExclusionHelper.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class Intents {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Intents.java"/>
+ </issue>
+
+ <issue
+ id="PrivateConstructorForUtilityClass"
+ message="Utility class is missing private constructor"
+ errorLine1="public class SystemGestureExclusionHelper {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/SystemGestureExclusionHelper.java"/>
+ </issue>
+
+ <issue
+ id="AppCompatCustomView"
+ message="This custom view should extend `androidx.appcompat.widget.AppCompatEditText` instead"
+ errorLine1="public class SearchEditText extends EditText {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/SearchEditText.java"/>
+ </issue>
+
+ <issue
+ id="RestrictedApiAndroidX"
+ message="MotionUtils.resolveThemeDuration can only be called from within the same library group (referenced groupId=`com.google.android.material` from groupId=`androidx.pdf`)"
+ errorLine1=" MotionUtils.resolveThemeDuration("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="RestrictedApiAndroidX"
+ message="MotionUtils.resolveThemeDuration can only be called from within the same library group (referenced groupId=`com.google.android.material` from groupId=`androidx.pdf`)"
+ errorLine1=" getContext(), com.google.android.material.R.attr.motionDurationMedium1,"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="RestrictedApiAndroidX"
+ message="MotionUtils.resolveThemeDuration can only be called from within the same library group (referenced groupId=`com.google.android.material` from groupId=`androidx.pdf`)"
+ errorLine1=" getContext(), com.google.android.material.R.attr.motionDurationMedium1,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="ObsoleteSdkInt"
+ message="Unnecessary; SDK_INT is always >= 30"
+ errorLine1=" if (VERSION.SDK_INT >= VERSION_CODES.Q) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ExifThumbnailUtils.java"/>
+ </issue>
+
+ <issue
+ id="ObsoleteSdkInt"
+ message="Unnecessary; SDK_INT is always >= 17"
+ errorLine1=" @TargetApi(17) // Guarded by elapsedRealtimeNanosExists()"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/persistence/SystemClockImpl.java"/>
+ </issue>
+
+ <issue
+ id="ObsoleteSdkInt"
+ message="Unnecessary; SDK_INT is always >= 30"
+ errorLine1=" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/persistence/SystemClockImpl.java"/>
+ </issue>
+
+ <issue
+ id="ObsoleteSdkInt"
+ message="Unnecessary; SDK_INT is always >= 30"
+ errorLine1=" return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/SystemGestureExclusionHelper.java"/>
+ </issue>
+
+ <issue
+ id="ObsoleteSdkInt"
+ message="Unnecessary; SDK_INT is always >= 29"
+ errorLine1=" @TargetApi(Build.VERSION_CODES.Q)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/SystemGestureExclusionHelper.java"/>
+ </issue>
+
+ <issue
+ id="KotlinPropertyAccess"
+ message="This getter should be public such that `model` can be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes"
+ errorLine1=" protected PaginationModel getModel() {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java"/>
+ </issue>
+
+ <issue
+ id="KotlinPropertyAccess"
+ message="This getter should be public such that `initialZoom` can be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes"
+ errorLine1=" private float getInitialZoom() {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="KotlinPropertyAccess"
+ message="This getter should be public such that `maxZoom` can be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes"
+ errorLine1=" private float getMaxZoom() {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="LambdaLast"
+ message="Functional interface parameters (such as parameter 1, "obs", in androidx.pdf.data.FutureValues.observeAsFuture) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions"
+ errorLine1=" T... target) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FutureValues.java"/>
+ </issue>
+
+ <issue
+ id="LambdaLast"
+ message="Functional interface parameters (such as parameter 1, "r", in androidx.pdf.util.ThreadUtils.postOnUiThreadDelayed) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions"
+ errorLine1=" public static void postOnUiThreadDelayed(Runnable r, long delay) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ThreadUtils.java"/>
+ </issue>
+
+ <issue
+ id="LambdaLast"
+ message="Functional interface parameters (such as parameter 1, "sourceFuture", in androidx.pdf.data.UiFutureValues.pipe) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions"
+ errorLine1=" public static <T> void pipe(FutureValue<T> sourceFuture, SettableFutureValue<T> targetFuture) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="LambdaLast"
+ message="Functional interface parameters (such as parameter 2, "converter", in androidx.pdf.data.UiFutureValues.convert) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions"
+ errorLine1=" final SettableFutureValue<T> destFuture) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public AbstractPaginatedView(Context context) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public AbstractPaginatedView(Context context, AttributeSet attrs) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public AbstractPaginatedView(Context context, AttributeSet attrs) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public AbstractPaginatedView(Context context, AttributeSet attrs, int defStyleAttr) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public AbstractPaginatedView(Context context, AttributeSet attrs, int defStyleAttr) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/AbstractPaginatedView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static final Accessibility get() {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Accessibility.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean isTouchExplorationEnabled(Context context) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Accessibility.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean isAccessibilityEnabled(Context context) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Accessibility.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void announce(Context context, View source, String message) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Accessibility.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void announce(Context context, View source, String message) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Accessibility.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void announce(Context context, View source, String message) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Accessibility.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void announce(Context context, View source, int messageId) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Accessibility.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void announce(Context context, View source, int messageId) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Accessibility.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" Context context,"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/AccessibilityPageWrapper.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" PageMosaicView pageView,"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/AccessibilityPageWrapper.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" PageLinksView pageLinksView) {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/AccessibilityPageWrapper.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PageMosaicView getPageView() {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/AccessibilityPageWrapper.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public View asView() {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/AccessibilityPageWrapper.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String getAppVersion() {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/AppInfo.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String getPackageName() {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/AppInfo.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public BitmapParcel(Bitmap bitmap) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/BitmapParcel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void receiveBitmap(Bitmap bitmap, ParcelFileDescriptor sourceFd) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/BitmapParcel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void receiveBitmap(Bitmap bitmap, ParcelFileDescriptor sourceFd) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/BitmapParcel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Bitmap obtainBitmap(Dimensions dimensions) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/BitmapRecycler.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static int getMemSizeKb(Bitmap bitmap) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/BitmapRecycler.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Map<String, String> getMapFrom(Bundle bundle) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/BundleUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static boolean bundleEquals(Bundle b1, Bundle b2) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/BundleUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static boolean bundleEquals(Bundle b1, Bundle b2) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/BundleUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ChoiceOption(int index, String label, boolean selected) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ChoiceOption.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ChoiceOption(ChoiceOption option) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ChoiceOption.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected ChoiceOption(Parcel in) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ChoiceOption.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String getLabel() {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ChoiceOption.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setLabel(String label) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ChoiceOption.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void writeToParcel(Parcel dest, int flags) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ChoiceOption.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> Iterable<T> asIterable(final SparseArray<T> array) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/CollectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> Iterable<T> asIterable(final SparseArray<T> array) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/CollectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> Iterator<T> makeIterator(final SparseArray<T> array) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/CollectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> Iterator<T> makeIterator(final SparseArray<T> array) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/CollectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Iterable<Integer> iterableKeys(final SparseArray<?> array) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/CollectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Iterable<Integer> iterableKeys(final SparseArray<?> array) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/CollectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Iterator<Integer> makeKeyIterator(final SparseArray<?> array) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/CollectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Iterator<Integer> makeKeyIterator(final SparseArray<?> array) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/CollectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ContentOpenable(@NonNull Uri uri, String contentType) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/ContentOpenable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ContentOpenable(@NonNull Uri uri, Dimensions size) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/ContentOpenable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Uri getContentUri() {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/ContentOpenable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Open openWith(Opener opener) throws IOException {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/ContentOpenable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Open openWith(Opener opener) throws IOException {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/ContentOpenable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void writeToParcel(Parcel dest, int flags) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/ContentOpenable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ContentUriOpener(ContentResolver contentResolver) {"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ContentUriOpener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public AssetFileDescriptor openPreview(Uri contentUri, Point size)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ContentUriOpener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public AssetFileDescriptor openPreview(Uri contentUri, Point size)"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ContentUriOpener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public AssetFileDescriptor openPreview(Uri contentUri, Point size)"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ContentUriOpener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public int getExifOrientation(Uri contentUri) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ContentUriOpener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public AssetFileDescriptor open(Uri contentUri, String contentType)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ContentUriOpener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public AssetFileDescriptor open(Uri contentUri, String contentType)"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ContentUriOpener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public AssetFileDescriptor open(Uri contentUri, String contentType)"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ContentUriOpener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String getContentType(Uri contentUri) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ContentUriOpener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String[] getAvailableTypes(Uri contentUri) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ContentUriOpener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String[] getAvailableTypes(Uri contentUri) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ContentUriOpener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static String extractContentName(ContentResolver contentResolver, Uri contentUri) {"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ContentUriOpener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static String extractContentName(ContentResolver contentResolver, Uri contentUri) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ContentUriOpener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static CycleRange of(int start, int size, Direction direction) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/CycleRange.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static CycleRange of(int start, int size, Direction direction) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/CycleRange.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Iterator iterator() {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/CycleRange.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Integer peekNext() {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/CycleRange.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Direction getDirection() {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/CycleRange.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Dimensions(Rect rect) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/Dimensions.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void writeToParcel(Parcel parcel, int flags) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/Dimensions.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public DiskCache(Context context) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/DiskCache.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String getCachedMimeType(Uri uri) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/DiskCache.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static File getLongTermCacheDir(Context context) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/DiskCache.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static File getLongTermCacheDir(Context context) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/DiskCache.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Uri getUri() {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/DisplayData.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String getName() {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/DisplayData.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Openable getOpenable() {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/DisplayData.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public DrawSpec(Paint paint, List<Rect> rects) {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/DrawSpec.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public DrawSpec(Paint paint, List<Rect> rects) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/DrawSpec.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public abstract void draw(Canvas canvas);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/DrawSpec.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <E extends Enum<E>> String createKey(@Nullable Collection<E> enums) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/EnumKeyGenerator.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void log(String tag, String message) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void log(String tag, String message) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void log(String tag, String message, String details) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void log(String tag, String message, String details) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void log(String tag, String message, String details) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void log(String tag, String method, Throwable e) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void log(String tag, String method, Throwable e) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void log(String tag, String method, Throwable e) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void logAndThrow(String tag, String method, Throwable e) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void logAndThrow(String tag, String method, Throwable e) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void logAndThrow(String tag, String method, Throwable e) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void logAndThrow(String tag, String method, String message) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void logAndThrow(String tag, String method, String message) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void logAndThrow(String tag, String method, String message) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void logAndAlwaysThrow(String tag, String method, Throwable e) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void logAndAlwaysThrow(String tag, String method, Throwable e) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void logAndAlwaysThrow(String tag, String method, Throwable e) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void checkState(boolean condition, String tag, String method, String message) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void checkState(boolean condition, String tag, String method, String message) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void checkState(boolean condition, String tag, String method, String message) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static String bracketValue(int value) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ErrorLog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static int getExifOrientation(Uri contentUri, ContentResolver contentResolver) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ExifThumbnailUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static int getExifOrientation(Uri contentUri, ContentResolver contentResolver) {"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ExifThumbnailUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void open(String url, Activity activity) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ExternalLinks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void open(String url, Activity activity) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ExternalLinks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void open(Uri uri, Activity activity) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ExternalLinks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void open(Uri uri, Activity activity) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ExternalLinks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static String getDescription(String url, Context context) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ExternalLinks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static String getDescription(String url, Context context) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ExternalLinks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static String getDescription(String url, Context context) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ExternalLinks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void setFastScrollListener(FastScrollListener listener);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/FastScrollContentModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FastScrollView(Context context, AttributeSet attrs) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/FastScrollView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FastScrollView(Context context, AttributeSet attrs) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/FastScrollView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FastScrollView(Context context, AttributeSet attrs, int defStyle) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/FastScrollView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FastScrollView(Context context, AttributeSet attrs, int defStyle) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/FastScrollView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setScrollable(FastScrollContentModel scrollable) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/FastScrollView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ObservableValue<Integer> getScrollerPositionY() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/FastScrollView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Fetcher build(Context context) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Fetcher build(Context context) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Fetcher build(Context context, int numThreads) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Fetcher build(Context context, int numThreads) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected Fetcher(Context ctx, DiskCache diskCache, int numThreads) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected Fetcher(Context ctx, DiskCache diskCache, int numThreads) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public DiskCache getCache() {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FutureValue<Openable> loadLocal(Uri localUri) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FutureValue<Openable> loadLocal(Uri localUri) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FutureValue<Openable> loadContent(Uri contentUri) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FutureValue<Openable> loadContent(Uri contentUri) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FutureValue<Openable> loadContent(Uri contentUri, String useType) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FutureValue<Openable> loadContent(Uri contentUri, String useType) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FutureValue<Openable> loadContent(Uri contentUri, String useType) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FutureValue<Openable> loadContent(Uri contentUri, Dimensions size) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FutureValue<Openable> loadContent(Uri contentUri, Dimensions size) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FutureValue<Openable> loadContent(Uri contentUri, Dimensions size) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FutureValue<Openable> loadFile(Uri fileUri) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FutureValue<Openable> loadFile(Uri fileUri) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/fetcher/Fetcher.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FileOpenable(File file, @Nullable String mimeType) throws FileNotFoundException {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FileOpenable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public FileOpenable(Uri uri) throws FileNotFoundException {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FileOpenable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Open openWith(Opener opener) throws IOException {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FileOpenable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Open openWith(Opener opener) throws IOException {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FileOpenable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String getFileName() {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FileOpenable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Uri getFileUri() {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FileOpenable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void writeToParcel(Parcel dest, int flags) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FileOpenable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" boolean onQueryTextChange(String query);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/find/FindInFileListener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" boolean onFindNextMatch(String query, boolean backwards);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/find/FindInFileListener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void get(Callback<T> callback);"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FutureValue.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void failed(Throwable thrown);"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FutureValue.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> FutureValue<T> newImmediateValue(T value) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> FutureValue<T> immediateFail(final Exception error) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" T... target) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void failed(Throwable thrown) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void failed(Throwable thrown) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public DeferredFutureValue(Supplier<FutureValue<T>> computation) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void get(Callback<T> callback) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void fail(Throwable thrown) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void fail(String errorMessage) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void get(Callback<T> callback) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/FutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public GestureTracker(String tag, Context context) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTracker.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public GestureTracker(String tag, Context context) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTracker.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setDelegateHandler(GestureHandler handler) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTracker.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean feed(MotionEvent event, boolean handle) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTracker.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void handleDoubleTap(MotionEvent ev) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTracker.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean matches(Gesture... gestures) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTracker.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String getLog() {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTracker.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean onScale(ScaleGestureDetector detector) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTracker.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean onScaleBegin(ScaleGestureDetector detector) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTracker.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void onScaleEnd(ScaleGestureDetector detector) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTracker.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void onGestureEnd(Gesture gesture) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTracker.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public GestureTrackingView(Context context) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTrackingView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public GestureTrackingView(Context context, AttributeSet attrs) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTrackingView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public GestureTrackingView(Context context, AttributeSet attrs) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTrackingView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public GestureTrackingView(Context context, AttributeSet attrs, int defStyleAttr) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTrackingView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public GestureTrackingView(Context context, AttributeSet attrs, int defStyleAttr) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTrackingView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public GestureTrackingView(Context ctx, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTrackingView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public GestureTrackingView(Context ctx, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTrackingView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected abstract boolean interceptGesture(GestureTracker gestureTracker);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTrackingView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected OnGestureListener patchGestureListener(OnGestureListener original) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTrackingView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected OnGestureListener patchGestureListener(OnGestureListener original) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/GestureTrackingView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected HighlightOverlay(DrawSpec... drawSpecs) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/HighlightOverlay.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void draw(Canvas canvas) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/HighlightOverlay.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setColorFilter(ColorFilter cf) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/HighlightOverlay.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Paint getOutlinedCommentAnchorPaint() {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/HighlightPaint.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Paint getOutlinedSelectedCommentAnchorPaint() {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/HighlightPaint.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Paint getWhiteOutlinePaint() {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/HighlightPaint.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public LinkRects(List<Rect> rects, List<Integer> linkToRect, List<String> urls) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/LinkRects.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public LinkRects(List<Rect> rects, List<Integer> linkToRect, List<String> urls) {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/LinkRects.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public LinkRects(List<Rect> rects, List<Integer> linkToRect, List<String> urls) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/LinkRects.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String getUrl(int link) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/LinkRects.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void writeToParcel(Parcel parcel, int flags) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/LinkRects.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ListOfList(List<T> values, List<Integer> indexToFirstValue) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/ListOfList.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ListOfList(List<T> values, List<Integer> indexToFirstValue) {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/ListOfList.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public List<T> flatten() {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/ListOfList.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected Fetcher mFetcher;"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/LoadingViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Viewer feed(DisplayData contents) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/LoadingViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Viewer feed(DisplayData contents) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/LoadingViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/LoadingViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/LoadingViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/LoadingViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/LoadingViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void postContentsAvailable(final DisplayData contents,"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/LoadingViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public LoadingViewer setFetcher(@NonNull Fetcher fetcher) {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/LoadingViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public MatchRects(List<Rect> rects, List<Integer> matchToRect, List<Integer> charIndexes) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/MatchRects.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public MatchRects(List<Rect> rects, List<Integer> matchToRect, List<Integer> charIndexes) {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/MatchRects.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public MatchRects(List<Rect> rects, List<Integer> matchToRect, List<Integer> charIndexes) {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/MatchRects.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Rect getFirstRect(int match) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/MatchRects.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public List<Rect> flattenExcludingMatch(int match) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/MatchRects.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void writeToParcel(Parcel parcel, int flags) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/MatchRects.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static int getMaxTileSize(Context context) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected BitmapRecycler mBitmapRecycler;"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected BitmapSource mBitmapSource;"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public MosaicView(Context context) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public MosaicView(Context context, AttributeSet attrs) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public MosaicView(Context context, AttributeSet attrs) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public MosaicView(Context context, AttributeSet attrs, int defStyle) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public MosaicView(Context context, AttributeSet attrs, int defStyle) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void init(Dimensions dimensions, BitmapRecycler bitmapRecycler,"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void init(Dimensions dimensions, BitmapRecycler bitmapRecycler,"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" BitmapSource bitmapSource) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void requestPageBitmap(Dimensions pageSize, boolean alsoRequestingTiles);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void requestNewTiles(Dimensions pageSize, Iterable<TileBoard.TileInfo> newTiles);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void requestNewTiles(Dimensions pageSize, Iterable<TileBoard.TileInfo> newTiles);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void cancelTiles(Iterable<Integer> tileIds);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected Dimensions getPageDimensionsAtWidth(int width) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void addOverlay(String key, Drawable overlay) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void addOverlay(String key, Drawable overlay) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean hasOverlay(String key) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void removeOverlay(String key) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected TileBoard createTileBoard(Dimensions viewSize) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected TileBoard createTileBoard(Dimensions viewSize) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setFailure(String message) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void requestRedrawAreas(final List<Rect> invalidRects) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setViewArea(Rect viewArea) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Rect getViewArea() {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected boolean clipAreaToPageSize(Rect scaledViewArea, Dimensions pageSize) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected boolean clipAreaToPageSize(Rect scaledViewArea, Dimensions pageSize) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setTileBitmap(TileBoard.TileInfo tileInfo, Bitmap tileBitmap) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setTileBitmap(TileBoard.TileInfo tileInfo, Bitmap tileBitmap) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void onDraw(Canvas canvas) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void dispatchDraw(Canvas canvas) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected boolean drawChild(Canvas canvas, View child, long drawingTime) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/MosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" Object addObserver(T observer);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Observable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void removeObserver(Object observerKey);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Observable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" Iterable<Integer> keys();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ObservableArray.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Object addObserver(O observer) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Observables.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void removeObserver(Object observer) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Observables.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Iterator<O> iterator() {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Observables.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <V> ExposedValue<V> newExposedValue() {"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Observables.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <V> ExposedValue<V> newExposedValueWithInitialValue(V initialValue) {"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Observables.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Iterable<Integer> keys() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Observables.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <V> ExposedArray<V> newExposedArray() {"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Observables.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Object addObserver(O observer) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Observables.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void removeObserver(Object observer) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Observables.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected Iterable<O> getObservers() {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Observables.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" Open openWith(Opener opener) throws IOException;"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Openable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" InputStream getInputStream() throws IOException;"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Openable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" ParcelFileDescriptor getFd() throws IOException;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Openable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" String getContentType();"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Openable.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Opener(Context ctx) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Opener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Opener(ContentUriOpener contentOpener) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Opener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected Open open(ContentOpenable content) throws FileNotFoundException {"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Opener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Open openLocal(Uri localUri) throws IOException {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Opener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public int getContentExifOrientation(ContentOpenable contentOpenable) {"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Opener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String getContentType(Uri uri) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Opener.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PageLinksView(Context context, ObservableValue<ZoomView.ZoomScroll> zoomScroll) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageLinksView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PageLinksView(Context context, ObservableValue<ZoomView.ZoomScroll> zoomScroll) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageLinksView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" Context context,"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageMosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" Dimensions pageSize,"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageMosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" BitmapSource bitmapSource,"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageMosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" BitmapRecycler bitmapRecycler) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageMosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String getLinkUrl(Point p) {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageMosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PageMosaicView getPageView() {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageMosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public View asView() {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageMosaicView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PageSelection(int page, SelectionBoundary start, SelectionBoundary stop,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/PageSelection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PageSelection(int page, SelectionBoundary start, SelectionBoundary stop,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/PageSelection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" List<Rect> rects, String text) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/PageSelection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" List<Rect> rects, String text) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/PageSelection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public List<Rect> getRects() {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/PageSelection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String getText() {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/PageSelection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void writeToParcel(Parcel parcel, int flags) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/PageSelection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" PageMosaicView getPageView();"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageViewFactory.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" View asView();"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageViewFactory.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static PageView createPageView("
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageViewFactory.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" Context context,"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageViewFactory.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" Dimensions pageSize,"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageViewFactory.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" MosaicView.BitmapSource bitmapSource,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageViewFactory.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" BitmapRecycler bitmapRecycler,"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageViewFactory.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" ObservableValue<ZoomView.ZoomScroll> zoomScroll) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PageViewFactory.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PaginatedView(Context context) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginatedView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PaginatedView(Context context, AttributeSet attrs) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginatedView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PaginatedView(Context context, AttributeSet attrs) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginatedView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PaginatedView(Context context, AttributeSet attrs, int defStyle) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginatedView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PaginatedView(Context context, AttributeSet attrs, int defStyle) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginatedView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void addView(PageView pageView) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginatedView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public List<PageMosaicView> getChildViews() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginatedView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void addPage(int pageNum, Dimensions pageSize) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginationModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Range getPagesInWindow(Range intervalPx, boolean includePartial) {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginationModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Range getPagesInWindow(Range intervalPx, boolean includePartial) {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginationModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setViewArea(Rect viewArea) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginationModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Rect getPageLocation(int pageNum) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginationModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Dimensions getPageSize(int pageNum) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginationModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Rect getViewArea() {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginationModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public synchronized void addObserver(PaginationModelObserver observer) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginationModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public synchronized void removeObserver(PaginationModelObserver observer) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginationModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public synchronized Iterator<PaginationModelObserver> iterator() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PaginationModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public abstract void sendPassword(EditText textField);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/password/PasswordDialog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setOnConnectInitializer(Runnable onConnect) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfConnection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setConnectionFailureHandler(Runnable onConnectFailure) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfConnection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PdfDocumentRemote getPdfDocument(String forTask) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfConnection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PdfDocumentRemote getPdfDocument(String forTask) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfConnection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PdfDocumentRemoteProto(PdfDocumentRemote remote) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PdfDocumentRemote getPdfDocumentRemote() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/pdflib/PdfDocumentRemoteProto.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PdfHighlightOverlay(PageSelection selection) {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfHighlightOverlay.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PdfHighlightOverlay(MatchRects matchRects) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfHighlightOverlay.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PdfHighlightOverlay(MatchRects matchRects, int currentMatch) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfHighlightOverlay.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void searchPageText(int pageNum, String query) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoader.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void selectPageText(int pageNum, SelectionBoundary start, SelectionBoundary stop) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoader.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void selectPageText(int pageNum, SelectionBoundary start, SelectionBoundary stop) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoader.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected PdfDocumentRemote getPdfDocument(String forTask) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoader.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected PdfDocumentRemote getPdfDocument(String forTask) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoader.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected PdfDocumentRemote getLoadedPdfDocument(String forTask) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoader.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected WeakPdfLoaderCallbacks getCallbacks() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoader.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void documentNotLoaded(PdfStatus status);"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void setPageDimensions(int pageNum, Dimensions dimensions);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void setPageBitmap(int pageNum, Bitmap bitmap);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void setTileBitmap(int pageNum, TileInfo tileInfo, Bitmap bitmap);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void setTileBitmap(int pageNum, TileInfo tileInfo, Bitmap bitmap);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void setPageText(int pageNum, String text);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void setSearchResults(String query, int pageNum, MatchRects matches);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void setSearchResults(String query, int pageNum, MatchRects matches);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void setSelection(int pageNum, PageSelection selection);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void setPageUrlLinks(int pageNum, LinkRects result);"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void setInvalidRects(int pageNum, List<Rect> invalidRects);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void loadPageBitmap(Dimensions pageSize) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void loadTilesBitmaps(Dimensions pageSize, Iterable<TileInfo> tiles) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void loadTilesBitmaps(Dimensions pageSize, Iterable<TileInfo> tiles) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void searchPageText(String query) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void selectPageText(SelectionBoundary start, SelectionBoundary stop) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void selectPageText(SelectionBoundary start, SelectionBoundary stop) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfPageLoader.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void sendPassword(EditText textField) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfPasswordDialog.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" PdfSelectionModel selectionModel, ZoomView zoomView, PaginatedView pdfView) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfSelectionHandles.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" PdfSelectionModel selectionModel, ZoomView zoomView, PaginatedView pdfView) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfSelectionHandles.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" PdfSelectionModel selectionModel, ZoomView zoomView, PaginatedView pdfView) {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfSelectionHandles.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PdfSelectionModel(PdfLoader pdfLoader) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfSelectionModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String getText() {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfSelectionModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void updateSelectionAsync(SelectionBoundary start, SelectionBoundary stop) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfSelectionModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void updateSelectionAsync(SelectionBoundary start, SelectionBoundary stop) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfSelectionModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public synchronized void schedule(AbstractPdfTask<?> task) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/PdfTaskExecutor.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected String getLogTag() {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PdfViewer setQuitOnError(boolean quit) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PdfViewer setExitOnPasswordCancel(boolean shouldExitOnPasswordCancel) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void onCreate(Bundle savedInstanceState) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void onActivityCreated(Bundle savedInstanceState) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void onContentsAvailable(DisplayData contents, @Nullable Bundle savedState) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setPassword(String password) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void onSaveInstanceState(Bundle outState) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setFastScrollListener(final FastScrollListener listener) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/PdfViewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void checkState(boolean state, String message) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Preconditions.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void checkArgument(boolean state, String message)"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Preconditions.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean shouldSkipZoomDetector(MotionEvent event, GestureTracker.EventId lastEvent) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/QuickScaleBypassDecider.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean shouldSkipZoomDetector(MotionEvent event, GestureTracker.EventId lastEvent) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/QuickScaleBypassDecider.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setLastGesture(GestureTracker.Gesture gesture) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/QuickScaleBypassDecider.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Range union(int value) {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Range.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Range union(Range other) {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Range.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Range union(Range other) {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Range.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Range intersect(Range other) {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Range.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Range intersect(Range other) {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Range.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Range[] minus(Range other) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Range.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Range[] minus(Range other) {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Range.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean contains(Range other) {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Range.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Range expand(int margin, Range bounds) {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Range.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Range expand(int margin, Range bounds) {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Range.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Iterator<Integer> iterator() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Range.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public RectDrawSpec(Paint paint, List<Rect> shapes) {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/RectDrawSpec.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public RectDrawSpec(Paint paint, List<Rect> shapes) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/RectDrawSpec.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void draw(Canvas canvas) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/RectDrawSpec.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Rect scale(Rect rect, float scale) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/RectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Rect scale(Rect rect, float scale) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/RectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Rect scale(Rect rect, float scaleX, float scaleY) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/RectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Rect scale(Rect rect, float scaleX, float scaleY) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/RectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static int area(Rect rect) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/RectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Rect fromDimensions(Dimensions dimensions) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/RectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Rect fromDimensions(Dimensions dimensions) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/RectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Rect getInnerIntersection(@NonNull Rect rect1, @NonNull Rect rect2) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/RectUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ReusableToast(View view) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ReusableToast.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public View getView() {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ReusableToast.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Screen(Context ctx) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Screen.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String getString(int id, Object... formatArgs) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Screen.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String getString(int id, Object... formatArgs) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Screen.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void reportWindowInsets(WindowInsetsCompat windowInsets) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Screen.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public SearchEditText(Context context) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/SearchEditText.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public SearchEditText(Context context, AttributeSet attrs) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/SearchEditText.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public SearchEditText(Context context, AttributeSet attrs) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/SearchEditText.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public SearchEditText(Context context, AttributeSet attrs, int defStyle) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/SearchEditText.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public SearchEditText(Context context, AttributeSet attrs, int defStyle) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/SearchEditText.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/SearchEditText.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public SearchModel(PdfLoader pdfLoader) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ObservableValue<String> query() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ObservableValue<SelectedMatch> selectedMatch() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ObservableValue<MatchCount> matchCount() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean updateMatches(String matchesQuery, int page, MatchRects matches) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean updateMatches(String matchesQuery, int page, MatchRects matches) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void selectNextMatch(Direction direction, int viewingPage) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PdfHighlightOverlay getOverlay(String matchesQuery, int page, MatchRects matches) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public PdfHighlightOverlay getOverlay(String matchesQuery, int page, MatchRects matches) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static String whiteSpaceToNull(String query) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/SearchModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static SelectionBoundary atIndex(int index) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/SelectionBoundary.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static SelectionBoundary atPoint(int x, int y) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/SelectionBoundary.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static SelectionBoundary atPoint(Point p) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/SelectionBoundary.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static SelectionBoundary atPoint(Point p) {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/SelectionBoundary.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void writeToParcel(Parcel parcel, int flags) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/aidl/SelectionBoundary.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ObservableValue<S> selection() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/select/SelectionModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public abstract String getText();"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/select/SelectionModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void updateSelectionAsync(SelectionBoundary start, SelectionBoundary stop) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/select/SelectionModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void updateSelectionAsync(SelectionBoundary start, SelectionBoundary stop) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/select/SelectionModel.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" T supply(Progress progress) throws Exception;"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/Supplier.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static List<Rect> createExclusionRectsForCorners("
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/SystemGestureExclusionHelper.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" Rect rect, int systemGestureInsetsWidthPx, int bufferDistancePx, int screenWidthPx) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/SystemGestureExclusionHelper.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Rect createLeftSideExclusionRect("
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/SystemGestureExclusionHelper.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Rect createRightSideExclusionRect("
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/SystemGestureExclusionHelper.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static boolean setSystemGestureExclusionRects(View view, List<Rect> exclusionRects) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/SystemGestureExclusionHelper.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static boolean setSystemGestureExclusionRects(View view, List<Rect> exclusionRects) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/SystemGestureExclusionHelper.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public TaskCancelledException(String detailMessage) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/exceptions/TaskCancelledException.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void maybeDenyListTask(String task) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/TaskDenyList.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public TextSelection(SelectionBoundary start, SelectionBoundary stop) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/TextSelection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public TextSelection(SelectionBoundary start, SelectionBoundary stop) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/TextSelection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public SelectionBoundary getStart() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/TextSelection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public SelectionBoundary getStop() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/TextSelection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void writeToParcel(Parcel parcel, int flags) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/TextSelection.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void runOnUiThread(Runnable r) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ThreadUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void runInBackground(Runnable r) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ThreadUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void postOnUiThreadDelayed(Runnable r, long delay) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ThreadUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void removeCallbackOnUiThread(Runnable r) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ThreadUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static void postOnUiThread(Runnable r) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ThreadUtils.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean setTile(TileInfo tileInfo, Bitmap tile) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean setTile(TileInfo tileInfo, Bitmap tile) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean updateViewArea(Rect viewArea, ViewAreaUpdateCallback callback) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean updateViewArea(Rect viewArea, ViewAreaUpdateCallback callback) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected Area getExpandedArea(Rect viewArea) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected Area getExpandedArea(Rect viewArea) {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void cancelTiles(Iterable<Integer> tileIds);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void requestNewTiles(Iterable<TileInfo> tiles);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void discardTiles(Iterable<Integer> tileIds);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Iterable<Integer> getVisibleTileIndexes() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean isTileVisible(TileInfo tileInfo) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public List<TileInfo> findTileInfosForRects(List<Rect> rects) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public List<TileInfo> findTileInfosForRects(List<Rect> rects) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public boolean belongsTo(TileBoard board) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Dimensions getSize() {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Dimensions getExactSize() {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Point getOffset() {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Rect getBounds() {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/TileBoard.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public TileView(Context context, TileInfo tileInfo) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/TileView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public TileView(Context context, TileInfo tileInfo) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/TileView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Point getOffset() {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/TileView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public TileInfo getTileInfo() {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/TileView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void onDraw(Canvas canvas) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/TileView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static Timer start() {"
+ errorLine2=" ~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Timer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public LogBuilder track(String event) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Timer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public LogBuilder track(String event) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Timer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public LogBuilder trackFmt(String eventFmt, Object... args) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Timer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public LogBuilder trackFmt(String eventFmt, Object... args) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Timer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public LogBuilder trackFmt(String eventFmt, Object... args) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Timer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void start(String key) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Timer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void track(String key, String event) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Timer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void track(String key, String event) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Timer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String stop(String key) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Timer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public String stop(String key) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Timer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void popToast(Context context, int resId, Object... args) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Toaster.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void popToast(Context context, int resId, Object... args) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Toaster.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void popToast(Context context, String message) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Toaster.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void popToast(Context context, String message) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Toaster.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> FutureValue<T> immediateValue(final T value) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> FutureValue<T> immediateFail(final Exception error) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> FutureValue<T> immediateFail(final Exception error) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <F, T> Supplier<T> postConvert(final Supplier<F> supplier,"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <F, T> Supplier<T> postConvert(final Supplier<F> supplier,"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" final Converter<F, T> converter) {"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> FutureValue<T> executeAsync(Supplier<T> supplier) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> FutureValue<T> executeAsync(Supplier<T> supplier) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> FutureValue<T> execute(Supplier<T> supplier) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> FutureValue<T> execute(Supplier<T> supplier) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> FutureValue<T> execute(final FutureValue<T> sourceFuture) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> FutureValue<T> execute(final FutureValue<T> sourceFuture) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> void pipe(FutureValue<T> sourceFuture, SettableFutureValue<T> targetFuture) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <T> void pipe(FutureValue<T> sourceFuture, SettableFutureValue<T> targetFuture) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <F, T> void convert(FutureValue<F> sourceFuture, Converter<F, T> converter,"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static <F, T> void convert(FutureValue<F> sourceFuture, Converter<F, T> converter,"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" final SettableFutureValue<T> destFuture) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/data/UiFutureValues.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static String extractName(@NonNull Uri uri, @NonNull ContentResolver contentResolver) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Uris.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static boolean isFileUriInSamePackageDataDir(Uri uri) {"
+ errorLine2=" ~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/Uris.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void addOverlay(String key, Drawable overlay);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/overlays/ViewWithOverlays.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void addOverlay(String key, Drawable overlay);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/overlays/ViewWithOverlays.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected abstract String getLogTag();"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/Viewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected ViewGroup mContainer;"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/Viewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected ExposedValue<ViewState> mViewState = Observables.newExposedValueWithInitialValue("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/Viewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ObservableValue<ViewState> viewState() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/Viewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void onCreate(Bundle savedInstanceState) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/Viewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/Viewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/Viewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/Viewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void onActivityCreated(Bundle savedInstanceState) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/Viewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void saveToArguments(DisplayData data) {"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/Viewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void log(char tag, String step) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/Viewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected String getEventlog() {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/Viewer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static WeakPdfLoaderCallbacks wrap(PdfLoaderCallbacks delegate) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/WeakPdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static WeakPdfLoaderCallbacks wrap(PdfLoaderCallbacks delegate) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/WeakPdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected WeakPdfLoaderCallbacks(PdfLoaderCallbacks delegate) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/WeakPdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void documentNotLoaded(PdfStatus status) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/WeakPdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setPageDimensions(int pageNum, Dimensions dimensions) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/WeakPdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setPageBitmap(int pageNum, Bitmap bitmap) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/WeakPdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setTileBitmap(int pageNum, TileInfo tileInfo, Bitmap bitmap) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/WeakPdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setTileBitmap(int pageNum, TileInfo tileInfo, Bitmap bitmap) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/WeakPdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setPageText(int pageNum, String text) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/WeakPdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setSearchResults(String query, int pageNum, MatchRects matches) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/WeakPdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setSearchResults(String query, int pageNum, MatchRects matches) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/WeakPdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setSelection(int pageNum, PageSelection selection) {"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/WeakPdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setPageUrlLinks(int pageNum, LinkRects links) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/WeakPdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void setInvalidRects(int pageNum, List<Rect> invalidRects) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/viewer/loader/WeakPdfLoaderCallbacks.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static WidgetType of(int id) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/WidgetType.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" void attemptRestorePosition(ZoomScroll position);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/util/ZoomScrollRestorer.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView(Context context, AttributeSet attrs) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView(Context context, AttributeSet attrs) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView(Context context, AttributeSet attrs, int defStyle) {"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView(Context context, AttributeSet attrs, int defStyle) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView setFitMode(int fitMode) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView setInitialZoomMode(int initialZoomMode) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView setRotateMode(int rotateMode) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView setContentResizedMode(int contentResizedMode) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView setContentResizedModeX(int contentResizedMode) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView setContentResizedModeY(int contentResizedMode) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView setContentResizedModeZoom(int contentResizedMode) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView setKeepFitZoomOnRotate(boolean keepFitZoomOnRotate) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView setOverrideMinZoomToFit(boolean overrideMinZoomToFit) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView setOverrideMaxZoomToFit(boolean overrideMaxZoomToFit) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView setAllowParentToHandleScaleEvents(boolean allowParentToHandleScaleEvents) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView setEnableDoubleTap(boolean doubleTapEnabled) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ObservableValue<ZoomScroll> zoomScroll() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView setInitialZoom(float initialZoom) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public ZoomView setMinZoom(float minZoom) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Rect getUsableAreaInContentCoords() {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Rect getVisibleAreaInContentCoords() {"
+ errorLine2=" ~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" float x, float y, float newZoom, ValueAnimator.AnimatorUpdateListener updateListener) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected Parcelable onSaveInstanceState() {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public void attemptRestorePosition(ZoomScroll position) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected boolean interceptGesture(GestureTracker gestureTracker) {"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static ZoomScroll fromBundle(Bundle bundle) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public static ZoomScroll fromBundle(Bundle bundle) {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" public Bundle asBundle() {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomView.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected ZoomableSelectionHandles(ZoomView zoomView, ViewGroup handleParent,"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected ZoomableSelectionHandles(ZoomView zoomView, ViewGroup handleParent,"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" ObservableValue<S> selectionObservable) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected Object createSelectionObserver() {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected Object createZoomViewObserver() {"
+ errorLine2=" ~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void showHandle(ImageView handle, float rawX, float rawY, boolean isRight) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected ImageView createHandle(ViewGroup parent, boolean isStop) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected ImageView createHandle(ViewGroup parent, boolean isStop) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java"/>
+ </issue>
+
+ <issue
+ id="UnknownNullness"
+ message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+ errorLine1=" protected void destroyHandle(ImageView handle) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="src/main/java/androidx/pdf/widget/ZoomableSelectionHandles.java"/>
+ </issue>
+
+</issues>
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/aidl/Dimensions.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/aidl/Dimensions.java
new file mode 100644
index 0000000..07462ab
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/aidl/Dimensions.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.aidl;
+
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Dimensions of a rectangular area: width and height.
+ * Objects of this class are immutable.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class Dimensions implements Parcelable {
+ public static final Creator<Dimensions> CREATOR = new Creator<Dimensions>() {
+ @Override
+ public Dimensions createFromParcel(Parcel parcel) {
+ return new Dimensions(parcel.readInt(), parcel.readInt());
+ }
+
+ @Override
+ public Dimensions[] newArray(int size) {
+ return new Dimensions[size];
+ }
+ };
+
+ private final int mWidth;
+ private final int mHeight;
+
+ public Dimensions(int width, int height) {
+ this.mWidth = width;
+ this.mHeight = height;
+ }
+
+ public Dimensions(Rect rect) {
+ this.mWidth = rect.width();
+ this.mHeight = rect.height();
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof Dimensions) {
+ Dimensions other = (Dimensions) o;
+ return mWidth == other.mWidth && mHeight == other.mHeight;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * mWidth + mHeight;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return String.format("Dimensions (w:%d, h:%d)", mWidth, mHeight);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mWidth);
+ parcel.writeInt(mHeight);
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/aidl/LinkRects.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/aidl/LinkRects.java
new file mode 100644
index 0000000..3739224
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/aidl/LinkRects.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.aidl;
+
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.data.ListOfList;
+import androidx.pdf.util.Preconditions;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents the bounds of links as a {@code List<List<Rect>>}, where
+ * the first {@code List<Rect>} is all of the rectangles needed to bound the
+ * first link, and so on. Most links will be surrounded with a single Rect.
+ * <p>
+ * Internally, data is stored as 1-dimensional Lists, to avoid the overhead of
+ * a large amount of single-element lists.
+ * <p>
+ * Also contains the URL index of each link - so {@link #get} returns the
+ * rectangles that bound the link, and {@link #getUrl} returns the URL that is
+ * linked to.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressWarnings("deprecation")
+public class LinkRects extends ListOfList<Rect> implements Parcelable {
+ public static final LinkRects NO_LINKS = new LinkRects(Collections.emptyList(),
+ Collections.emptyList(), Collections.emptyList());
+
+ public static final Creator<LinkRects> CREATOR = new Creator<LinkRects>() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public LinkRects createFromParcel(Parcel parcel) {
+ return new LinkRects(parcel.readArrayList(Rect.class.getClassLoader()),
+ parcel.readArrayList(Integer.class.getClassLoader()),
+ parcel.readArrayList(String.class.getClassLoader()));
+ }
+
+ @Override
+ public LinkRects[] newArray(int size) {
+ return new LinkRects[size];
+ }
+ };
+
+ private final List<Rect> mRects;
+ private final List<Integer> mLinkToRect;
+ private final List<String> mUrls;
+
+ public LinkRects(List<Rect> rects, List<Integer> linkToRect, List<String> urls) {
+ super(rects, linkToRect);
+ this.mRects = Preconditions.checkNotNull(rects);
+ this.mLinkToRect = Preconditions.checkNotNull(linkToRect);
+ this.mUrls = Preconditions.checkNotNull(urls);
+ }
+
+ /** Return the URL corresponding to the given link. */
+ public String getUrl(int link) {
+ return mUrls.get(link);
+ }
+
+ /** Return the URL corresponding to the given point. */
+ @Nullable
+ public String getUrlAtPoint(int x, int y) {
+ for (int rect = 0; rect < mRects.size(); rect++) {
+ if (mRects.get(rect).contains(x, y)) {
+ for (int link = 1; link <= mLinkToRect.size(); link++) {
+ if (indexToFirstValue(link) > rect) {
+ return mUrls.get(link - 1);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return size() + " links";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeList(mRects);
+ parcel.writeList(mLinkToRect);
+ parcel.writeList(mUrls);
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/aidl/MatchRects.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/aidl/MatchRects.java
new file mode 100644
index 0000000..4ab9bed
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/aidl/MatchRects.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.aidl;
+
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.RestrictTo;
+import androidx.pdf.data.ListOfList;
+import androidx.pdf.util.Preconditions;
+
+import java.util.AbstractList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents the bounds of search matches as a {@code List<List<Rect>>}, where
+ * the first {@code List<Rect>} is all of the rectangles needed to bound the
+ * first match, and so on. Most matches will be surrounded with a single Rect.
+ * <p>
+ * Internally, data is stored as 1-dimensional Lists, to avoid the overhead of
+ * a large amount of single-element lists.
+ * <p>
+ * Also contains data about the character index of each match - so {@link #get}
+ * returns the rectangles that bound the match, and {@link #getCharIndex}
+ * returns the character index that the match starts at.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressWarnings("deprecation")
+public class MatchRects extends ListOfList<Rect> implements Parcelable {
+ public static final MatchRects NO_MATCHES = new MatchRects(Collections.emptyList(),
+ Collections.emptyList(), Collections.emptyList());
+
+ public static final Creator<MatchRects> CREATOR = new Creator<MatchRects>() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public MatchRects createFromParcel(Parcel parcel) {
+ return new MatchRects(parcel.readArrayList(Rect.class.getClassLoader()),
+ parcel.readArrayList(Integer.class.getClassLoader()),
+ parcel.readArrayList(Integer.class.getClassLoader()));
+ }
+
+ @Override
+ public MatchRects[] newArray(int size) {
+ return new MatchRects[size];
+ }
+ };
+
+ private final List<Rect> mRects;
+ private final List<Integer> mMatchToRect;
+ private final List<Integer> mCharIndexes;
+
+ public MatchRects(List<Rect> rects, List<Integer> matchToRect, List<Integer> charIndexes) {
+ super(rects, matchToRect);
+ this.mRects = Preconditions.checkNotNull(rects);
+ this.mMatchToRect = Preconditions.checkNotNull(matchToRect);
+ this.mCharIndexes = Preconditions.checkNotNull(charIndexes);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof MatchRects)) {
+ return false;
+ }
+ MatchRects that = (MatchRects) other;
+ return this.mRects.equals(that.mRects)
+ && this.mMatchToRect.equals(that.mMatchToRect)
+ && this.mCharIndexes.equals(that.mCharIndexes);
+ }
+
+ @Override
+ public int hashCode() {
+ return mRects.hashCode() + 31 * mMatchToRect.hashCode() + 101 * mCharIndexes.hashCode();
+ }
+
+ /** Returns the character index corresponding to the given match. */
+ public int getCharIndex(int match) {
+ return mCharIndexes.get(match);
+ }
+
+ /** Returns the first rect for a given match. */
+ public Rect getFirstRect(int match) {
+ return mRects.get(mMatchToRect.get(match));
+ }
+
+ /**
+ * Returns the flattened, one-dimensional list of all rectangles that surround
+ * all matches <strong>except</strong> for the given match.
+ */
+ public List<Rect> flattenExcludingMatch(int match) {
+ if (match < 0 || match >= mMatchToRect.size()) {
+ throw new ArrayIndexOutOfBoundsException(match);
+ }
+ final int startExclude = indexToFirstValue(match);
+ final int stopExclude = indexToFirstValue(match + 1);
+ return new AbstractList<Rect>() {
+ @Override
+ public Rect get(int index) {
+ return (index < startExclude) ? mRects.get(index)
+ : mRects.get(index - startExclude + stopExclude);
+ }
+
+ @Override
+ public int size() {
+ return mRects.size() - (stopExclude - startExclude);
+ }
+ };
+ }
+
+ /**
+ * When the search term is updated, we automatically find the match that occurs
+ * at the same character index (if it still matches), or next in the text (if
+ * it no longer matches after the query changes).
+ */
+ public int getMatchNearestCharIndex(int oldCharIndex) {
+ if (size() <= 1) {
+ return size() - 1;
+ }
+ int searchResult = Collections.binarySearch(mCharIndexes, oldCharIndex);
+ if (searchResult >= 0) {
+ return searchResult;
+ }
+ return Math.min(size() - 1, -searchResult - 1);
+ }
+
+ @Override
+ public String toString() {
+ return size() + " matches";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeList(mRects);
+ parcel.writeList(mMatchToRect);
+ parcel.writeList(mCharIndexes);
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/aidl/PageSelection.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/aidl/PageSelection.java
new file mode 100644
index 0000000..794db95
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/aidl/PageSelection.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.aidl;
+
+import android.graphics.Rect;
+import android.os.Parcel;
+
+import androidx.annotation.RestrictTo;
+import androidx.pdf.data.TextSelection;
+
+import java.util.List;
+
+
+/** Represents text selection on a particular page of a PDF. Immutable. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressWarnings("deprecation")
+public class PageSelection extends TextSelection {
+
+ @SuppressWarnings("hiding")
+ public static final Creator<PageSelection> CREATOR = new Creator<PageSelection>() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public PageSelection createFromParcel(Parcel parcel) {
+ return new PageSelection(parcel.readInt(), (SelectionBoundary) parcel.readParcelable(
+ SelectionBoundary.class.getClassLoader()),
+ (SelectionBoundary) parcel.readParcelable(
+ SelectionBoundary.class.getClassLoader()),
+ (List<Rect>) parcel.readArrayList(Rect.class.getClassLoader()),
+ parcel.readString());
+ }
+
+ @Override
+ public PageSelection[] newArray(int size) {
+ return new PageSelection[size];
+ }
+ };
+
+ /** The page the selection is on. */
+ private final int mPage;
+
+ /** The bounding boxes of the highlighted text. */
+ private final List<Rect> mRects;
+
+ /** The highlighted text. */
+ private final String mText;
+
+ public PageSelection(int page, SelectionBoundary start, SelectionBoundary stop,
+ List<Rect> rects, String text) {
+ super(start, stop);
+ this.mPage = page;
+ this.mRects = rects;
+ this.mText = text;
+ }
+
+ public int getPage() {
+ return mPage;
+ }
+
+ public List<Rect> getRects() {
+ return mRects;
+ }
+
+ public String getText() {
+ return mText;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("PageSelection(page=%d, start=%s, stop=%s, %d rects)", mPage,
+ getStart(),
+ getStop(), mRects.size());
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mPage);
+ parcel.writeParcelable(getStart(), 0);
+ parcel.writeParcelable(getStop(), 0);
+ parcel.writeList(mRects);
+ parcel.writeString(mText);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/aidl/SelectionBoundary.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/aidl/SelectionBoundary.java
new file mode 100644
index 0000000..14c6157
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/aidl/SelectionBoundary.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.aidl;
+
+import android.graphics.Point;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.RestrictTo;
+
+
+/**
+ * Represents one edge of the selected text. A boundary can be defined by
+ * either an index into the text, a point on the page, or both.
+ * Should be a nested class of {@link PageSelection}, but AIDL prevents that.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class SelectionBoundary implements Parcelable {
+ public static final SelectionBoundary PAGE_START = SelectionBoundary.atIndex(0);
+ public static final SelectionBoundary PAGE_END = SelectionBoundary.atIndex(Integer.MAX_VALUE);
+
+ public static final Creator<SelectionBoundary> CREATOR = new Creator<SelectionBoundary>() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public SelectionBoundary createFromParcel(Parcel parcel) {
+ int[] state = new int[4];
+ parcel.readIntArray(state);
+ return new SelectionBoundary(state[0], state[1], state[2], state[3] > 0);
+ }
+
+ @Override
+ public SelectionBoundary[] newArray(int size) {
+ return new SelectionBoundary[size];
+ }
+ };
+
+ private final int mIndex;
+
+ private final int mX;
+
+ private final int mY;
+
+ private final boolean mIsRtl;
+
+ public SelectionBoundary(int index, int x, int y, boolean isRtl) {
+ this.mIndex = index;
+ this.mX = x;
+ this.mY = y;
+ this.mIsRtl = isRtl;
+ }
+
+ public int getIndex() {
+ return mIndex;
+ }
+
+ public int getX() {
+ return mX;
+ }
+
+ public int getY() {
+ return mY;
+ }
+
+ public boolean isRtl() {
+ return mIsRtl;
+ }
+
+ /** Create a boundary that has a particular index, but the position is not known. */
+ public static SelectionBoundary atIndex(int index) {
+ return new SelectionBoundary(index, -1, -1, false);
+ }
+
+ /** Create a boundary at a particular point, but the index is not known. */
+ public static SelectionBoundary atPoint(int x, int y) {
+ return new SelectionBoundary(-1, x, y, false);
+ }
+
+ /** Create a boundary at a particular point, but the index is not known. */
+ public static SelectionBoundary atPoint(Point p) {
+ return new SelectionBoundary(-1, p.x, p.y, false);
+ }
+
+ @Override
+ public String toString() {
+ String indexStr = (mIndex == Integer.MAX_VALUE) ? "MAX" : Integer.toString(mIndex);
+ return String.format("@%s (%d,%d)", indexStr, mX, mY);
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeIntArray(new int[]{mIndex, mX, mY, mIsRtl ? 1 : 0});
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof SelectionBoundary)) {
+ return false;
+ }
+ SelectionBoundary other = (SelectionBoundary) obj;
+ return this.mX == other.mX && this.mY == other.mY && this.mIndex == other.mIndex
+ && this.mIsRtl == other.mIsRtl;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + mX;
+ result = 31 * result + mY;
+ result = 31 * result + mIndex;
+ result = 31 * result + (mIsRtl ? 1231 : 1237);
+ return result;
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/data/ContentOpenable.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/ContentOpenable.java
new file mode 100644
index 0000000..72c3be7
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/ContentOpenable.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.data;
+
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.aidl.Dimensions;
+import androidx.pdf.util.Preconditions;
+import androidx.pdf.util.Uris;
+
+import java.io.IOException;
+
+/**
+ * An {@link Openable} on a 'content' asset, wrapping a {@link AssetFileDescriptor}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class ContentOpenable implements Openable, Parcelable {
+
+ private static final String TAG = ContentOpenable.class.getSimpleName();
+
+ /** The content Uri this {@link Openable} opens. */
+ private final Uri mContentUri;
+
+ /** The content-type that this content should be opened as (e.g. when more than one
+ * available). */
+ @Nullable
+ private final String mContentType;
+
+ /**
+ * If not null, this {@link Openable} will open an image preview of the actual contents.
+ * The preview will be requested with these specified dimensions.
+ */
+ @Nullable
+ private final Dimensions mSize;
+
+ @Nullable
+ private Open mOpen;
+
+ /** Creates an {@link Openable} for the contents @ uri with its default content-type. */
+ public ContentOpenable(@NonNull Uri uri) {
+ this(uri, null, null);
+ }
+
+ /** Creates an {@link Openable} for the contents @ uri with the given content-type. */
+ public ContentOpenable(@NonNull Uri uri, String contentType) {
+ this(uri, contentType, null);
+ }
+
+ /** Creates an {@link Openable} for an image preview (of the given size) of the contents @
+ * uri. */
+ public ContentOpenable(@NonNull Uri uri, Dimensions size) {
+ this(uri, null, size);
+ }
+
+ private ContentOpenable(@NonNull Uri uri, String contentType, Dimensions size) {
+ Preconditions.checkNotNull(uri);
+ Preconditions.checkArgument(Uris.isContentUri(uri),
+ "Does not accept Uri " + uri.getScheme());
+ this.mContentUri = uri;
+ this.mContentType = contentType;
+ this.mSize = size;
+ }
+
+ public Uri getContentUri() {
+ return mContentUri;
+ }
+
+ @Nullable
+ public Dimensions getSize() {
+ return mSize;
+ }
+
+ /**
+ * Returns a new instance of {@link androidx.pdf.data.Openable.Open}.
+ *
+ * NOTE: Clients are responsible for closing each instance that they obtain from this method.
+ *
+ * @return The {@link androidx.pdf.data.Openable.Open} for this Openable.
+ */
+ @Override
+ public Open openWith(Opener opener) throws IOException {
+ /*
+ * We want to explicitly return {@link Opener#open(ContentOpenable)} every time instead
+ * of just
+ * returning {@link #open} if it's not null, in case the underlying data is backed by a
+ * pipe,
+ * in which case we can't seek or re-read the resulting {@link android.os
+ * .ParcelFileDescriptor},
+ * so callers can call this again to get a fresh handle on the underlying data.
+ */
+ mOpen = opener.open(this);
+ return mOpen;
+ }
+
+ @Override
+ @Nullable
+ public String getContentType() {
+ return mContentType;
+ }
+
+ @Override
+ public long length() {
+ return mOpen != null ? mOpen.length() : -1;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s [%s]: %s / @%s", TAG, mContentType, mContentUri, mSize);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mContentUri, flags);
+ if (mContentType != null) {
+ dest.writeString(mContentType);
+ } else {
+ dest.writeString("");
+ }
+ if (mSize != null) {
+ /* Value of 1 indicates that {@code size} is not null, to avoid un-parceling errors. */
+ dest.writeInt(1);
+ dest.writeParcelable(mSize, flags);
+ } else {
+ dest.writeInt(0);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @SuppressWarnings("deprecation")
+ public static final Creator<ContentOpenable> CREATOR = new Creator<ContentOpenable>() {
+ @Override
+ public ContentOpenable createFromParcel(Parcel parcel) {
+ Uri uri = parcel.readParcelable(Uri.class.getClassLoader());
+ String contentType = parcel.readString();
+ if (contentType.isEmpty()) {
+ contentType = null;
+ }
+ Dimensions size = null;
+ boolean sizeIsPresent = parcel.readInt() > 0;
+ if (sizeIsPresent) {
+ size = parcel.readParcelable(Dimensions.class.getClassLoader());
+ }
+ return new ContentOpenable(uri, contentType, size);
+ }
+
+ @Override
+ public ContentOpenable[] newArray(int size) {
+ return new ContentOpenable[size];
+ }
+ };
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/data/DisplayData.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/DisplayData.java
new file mode 100644
index 0000000..d78240d
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/DisplayData.java
@@ -0,0 +1,141 @@
+/*
+ * 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.pdf.data;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.data.Openable.Open;
+import androidx.pdf.util.ErrorLog;
+import androidx.pdf.util.Preconditions;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * File data that can be displayed in a Viewer. This class contains meta-data specific to Projector
+ * (e.g. display type), and an {@link Openable} that can be used to access the data.
+ * Instances are parcelable (in the form of a {@link Bundle}).
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class DisplayData {
+
+ private static final String TAG = DisplayData.class.getSimpleName();
+
+ private static final String KEY_NAME = "n";
+ private static final String KEY_URI = "uri";
+ private static final String KEY_PARCELABLE_OPENABLE = "po";
+
+ /**
+ * This is used for identifying this data, and as the base Uri in the case of HTML. In order to
+ * actually access the data, {@link #mOpenable} should be used instead.
+ */
+ private final Uri mUri;
+
+ private final String mName;
+
+ private final Openable mOpenable;
+
+ public DisplayData(
+ @NonNull Uri uri,
+ @NonNull String name,
+ @NonNull Openable openable) {
+ this.mName = Preconditions.checkNotNull(name);
+ this.mUri = Preconditions.checkNotNull(uri);
+ this.mOpenable = Preconditions.checkNotNull(openable);
+ }
+
+ public Uri getUri() {
+ return mUri;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public Openable getOpenable() {
+ return mOpenable;
+ }
+
+ /** Converts an Opener into ParcelFileDescriptor */
+ @Nullable
+ public ParcelFileDescriptor openFd(@NonNull Opener opener) {
+ // TODO: StrictMode: close() not explicitly called on PFD.
+ try {
+ return open(opener).getFd();
+ } catch (IOException e) {
+ ErrorLog.log(TAG, "openFd", e);
+ return null;
+ }
+ }
+
+ /** Converts Opener to InputStream */
+ @Nullable
+ public InputStream openInputStream(@NonNull Opener opener) throws IOException {
+ return open(opener).getInputStream();
+ }
+
+ /**
+ */
+ public long length() {
+ return mOpenable.length();
+ }
+
+ /**
+ */
+ @NonNull
+ private Open open(Opener opener) throws IOException {
+ return mOpenable.openWith(opener);
+ }
+
+ /**
+ */
+ @NonNull
+ public Bundle asBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_NAME, mName);
+ bundle.putParcelable(KEY_URI, mUri);
+ bundle.putParcelable(KEY_PARCELABLE_OPENABLE, mOpenable);
+
+ return bundle;
+ }
+
+ /**
+ */
+ @NonNull
+ @SuppressWarnings("deprecation")
+ public static DisplayData fromBundle(@NonNull Bundle bundle) {
+ bundle.setClassLoader(DisplayData.class.getClassLoader());
+ Uri uri = bundle.getParcelable(KEY_URI);
+ String name = bundle.getString(KEY_NAME);
+ Openable openable = bundle.getParcelable(KEY_PARCELABLE_OPENABLE);
+
+ return new DisplayData(uri, name, openable);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return String.format(
+ "Display Data [%s] +%s, uri: %s",
+ mName, mOpenable.getClass().getSimpleName(), mUri);
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/data/FileOpenable.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/FileOpenable.java
new file mode 100644
index 0000000..bd2e58f
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/FileOpenable.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.data;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.pdf.util.ErrorLog;
+import androidx.pdf.util.Preconditions;
+import androidx.pdf.util.Uris;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A file's data that is saved on disk (e.g. in cache).
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class FileOpenable implements Openable, Parcelable {
+
+ private static final String TAG = FileOpenable.class.getSimpleName();
+
+ /**
+ * Turns this {@link Uri} into a {@link File} if possible
+ *
+ * @throws IllegalArgumentException If the Uri was not a 'file:' one.
+ */
+ private static File getFile(Uri fileUri) {
+ Preconditions.checkArgument(Uris.isFileUri(fileUri),
+ "FileOpenable only valid for file Uris");
+ return new File(fileUri.getPath());
+ }
+
+ @Nullable
+ private final String mContentType;
+
+ private final File mFile;
+
+ /**
+ * Constructs an {@link Openable} from a file and a given content-type.
+ *
+ * @throws FileNotFoundException If the file does not exist.
+ */
+ public FileOpenable(File file, @Nullable String mimeType) throws FileNotFoundException {
+ if (!file.exists()) {
+ throw new FileNotFoundException("file does not exist");
+ }
+ this.mFile = Preconditions.checkNotNull(file);
+ this.mContentType = mimeType;
+ }
+
+ /**
+ * Constructs an {@link Openable} from a file Uri.
+ *
+ * @throws IllegalArgumentException If the Uri was not a 'file:' one.
+ * @throws FileNotFoundException If the file does not exist.
+ */
+ public FileOpenable(Uri uri) throws FileNotFoundException {
+ this(getFile(uri), Uris.extractContentType(uri));
+ }
+
+ @Override
+ public Open openWith(Opener opener) throws IOException {
+ return new Open() {
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return new FileInputStream(mFile);
+ }
+
+ @Override
+ public ParcelFileDescriptor getFd() throws IOException {
+ return ParcelFileDescriptor.open(mFile, ParcelFileDescriptor.MODE_READ_ONLY);
+ }
+
+ @Override
+ public long length() {
+ return mFile.length();
+ }
+
+ @Override
+ public String getContentType() {
+ return mContentType;
+ }
+ };
+ }
+
+ @Override
+ @Nullable
+ public String getContentType() {
+ return mContentType;
+ }
+
+ @Override
+ public long length() {
+ return mFile.length();
+ }
+
+ public String getFileName() {
+ return mFile.getName();
+ }
+
+ public Uri getFileUri() {
+ return Uri.fromFile(mFile);
+ }
+
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mFile.getPath());
+ dest.writeString(mContentType);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<FileOpenable> CREATOR =
+ new Creator<FileOpenable>() {
+ @Nullable
+ @Override
+ public FileOpenable createFromParcel(Parcel parcel) {
+ try {
+ return new FileOpenable(makeFile(parcel.readString()), parcel.readString());
+ } catch (FileNotFoundException e) {
+ ErrorLog.log(TAG, "File not found.", e);
+ return null;
+ }
+ }
+
+ private File makeFile(String filePath) {
+ return new File(filePath);
+ }
+
+ @Override
+ public FileOpenable[] newArray(int size) {
+ return new FileOpenable[size];
+ }
+ };
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/data/ListOfList.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/ListOfList.java
new file mode 100644
index 0000000..19368b1
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/ListOfList.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.data;
+
+import androidx.annotation.RestrictTo;
+import androidx.pdf.util.Preconditions;
+
+import java.util.AbstractList;
+import java.util.List;
+
+/**
+ * Represents a List of List of type T, but only uses two 1-dimensional
+ * lists internally to minimize overhead. Particularly useful for a large outer
+ * list that contains mostly single-element inner lists.
+ *
+ * @param <T> the type of the elements in the list
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class ListOfList<T> extends AbstractList<List<T>> {
+ private final List<T> mValues;
+ private final List<Integer> mIndexToFirstValue;
+
+ public ListOfList(List<T> values, List<Integer> indexToFirstValue) {
+ this.mValues = Preconditions.checkNotNull(values);
+ this.mIndexToFirstValue = Preconditions.checkNotNull(indexToFirstValue);
+ }
+
+ @Override
+ public List<T> get(int index) {
+ if (index < 0 || index >= mIndexToFirstValue.size()) {
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ int start = indexToFirstValue(index);
+ int stop = indexToFirstValue(index + 1);
+ Preconditions.checkState(start < stop, "Empty inner lists are not allowed.");
+ return mValues.subList(start, stop);
+ }
+
+ @Override
+ public int size() {
+ return mIndexToFirstValue.size();
+ }
+
+ /** Returns the flattened, one-dimensional list of all values. */
+ public List<T> flatten() {
+ return mValues;
+ }
+
+ protected int indexToFirstValue(int match) {
+ return (match < mIndexToFirstValue.size()) ? mIndexToFirstValue.get(match) : mValues.size();
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/data/Openable.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/Openable.java
new file mode 100644
index 0000000..67c3b03
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/Openable.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.data;
+
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A portable (i.e. {@link Parcelable}) handle to some data that can be opened with an
+ * {@link Opener}. In addition to an Uri, this also includes additional parameters to fully resolve
+ * requests to obtain the contents, e.g. a choice of one single content type.
+ * <br>
+ * {@link Open} instances may create {@link ParcelFileDescriptor}/{@link InputStream}
+ * instances lazily or eagerly. Create a new {@code Open} each time a new instance of either is
+ * required.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface Openable extends Parcelable {
+
+ /** Open this data and return an object that allows reading it. */
+ @NonNull
+ Open openWith(Opener opener) throws IOException;
+
+ /** Returns the length of the data in bytes (pre-connection, so might be an estimate). */
+ long length();
+
+ /** Returns the MIME type of the data (pre-connection, so might not be available). */
+ @Nullable
+ String getContentType();
+
+ /** An object that represents an open connection to obtain the data, and gives ways to read
+ * it. */
+ interface Open {
+
+ /**
+ * Gives an {@link InputStream} to read the data.
+ * <br>
+ * Callers take ownership of the returned InputStream and are responsible for closing it.
+ */
+ InputStream getInputStream() throws IOException;
+
+ /**
+ * Returns a file descriptor on this data, if available (e.g. doesn't work for http).
+ * <br>
+ * Callers take ownership of the returned ParcelFileDescriptor and are responsible for
+ * closing
+ * it.
+ */
+ ParcelFileDescriptor getFd() throws IOException;
+
+ /** Returns the declared length of the data. */
+ long length();
+
+ /** Returns the declared content-type of the data. */
+ String getContentType();
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/data/Opener.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/Opener.java
new file mode 100644
index 0000000..de535a0
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/Opener.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.data;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.graphics.Point;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.pdf.data.Openable.Open;
+import androidx.pdf.util.ContentUriOpener;
+import androidx.pdf.util.Preconditions;
+import androidx.pdf.util.Uris;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Opens an {@link Openable} into a ready-to-use {@link Open} object.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class Opener {
+
+ private static final String TAG = Opener.class.getSimpleName();
+
+ private final ContentUriOpener mContentOpener;
+
+ public Opener(Context ctx) {
+ Context app = ctx.getApplicationContext();
+ mContentOpener = new ContentUriOpener(app.getContentResolver());
+ }
+
+ @VisibleForTesting
+ public Opener(ContentUriOpener contentOpener) {
+ this.mContentOpener = contentOpener;
+ }
+
+ @NonNull
+ protected Open open(ContentOpenable content) throws FileNotFoundException {
+ String contentType = content.getContentType();
+ AssetFileDescriptor afd;
+ if (content.getSize() != null) {
+ // Opens an image preview, not the actual contents.
+ Point sizePoint = new Point(content.getSize().getWidth(),
+ content.getSize().getHeight());
+ afd = mContentOpener.openPreview(content.getContentUri(), sizePoint);
+ } else {
+ afd = mContentOpener.open(content.getContentUri(), contentType);
+ }
+ if (afd == null) {
+ throw new FileNotFoundException("Can't open " + content.getContentUri());
+ }
+ return new OpenContent(afd, contentType);
+ }
+
+ /** Opens the given local Uri and returns an {@link Open} object to read its data. */
+ @NonNull
+ public Open openLocal(Uri localUri) throws IOException {
+ Preconditions.checkNotNull(localUri);
+ if (Uris.isContentUri(localUri)) {
+ ContentOpenable content = new ContentOpenable(localUri);
+ return open(content);
+ } else if (Uris.isFileUri(localUri)) {
+ FileOpenable file = new FileOpenable(localUri);
+ return file.openWith(this);
+ } else {
+ throw new IllegalArgumentException("Uri in not local: " + localUri);
+ }
+ }
+
+ /** Returns the Exif orientation rotation value for a content thumbnail. */
+ public int getContentExifOrientation(ContentOpenable contentOpenable) {
+ return mContentOpener.getExifOrientation(contentOpenable.getContentUri());
+ }
+
+ /**
+ *
+ */
+ @Nullable
+ public String getContentType(Uri uri) {
+ if (Uris.isContentUri(uri)) {
+ return mContentOpener.getContentType(uri);
+ } else {
+ return Uris.extractContentType(uri);
+ }
+ }
+
+ /** An {@link Open} connection to data from a content provider. */
+ private static class OpenContent implements Open {
+
+ private final AssetFileDescriptor mAsset;
+ private final String mContentType;
+
+ OpenContent(AssetFileDescriptor asset, String type) {
+ this.mAsset = asset;
+ this.mContentType = type;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return mAsset.createInputStream();
+ }
+
+ @Override
+ public ParcelFileDescriptor getFd() {
+ return mAsset.getParcelFileDescriptor();
+ }
+
+ @Override
+ public long length() {
+ try {
+ return mAsset.getLength();
+ } catch (IllegalArgumentException iax) {
+ // TODO: Fix IAE. Check bug in legacy code.
+ return AssetFileDescriptor.UNKNOWN_LENGTH;
+ }
+ }
+
+ @Override
+ public String getContentType() {
+ return mContentType;
+ }
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/data/TextSelection.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/TextSelection.java
new file mode 100644
index 0000000..cdb2107
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/data/TextSelection.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.data;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.RestrictTo;
+import androidx.pdf.aidl.SelectionBoundary;
+
+/** Represents the selection of part of a piece of text - a start and a stop. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class TextSelection implements Parcelable {
+
+ public static final TextSelection EMPTY_SELECTION = new TextSelection(
+ SelectionBoundary.PAGE_START, SelectionBoundary.PAGE_START);
+
+ @SuppressWarnings("deprecation")
+ public static final Creator<TextSelection> CREATOR = new Creator<TextSelection>() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public TextSelection createFromParcel(Parcel parcel) {
+ return new TextSelection((SelectionBoundary) parcel.readParcelable(
+ SelectionBoundary.class.getClassLoader()),
+ (SelectionBoundary) parcel.readParcelable(
+ SelectionBoundary.class.getClassLoader()));
+ }
+
+ @Override
+ public TextSelection[] newArray(int size) {
+ return new TextSelection[size];
+ }
+ };
+
+ /** The start of the selection - index is inclusive. */
+ private final SelectionBoundary mStart;
+
+ /** The end of the selection - index is exclusive. */
+ private final SelectionBoundary mStop;
+
+ public TextSelection(SelectionBoundary start, SelectionBoundary stop) {
+ this.mStart = start;
+ this.mStop = stop;
+ }
+
+ public SelectionBoundary getStart() {
+ return mStart;
+ }
+
+ public SelectionBoundary getStop() {
+ return mStop;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("TextSelection(start=%s, stop=%s)", mStart, mStop);
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mStart, 0);
+ parcel.writeParcelable(mStop, 0);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/package-info.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/package-info.java
index 4a5a138..4c973e2 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/package-info.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/package-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,6 +15,5 @@
*/
/**
- * Insert package level documentation here
*/
package androidx.pdf;
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/AppInfo.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/AppInfo.java
new file mode 100644
index 0000000..815f63f
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/AppInfo.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.util;
+
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** Information about the installed app (package). */
+// TODO: Clean up this class to get package name alternatively
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class AppInfo {
+ private static final String TAG = "AppInfo";
+ private static final String NO_PACKAGE_NAME = "no.pkg";
+ private static final String NO_VERSION = "no-version";
+
+ private static AppInfo sAppInfo = new AppInfo();
+ private final String mAppVersion;
+ private final String mPackageName;
+
+ /** Bootstrap {@link AppInfo} from any {@link Context}. Can be called multiple times. */
+ public static synchronized void bootstrap(@NonNull Context context) {
+ sAppInfo = new AppInfo(context.getApplicationContext());
+ }
+
+ /** Singleton-style getter for the {@link AppInfo} instance. Always non-null. */
+ @NonNull
+ public static AppInfo get() {
+ return sAppInfo;
+ }
+
+ private AppInfo(Context appContext) {
+ PackageManager pm = appContext.getPackageManager();
+ String pkg = appContext.getPackageName();
+ PackageInfo pi = getPackageInfo(pm, pkg);
+ mAppVersion = pi.versionName != null ? pi.versionName : NO_VERSION;
+ mPackageName = pi.packageName != null ? pi.packageName : NO_PACKAGE_NAME;
+ }
+
+ private AppInfo() {
+ mPackageName = NO_PACKAGE_NAME;
+ mAppVersion = NO_VERSION;
+ }
+
+ public String getAppVersion() {
+ return mAppVersion;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ private static PackageInfo getPackageInfo(PackageManager pkgManager, String pkg) {
+ try {
+ return pkgManager.getPackageInfo(pkg, 0);
+ } catch (NameNotFoundException e) {
+ ErrorLog.log(TAG, String.format("Can't find our own package info?? %s", pkg), e);
+ return new PackageInfo() {
+ {
+ packageName = NO_PACKAGE_NAME;
+ versionName = NO_VERSION;
+ }
+ };
+ }
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ContentUriOpener.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ContentUriOpener.java
new file mode 100644
index 0000000..8d803b7
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ContentUriOpener.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.util;
+
+import android.content.ContentResolver;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.graphics.Point;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore.MediaColumns;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.io.FileNotFoundException;
+
+/**
+ * Opens content {@link Uri}s. Adds support for contents stored in the Media Provider (query name).
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class ContentUriOpener {
+ private static final String TAG = ContentUriOpener.class.getSimpleName();
+
+ private static final String[] NAME_COLUMNS = {MediaColumns.DISPLAY_NAME, MediaColumns.TITLE};
+
+ private final ContentResolver mContentResolver;
+
+ public ContentUriOpener(ContentResolver contentResolver) {
+ this.mContentResolver = contentResolver;
+ }
+
+ /** Opens an image preview (of the given size) of this content. */
+ public AssetFileDescriptor openPreview(Uri contentUri, Point size)
+ throws FileNotFoundException {
+ Preconditions.checkNotRunOnUIThread();
+ Bundle extraSize = new Bundle();
+ extraSize.putParcelable("android.content.extra.SIZE", size);
+ return mContentResolver.openTypedAssetFileDescriptor(contentUri, "image/*", extraSize);
+ }
+
+ /**
+ * Returns Exif orientation rotation value for this content.
+ *
+ * <p>Normally, we wouldn't need to get Exif orientation at this layer, as we would read and
+ * apply
+ * Exif data during bitmap decoding. However, when we request a lo-res thumbnail from a
+ * contentResolver, the Exif data of the original file is not transferred to the thumbnail,
+ * so we
+ * use this to get the Exif orientation from the original file and manually apply it to the
+ * thumbnail.
+ */
+ public int getExifOrientation(Uri contentUri) {
+ Preconditions.checkNotRunOnUIThread();
+ return ExifThumbnailUtils.getExifOrientation(contentUri, mContentResolver);
+ }
+
+ /**
+ * Opens this content as a specified content-type.
+ *
+ * @param contentUri the content Uri
+ * @param contentType the requested content type. If null, will use the default.
+ */
+ public AssetFileDescriptor open(Uri contentUri, String contentType)
+ throws FileNotFoundException {
+ Preconditions.checkNotRunOnUIThread();
+ if (contentType == null) {
+ contentType = getContentType(contentUri);
+ }
+ return mContentResolver.openTypedAssetFileDescriptor(contentUri, contentType, null);
+ }
+
+ /**
+ */
+ @Nullable
+ public String getContentType(Uri contentUri) {
+ try {
+ String[] availableTypes = mContentResolver.getStreamTypes(contentUri, "*/*");
+ String declaredType = mContentResolver.getType(contentUri);
+ // Sometimes the declared type is actually not available, then pick an available type
+ // instead.
+ String useType = null;
+ if (availableTypes != null) {
+ for (String type : availableTypes) {
+ if (useType == null) {
+ useType = type;
+ } else if (type.equals(declaredType)) {
+ useType = declaredType;
+ }
+ Log.v(TAG, String.format("available type: %s", type));
+ }
+ }
+ Log.v(TAG,
+ String.format("Use content type %s (declared was %s)", useType, declaredType));
+ if (useType == null) {
+ useType = declaredType;
+ }
+ return useType;
+ } catch (SecurityException se) {
+ ErrorLog.log(TAG, "content:" + contentUri.getAuthority(), se);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the various content types that this content can be streamed as. If the content
+ * provider
+ * doesn't declare any (usual for older ones), the main content type is returned, but there
+ * is no
+ * guarantee the corresponding content can be streamed.
+ */
+ public String[] getAvailableTypes(Uri contentUri) {
+ Preconditions.checkArgument(Uris.isContentUri(contentUri),
+ "Can't handle Uri " + contentUri.getScheme());
+ try {
+ String[] streamTypes = mContentResolver.getStreamTypes(contentUri, "*/*");
+ if (streamTypes != null) {
+ return streamTypes;
+ } else {
+ return new String[]{mContentResolver.getType(contentUri)};
+ }
+ } catch (SecurityException se) {
+ ErrorLog.log(TAG, "content:" + contentUri.getAuthority(), se);
+ return new String[]{};
+ }
+ }
+
+ /**
+ */
+ @Nullable
+ public static String extractContentName(ContentResolver contentResolver, Uri contentUri) {
+ Cursor cursor = null;
+ String[] queryColumn = new String[1];
+ String name = null;
+ for (String colName : NAME_COLUMNS) {
+ queryColumn[0] = colName;
+ try {
+ cursor = contentResolver.query(contentUri, queryColumn, null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ name = extractColumn(cursor, colName);
+ if (name != null) {
+ break;
+ }
+ }
+ } catch (Exception e) {
+ // Misbehaved app!
+ ErrorLog.log(TAG, "extractName", e);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ return name;
+ }
+
+ @Nullable
+ private static String extractColumn(Cursor cursor, String columnName) {
+ int columnIndex = cursor.getColumnIndex(columnName);
+ if (columnIndex >= 0) {
+ String result = cursor.getString(columnIndex);
+ if (!TextUtils.isEmpty(result)) {
+ return result;
+ }
+ }
+ return null;
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ErrorLog.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ErrorLog.java
new file mode 100644
index 0000000..0d82d35
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ErrorLog.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.util;
+
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Error logging utility. Logs errors on the internal Log system and tracks analytics.
+ *
+ * <p>Guidelines for this class:
+ *
+ * <ul>
+ * <li>Privacy: Do not track any PII (user personal info)
+ * <li>Only track Exception classes and generic messages (no getMessage(), no user info)
+ * <li>(Re-)throw RuntimeExceptions only in Dev builds, swallow them on release builds.
+ * </ul>
+ */
+// TODO: Track errors.
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class ErrorLog {
+
+ private ErrorLog() {}
+
+ /** Logs and tracks the error message. */
+ public static void log(String tag, String message) {
+ Log.e(tag, message);
+ }
+
+ /** Logs and tracks the error message. The 'details' arg is only used locally. */
+ public static void log(String tag, String message, String details) {
+ Log.e(tag, message + " " + details);
+ }
+
+ /**
+ * Logs and tracks the exception (tracks only the exception name). Doesn't re-throw the
+ * exception.
+ */
+ public static void log(String tag, String method, Throwable e) {
+ Log.e(tag, method, e);
+ }
+
+ /** Logs and tracks the exception. If running a dev build, re-throws the exception. */
+ public static void logAndThrow(String tag, String method, Throwable e) {
+ log(tag, method, e);
+ if (isDebuggable()) {
+ Log.e(tag, "In method " + method + ": ", e);
+ throw asRuntimeException(e);
+ }
+ }
+
+ /** Logs and tracks the error message. If running a dev build, throws a runtime exception. */
+ public static void logAndThrow(String tag, String method, String message) {
+ Log.e(tag, method + ": " + message);
+ if (isDebuggable()) {
+ throw new RuntimeException(message);
+ }
+ }
+
+ private static RuntimeException asRuntimeException(Throwable e) {
+ if (e instanceof RuntimeException) {
+ return (RuntimeException) e;
+ }
+ return new RuntimeException(e);
+ }
+
+ /** Logs and tracks the exception, then rethrows it. */
+ public static void logAndAlwaysThrow(String tag, String method, Throwable e) {
+ log(tag, method, e);
+ throw asRuntimeException(e);
+ }
+
+ /**
+ * A safer version of Preconditions.checkState that will log instead of throw in release builds.
+ *
+ * <p>Checks <code>condition</code> and {@link #logAndThrow} the error message as an {@link
+ * IllegalArgumentException} if it is false.
+ */
+ public static void checkState(boolean condition, String tag, String method, String message) {
+ if (!condition) {
+ IllegalArgumentException e = new IllegalArgumentException(message);
+ logAndThrow(tag, method, e);
+ }
+ }
+
+ /** Convert int value to range. */
+ public static String bracketValue(int value) {
+ if (value == 0) {
+ return "0";
+ } else if (value == 1) {
+ return "1";
+ } else if (value <= 10) {
+ return "up to 10";
+ } else if (value <= 100) {
+ return "up to 100";
+ } else if (value <= 1000) {
+ return "up to 1000";
+ } else {
+ return "more than 1000";
+ }
+ }
+
+ private static boolean isDebuggable() {
+ return "eng".equals(Build.TYPE) || "userdebug".equals(Build.TYPE);
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ExifThumbnailUtils.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ExifThumbnailUtils.java
new file mode 100644
index 0000000..2db1c86
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/ExifThumbnailUtils.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.util;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+
+import androidx.annotation.RestrictTo;
+import androidx.exifinterface.media.ExifInterface;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Handles extracting Exif data for content Uri thumbnails, which don't have exif data embedded.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class ExifThumbnailUtils {
+ private static final String TAG = ExifThumbnailUtils.class.getSimpleName();
+
+ private ExifThumbnailUtils() {
+ }
+
+ /**
+ * Get the {@link ExifInterface#TAG_ORIENTATION} value for the file represented by the
+ * contentUri.
+ *
+ * <p>Normally, we read the Exif orientation data from the file itself, but before Android Q,
+ * Exif
+ * data is not transferred to lo-res thumbnails requested from the ContentResolver, so this
+ * gives
+ * us a way to read the original file's Exif orientation and apply it manually to the Thumbnail.
+ */
+ public static int getExifOrientation(Uri contentUri, ContentResolver contentResolver) {
+ if (VERSION.SDK_INT >= VERSION_CODES.Q) {
+ // On Q and above, the system takes care of applying the exif orientation to the
+ // thumbnail.
+ return 0;
+ }
+ try {
+ InputStream is = contentResolver.openInputStream(contentUri);
+ if (is == null) {
+ return 0;
+ }
+ ExifInterface exifInterface = new ExifInterface(is);
+ return exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0);
+ } catch (IOException e) {
+ ErrorLog.log(TAG, "Unable to getExifOrientation.", e);
+ }
+ return 0;
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/Preconditions.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/Preconditions.java
new file mode 100644
index 0000000..a31af808
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/Preconditions.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.util;
+
+import android.os.Looper;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+/**
+ * Simple parameter checking.
+ *
+ * <p>Subset of functions from {@code com.google.common.base.Preconditions}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public final class Preconditions {
+ private Preconditions() {
+ }
+
+ /**
+ * Check if the value is not null.
+ */
+ public static <T> T checkNotNull(T parameter) {
+ return checkNotNull(parameter, null);
+ }
+
+ /**
+ * Check if the value is not null.
+ */
+ @CanIgnoreReturnValue
+ public static <T> T checkNotNull(T parameter, @Nullable String message) {
+ if (parameter == null) {
+ throw new NullPointerException(message);
+ }
+ return parameter;
+ }
+
+ /**
+ * Check if the state is true otherwise throws the string exception.
+ */
+ public static void checkState(boolean state) {
+ if (!state) {
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Check if the state is true otherwise throws the string exception.
+ */
+ public static void checkState(boolean state, String message) {
+ if (!state) {
+ throw new IllegalStateException(message);
+ }
+ }
+
+ /**
+ * Check if the argument is true otherwise throws the string exception.
+ */
+ public static void checkArgument(boolean state, String message)
+ throws IllegalArgumentException {
+ if (!state) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ /**
+ * Check if the process runs on Ui thread.
+ */
+ public static void checkRunOnUIThread() {
+ Preconditions.checkState(
+ Looper.getMainLooper().getThread() == Thread.currentThread(),
+ "Error - not running on the UI thread.");
+ }
+
+ /**
+ * Check if the process runs on Ui thread.
+ */
+ public static void checkNotRunOnUIThread() {
+ Preconditions.checkState(Looper.getMainLooper().getThread() != Thread.currentThread(),
+ "Error - running on the UI thread.");
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/StrictModeUtils.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/StrictModeUtils.java
new file mode 100644
index 0000000..98ef92e
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/StrictModeUtils.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.util;
+
+import android.os.StrictMode;
+import android.os.StrictMode.ThreadPolicy;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/** Utility class for temporarily disabling StrictMode to do I/O on UI threads. */
+// TODO: Cleanup this class as I/O ops should not be done on UI thread
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class StrictModeUtils {
+
+ private StrictModeUtils() {}
+
+ /** Temporarily disable StrictMode, execute a code block and return the result. */
+ public static <T> T bypassAndReturn(@NonNull CallbackWithReturnValue<T> callback) {
+ ThreadPolicy policy = StrictMode.getThreadPolicy();
+ try {
+ StrictMode.setThreadPolicy(
+ new ThreadPolicy.Builder().permitDiskReads().permitDiskWrites().build());
+ return callback.run();
+ } finally {
+ StrictMode.setThreadPolicy(policy);
+ }
+ }
+
+ /** Temporarily disable StrictMode, execute a code block. */
+ public static void bypass(@NonNull CallbackWithoutReturnValue callback) {
+ bypassAndReturn((CallbackWithReturnValue<?>) () -> {
+ callback.run();
+ return null;
+ });
+ }
+
+ /**
+ * Callback to be executed with return value.
+ *
+ * @param <T> the type of the return value
+ */
+ public interface CallbackWithReturnValue<T> {
+ /** Method to execute the callback. */
+ T run();
+ }
+
+ /**
+ * Callback to be executed without return values.
+ */
+ public interface CallbackWithoutReturnValue {
+ /** Method to execute the callback. */
+ void run();
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/Uris.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/Uris.java
new file mode 100644
index 0000000..4ccc361
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/Uris.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.util;
+
+import static android.content.ContentResolver.SCHEME_CONTENT;
+import static android.content.ContentResolver.SCHEME_FILE;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.webkit.MimeTypeMap;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Helpers with {@link Uri}s
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class Uris {
+ private Uris() {
+ }
+
+ public static final String SCHEME_HTTP = "http";
+ public static final String SCHEME_HTTPS = "https";
+ private static final String DATA_DIR = "data/data/";
+
+ /** Returns true if the Uri is an 'http:' one. */
+ public static boolean isHttp(@NonNull Uri uri) {
+ return SCHEME_HTTP.equals(uri.getScheme());
+ }
+
+ /** Returns true if the Uri is an 'https:' one. */
+ public static boolean isHttps(@NonNull Uri uri) {
+ return SCHEME_HTTPS.equals(uri.getScheme());
+ }
+
+ /** Returns true if the Uri is a remote (http/s) one. */
+ public static boolean isRemote(@NonNull Uri uri) {
+ String scheme = uri.getScheme();
+ return SCHEME_HTTP.equals(scheme) || SCHEME_HTTPS.equals(scheme);
+ }
+
+ /** Returns true if the Uri is a local (on-device) one. */
+ public static boolean isLocal(@NonNull Uri uri) {
+ String scheme = uri.getScheme();
+ return SCHEME_FILE.equals(scheme) || SCHEME_CONTENT.equals(scheme);
+ }
+
+ /** Returns true if the Uri is a 'content:' one. */
+ public static boolean isContentUri(@NonNull Uri uri) {
+ return SCHEME_CONTENT.equals(uri.getScheme());
+ }
+
+ /** Returns true if the Uri is a 'file:' one. */
+ public static boolean isFileUri(@NonNull Uri uri) {
+ return SCHEME_FILE.equals(uri.getScheme());
+ }
+
+ /**
+ * Extract a content-type from the given {@link Uri} by mapping its file extension to a known
+ * mime-type. This is based on the Uri only, it doesn't open any connection.
+ */
+ @Nullable
+ public static String extractContentType(@NonNull Uri uri) {
+ // Note: MimeTypeMap.getFileExtensionFromUrl(path); fails on unusual characters in path.
+ String name = uri.getLastPathSegment();
+ if (name != null) {
+ int dot = name.lastIndexOf('.');
+ if (dot >= 0) {
+ String extension = name.substring(dot + 1).toLowerCase();
+ return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Extracts a file name from the given {@link Uri} - either its last segment of the whole Uri.
+ * This is based on the Uri only, it doesn't open any connection.
+ */
+ @NonNull
+ public static String extractFileName(@NonNull Uri uri) {
+ String name = uri.getLastPathSegment();
+ if (name == null) {
+ name = uri.toString();
+ }
+ return name;
+ }
+
+ /**
+ * Extracts the name of the file from the Uri - either the last segment of the Uri, or for
+ * content
+ * Uris, the name must be queried from the contentResolver.
+ */
+ public static String extractName(@NonNull Uri uri, @NonNull ContentResolver contentResolver) {
+ if (Uris.isContentUri(uri)) {
+ return ContentUriOpener.extractContentName(contentResolver, uri);
+ } else {
+ return Uris.extractFileName(uri);
+ }
+ }
+
+ /**
+ * Returns true if the Uri is a 'file:' one, and if it points to a file in the
+ * data/data/package.name directory. We should not support these uris, as the request could be a
+ * QUICK_VIEW intent from a thirdparty app.
+ */
+ public static boolean isFileUriInSamePackageDataDir(Uri uri) {
+ return isFileUri(uri)
+ && uri.getPath() != null
+ && uri.getPath().contains(DATA_DIR + AppInfo.get().getPackageName());
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/persistence/Clock.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/persistence/Clock.java
new file mode 100644
index 0000000..83a5813
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/persistence/Clock.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.util.persistence;
+
+import androidx.annotation.RestrictTo;
+
+import com.google.errorprone.annotations.CheckReturnValue;
+
+/**
+ * Interface of SystemClock; real instances should just delegate the calls to the static methods,
+ * while test instances return values set manually; see {@link android.os.SystemClock}. In addition,
+ * this interface also has instance methods for {@link System#currentTimeMillis} and {@link
+ * System#nanoTime}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface Clock {
+ /**
+ * Returns the current system time in milliseconds since January 1, 1970 00:00:00 UTC. This
+ * method shouldn't be used for measuring timeouts or other elapsed time measurements, as
+ * changing the system time can affect the results.
+ *
+ * @return the local system time in milliseconds.
+ */
+ @CheckReturnValue
+ long currentTimeMillis();
+
+ /**
+ * Returns the current value of the running Java Virtual Machine's high-resolution time source,
+ * in nanoseconds.
+ *
+ * @return the current value of the running Java Virtual Machine's high-resolution time source,
+ * in nanoseconds
+ * @see System#nanoTime()
+ */
+ long nanoTime();
+
+ /**
+ * Returns the number of milliseconds that the current thread has been running. Does not advance
+ * while the thread's execution is suspended.
+ *
+ * @return milliseconds running in the current thread.
+ */
+ long currentThreadTimeMillis();
+
+ /**
+ * Returns milliseconds since boot, including time spent in sleep.
+ *
+ * @return elapsed milliseconds since boot.
+ */
+ long elapsedRealtime();
+
+ /**
+ * Returns nanoseconds since boot, including time spent in sleep.
+ *
+ * @return elapsed nanoseconds since boot.
+ */
+ long elapsedRealtimeNanos();
+
+ /**
+ * Returns milliseconds since boot, not counting time spent in deep sleep.
+ *
+ * @return milliseconds of non-sleep uptime since boot.
+ */
+ long uptimeMillis();
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/persistence/JsonEx.kt b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/persistence/JsonEx.kt
new file mode 100644
index 0000000..8a4fdf0
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/persistence/JsonEx.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.util.persistence
+
+import androidx.annotation.RestrictTo
+import org.json.JSONArray
+import org.json.JSONObject
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+fun JSONArray.toList(): List<JSONObject> {
+ // TODO: Simplify
+ // jsonArray is not an array so we have to loop and map ourselves
+ val list = mutableListOf<JSONObject?>()
+ for (i in 0 until length()) {
+ list.add(optJSONObject(i))
+ }
+ return list.filterNotNull()
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+fun Collection<JSONObject>.toJSONArray(): JSONArray = JSONArray().apply { forEach { put(it) } }
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+interface Json {
+ val json: JSONObject
+}
+
+@get:RestrictTo(RestrictTo.Scope.LIBRARY)
+val Collection<Json>.jsonString
+ get() = map { it.json }.toJSONArray().toString()
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/util/persistence/SystemClockImpl.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/persistence/SystemClockImpl.java
new file mode 100644
index 0000000..21299c7
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/util/persistence/SystemClockImpl.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.util.persistence;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.os.SystemClock;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Implementation of {@link Clock} that delegates to the system clock.
+ *
+ * <p>This class is intended for use only in contexts where injection is impossible. Where possible,
+ * prefer to simply inject a {@link Clock}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public final class SystemClockImpl implements Clock {
+
+ @Override
+ public long currentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ @Override
+ public long nanoTime() {
+ return System.nanoTime();
+ }
+
+ @Override
+ public long currentThreadTimeMillis() {
+ return SystemClock.currentThreadTimeMillis();
+ }
+
+ @Override
+ public long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ @Override
+ public long elapsedRealtimeNanos() {
+ return ElapsedRealtimeNanosImpl.elapsedRealtimeNanos();
+ }
+
+ @Override
+ public long uptimeMillis() {
+ return SystemClock.uptimeMillis();
+ }
+
+ // This companion object is required to work around AppReduce which prevents
+ // inlining of all the methods above.
+ //
+ // This actually *reduces* the number of classes in an optimized build by allowing
+ // Clock+SystemClockImpl to be removed.
+ private static final class ElapsedRealtimeNanosImpl {
+ /** Number of nanoseconds in a single millisecond. */
+ private static final long NS_IN_MS = 1_000_000L;
+
+ private static final boolean ELAPSED_REALTIME_NANOS_EXISTS = elapsedRealtimeNanosExists();
+
+ @TargetApi(17) // Guarded by elapsedRealtimeNanosExists()
+ static long elapsedRealtimeNanos() {
+ return ELAPSED_REALTIME_NANOS_EXISTS
+ ? SystemClock.elapsedRealtimeNanos()
+ // Note: this multiplication overflows after ~292 years of uptime, which is
+ // probably fine?
+ : SystemClock.elapsedRealtime() * NS_IN_MS;
+ }
+
+ private static boolean elapsedRealtimeNanosExists() {
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ long unused = SystemClock.elapsedRealtimeNanos();
+ return true;
+ }
+ } catch (Throwable ignored) {
+ // Some vendors have a SystemClock that doesn't contain elapsedRealtimeNanos()
+ // even though
+ // the SDK should contain it. Also if a test is running Android code but isn't an
+ // android
+ // test or Robolectric test, we don't want to throw here.
+ }
+ return false;
+ }
+
+ private ElapsedRealtimeNanosImpl() {
+ }
+ }
+}
+
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ChoiceOption.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ChoiceOption.java
new file mode 100644
index 0000000..d12fb7a
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/ChoiceOption.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.widget;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.RestrictTo;
+
+/** Represents a single option in a Combobox or Listbox PDF form widget. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class ChoiceOption implements Parcelable {
+
+ public static final Creator<ChoiceOption> CREATOR = new Creator<ChoiceOption>() {
+ @Override
+ public ChoiceOption createFromParcel(Parcel in) {
+ return new ChoiceOption(in);
+ }
+
+ @Override
+ public ChoiceOption[] newArray(int size) {
+ return new ChoiceOption[size];
+ }
+ };
+ private int mIndex;
+ private String mLabel;
+ private boolean mSelected;
+
+ public ChoiceOption(int index, String label, boolean selected) {
+ this.mIndex = index;
+ this.mLabel = label;
+ this.mSelected = selected;
+ }
+
+ /** Copy constructor. */
+ public ChoiceOption(ChoiceOption option) {
+ this(option.mIndex, option.mLabel, option.mSelected);
+ }
+
+ protected ChoiceOption(Parcel in) {
+ mIndex = in.readInt();
+ mLabel = in.readString();
+ mSelected = in.readInt() != 0;
+ }
+
+ public int getIndex() {
+ return mIndex;
+ }
+
+ public void setIndex(int index) {
+ this.mIndex = index;
+ }
+
+ public String getLabel() {
+ return mLabel;
+ }
+
+ public void setLabel(String label) {
+ this.mLabel = label;
+ }
+
+ public boolean isSelected() {
+ return mSelected;
+ }
+
+ public void setSelected(boolean selected) {
+ this.mSelected = selected;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mIndex);
+ dest.writeString(mLabel);
+ dest.writeInt(mSelected ? 1 : 0);
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/WidgetType.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/WidgetType.java
new file mode 100644
index 0000000..17d17e0
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/widget/WidgetType.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.widget;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.RestrictTo;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represents a type of form widget.
+ * Ids must be kept in sync with the definitions in third_party/pdfium/public/cpp/fpdf_formfill.h.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public enum WidgetType {
+ NONE(-1), UNKNOWN(0), PUSHBUTTON(1), CHECKBOX(2), RADIOBUTTON(3), COMBOBOX(4), LISTBOX(
+ 5), TEXTFIELD(6), SIGNATURE(7);
+
+ /**
+ * Map is preferred to SparseArray because Android classes cannot be static in Robolectric
+ * tests.
+ */
+ @SuppressLint("UseSparseArrays")
+ private static final Map<Integer, WidgetType> LOOKUP_MAP = new HashMap<>();
+
+ static {
+ for (WidgetType widgetType : WidgetType.values()) {
+ LOOKUP_MAP.put(widgetType.mId, widgetType);
+ }
+ }
+
+ private final int mId;
+
+ WidgetType(int id) {
+ this.mId = id;
+ }
+
+ /** Returns the WidgetType corresponding to the id. */
+ public static WidgetType of(int id) {
+ return LOOKUP_MAP.get(id);
+ }
+
+ public int getId() {
+ return mId;
+ }
+}
diff --git a/pdf/pdf-viewer/src/main/native/CMakeLists.txt b/pdf/pdf-viewer/src/main/native/CMakeLists.txt
new file mode 100644
index 0000000..e687666
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/native/CMakeLists.txt
@@ -0,0 +1,52 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# For more information about using CMake with Android Studio, read the
+# documentation: https://d.android.com/studio/projects/add-native-code.html.
+# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
+
+# Sets the minimum CMake version required for this project.
+cmake_minimum_required(VERSION 3.22.1)
+
+# Declares the project name. The project name can be accessed via ${PROJECT_NAME}.
+# Since this is the top level CMakeLists.txt, the project name is also accessible
+# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
+# build script scope).
+project("bitmapParcel")
+
+# Creates and names a library, sets it as either STATIC
+# or SHARED, and provides the relative paths to its source code.
+# You can define multiple libraries, and CMake builds them for you.
+# Gradle automatically packages shared libraries with your APK.
+#
+# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
+# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
+# is preferred for the same purpose.
+#
+# In order to load a library into your app from Java/Kotlin, you must call
+# System.loadLibrary() and pass the name of the library defined here;
+# for GameActivity/NativeActivity derived applications, the same library name must be
+# used in the AndroidManifest.xml file.
+add_library(${CMAKE_PROJECT_NAME} SHARED
+ # List C/C++ source files with relative paths to this CMakeLists.txt.
+ bitmap_parcel.cc
+ extractors.cc
+)
+
+# Specifies libraries CMake should link to your target library. You
+# can link libraries from various origins, such as libraries defined in this
+# build script, prebuilt third-party libraries, or Android system libraries.
+target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE log)
+target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE android)
+target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE jnigraphics)
diff --git a/pdf/pdf-viewer/src/main/native/bitmap_parcel.cc b/pdf/pdf-viewer/src/main/native/bitmap_parcel.cc
new file mode 100644
index 0000000..036437f
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/native/bitmap_parcel.cc
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://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.
+ */
+
+#include "bitmap_parcel.h"
+
+#include <android/bitmap.h>
+#include <jni.h>
+#include <stdint.h>
+
+#include "extractors.h"
+#include "logging.h"
+
+#define LOG_TAG "bitmap_parcel"
+
+using pdflib::Extractor;
+using pdflib::BufferReader;
+using pdflib::FdReader;
+
+namespace {
+
+ static const int kBytesPerPixel = 4;
+
+ bool FeedBitmap(JNIEnv *env, jobject jbitmap, Extractor *source);
+
+} // namespace
+
+extern "C" JNIEXPORT jboolean JNICALL
+Java_androidx_pdf_util_BitmapParcel_readIntoBitmap
+ (JNIEnv *env, jclass, jobject jbitmap, int fd) {
+ FdReader source(fd);
+ return FeedBitmap(env, jbitmap, &source);
+}
+
+namespace {
+
+ bool FeedBitmap(JNIEnv *env, jobject jbitmap, Extractor *source) {
+ void *bitmap_pixels;
+ int ret;
+ if ((ret = AndroidBitmap_lockPixels(env, jbitmap, &bitmap_pixels)) < 0) {
+ LOGE("AndroidBitmap_lockPixels() failed! error=%d", ret);
+ return false;
+ }
+
+
+ AndroidBitmapInfo info;
+ AndroidBitmap_getInfo(env, jbitmap, &info);
+
+ int num_bytes = info.width * info.height * kBytesPerPixel;
+ uint8_t *bitmap_bytes = reinterpret_cast<uint8_t *>(bitmap_pixels);
+ source->extract(bitmap_bytes, num_bytes);
+
+ AndroidBitmap_unlockPixels(env, jbitmap);
+ LOGV("Copied %d bytes into bitmap", num_bytes);
+ return true;
+ }
+
+} // namespace
diff --git a/pdf/pdf-viewer/src/main/native/bitmap_parcel.h b/pdf/pdf-viewer/src/main/native/bitmap_parcel.h
new file mode 100644
index 0000000..8f9832e
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/native/bitmap_parcel.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://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.
+ */
+
+#include <jni.h>
+
+#ifndef ANDROIDX_PDF_BITMAP_PARCEL_H_
+#define ANDROIDX_PDF_BITMAP_PARCEL_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+JNIEXPORT jboolean JNICALL
+Java_androidx_pdf_util_BitmapParcel_readIntoBitmap
+ (JNIEnv *env, jclass, jobject jbitmap, jint jfd);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // ANDROIDX_PDF_BITMAP_PARCEL_H_
diff --git a/pdf/pdf-viewer/src/main/native/extractors.cc b/pdf/pdf-viewer/src/main/native/extractors.cc
new file mode 100644
index 0000000..c7ead25
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/native/extractors.cc
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://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.
+ */
+
+#include "extractors.h"
+
+#include <stdint.h>
+#include <unistd.h>
+
+#include <cstring>
+
+#include "logging.h"
+
+#define LOG_TAG "extractor"
+
+namespace pdflib {
+
+ Extractor::~Extractor() {}
+
+ BufferWriter::~BufferWriter() {}
+
+ bool BufferWriter::extract(uint8_t *source, int num_bytes) {
+ memcpy(buffer_, source, num_bytes);
+ return true;
+ }
+
+ BufferReader::~BufferReader() {}
+
+ bool BufferReader::extract(uint8_t *destination, int num_bytes) {
+ memcpy(destination, buffer_, num_bytes);
+ return true;
+ }
+
+ FdWriter::~FdWriter() {}
+
+ bool FdWriter::extract(uint8_t *source, int num_bytes) {
+ LOGV("FdWriter Extracting %d bytes on %d", num_bytes, fd_);
+ bool ret = true;
+ while (num_bytes > 0) {
+ int len = write(fd_, source, num_bytes);
+ if (len == -1 || len == 0) {
+ ret = false;
+ LOGD("FdWriter extract failed at %d on %d", num_bytes, fd_);
+ break;
+ }
+ num_bytes -= len;
+ source += len;
+ }
+ close(fd_);
+ return ret;
+ }
+
+ FdReader::~FdReader() {}
+
+ bool FdReader::extract(uint8_t *destination, int num_bytes) {
+ LOGV("FdReader Extracting %d bytes from %d", num_bytes, fd_);
+ bool ret = true;
+ while (num_bytes > 0) {
+ int len = read(fd_, destination, num_bytes);
+ if (len == -1 || len == 0) {
+ ret = false;
+ LOGD("FdWriter extract failed at %d on %d", num_bytes, fd_);
+ break;
+ }
+ num_bytes -= len;
+ destination += len;
+ }
+ close(fd_);
+ return ret;
+ }
+
+} // namespace pdflib
diff --git a/pdf/pdf-viewer/src/main/native/extractors.h b/pdf/pdf-viewer/src/main/native/extractors.h
new file mode 100644
index 0000000..493ac51
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/native/extractors.h
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://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.
+ */
+
+#ifndef ANDROIDX_PDF_EXTRACTORS_H
+#define ANDROIDX_PDF_EXTRACTORS_H
+
+#include <stdint.h>
+
+namespace pdflib {
+
+// Interface for extracting bytes from or into an underlying something.
+ class Extractor {
+ public:
+ // Transfers {num_bytes} bytes between the underlying something and {buffer}.
+ virtual ~Extractor();
+
+ virtual bool extract(uint8_t *buffer, int num_bytes) = 0;
+ };
+
+
+// An Extractor that copies bytes on the given buffer.
+ class BufferWriter : public pdflib::Extractor {
+ public:
+ explicit BufferWriter(uint8_t *buffer) : buffer_(buffer) {}
+
+ ~BufferWriter() override;
+
+ bool extract(uint8_t *source, int num_bytes) override;
+
+ private:
+ uint8_t *buffer_;
+ };
+
+// An Extractor that copies bytes from the given buffer.
+ class BufferReader : public pdflib::Extractor {
+ public:
+ explicit BufferReader(uint8_t *buffer) : buffer_(buffer) {}
+
+ ~BufferReader() override;
+
+ bool extract(uint8_t *source, int num_bytes) override;
+
+ private:
+ uint8_t *buffer_;
+ };
+
+// An extractor that writes bytes on the given fd. It closes the fd thereafter.
+ class FdWriter : public pdflib::Extractor {
+ public:
+ explicit FdWriter(int fd) : fd_(fd) {}
+
+ ~FdWriter() override;
+
+ bool extract(uint8_t *source, int num_bytes) override;
+
+ private:
+ int fd_;
+ };
+
+// An extractor that read bytes from the given fd. It closes the fd thereafter.
+ class FdReader : public pdflib::Extractor {
+ public:
+ explicit FdReader(int fd) : fd_(fd) {}
+
+ ~FdReader() override;
+
+ bool extract(uint8_t *destination, int num_bytes) override;
+
+ private:
+ int fd_;
+ };
+
+} // namespace pdfClient
+
+#endif // ANDROIDX_PDF_EXTRACTORS_H
diff --git a/pdf/pdf-viewer/src/main/native/logging.h b/pdf/pdf-viewer/src/main/native/logging.h
new file mode 100644
index 0000000..a04bed4
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/native/logging.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://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.
+ */
+
+#ifndef ANDROIDX_PDF_LOGGING_H
+#define ANDROIDX_PDF_LOGGING_H
+
+#include <android/log.h>
+
+#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
+#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
+#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
+
+#endif //ANDROIDX_PDF_LOGGING_H
diff --git a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/Dimensions.aidl b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/Dimensions.aidl
new file mode 100644
index 0000000..8b1e85c
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/Dimensions.aidl
@@ -0,0 +1,5 @@
+package androidx.pdf.aidl;
+
+@JavaOnlyStableParcelable
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+parcelable Dimensions;
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/LinkRects.aidl b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/LinkRects.aidl
new file mode 100644
index 0000000..9229074
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/LinkRects.aidl
@@ -0,0 +1,5 @@
+package androidx.pdf.aidl;
+
+@JavaOnlyStableParcelable
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+parcelable LinkRects;
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/MatchRects.aidl b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/MatchRects.aidl
new file mode 100644
index 0000000..4130e06
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/MatchRects.aidl
@@ -0,0 +1,5 @@
+package androidx.pdf.aidl;
+
+@JavaOnlyStableParcelable
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+parcelable MatchRects;
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/PageSelection.aidl b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/PageSelection.aidl
new file mode 100644
index 0000000..e63ccf4
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/PageSelection.aidl
@@ -0,0 +1,5 @@
+package androidx.pdf.aidl;
+
+@JavaOnlyStableParcelable
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+parcelable PageSelection;
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/PdfDocumentRemote.aidl b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/PdfDocumentRemote.aidl
new file mode 100644
index 0000000..381fae7
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/PdfDocumentRemote.aidl
@@ -0,0 +1,42 @@
+package androidx.pdf.aidl;
+
+import android.graphics.Rect;
+
+import android.os.ParcelFileDescriptor;
+import androidx.pdf.aidl.Dimensions;
+import androidx.pdf.aidl.MatchRects;
+import androidx.pdf.aidl.PageSelection;
+import androidx.pdf.aidl.SelectionBoundary;
+import androidx.pdf.aidl.LinkRects;
+
+/** Remote interface around a PdfDocument. */
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+interface PdfDocumentRemote {
+ int create(in ParcelFileDescriptor pfd, String password);
+
+ int numPages();
+ Dimensions getPageDimensions(int pageNum);
+
+ boolean renderPage(int pageNum, in Dimensions size, boolean hideTextAnnots,
+ in ParcelFileDescriptor output);
+ boolean renderTile(int pageNum, int pageWidth, int pageHeight, int left, int top,
+ in Dimensions tileSize, boolean hideTextAnnots, in ParcelFileDescriptor output);
+
+ String getPageText(int pageNum);
+ List<String> getPageAltText(int pageNum);
+
+ MatchRects searchPageText(int pageNum, String query);
+ PageSelection selectPageText(int pageNum, in SelectionBoundary start, in SelectionBoundary stop);
+
+ LinkRects getPageLinks(int pageNum);
+
+ byte[] getPageGotoLinksByteArray(int pageNum);
+
+ boolean isPdfLinearized();
+
+ boolean cloneWithoutSecurity(in ParcelFileDescriptor destination);
+
+ boolean saveAs(in ParcelFileDescriptor destination);
+
+ // The PdfDocument is destroyed when this service is destroyed.
+}
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/SelectionBoundary.aidl b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/SelectionBoundary.aidl
new file mode 100644
index 0000000..efcaedb
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/stableAidl/androidx/pdf/aidl/SelectionBoundary.aidl
@@ -0,0 +1,5 @@
+package androidx.pdf.aidl;
+
+@JavaOnlyStableParcelable
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+parcelable SelectionBoundary;
\ No newline at end of file
diff --git a/pdf/pdf-viewer/src/test/assets/pdf/AcroJS.pdf b/pdf/pdf-viewer/src/test/assets/pdf/AcroJS.pdf
new file mode 100644
index 0000000..b08245e
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/assets/pdf/AcroJS.pdf
Binary files differ
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/aidl/LinkRectsTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/aidl/LinkRectsTest.java
new file mode 100644
index 0000000..9f93dc8
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/aidl/LinkRectsTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.aidl;
+
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Rect;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class LinkRectsTest {
+
+ private LinkRects mLinkRects = createLinkRects(5, new Integer[]{0, 2, 3},
+ new String[]{"http://first.com", "http://second.org", "http://third.net"});
+
+ @Test
+ public void testGet() {
+ assertThat(mLinkRects.size()).isEqualTo(3);
+
+ assertThat(mLinkRects.get(0).size()).isEqualTo(2);
+ assertThat(mLinkRects.get(1).size()).isEqualTo(1);
+ assertThat(mLinkRects.get(2).size()).isEqualTo(2);
+
+ try {
+ mLinkRects.get(3);
+ } catch (Exception e) {
+ // As expected.
+ }
+ }
+
+ @Test
+ public void testGetUrlAtPoint() {
+ assertThat(mLinkRects.getUrlAtPoint(100, 100)).isEqualTo("http://first.com");
+ assertThat(mLinkRects.getUrlAtPoint(200, 201)).isEqualTo("http://first.com");
+ assertThat(mLinkRects.getUrlAtPoint(301, 302)).isEqualTo("http://second.org");
+ assertThat(mLinkRects.getUrlAtPoint(403, 400)).isEqualTo("http://third.net");
+ assertThat(mLinkRects.getUrlAtPoint(502, 501)).isEqualTo("http://third.net");
+
+ assertThat(mLinkRects.getUrlAtPoint(600, 600)).isNull();
+ assertThat(mLinkRects.getUrlAtPoint(100, 200)).isNull();
+ assertThat(mLinkRects.getUrlAtPoint(510, 500)).isNull();
+ }
+
+ private static LinkRects createLinkRects(int numRects, Integer[] linkToRect, String[] urls) {
+ List<Rect> rects = new ArrayList<Rect>();
+ for (int i = 1; i <= numRects; i++) {
+ rects.add(new Rect(i * 100, i * 100, i * 101, i * 101));
+ }
+ return new LinkRects(rects, Arrays.asList(linkToRect), Arrays.asList(urls));
+ }
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/aidl/MatchRectsTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/aidl/MatchRectsTest.java
new file mode 100644
index 0000000..fd27016
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/aidl/MatchRectsTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.aidl;
+
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Rect;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class MatchRectsTest {
+
+ private MatchRects mMatchRects = createMatchRects(5, 0, 2, 3);
+
+ @Test
+ public void testGetRectsForMatch() {
+ assertThat(mMatchRects.size()).isEqualTo(3);
+
+ assertThat(mMatchRects.get(0).size()).isEqualTo(2);
+ assertThat(mMatchRects.get(1).size()).isEqualTo(1);
+ assertThat(mMatchRects.get(2).size()).isEqualTo(2);
+
+ assertThat(mMatchRects.get(0)).isEqualTo(mMatchRects.flatten().subList(0, 2));
+ assertThat(mMatchRects.get(1)).isEqualTo(mMatchRects.flatten().subList(2, 3));
+ assertThat(mMatchRects.get(2)).isEqualTo(mMatchRects.flatten().subList(3, 5));
+ }
+
+ @Test
+ public void testFlatten() {
+ List<Rect> rects = mMatchRects.flatten();
+ assertThat(rects.size()).isEqualTo(5);
+
+ List<Rect> rectsExcludingMatchOne = Arrays.asList(
+ rects.get(0), rects.get(1), rects.get(3), rects.get(4));
+ assertThat(mMatchRects.flattenExcludingMatch(1)).isEqualTo(rectsExcludingMatchOne);
+ }
+
+ private static MatchRects createMatchRects(int numRects, Integer... matchToRect) {
+ List<Rect> rects = new ArrayList<>();
+ List<Integer> charIndexes = new ArrayList<>();
+ for (int i = 0; i < numRects; i++) {
+ rects.add(new Rect(i * 100, i * 100, i * 101, i * 101));
+ charIndexes.add(i * 10);
+ }
+
+ return new MatchRects(rects, Arrays.asList(matchToRect), charIndexes);
+ }
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/data/ContentOpenableTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/data/ContentOpenableTest.java
new file mode 100644
index 0000000..99a7cfe
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/data/ContentOpenableTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.data;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+import android.os.Parcel;
+
+import androidx.pdf.aidl.Dimensions;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link ContentOpenable}. */
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class ContentOpenableTest {
+
+ private ContentOpenable mContentOpenable;
+ private final Uri mUri = Uri.parse("content://com.google.android.apps.test");
+ private final String mType = "application/pdf";
+ private final Dimensions mSize = new Dimensions(32, 43);
+
+ @Test
+ public void testParcellingWithOnlyUri() {
+ mContentOpenable = new ContentOpenable(mUri);
+ Parcel parcel = Parcel.obtain();
+ mContentOpenable.writeToParcel(parcel, 0);
+
+ parcel.setDataPosition(0);
+
+ ContentOpenable result = ContentOpenable.CREATOR.createFromParcel(parcel);
+ assertThat(result.toString()).isEqualTo(mContentOpenable.toString());
+ }
+
+ @Test
+ public void testParcellingWithUriAndType() {
+ mContentOpenable = new ContentOpenable(mUri, mType);
+ Parcel parcel = Parcel.obtain();
+ mContentOpenable.writeToParcel(parcel, 0);
+
+ parcel.setDataPosition(0);
+
+ ContentOpenable result = ContentOpenable.CREATOR.createFromParcel(parcel);
+ assertThat(result.toString()).isEqualTo(mContentOpenable.toString());
+ }
+
+ @Test
+ public void testParcellingWithUriAndSize() {
+ mContentOpenable = new ContentOpenable(mUri, mSize);
+ Parcel parcel = Parcel.obtain();
+ mContentOpenable.writeToParcel(parcel, 0);
+
+ parcel.setDataPosition(0);
+
+ ContentOpenable result = ContentOpenable.CREATOR.createFromParcel(parcel);
+ assertThat(result.toString()).isEqualTo(mContentOpenable.toString());
+ }
+}
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/data/FileOpenableTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/data/FileOpenableTest.java
new file mode 100644
index 0000000..d0c961a
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/data/FileOpenableTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.data;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Parcelable.Creator;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+/**
+ * Tests for {@link FileOpenable}.
+ */
+@RunWith(RobolectricTestRunner.class)
+public class FileOpenableTest {
+
+ private static final String PDF_MIME_TYPE = "application/pdf";
+ private static final String BASE_ASSET_PATH = "src/test/assets";
+
+ @Test
+ public void testFileUriOpenable() throws FileNotFoundException {
+ File file = getTestFile("pdf/AcroJS.pdf");
+ Uri uri = Uri.fromFile(file);
+ FileOpenable original = new FileOpenable(uri);
+ Openable clone = writeAndReadFromParcel(original, FileOpenable.CREATOR);
+ assertThat(clone).isNotNull();
+ assertThat(clone.getContentType()).isEqualTo(original.getContentType());
+ assertThat(clone.length()).isEqualTo(original.length());
+ }
+
+ @Test
+ public void testFileOpenable() throws FileNotFoundException {
+ File file = getTestFile("pdf/AcroJS.pdf");
+ FileOpenable original = new FileOpenable(file, PDF_MIME_TYPE);
+ FileOpenable clone = writeAndReadFromParcel(original, FileOpenable.CREATOR);
+ assertThat(clone).isNotNull();
+ assertThat(clone.getContentType()).isEqualTo(original.getContentType());
+ assertThat(clone.length()).isEqualTo(original.length());
+ }
+
+ private static <T extends Parcelable> T writeAndReadFromParcel(T openable, Creator<T> creator) {
+ Parcel parcel = Parcel.obtain();
+ openable.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ T clone = creator.createFromParcel(parcel);
+ return clone;
+ }
+
+ private File getTestFile(String name) {
+ return new File(BASE_ASSET_PATH, name);
+ }
+}
+
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/util/UrisTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/UrisTest.java
new file mode 100644
index 0000000..b9c980e
--- /dev/null
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/util/UrisTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Unit tests for {@link Uris}. */
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+public class UrisTest {
+
+ @Test
+ public void testExtractFileName() {
+ assertThat(Uris.extractFileName(Uri.parse("http://example.com/bigtable.pdf")))
+ .isEqualTo("bigtable.pdf");
+ assertThat(Uris.extractFileName(Uri.parse("http://example.com")))
+ .isEqualTo("http://example.com");
+ }
+
+ @Test
+ public void testHttp() {
+ Uri http = Uri.parse("http://example.com/bigtable.pdf");
+ assertThat(Uris.isHttp(http)).isTrue();
+ assertThat(Uris.isHttps(http)).isFalse();
+ assertThat(Uris.isRemote(http)).isTrue();
+ assertThat(Uris.isLocal(http)).isFalse();
+ assertThat(Uris.extractFileName(http)).isEqualTo("bigtable.pdf");
+ }
+
+ @Test
+ public void testHttps() {
+ Uri https = Uri.parse("https://example.com/bigtable.pdf");
+ assertThat(Uris.isHttps(https)).isTrue();
+ assertThat(Uris.isHttp(https)).isFalse();
+ assertThat(Uris.isRemote(https)).isTrue();
+ assertThat(Uris.isLocal(https)).isFalse();
+
+ assertThat(Uris.extractFileName(https)).isEqualTo("bigtable.pdf");
+ }
+
+ @Test
+ public void testContent() {
+ Uri content = Uri.parse("content:app/res");
+ assertThat(Uris.isHttp(content)).isFalse();
+ assertThat(Uris.isHttps(content)).isFalse();
+ assertThat(Uris.isRemote(content)).isFalse();
+ assertThat(Uris.isLocal(content)).isTrue();
+ assertThat(Uris.isContentUri(content)).isTrue();
+ assertThat(Uris.isFileUri(content)).isFalse();
+ }
+
+ @Test
+ public void testFile() {
+ Uri file = Uri.parse("file://sdcard/file.png");
+ assertThat(Uris.isHttp(file)).isFalse();
+ assertThat(Uris.isHttps(file)).isFalse();
+ assertThat(Uris.isRemote(file)).isFalse();
+ assertThat(Uris.isLocal(file)).isTrue();
+ assertThat(Uris.isContentUri(file)).isFalse();
+ assertThat(Uris.isFileUri(file)).isTrue();
+
+ assertThat(Uris.extractFileName(file)).isEqualTo("file.png");
+ }
+}
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index 72a0cf7..390858c 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -26,5 +26,5 @@
# Disable docs
androidx.enableDocumentation=false
androidx.playground.snapshotBuildId=11349412
-androidx.playground.metalavaBuildId=11595754
+androidx.playground.metalavaBuildId=11659063
androidx.studio.type=playground
\ No newline at end of file
diff --git a/preference/preference/res/values-ar/strings.xml b/preference/preference/res/values-ar/strings.xml
index c1f2e6c..2b35f9f 100644
--- a/preference/preference/res/values-ar/strings.xml
+++ b/preference/preference/res/values-ar/strings.xml
@@ -2,7 +2,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="v7_preference_on" msgid="89551595707643515">"مفعّلة"</string>
- <string name="v7_preference_off" msgid="3140233346420563315">"إيقاف"</string>
+ <string name="v7_preference_off" msgid="3140233346420563315">"غير مفعّل"</string>
<string name="expand_button_title" msgid="2427401033573778270">"إعدادات متقدمة"</string>
<string name="summary_collapsed_preference_list" msgid="9167775378838880170">"<xliff:g id="CURRENT_ITEMS">%1$s</xliff:g>، <xliff:g id="ADDED_ITEMS">%2$s</xliff:g>"</string>
<string name="copy" msgid="6083905920877235314">"نسخ"</string>
diff --git a/privacysandbox/activity/activity-core/src/main/java/androidx/privacysandbox/activity/core/SdkActivityLauncher.kt b/privacysandbox/activity/activity-core/src/main/java/androidx/privacysandbox/activity/core/SdkActivityLauncher.kt
index 8fe6567..9b3f4da 100644
--- a/privacysandbox/activity/activity-core/src/main/java/androidx/privacysandbox/activity/core/SdkActivityLauncher.kt
+++ b/privacysandbox/activity/activity-core/src/main/java/androidx/privacysandbox/activity/core/SdkActivityLauncher.kt
@@ -30,16 +30,18 @@
* and send the resulting bundle.
*
* SDKs can create launchers from an app-provided bundle by calling
- * [createFromLauncherInfo][androidx.privacysandbox.activity.provider.SdkActivityLauncherFactory.createFromLauncherInfo].
+ * [fromLauncherInfo][androidx.privacysandbox.activity.provider.SdkActivityLauncherFactory.fromLauncherInfo].
*/
interface SdkActivityLauncher {
/**
- * Tries to launch a new SDK activity using the given [sdkActivityHandlerToken],
- * assumed to be registered in the [SdkSandboxControllerCompat][androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat].
+ * Tries to launch a new SDK activity using the given [sdkActivityHandlerToken].
*
* Returns true if the SDK activity intent was sent, false if the launch was rejected for any
* reason.
+ *
+ * A valid [sdkActivityHandlerToken] can be obtained by registering an SDK activity with
+ * [registerSdkSandboxActivityHandler][androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat.registerSdkSandboxActivityHandler].
*/
suspend fun launchSdkActivity(sdkActivityHandlerToken: IBinder): Boolean
}
diff --git a/privacysandbox/ui/integration-tests/testapp/build.gradle b/privacysandbox/ui/integration-tests/testapp/build.gradle
index 86532f0..f6cbf25 100644
--- a/privacysandbox/ui/integration-tests/testapp/build.gradle
+++ b/privacysandbox/ui/integration-tests/testapp/build.gradle
@@ -42,6 +42,7 @@
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'com.google.android.material:material:1.6.0'
implementation "androidx.activity:activity-ktx:1.7.2"
+ implementation "androidx.drawerlayout:drawerlayout:1.2.0"
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation project(':privacysandbox:sdkruntime:sdkruntime-client')
implementation project(':privacysandbox:ui:integration-tests:testaidl')
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
new file mode 100644
index 0000000..2513b5d
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.integration.testapp
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat
+import androidx.privacysandbox.ui.client.view.SandboxedSdkView
+import androidx.privacysandbox.ui.integration.testaidl.ISdkApi
+
+/**
+ * Base fragment to be used for testing different manual flows.
+ *
+ * Create a new subclass of this for each independent flow you wish to test. There will only be
+ * one active fragment in the app's main activity at any time. Use [getSdkApi] to get a handle
+ * to the SDK.
+ */
+abstract class BaseFragment : Fragment() {
+ private lateinit var sdkApi: ISdkApi
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val sdkSandboxManager = SdkSandboxManagerCompat.from(requireContext())
+ val loadedSdks = sdkSandboxManager.getSandboxedSdks()
+ val loadedSdk = loadedSdks.firstOrNull { it.getSdkInfo()?.name == SDK_NAME }
+ if (loadedSdk == null) {
+ throw IllegalStateException("SDK not loaded")
+ }
+ sdkApi = ISdkApi.Stub.asInterface(loadedSdk.getInterface())
+ }
+
+ /**
+ * Returns a handle to the already loaded SDK.
+ */
+ fun getSdkApi(): ISdkApi {
+ return sdkApi
+ }
+
+ /**
+ * Called when the app's drawer layout state changes. When called, change the Z-order of
+ * any [SandboxedSdkView] owned by the fragment to ensure that the remote UI is not drawn over
+ * the drawer. If the drawer is open, move all remote views to Z-below, otherwise move them
+ * to Z-above.
+ */
+ abstract fun handleDrawerStateChange(isDrawerOpen: Boolean)
+
+ companion object {
+ private const val SDK_NAME = "androidx.privacysandbox.ui.integration.testsdkprovider"
+ const val TAG = "TestSandboxClient"
+ }
+}
diff --git a/lifecycle/lifecycle-viewmodel/src/jvmMain/kotlin/androidx/lifecycle/viewmodel/internal/Lock.jvm.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/EmptyFragment.kt
similarity index 74%
copy from lifecycle/lifecycle-viewmodel/src/jvmMain/kotlin/androidx/lifecycle/viewmodel/internal/Lock.jvm.kt
copy to privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/EmptyFragment.kt
index 9d20353..3a27fde 100644
--- a/lifecycle/lifecycle-viewmodel/src/jvmMain/kotlin/androidx/lifecycle/viewmodel/internal/Lock.jvm.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/EmptyFragment.kt
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-package androidx.lifecycle.viewmodel.internal
+package androidx.privacysandbox.ui.integration.testapp
-internal actual class Lock actual constructor() {
- actual inline fun <T> withLock(crossinline block: () -> T): T =
- synchronized(lock = this, block)
+class EmptyFragment : BaseFragment() {
+ override fun handleDrawerStateChange(isDrawerOpen: Boolean) {
+ }
}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
index 1498d46..ae13cb1 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
@@ -20,46 +20,32 @@
import android.os.ext.SdkExtensions
import android.util.Log
import android.view.View
-import android.view.ViewGroup
import android.widget.Button
-import android.widget.LinearLayout
-import android.widget.TextView
import androidx.annotation.RequiresExtension
import androidx.appcompat.app.AppCompatActivity
+import androidx.drawerlayout.widget.DrawerLayout
+import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
import androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat
import androidx.privacysandbox.sdkruntime.core.AppOwnedSdkSandboxInterfaceCompat
import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
-import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
-import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
-import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionState
-import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionStateChangedListener
-import androidx.privacysandbox.ui.client.view.SandboxedSdkView
-import androidx.privacysandbox.ui.integration.testaidl.ISdkApi
-import com.google.android.material.switchmaterial.SwitchMaterial
+import com.google.android.material.navigation.NavigationView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var sdkSandboxManager: SdkSandboxManagerCompat
-
- private lateinit var sdkApi: ISdkApi
-
- private lateinit var webViewBannerView: SandboxedSdkView
- private lateinit var bottomBannerView: SandboxedSdkView
- private lateinit var resizableBannerView: SandboxedSdkView
- private lateinit var newAdButton: Button
- private lateinit var resizeButton: Button
- private lateinit var resizeSdkButton: Button
- private lateinit var mediationSwitch: SwitchMaterial
- private lateinit var localWebViewToggle: SwitchMaterial
- private lateinit var appOwnedMediateeToggleButton: SwitchMaterial
+ private lateinit var drawerLayout: DrawerLayout
+ private lateinit var navigationView: NavigationView
+ private lateinit var currentFragment: BaseFragment
// TODO(b/257429573): Remove this line once fixed.
@RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 5)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
+ drawerLayout = findViewById(R.id.drawer)
+ navigationView = findViewById(R.id.navigation_view)
sdkSandboxManager = SdkSandboxManagerCompat.from(applicationContext)
@@ -67,9 +53,9 @@
CoroutineScope(Dispatchers.Default).launch {
try {
val loadedSdks = sdkSandboxManager.getSandboxedSdks()
- var loadedSdk = loadedSdks.firstOrNull { it.getSdkInfo()?.name == SDK_NAME }
+ val loadedSdk = loadedSdks.firstOrNull { it.getSdkInfo()?.name == SDK_NAME }
if (loadedSdk == null) {
- loadedSdk = sdkSandboxManager.loadSdk(SDK_NAME, Bundle())
+ sdkSandboxManager.loadSdk(SDK_NAME, Bundle())
sdkSandboxManager.loadSdk(MEDIATEE_SDK_NAME, Bundle())
sdkSandboxManager.registerAppOwnedSdkSandboxInterface(
AppOwnedSdkSandboxInterfaceCompat(
@@ -79,133 +65,72 @@
)
)
}
- onLoadedSdk(loadedSdk)
+ switchContentFragment(MainFragment(), "Main CUJ")
+ initializeOptionsButton()
+ initializeDrawer()
} catch (e: LoadSdkCompatException) {
- Log.i(TAG, "loadSdk failed with errorCode: " + e.loadSdkErrorCode +
- " and errorMsg: " + e.message)
+ Log.i(
+ TAG, "loadSdk failed with errorCode: " + e.loadSdkErrorCode +
+ " and errorMsg: " + e.message
+ )
}
}
}
- private fun onLoadedSdk(sandboxedSdk: SandboxedSdkCompat) {
- Log.i(TAG, "Loaded successfully")
- sdkApi = ISdkApi.Stub.asInterface(sandboxedSdk.getInterface())
-
- webViewBannerView = findViewById(R.id.webview_ad_view)
- bottomBannerView = SandboxedSdkView(this@MainActivity)
- resizableBannerView = findViewById(R.id.resizable_ad_view)
- newAdButton = findViewById(R.id.new_ad_button)
- resizeButton = findViewById(R.id.resize_button)
- resizeSdkButton = findViewById(R.id.resize_sdk_button)
- mediationSwitch = findViewById(R.id.mediation_switch)
- localWebViewToggle = findViewById(R.id.local_to_internet_switch)
- appOwnedMediateeToggleButton = findViewById(R.id.app_owned_mediatee_switch)
-
- loadWebViewBannerAd()
- loadBottomBannerAd()
- loadResizableBannerAd()
- }
-
- private fun loadWebViewBannerAd() {
- webViewBannerView.addStateChangedListener(StateChangeListener(webViewBannerView))
- webViewBannerView.setAdapter(SandboxedUiAdapterFactory.createFromCoreLibInfo(
- sdkApi.loadLocalWebViewAd()
- ))
-
- localWebViewToggle.setOnCheckedChangeListener { _: View, isChecked: Boolean ->
- if (isChecked) {
- webViewBannerView.setAdapter(SandboxedUiAdapterFactory.createFromCoreLibInfo(
- sdkApi.loadLocalWebViewAd()
- ))
+ private fun initializeOptionsButton() {
+ val button: Button = findViewById(R.id.toggle_drawer_button)
+ button.setOnClickListener {
+ if (drawerLayout.isOpen) {
+ drawerLayout.closeDrawers()
} else {
- webViewBannerView.setAdapter(SandboxedUiAdapterFactory.createFromCoreLibInfo(
- sdkApi.loadWebViewAd()
- ))
+ currentFragment.handleDrawerStateChange(true)
+ drawerLayout.open()
}
}
}
- private fun loadBottomBannerAd() {
- bottomBannerView.addStateChangedListener(StateChangeListener(bottomBannerView))
- bottomBannerView.layoutParams = findViewById<LinearLayout>(
- R.id.bottom_banner_container).layoutParams
- runOnUiThread {
- findViewById<LinearLayout>(R.id.bottom_banner_container).addView(bottomBannerView)
- }
- bottomBannerView.setAdapter(SandboxedUiAdapterFactory.createFromCoreLibInfo(
- sdkApi.loadTestAd(/*text=*/ "Hey!")
- ))
- }
-
- private fun loadResizableBannerAd() {
- resizableBannerView.addStateChangedListener(
- StateChangeListener(resizableBannerView))
- resizableBannerView.setAdapter(SandboxedUiAdapterFactory.createFromCoreLibInfo(
- sdkApi.loadTestAdWithWaitInsideOnDraw(/*text=*/ "Resizable View")
- ))
-
- var count = 1
- var loadMediateeFromApp = false
- appOwnedMediateeToggleButton.setOnCheckedChangeListener { _, isChecked ->
- loadMediateeFromApp = isChecked
- }
- newAdButton.setOnClickListener {
- if (mediationSwitch.isChecked) {
- resizableBannerView.setAdapter(
- SandboxedUiAdapterFactory.createFromCoreLibInfo(
- sdkApi.loadMediatedTestAd(count, loadMediateeFromApp)
- ))
- } else {
- resizableBannerView.setAdapter(
- SandboxedUiAdapterFactory.createFromCoreLibInfo(
- sdkApi.loadTestAdWithWaitInsideOnDraw(/*text=*/ "Ad #$count")
- ))
+ private fun initializeDrawer() {
+ drawerLayout.addDrawerListener(object : DrawerListener {
+ override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
}
- count++
- }
- val maxWidthPixels = 1000
- val maxHeightPixels = 1000
- val newSize = { currentSize: Int, maxSize: Int ->
- (currentSize + (100..200).random()) % maxSize
- }
-
- resizeButton.setOnClickListener {
- val newWidth = newSize(resizableBannerView.width, maxWidthPixels)
- val newHeight = newSize(resizableBannerView.height, maxHeightPixels)
- resizableBannerView.layoutParams =
- resizableBannerView.layoutParams.apply {
- width = newWidth
- height = newHeight
+ override fun onDrawerOpened(drawerView: View) {
+ // we handle this in the button onClick instead
}
- }
- resizeSdkButton.setOnClickListener {
- val newWidth = newSize(resizableBannerView.width, maxWidthPixels)
- val newHeight = newSize(resizableBannerView.height, maxHeightPixels)
- sdkApi.requestResize(newWidth, newHeight)
- }
- }
+ override fun onDrawerClosed(drawerView: View) {
+ currentFragment.handleDrawerStateChange(false)
+ }
- private inner class StateChangeListener(val view: SandboxedSdkView) :
- SandboxedSdkUiSessionStateChangedListener {
- override fun onStateChanged(state: SandboxedSdkUiSessionState) {
- Log.i(TAG, "UI session state changed to: " + state.toString())
- if (state is SandboxedSdkUiSessionState.Error) {
- // If the session fails to open, display the error.
- val parent = view.parent as ViewGroup
- val index = parent.indexOfChild(view)
- val textView = TextView(this@MainActivity)
- textView.text = state.throwable.message
-
- runOnUiThread {
- parent.removeView(view)
- parent.addView(textView, index)
+ override fun onDrawerStateChanged(newState: Int) {
+ }
+ })
+ navigationView.setNavigationItemSelectedListener {
+ val itemId = it.itemId
+ when (itemId) {
+ R.id.item_main -> switchContentFragment(MainFragment(), it.title)
+ R.id.item_empty -> switchContentFragment(EmptyFragment(), it.title)
+ else -> {
+ Log.e(TAG, "Invalid fragment option")
+ true
}
}
}
}
+ private fun switchContentFragment(fragment: BaseFragment, title: CharSequence?): Boolean {
+ drawerLayout.closeDrawers()
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.content_fragment_container, fragment).commit()
+ currentFragment = fragment
+ title?.let {
+ runOnUiThread {
+ setTitle(it)
+ }
+ }
+ return true
+ }
+
companion object {
private const val TAG = "TestSandboxClient"
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainFragment.kt
new file mode 100644
index 0000000..7944323
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainFragment.kt
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.integration.testapp
+
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
+import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionState
+import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionStateChangedListener
+import androidx.privacysandbox.ui.client.view.SandboxedSdkView
+import androidx.privacysandbox.ui.integration.testaidl.ISdkApi
+import com.google.android.material.switchmaterial.SwitchMaterial
+
+class MainFragment : BaseFragment() {
+
+ private lateinit var webViewBannerView: SandboxedSdkView
+ private lateinit var bottomBannerView: SandboxedSdkView
+ private lateinit var resizableBannerView: SandboxedSdkView
+ private lateinit var newAdButton: Button
+ private lateinit var resizeButton: Button
+ private lateinit var resizeSdkButton: Button
+ private lateinit var mediationSwitch: SwitchMaterial
+ private lateinit var localWebViewToggle: SwitchMaterial
+ private lateinit var appOwnedMediateeToggleButton: SwitchMaterial
+ private lateinit var inflatedView: View
+ private lateinit var sdkApi: ISdkApi
+
+ override fun handleDrawerStateChange(isDrawerOpen: Boolean) {
+ webViewBannerView.orderProviderUiAboveClientUi(!isDrawerOpen)
+ bottomBannerView.orderProviderUiAboveClientUi(!isDrawerOpen)
+ resizableBannerView.orderProviderUiAboveClientUi(!isDrawerOpen)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ inflatedView = inflater.inflate(R.layout.fragment_main, container, false)
+ sdkApi = getSdkApi()
+ onLoadedSdk()
+ return inflatedView
+ }
+
+ private fun onLoadedSdk() {
+ webViewBannerView = inflatedView.findViewById(R.id.webview_ad_view)
+ bottomBannerView = SandboxedSdkView(requireActivity())
+ resizableBannerView = inflatedView.findViewById(R.id.resizable_ad_view)
+ newAdButton = inflatedView.findViewById(R.id.new_ad_button)
+ resizeButton = inflatedView.findViewById(R.id.resize_button)
+ resizeSdkButton = inflatedView.findViewById(R.id.resize_sdk_button)
+ mediationSwitch = inflatedView.findViewById(R.id.mediation_switch)
+ localWebViewToggle = inflatedView.findViewById(R.id.local_to_internet_switch)
+ appOwnedMediateeToggleButton = inflatedView.findViewById(R.id.app_owned_mediatee_switch)
+
+ loadWebViewBannerAd()
+ loadBottomBannerAd()
+ loadResizableBannerAd()
+ }
+
+ private fun loadWebViewBannerAd() {
+ webViewBannerView.addStateChangedListener(StateChangeListener(webViewBannerView))
+ webViewBannerView.setAdapter(
+ SandboxedUiAdapterFactory.createFromCoreLibInfo(
+ sdkApi.loadLocalWebViewAd()
+ ))
+
+ localWebViewToggle.setOnCheckedChangeListener { _: View, isChecked: Boolean ->
+ if (isChecked) {
+ webViewBannerView.setAdapter(
+ SandboxedUiAdapterFactory.createFromCoreLibInfo(
+ sdkApi.loadLocalWebViewAd()
+ ))
+ } else {
+ webViewBannerView.setAdapter(
+ SandboxedUiAdapterFactory.createFromCoreLibInfo(
+ sdkApi.loadWebViewAd()
+ ))
+ }
+ }
+ }
+
+ private fun loadBottomBannerAd() {
+ bottomBannerView.addStateChangedListener(StateChangeListener(bottomBannerView))
+ bottomBannerView.layoutParams = inflatedView.findViewById<LinearLayout>(
+ R.id.bottom_banner_container).layoutParams
+ requireActivity().runOnUiThread {
+ inflatedView.findViewById<LinearLayout>(
+ R.id.bottom_banner_container).addView(bottomBannerView)
+ }
+ bottomBannerView.setAdapter(
+ SandboxedUiAdapterFactory.createFromCoreLibInfo(
+ sdkApi.loadTestAd(/*text=*/ "Hey!")
+ ))
+ }
+
+ private fun loadResizableBannerAd() {
+ resizableBannerView.addStateChangedListener(
+ StateChangeListener(resizableBannerView))
+ resizableBannerView.setAdapter(
+ SandboxedUiAdapterFactory.createFromCoreLibInfo(
+ sdkApi.loadTestAdWithWaitInsideOnDraw(/*text=*/ "Resizable View")
+ ))
+
+ var count = 1
+ var loadMediateeFromApp = false
+ appOwnedMediateeToggleButton.setOnCheckedChangeListener { _, isChecked ->
+ loadMediateeFromApp = isChecked
+ }
+ newAdButton.setOnClickListener {
+ if (mediationSwitch.isChecked) {
+ resizableBannerView.setAdapter(
+ SandboxedUiAdapterFactory.createFromCoreLibInfo(
+ sdkApi.loadMediatedTestAd(count, loadMediateeFromApp)
+ ))
+ } else {
+ resizableBannerView.setAdapter(
+ SandboxedUiAdapterFactory.createFromCoreLibInfo(
+ sdkApi.loadTestAdWithWaitInsideOnDraw(/*text=*/ "Ad #$count")
+ ))
+ }
+ count++
+ }
+
+ val maxWidthPixels = 1000
+ val maxHeightPixels = 1000
+ val newSize = { currentSize: Int, maxSize: Int ->
+ (currentSize + (100..200).random()) % maxSize
+ }
+
+ resizeButton.setOnClickListener {
+ val newWidth = newSize(resizableBannerView.width, maxWidthPixels)
+ val newHeight = newSize(resizableBannerView.height, maxHeightPixels)
+ resizableBannerView.layoutParams =
+ resizableBannerView.layoutParams.apply {
+ width = newWidth
+ height = newHeight
+ }
+ }
+
+ resizeSdkButton.setOnClickListener {
+ val newWidth = newSize(resizableBannerView.width, maxWidthPixels)
+ val newHeight = newSize(resizableBannerView.height, maxHeightPixels)
+ sdkApi.requestResize(newWidth, newHeight)
+ }
+ }
+
+ private inner class StateChangeListener(val view: SandboxedSdkView) :
+ SandboxedSdkUiSessionStateChangedListener {
+ override fun onStateChanged(state: SandboxedSdkUiSessionState) {
+ Log.i(TAG, "UI session state changed to: $state")
+ if (state is SandboxedSdkUiSessionState.Error) {
+ // If the session fails to open, display the error.
+ val parent = view.parent as ViewGroup
+ val index = parent.indexOfChild(view)
+ val textView = TextView(requireActivity())
+ textView.text = state.throwable.message
+
+ requireActivity().runOnUiThread {
+ parent.removeView(view)
+ parent.addView(textView, index)
+ }
+ }
+ }
+ }
+}
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/action_menu.xml b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/action_menu.xml
new file mode 100644
index 0000000..836375e
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/action_menu.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/item_main"
+ android:title="Main CUJ" />
+ <item
+ android:id="@+id/item_empty"
+ android:title="Empty CUJ" />
+</menu>
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml
index 6c5e337..1b2df9f 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -15,121 +15,32 @@
limitations under the License.
-->
-<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
+<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:weightSum="5"
- android:orientation="vertical"
tools:context=".MainActivity">
-
- <ScrollView
- android:id="@+id/scroll_view"
- android:layout_width="match_parent"
- android:layout_weight="4"
- android:layout_height="0dp"
- android:orientation="vertical">
- <LinearLayout
- android:id="@+id/ad_layout"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
-
- <TextView
- android:id="@+id/headerTextView"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="16dp"
- android:layout_marginStart="16dp"
- android:layout_marginEnd="16dp"
- android:text="@string/app_name"/>
-
- <com.google.android.material.switchmaterial.SwitchMaterial
- android:id="@+id/local_to_internet_switch"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/local_to_internet_switch"
- android:checked="true"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
-
- <androidx.privacysandbox.ui.client.view.SandboxedSdkView
- android:id="@+id/webview_ad_view"
- android:background="#FF0000"
- android:layout_width="match_parent"
- android:layout_marginStart="16dp"
- android:layout_marginEnd="16dp"
- android:layout_marginBottom="16dp"
- android:layout_marginTop="16dp"
- android:layout_height="400dp" />
-
- <androidx.privacysandbox.ui.client.view.SandboxedSdkView
- android:id="@+id/resizable_ad_view"
- android:layout_width="wrap_content"
- android:layout_height="100dp"
- android:layout_marginBottom="16dp"
- android:layout_marginEnd="16dp"
- android:layout_marginStart="16dp"
- android:layout_marginTop="16dp"
- android:background="#FF0000" />
-
- <com.google.android.material.switchmaterial.SwitchMaterial
- android:id="@+id/mediation_switch"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:text="@string/mediation_switch"
- android:checked="false" />
-
- <com.google.android.material.switchmaterial.SwitchMaterial
- android:id="@+id/app_owned_mediatee_switch"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:text="@string/app_owned_mediatee_switch"
- android:checked="false" />
-
- <LinearLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
-
- <Button
- android:id="@+id/new_ad_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:text="new_ad"
- android:textAllCaps="false" />
-
- <Button
- android:id="@+id/resize_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="5dp"
- android:text="resize"
- android:textAllCaps="false" />
-
- <Button
- android:id="@+id/resize_sdk_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="5dp"
- android:text="@string/resizeFromSdk"
- android:textAllCaps="false" />
- </LinearLayout>
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/long_text" />
- </LinearLayout>
- </ScrollView>
<LinearLayout
android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:id="@+id/bottom_banner_container"
- android:orientation="vertical" />
-</androidx.appcompat.widget.LinearLayoutCompat>
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <Button
+ android:layout_width="match_parent"
+ android:layout_height="50dp"
+ android:id="@+id/toggle_drawer_button"
+ android:text="Open Options"/>
+ <FrameLayout
+ android:id="@+id/content_fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+ </LinearLayout>
+ <com.google.android.material.navigation.NavigationView
+ android:id="@+id/navigation_view"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ app:menu="@layout/action_menu" />
+</androidx.drawerlayout.widget.DrawerLayout>
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/fragment_main.xml b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/fragment_main.xml
new file mode 100644
index 0000000..1b769e8
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/res/layout/fragment_main.xml
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:weightSum="5"
+ android:orientation="vertical">
+
+ <ScrollView
+ android:id="@+id/scroll_view"
+ android:layout_width="match_parent"
+ android:layout_weight="4"
+ android:layout_height="0dp"
+ android:orientation="vertical">
+ <LinearLayout
+ android:id="@+id/ad_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/headerTextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:text="@string/app_name"/>
+
+ <com.google.android.material.switchmaterial.SwitchMaterial
+ android:id="@+id/local_to_internet_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/local_to_internet_switch"
+ android:checked="true"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <androidx.privacysandbox.ui.client.view.SandboxedSdkView
+ android:id="@+id/webview_ad_view"
+ android:background="#FF0000"
+ android:layout_width="match_parent"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginBottom="16dp"
+ android:layout_marginTop="16dp"
+ android:layout_height="400dp" />
+
+ <androidx.privacysandbox.ui.client.view.SandboxedSdkView
+ android:id="@+id/resizable_ad_view"
+ android:layout_width="wrap_content"
+ android:layout_height="100dp"
+ android:layout_marginBottom="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:background="#FF0000" />
+
+ <com.google.android.material.switchmaterial.SwitchMaterial
+ android:id="@+id/mediation_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:text="@string/mediation_switch"
+ android:checked="false" />
+
+ <com.google.android.material.switchmaterial.SwitchMaterial
+ android:id="@+id/app_owned_mediatee_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:text="@string/app_owned_mediatee_switch"
+ android:checked="false" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/new_ad_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:text="new_ad"
+ android:textAllCaps="false" />
+
+ <Button
+ android:id="@+id/resize_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="5dp"
+ android:text="resize"
+ android:textAllCaps="false" />
+
+ <Button
+ android:id="@+id/resize_sdk_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="5dp"
+ android:text="@string/resizeFromSdk"
+ android:textAllCaps="false" />
+ </LinearLayout>
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/long_text" />
+ </LinearLayout>
+ </ScrollView>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:id="@+id/bottom_banner_container"
+ android:orientation="vertical" />
+</androidx.appcompat.widget.LinearLayoutCompat>
\ No newline at end of file
diff --git a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseAutoMigrationTest.kt b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseAutoMigrationTest.kt
index 384ee4a..a5a991a 100644
--- a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseAutoMigrationTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseAutoMigrationTest.kt
@@ -23,7 +23,6 @@
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Entity
-import androidx.room.Ignore
import androidx.room.Insert
import androidx.room.PrimaryKey
import androidx.room.Query
@@ -31,6 +30,7 @@
import androidx.room.Update
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.use
+import kotlin.test.Ignore
import kotlin.test.Test
import kotlinx.coroutines.test.runTest
@@ -39,7 +39,7 @@
abstract fun getRoomDatabase(): AutoMigrationDatabase
@Test
- @Ignore // TODO (b/331622149) Investigate, there seems to be an issue in linuxX64Test
+ @Ignore // TODO (b/331622149) Investigate, there seems to be an issue in native.
fun migrateFromV1ToLatest() = runTest {
val migrationTestHelper = getTestHelper()
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LegacyIdentityHashTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LegacyIdentityHashTest.java
new file mode 100644
index 0000000..ed0eee1
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/LegacyIdentityHashTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.integration.testapp.test;
+
+import android.content.Context;
+
+import androidx.room.Database;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.room.RoomMasterTable;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LegacyIdentityHashTest {
+
+ private Context mTargetContext =
+ InstrumentationRegistry.getInstrumentation().getTargetContext();
+ private String mDatabaseName = "legacy-test.db";
+
+ @Before
+ public void setup() {
+ mTargetContext.deleteDatabase(mDatabaseName);
+ }
+
+ @After
+ public void teardown() {
+ mTargetContext.deleteDatabase(mDatabaseName);
+ }
+
+ @Test
+ public void openDatabaseWithLegacyHash() {
+ RoomDatabase.Builder<LegacyDatabase> dbBuilder = Room.databaseBuilder(
+ InstrumentationRegistry.getInstrumentation().getTargetContext(),
+ LegacyDatabase.class,
+ "legacy-test.db"
+ );
+
+ LegacyDatabase newDb = dbBuilder.build();
+ String insertQuery =
+ RoomMasterTable.createInsertQuery("d5249b2a35eb34d6c54d25ca1b7b9b74");
+ newDb.getOpenHelper().getWritableDatabase().execSQL(insertQuery);
+ newDb.close();
+
+ LegacyDatabase legacyDb = dbBuilder.build();
+ legacyDb.getOpenHelper().getWritableDatabase(); // force open db
+ legacyDb.close();
+ }
+
+ @Database(entities = TestDataEntity.class, version = 1, exportSchema = false)
+ abstract static class LegacyDatabase extends RoomDatabase {
+
+ }
+
+ @Entity
+ static class TestDataEntity {
+ @PrimaryKey @SuppressWarnings("unused") Long mId;
+ }
+}
diff --git a/room/room-common/api/current.ignore b/room/room-common/api/current.ignore
new file mode 100644
index 0000000..2aeb40f
--- /dev/null
+++ b/room/room-common/api/current.ignore
@@ -0,0 +1,27 @@
+// Baseline format: 1.0
+ChangedType: androidx.room.AutoMigration#spec():
+ Method androidx.room.AutoMigration.spec has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.Database#entities():
+ Method androidx.room.Database.entities has changed return type from java.lang.Class<?>[] to kotlin.reflect.KClass<?>[]
+ChangedType: androidx.room.Database#views():
+ Method androidx.room.Database.views has changed return type from java.lang.Class<?>[] to kotlin.reflect.KClass<?>[]
+ChangedType: androidx.room.Delete#entity():
+ Method androidx.room.Delete.entity has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.ForeignKey#entity():
+ Method androidx.room.ForeignKey.entity has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.Fts4#contentEntity():
+ Method androidx.room.Fts4.contentEntity has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.Insert#entity():
+ Method androidx.room.Insert.entity has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.Junction#value():
+ Method androidx.room.Junction.value has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.RawQuery#observedEntities():
+ Method androidx.room.RawQuery.observedEntities has changed return type from java.lang.Class<?>[] to kotlin.reflect.KClass<?>[]
+ChangedType: androidx.room.Relation#entity():
+ Method androidx.room.Relation.entity has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.TypeConverters#value():
+ Method androidx.room.TypeConverters.value has changed return type from java.lang.Class<?>[] to kotlin.reflect.KClass<?>[]
+ChangedType: androidx.room.Update#entity():
+ Method androidx.room.Update.entity has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.Upsert#entity():
+ Method androidx.room.Upsert.entity has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
diff --git a/room/room-common/api/current.txt b/room/room-common/api/current.txt
index 15d620b..4d8e5c4 100644
--- a/room/room-common/api/current.txt
+++ b/room/room-common/api/current.txt
@@ -3,10 +3,10 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface AutoMigration {
method public abstract int from();
- method public abstract Class<?> spec() default java.lang.Object;
+ method public abstract kotlin.reflect.KClass<?> spec() default java.lang.Object;
method public abstract int to();
property public abstract int from;
- property public abstract Class<?> spec;
+ property public abstract kotlin.reflect.KClass<?> spec;
property public abstract int to;
}
@@ -79,15 +79,15 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface Database {
method public abstract androidx.room.AutoMigration[] autoMigrations();
- method public abstract Class<?>[] entities();
+ method public abstract kotlin.reflect.KClass<?>[] entities();
method public abstract boolean exportSchema() default true;
method public abstract int version();
- method public abstract Class<?>[] views();
+ method public abstract kotlin.reflect.KClass<?>[] views();
property public abstract androidx.room.AutoMigration[] autoMigrations;
- property public abstract Class<?>[] entities;
+ property public abstract kotlin.reflect.KClass<?>[] entities;
property public abstract boolean exportSchema;
property public abstract int version;
- property public abstract Class<?>[] views;
+ property public abstract kotlin.reflect.KClass<?>[] views;
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface DatabaseView {
@@ -98,8 +98,8 @@
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Delete {
- method public abstract Class<?> entity() default java.lang.Object;
- property public abstract Class<?> entity;
+ method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
+ property public abstract kotlin.reflect.KClass<?> entity;
}
@java.lang.annotation.Repeatable(DeleteColumn.Entries::class) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface DeleteColumn {
@@ -147,13 +147,13 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={}) public @interface ForeignKey {
method public abstract String[] childColumns();
method public abstract boolean deferred() default false;
- method public abstract Class<?> entity();
+ method public abstract kotlin.reflect.KClass<?> entity();
method @androidx.room.ForeignKey.Action public abstract int onDelete() default androidx.room.ForeignKey.NO_ACTION;
method @androidx.room.ForeignKey.Action public abstract int onUpdate() default androidx.room.ForeignKey.NO_ACTION;
method public abstract String[] parentColumns();
property public abstract String[] childColumns;
property public abstract boolean deferred;
- property public abstract Class<?> entity;
+ property public abstract kotlin.reflect.KClass<?> entity;
property @androidx.room.ForeignKey.Action public abstract int onDelete;
property @androidx.room.ForeignKey.Action public abstract int onUpdate;
property public abstract String[] parentColumns;
@@ -184,7 +184,7 @@
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface Fts4 {
- method public abstract Class<?> contentEntity() default java.lang.Object;
+ method public abstract kotlin.reflect.KClass<?> contentEntity() default java.lang.Object;
method public abstract String languageId() default "";
method public abstract androidx.room.FtsOptions.MatchInfo matchInfo() default androidx.room.FtsOptions.MatchInfo.FTS4;
method public abstract String[] notIndexed();
@@ -192,7 +192,7 @@
method public abstract int[] prefix();
method public abstract String tokenizer() default androidx.room.FtsOptions.TOKENIZER_SIMPLE;
method public abstract String[] tokenizerArgs();
- property public abstract Class<?> contentEntity;
+ property public abstract kotlin.reflect.KClass<?> contentEntity;
property public abstract String languageId;
property public abstract androidx.room.FtsOptions.MatchInfo matchInfo;
property public abstract String[] notIndexed;
@@ -240,19 +240,19 @@
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Insert {
- method public abstract Class<?> entity() default java.lang.Object;
+ method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
- property public abstract Class<?> entity;
+ property public abstract kotlin.reflect.KClass<?> entity;
property @androidx.room.OnConflictStrategy public abstract int onConflict;
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={}) public @interface Junction {
method public abstract String entityColumn() default "";
method public abstract String parentColumn() default "";
- method public abstract Class<?> value();
+ method public abstract kotlin.reflect.KClass<?> value();
property public abstract String entityColumn;
property public abstract String parentColumn;
- property public abstract Class<?> value;
+ property public abstract kotlin.reflect.KClass<?> value;
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.TYPE) public @interface MapColumn {
@@ -313,18 +313,18 @@
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface RawQuery {
- method public abstract Class<?>[] observedEntities();
- property public abstract Class<?>[] observedEntities;
+ method public abstract kotlin.reflect.KClass<?>[] observedEntities();
+ property public abstract kotlin.reflect.KClass<?>[] observedEntities;
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface Relation {
method public abstract androidx.room.Junction associateBy() default androidx.room.Junction(java.lang.Object);
- method public abstract Class<?> entity() default java.lang.Object;
+ method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
method public abstract String entityColumn();
method public abstract String parentColumn();
method public abstract String[] projection();
property public abstract androidx.room.Junction associateBy;
- property public abstract Class<?> entity;
+ property public abstract kotlin.reflect.KClass<?> entity;
property public abstract String entityColumn;
property public abstract String parentColumn;
property public abstract String[] projection;
@@ -397,21 +397,21 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.CLASS}) public @interface TypeConverters {
method public abstract androidx.room.BuiltInTypeConverters builtInTypeConverters() default androidx.room.BuiltInTypeConverters();
- method public abstract Class<?>[] value();
+ method public abstract kotlin.reflect.KClass<?>[] value();
property public abstract androidx.room.BuiltInTypeConverters builtInTypeConverters;
- property public abstract Class<?>[] value;
+ property public abstract kotlin.reflect.KClass<?>[] value;
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Update {
- method public abstract Class<?> entity() default java.lang.Object;
+ method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
- property public abstract Class<?> entity;
+ property public abstract kotlin.reflect.KClass<?> entity;
property @androidx.room.OnConflictStrategy public abstract int onConflict;
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Upsert {
- method public abstract Class<?> entity() default java.lang.Object;
- property public abstract Class<?> entity;
+ method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
+ property public abstract kotlin.reflect.KClass<?> entity;
}
}
diff --git a/room/room-common/api/restricted_current.ignore b/room/room-common/api/restricted_current.ignore
new file mode 100644
index 0000000..2aeb40f
--- /dev/null
+++ b/room/room-common/api/restricted_current.ignore
@@ -0,0 +1,27 @@
+// Baseline format: 1.0
+ChangedType: androidx.room.AutoMigration#spec():
+ Method androidx.room.AutoMigration.spec has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.Database#entities():
+ Method androidx.room.Database.entities has changed return type from java.lang.Class<?>[] to kotlin.reflect.KClass<?>[]
+ChangedType: androidx.room.Database#views():
+ Method androidx.room.Database.views has changed return type from java.lang.Class<?>[] to kotlin.reflect.KClass<?>[]
+ChangedType: androidx.room.Delete#entity():
+ Method androidx.room.Delete.entity has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.ForeignKey#entity():
+ Method androidx.room.ForeignKey.entity has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.Fts4#contentEntity():
+ Method androidx.room.Fts4.contentEntity has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.Insert#entity():
+ Method androidx.room.Insert.entity has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.Junction#value():
+ Method androidx.room.Junction.value has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.RawQuery#observedEntities():
+ Method androidx.room.RawQuery.observedEntities has changed return type from java.lang.Class<?>[] to kotlin.reflect.KClass<?>[]
+ChangedType: androidx.room.Relation#entity():
+ Method androidx.room.Relation.entity has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.TypeConverters#value():
+ Method androidx.room.TypeConverters.value has changed return type from java.lang.Class<?>[] to kotlin.reflect.KClass<?>[]
+ChangedType: androidx.room.Update#entity():
+ Method androidx.room.Update.entity has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
+ChangedType: androidx.room.Upsert#entity():
+ Method androidx.room.Upsert.entity has changed return type from java.lang.Class<?> to kotlin.reflect.KClass<?>
diff --git a/room/room-common/api/restricted_current.txt b/room/room-common/api/restricted_current.txt
index d6c9b38..afdbd32 100644
--- a/room/room-common/api/restricted_current.txt
+++ b/room/room-common/api/restricted_current.txt
@@ -9,10 +9,10 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface AutoMigration {
method public abstract int from();
- method public abstract Class<?> spec() default java.lang.Object;
+ method public abstract kotlin.reflect.KClass<?> spec() default java.lang.Object;
method public abstract int to();
property public abstract int from;
- property public abstract Class<?> spec;
+ property public abstract kotlin.reflect.KClass<?> spec;
property public abstract int to;
}
@@ -85,15 +85,15 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface Database {
method public abstract androidx.room.AutoMigration[] autoMigrations();
- method public abstract Class<?>[] entities();
+ method public abstract kotlin.reflect.KClass<?>[] entities();
method public abstract boolean exportSchema() default true;
method public abstract int version();
- method public abstract Class<?>[] views();
+ method public abstract kotlin.reflect.KClass<?>[] views();
property public abstract androidx.room.AutoMigration[] autoMigrations;
- property public abstract Class<?>[] entities;
+ property public abstract kotlin.reflect.KClass<?>[] entities;
property public abstract boolean exportSchema;
property public abstract int version;
- property public abstract Class<?>[] views;
+ property public abstract kotlin.reflect.KClass<?>[] views;
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface DatabaseView {
@@ -104,8 +104,8 @@
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Delete {
- method public abstract Class<?> entity() default java.lang.Object;
- property public abstract Class<?> entity;
+ method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
+ property public abstract kotlin.reflect.KClass<?> entity;
}
@java.lang.annotation.Repeatable(DeleteColumn.Entries::class) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface DeleteColumn {
@@ -153,13 +153,13 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={}) public @interface ForeignKey {
method public abstract String[] childColumns();
method public abstract boolean deferred() default false;
- method public abstract Class<?> entity();
+ method public abstract kotlin.reflect.KClass<?> entity();
method @androidx.room.ForeignKey.Action public abstract int onDelete() default androidx.room.ForeignKey.NO_ACTION;
method @androidx.room.ForeignKey.Action public abstract int onUpdate() default androidx.room.ForeignKey.NO_ACTION;
method public abstract String[] parentColumns();
property public abstract String[] childColumns;
property public abstract boolean deferred;
- property public abstract Class<?> entity;
+ property public abstract kotlin.reflect.KClass<?> entity;
property @androidx.room.ForeignKey.Action public abstract int onDelete;
property @androidx.room.ForeignKey.Action public abstract int onUpdate;
property public abstract String[] parentColumns;
@@ -190,7 +190,7 @@
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface Fts4 {
- method public abstract Class<?> contentEntity() default java.lang.Object;
+ method public abstract kotlin.reflect.KClass<?> contentEntity() default java.lang.Object;
method public abstract String languageId() default "";
method public abstract androidx.room.FtsOptions.MatchInfo matchInfo() default androidx.room.FtsOptions.MatchInfo.FTS4;
method public abstract String[] notIndexed();
@@ -198,7 +198,7 @@
method public abstract int[] prefix();
method public abstract String tokenizer() default androidx.room.FtsOptions.TOKENIZER_SIMPLE;
method public abstract String[] tokenizerArgs();
- property public abstract Class<?> contentEntity;
+ property public abstract kotlin.reflect.KClass<?> contentEntity;
property public abstract String languageId;
property public abstract androidx.room.FtsOptions.MatchInfo matchInfo;
property public abstract String[] notIndexed;
@@ -246,19 +246,19 @@
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Insert {
- method public abstract Class<?> entity() default java.lang.Object;
+ method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
- property public abstract Class<?> entity;
+ property public abstract kotlin.reflect.KClass<?> entity;
property @androidx.room.OnConflictStrategy public abstract int onConflict;
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={}) public @interface Junction {
method public abstract String entityColumn() default "";
method public abstract String parentColumn() default "";
- method public abstract Class<?> value();
+ method public abstract kotlin.reflect.KClass<?> value();
property public abstract String entityColumn;
property public abstract String parentColumn;
- property public abstract Class<?> value;
+ property public abstract kotlin.reflect.KClass<?> value;
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.TYPE) public @interface MapColumn {
@@ -319,18 +319,18 @@
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface RawQuery {
- method public abstract Class<?>[] observedEntities();
- property public abstract Class<?>[] observedEntities;
+ method public abstract kotlin.reflect.KClass<?>[] observedEntities();
+ property public abstract kotlin.reflect.KClass<?>[] observedEntities;
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface Relation {
method public abstract androidx.room.Junction associateBy() default androidx.room.Junction(java.lang.Object);
- method public abstract Class<?> entity() default java.lang.Object;
+ method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
method public abstract String entityColumn();
method public abstract String parentColumn();
method public abstract String[] projection();
property public abstract androidx.room.Junction associateBy;
- property public abstract Class<?> entity;
+ property public abstract kotlin.reflect.KClass<?> entity;
property public abstract String entityColumn;
property public abstract String parentColumn;
property public abstract String[] projection;
@@ -413,21 +413,21 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.CLASS}) public @interface TypeConverters {
method public abstract androidx.room.BuiltInTypeConverters builtInTypeConverters() default androidx.room.BuiltInTypeConverters();
- method public abstract Class<?>[] value();
+ method public abstract kotlin.reflect.KClass<?>[] value();
property public abstract androidx.room.BuiltInTypeConverters builtInTypeConverters;
- property public abstract Class<?>[] value;
+ property public abstract kotlin.reflect.KClass<?>[] value;
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Update {
- method public abstract Class<?> entity() default java.lang.Object;
+ method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
- property public abstract Class<?> entity;
+ property public abstract kotlin.reflect.KClass<?> entity;
property @androidx.room.OnConflictStrategy public abstract int onConflict;
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Upsert {
- method public abstract Class<?> entity() default java.lang.Object;
- property public abstract Class<?> entity;
+ method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
+ property public abstract kotlin.reflect.KClass<?> entity;
}
}
diff --git a/room/room-common/build.gradle b/room/room-common/build.gradle
index 241d52e..d120965 100644
--- a/room/room-common/build.gradle
+++ b/room/room-common/build.gradle
@@ -76,5 +76,4 @@
inceptionYear = "2017"
description = "Android Room-Common"
legacyDisableKotlinStrictApiMode = true
- metalavaK2UastEnabled = true
}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index 3c223df..4b190b5 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -741,8 +741,11 @@
val src = Source.kotlin(
"Foo.kt",
"""
+ annotation class MyAnnotation
interface MyInterface {
- var x:Int
+ var x: Int
+ var y: Int
+ @MyAnnotation get
}
""".trimIndent()
)
@@ -755,6 +758,14 @@
element.getMethodByJvmName("setX").let {
assertThat(it.isAbstract()).isTrue()
}
+ element.getMethodByJvmName("getY").let {
+ if (!isPreCompiled && invocation.isKsp) {
+ // The modifier set is empty for both the property and accessor
+ assertThat(it.isAbstract()).isFalse()
+ } else {
+ assertThat(it.isAbstract()).isTrue()
+ }
+ }
}
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/OpenDelegateWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/OpenDelegateWriter.kt
index fa8d172..c7d43dd 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/OpenDelegateWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/OpenDelegateWriter.kt
@@ -55,7 +55,11 @@
private fun createOpenDelegate(scope: CodeGenScope): XTypeSpec {
return XTypeSpec.anonymousClassBuilder(
- scope.language, "%L, %S", database.version, database.identityHash
+ scope.language,
+ "%L, %S, %S",
+ database.version,
+ database.identityHash,
+ database.legacyIdentityHash
).apply {
superclass(RoomTypeNames.ROOM_OPEN_DELEGATE)
addFunction(createCreateAllTables(scope))
diff --git a/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java b/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java
index 5daa9b8..a15107e 100644
--- a/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java
+++ b/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java
@@ -30,7 +30,7 @@
@Override
@NonNull
protected RoomOpenDelegate createOpenDelegate() {
- final RoomOpenDelegate _openDelegate = new RoomOpenDelegate(1923, "12b646c55443feeefb567521e2bece85") {
+ final RoomOpenDelegate _openDelegate = new RoomOpenDelegate(1923, "12b646c55443feeefb567521e2bece85", "2f1dbf49584f5d6c91cb44f8a6ecfee2") {
@Override
public void createAllTables(@NonNull final SQLiteConnection connection) {
SQLiteKt.execSQL(connection, "CREATE TABLE IF NOT EXISTS `User` (`uid` INTEGER NOT NULL, `name` TEXT, `lastName` TEXT, `ageColumn` INTEGER NOT NULL, PRIMARY KEY(`uid`))");
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_internalVisibility.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_internalVisibility.kt
index fdf7b86..3febf79 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_internalVisibility.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_internalVisibility.kt
@@ -33,7 +33,7 @@
protected override fun createOpenDelegate(): RoomOpenDelegate {
val _openDelegate: RoomOpenDelegate = object : RoomOpenDelegate(1,
- "195d7974660177325bd1a32d2c7b8b8c") {
+ "195d7974660177325bd1a32d2c7b8b8c", "7458a901120796c5bbc554e2fefd262f") {
public override fun createAllTables(connection: SQLiteConnection) {
connection.execSQL("CREATE TABLE IF NOT EXISTS `MyEntity` (`pk` INTEGER NOT NULL, PRIMARY KEY(`pk`))")
connection.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)")
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt
index 3bfeeb8..ead51a6 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt
@@ -33,7 +33,7 @@
protected override fun createOpenDelegate(): RoomOpenDelegate {
val _openDelegate: RoomOpenDelegate = object : RoomOpenDelegate(1,
- "195d7974660177325bd1a32d2c7b8b8c") {
+ "195d7974660177325bd1a32d2c7b8b8c", "7458a901120796c5bbc554e2fefd262f") {
public override fun createAllTables(connection: SQLiteConnection) {
connection.execSQL("CREATE TABLE IF NOT EXISTS `MyEntity` (`pk` INTEGER NOT NULL, PRIMARY KEY(`pk`))")
connection.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)")
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt
index cf6ffd19..092892d 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt
@@ -37,7 +37,7 @@
protected override fun createOpenDelegate(): RoomOpenDelegate {
val _openDelegate: RoomOpenDelegate = object : RoomOpenDelegate(1,
- "89ba16fb8b062b50acf0eb06c853efcb") {
+ "89ba16fb8b062b50acf0eb06c853efcb", "8a71a68e07bdd62aa8c8324d870cf804") {
public override fun createAllTables(connection: SQLiteConnection) {
connection.execSQL("CREATE TABLE IF NOT EXISTS `MyParentEntity` (`parentKey` INTEGER NOT NULL, PRIMARY KEY(`parentKey`))")
connection.execSQL("CREATE TABLE IF NOT EXISTS `MyEntity` (`pk` INTEGER NOT NULL, `indexedCol` TEXT NOT NULL, PRIMARY KEY(`pk`), FOREIGN KEY(`indexedCol`) REFERENCES `MyParentEntity`(`parentKey`) ON UPDATE NO ACTION ON DELETE CASCADE )")
diff --git a/room/room-gradle-plugin/lint-baseline.xml b/room/room-gradle-plugin/lint-baseline.xml
index 4d2a9a6..c581b23 100644
--- a/room/room-gradle-plugin/lint-baseline.xml
+++ b/room/room-gradle-plugin/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.4.0-alpha09" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha09)" variant="all" version="8.4.0-alpha09">
+<issues format="6" by="lint 8.4.0-alpha12" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha12)" variant="all" version="8.4.0-alpha12">
<issue
id="WithPluginClasspathUsage"
@@ -10,4 +10,40 @@
file="src/test/java/androidx/room/gradle/GradleTestUtils.kt"/>
</issue>
+ <issue
+ id="WithTypeWithoutConfigureEach"
+ message="Avoid passing a closure to withType, use withType().configureEach instead"
+ errorLine1=" ) = project.tasks.withType(JavaCompile::class.java) { task ->"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/room/gradle/integration/AndroidPluginIntegration.kt"/>
+ </issue>
+
+ <issue
+ id="WithTypeWithoutConfigureEach"
+ message="Avoid passing a closure to withType, use withType().configureEach instead"
+ errorLine1=" project.tasks.withType(KaptTask::class.java) { task ->"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/room/gradle/integration/AndroidPluginIntegration.kt"/>
+ </issue>
+
+ <issue
+ id="WithTypeWithoutConfigureEach"
+ message="Avoid passing a closure to withType, use withType().configureEach instead"
+ errorLine1=" project.tasks.withType(KspTaskJvm::class.java) { task ->"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/room/gradle/integration/AndroidPluginIntegration.kt"/>
+ </issue>
+
+ <issue
+ id="WithTypeWithoutConfigureEach"
+ message="Avoid passing a closure to withType, use withType().configureEach instead"
+ errorLine1=" project.tasks.withType(KspTask::class.java) { task ->"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="src/main/java/androidx/room/gradle/integration/KotlinMultiplatformPluginIntegration.kt"/>
+ </issue>
+
</issues>
diff --git a/room/room-migration/build.gradle b/room/room-migration/build.gradle
index 4aed7d4..d600ae4 100644
--- a/room/room-migration/build.gradle
+++ b/room/room-migration/build.gradle
@@ -87,5 +87,4 @@
inceptionYear = "2017"
description = "Android Room Migration"
legacyDisableKotlinStrictApiMode = true
- metalavaK2UastEnabled = true
}
diff --git a/room/room-runtime/build.gradle b/room/room-runtime/build.gradle
index b2fa665..359ac13 100644
--- a/room/room-runtime/build.gradle
+++ b/room/room-runtime/build.gradle
@@ -229,5 +229,4 @@
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Room-Runtime"
- metalavaK2UastEnabled = true
}
diff --git a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/CoroutineRoomCancellationTest.kt b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/CoroutineRoomCancellationTest.kt
index 19995f7..0d8cebe 100644
--- a/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/CoroutineRoomCancellationTest.kt
+++ b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/CoroutineRoomCancellationTest.kt
@@ -197,7 +197,7 @@
private class TestDatabase : RoomDatabase() {
override fun createOpenDelegate(): RoomOpenDelegate {
- return object : RoomOpenDelegate(1, "") {
+ return object : RoomOpenDelegate(1, "", "") {
override fun onCreate(connection: SQLiteConnection) {}
override fun onPreMigrate(connection: SQLiteConnection) {}
override fun onValidateSchema(connection: SQLiteConnection): ValidationResult {
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt
index 1e6b4ec..e8f7d67 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt
@@ -164,7 +164,7 @@
* A no op implementation of [RoomOpenDelegate] used in compatibility mode with old gen code
* that relies on [RoomOpenHelper].
*/
- private class NoOpOpenDelegate : RoomOpenDelegate(-1, "") {
+ private class NoOpOpenDelegate : RoomOpenDelegate(-1, "", "") {
override fun onCreate(connection: SQLiteConnection) {
error("NOP delegate should never be called")
}
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
index f3f192c..4fbcdc5 100644
--- a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
+++ b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/InvalidationTrackerTest.kt
@@ -564,7 +564,7 @@
}
override fun createOpenDelegate(): RoomOpenDelegateMarker {
- return object : RoomOpenDelegate(0, "") {
+ return object : RoomOpenDelegate(0, "", "") {
override fun onCreate(connection: SQLiteConnection) {}
override fun onPreMigrate(connection: SQLiteConnection) {}
override fun onValidateSchema(connection: SQLiteConnection) =
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt
index e8d3c30..8191c55 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt
@@ -194,8 +194,10 @@
null
}
}
-
- if (openDelegate.identityHash != identityHash) {
+ if (
+ openDelegate.identityHash != identityHash &&
+ openDelegate.legacyIdentityHash != identityHash
+ ) {
error(
"Room cannot verify the data integrity. Looks like" +
" you've changed schema but forgot to update the version number. You can" +
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomOpenDelegate.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomOpenDelegate.kt
index 9211d50..62ac8db 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomOpenDelegate.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomOpenDelegate.kt
@@ -33,6 +33,7 @@
abstract class RoomOpenDelegate(
val version: Int,
val identityHash: String,
+ val legacyIdentityHash: String
) : RoomOpenDelegateMarker {
abstract fun onCreate(connection: SQLiteConnection)
abstract fun onPreMigrate(connection: SQLiteConnection)
diff --git a/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest_TestDatabase_Impl.kt b/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest_TestDatabase_Impl.kt
index 9d3f9c2..228e10d 100644
--- a/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest_TestDatabase_Impl.kt
+++ b/room/room-runtime/src/nativeTest/kotlin/androidx.room/BuilderTest_TestDatabase_Impl.kt
@@ -23,7 +23,7 @@
internal class BuilderTest_TestDatabase_Impl : BuilderTest.TestDatabase() {
override fun createOpenDelegate(): RoomOpenDelegateMarker {
- return object : RoomOpenDelegate(0, "") {
+ return object : RoomOpenDelegate(0, "", "") {
override fun onCreate(connection: SQLiteConnection) { }
override fun onPreMigrate(connection: SQLiteConnection) { }
override fun onValidateSchema(connection: SQLiteConnection): ValidationResult {
diff --git a/room/room-testing/build.gradle b/room/room-testing/build.gradle
index aa35ba0..0772841 100644
--- a/room/room-testing/build.gradle
+++ b/room/room-testing/build.gradle
@@ -95,5 +95,4 @@
publish = Publish.SNAPSHOT_AND_RELEASE
inceptionYear = "2017"
description = "Android Room Testing"
- metalavaK2UastEnabled = true
}
diff --git a/room/room-testing/src/commonMain/kotlin/androidx/room/testing/MigrationTestHelper.kt b/room/room-testing/src/commonMain/kotlin/androidx/room/testing/MigrationTestHelper.kt
index 64ccb0d..c13e68f 100644
--- a/room/room-testing/src/commonMain/kotlin/androidx/room/testing/MigrationTestHelper.kt
+++ b/room/room-testing/src/commonMain/kotlin/androidx/room/testing/MigrationTestHelper.kt
@@ -224,7 +224,11 @@
private sealed class TestOpenDelegate(
databaseBundle: DatabaseBundle
-) : RoomOpenDelegate(databaseBundle.version, databaseBundle.identityHash) {
+) : RoomOpenDelegate(
+ version = databaseBundle.version,
+ identityHash = databaseBundle.identityHash,
+ legacyIdentityHash = databaseBundle.identityHash
+) {
override fun onCreate(connection: SQLiteConnection) {}
override fun onPreMigrate(connection: SQLiteConnection) {}
override fun onPostMigrate(connection: SQLiteConnection) {}
diff --git a/settings.gradle b/settings.gradle
index 22c54a6..25610f7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -522,6 +522,7 @@
includeProject(":compose:material3:material3-adaptive-navigation-suite", [BuildType.COMPOSE])
includeProject(":compose:material3:material3-adaptive-navigation-suite:material3-adaptive-navigation-suite-samples", "compose/material3/material3-adaptive-navigation-suite/samples", [BuildType.COMPOSE])
includeProject(":compose:material3:material3-common", [BuildType.COMPOSE])
+includeProject(":compose:material3:material3-common:material3-common-samples", "compose/material3/material3-common/samples", [BuildType.COMPOSE])
includeProject(":compose:material3:material3-lint", [BuildType.COMPOSE])
includeProject(":compose:material3:material3-window-size-class", [BuildType.COMPOSE])
includeProject(":compose:material3:material3-window-size-class:material3-window-size-class-samples", "compose/material3/material3-window-size-class/samples", [BuildType.COMPOSE])
@@ -1001,6 +1002,7 @@
includeProject(":wear:compose:integration-tests:macrobenchmark", [BuildType.COMPOSE])
includeProject(":wear:compose:integration-tests:macrobenchmark-target", [BuildType.COMPOSE])
includeProject(":wear:compose:integration-tests:navigation", [BuildType.COMPOSE])
+includeProject(":wear:wear-core", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:wear-input", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:wear-input-samples", "wear/wear-input/samples", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:wear-input-testing", [BuildType.MAIN, BuildType.WEAR])
diff --git a/slice/slice-view/src/main/res/values-ky/strings.xml b/slice/slice-view/src/main/res/values-ky/strings.xml
index f3b7450..3de3fc3 100644
--- a/slice/slice-view/src/main/res/values-ky/strings.xml
+++ b/slice/slice-view/src/main/res/values-ky/strings.xml
@@ -20,7 +20,7 @@
<string name="abc_slice_more_content" msgid="7841223363798860154">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
<string name="abc_slice_more" msgid="4299234410808450900">"Дагы"</string>
<string name="abc_slice_show_more" msgid="1112789899890391107">"Дагы көрсөтүү"</string>
- <string name="abc_slice_updated" msgid="7932359091871934205">"<xliff:g id="TIME">%1$s</xliff:g> жаңыртылды"</string>
+ <string name="abc_slice_updated" msgid="7932359091871934205">"<xliff:g id="TIME">%1$s</xliff:g> жаңырды"</string>
<plurals name="abc_slice_duration_min" formatted="false" msgid="7664017844210142826">
<item quantity="other"><xliff:g id="ID_2">%d</xliff:g> мүн. мурун</item>
<item quantity="one"><xliff:g id="ID_1">%d</xliff:g> мүн. мурун</item>
diff --git a/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/UserResizeModeTest.kt b/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/UserResizeModeTest.kt
index c4bc17b..77631a9 100644
--- a/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/UserResizeModeTest.kt
+++ b/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/UserResizeModeTest.kt
@@ -415,6 +415,48 @@
.that(hasNonTransparentPixel)
.isTrue()
}
+
+ @Test
+ fun skippedMeasurePassIsCorrected() {
+ val context = InstrumentationRegistry.getInstrumentation().context
+ val spl = createTestSpl(context, collapsibleContentViews = true)
+
+ fun assertAdjacentSiblings(message: String) {
+ val (leftChild, rightChild) = spl.leftAndRightViews()
+ assertWithMessage("adjacent view edges: $message")
+ .that(rightChild.left)
+ .isEqualTo(leftChild.right)
+ }
+
+ assertAdjacentSiblings("initial layout")
+
+ spl.splitDividerPosition = 0
+ spl.measureAndLayoutForTest()
+
+ assertAdjacentSiblings("with splitDividerPosition = 0")
+
+ val (left, right) = spl.leftAndRightViews()
+ assertWithMessage("left child width")
+ .that(left.width)
+ .isEqualTo(0)
+ assertWithMessage("right child width")
+ .that(right.width)
+ .isEqualTo(100)
+ }
+}
+
+private fun SlidingPaneLayout.leftAndRightViews(): Pair<View, View> {
+ val isRtl = this.layoutDirection == View.LAYOUT_DIRECTION_RTL
+ val leftChild: View
+ val rightChild: View
+ if (isRtl) {
+ leftChild = this[1]
+ rightChild = this[0]
+ } else {
+ leftChild = this[0]
+ rightChild = this[1]
+ }
+ return leftChild to rightChild
}
private fun View.drawToBitmap(): Bitmap {
@@ -427,24 +469,37 @@
private fun createTestSpl(
context: Context,
setDividerDrawable: Boolean = true,
- childPanesAcceptTouchEvents: Boolean = false
+ childPanesAcceptTouchEvents: Boolean = false,
+ collapsibleContentViews: Boolean = false
): SlidingPaneLayout = SlidingPaneLayout(context).apply {
addView(
TestPaneView(context).apply {
- minimumWidth = 30
+ val lpWidth: Int
+ if (collapsibleContentViews) {
+ lpWidth = 0
+ } else {
+ minimumWidth = 30
+ lpWidth = LayoutParams.WRAP_CONTENT
+ }
acceptTouchEvents = childPanesAcceptTouchEvents
layoutParams = SlidingPaneLayout.LayoutParams(
- LayoutParams.WRAP_CONTENT,
+ lpWidth,
LayoutParams.MATCH_PARENT
).apply { weight = 1f }
}
)
addView(
TestPaneView(context).apply {
- minimumWidth = 30
+ val lpWidth: Int
+ if (collapsibleContentViews) {
+ lpWidth = 0
+ } else {
+ minimumWidth = 30
+ lpWidth = LayoutParams.WRAP_CONTENT
+ }
acceptTouchEvents = childPanesAcceptTouchEvents
layoutParams = SlidingPaneLayout.LayoutParams(
- LayoutParams.WRAP_CONTENT,
+ lpWidth,
LayoutParams.MATCH_PARENT
).apply { weight = 1f }
}
diff --git a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.kt b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.kt
index 319ff8f..3d3dc38 100644
--- a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.kt
+++ b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.kt
@@ -187,8 +187,19 @@
}
}
+/**
+ * Pulls the string interpolation and exception throwing bytecode out of the inlined
+ * [spLayoutParams] property at each call site
+ */
+private fun layoutParamsError(childView: View, layoutParams: LayoutParams?): Nothing {
+ error("SlidingPaneLayout child $childView had unexpected LayoutParams $layoutParams")
+}
+
private inline val View.spLayoutParams: SlidingPaneLayout.LayoutParams
- get() = layoutParams as SlidingPaneLayout.LayoutParams
+ get() = when (val layoutParams = layoutParams) {
+ is SlidingPaneLayout.LayoutParams -> layoutParams
+ else -> layoutParamsError(this, layoutParams)
+ }
/**
* SlidingPaneLayout provides a horizontal, multi-pane layout for use at the top level
@@ -459,7 +470,7 @@
* Position of the division between split panes when [isSlideable] is `false`.
* When the value is < 0 it should be one of the `SPLIT_DIVIDER_POSITION_*` constants,
* e.g. [SPLIT_DIVIDER_POSITION_AUTO]. When the value is >= 0 it represents a value in pixels
- * between 0 and [getWidth].
+ * between 0 and [getWidth]. The default value is [SPLIT_DIVIDER_POSITION_AUTO].
*
* Changing this property will result in a [requestLayout] and relayout of the contents
* of the [SlidingPaneLayout].
@@ -995,7 +1006,7 @@
if (child.visibility == GONE) return@forEachIndexed
val lp = child.spLayoutParams
val skippedFirstPass = !lp.canInfluenceParentSize || lp.weightOnlyWidth
- val measuredWidth = if (skippedFirstPass) 0 else child.measuredWidth
+ val firstPassMeasuredWidth = if (skippedFirstPass) 0 else child.measuredWidth
val newWidth = when {
// Child view consumes available space if the combined width cannot fit into
// the layout available width.
@@ -1007,7 +1018,7 @@
val widthToDistribute = widthRemaining.coerceAtLeast(0)
val addedWidth =
(lp.weight * widthToDistribute / weightSum).roundToInt()
- measuredWidth + addedWidth
+ firstPassMeasuredWidth + addedWidth
} else { // Explicit dividing line is defined
val clampedPos = dividerPos.coerceAtMost(width - paddingRight)
.coerceAtLeast(paddingLeft)
@@ -1025,9 +1036,9 @@
widthAvailable - lp.horizontalMargin - totalMeasuredWidth
}
lp.width > 0 -> lp.width
- else -> measuredWidth
+ else -> firstPassMeasuredWidth
}
- if (measuredWidth != newWidth) {
+ if (newWidth != child.measuredWidth) {
val childWidthSpec = MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY)
val childHeightSpec = getChildHeightMeasureSpec(
child,
diff --git a/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt
index 71e1465..aeb2f16 100644
--- a/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt
+++ b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt
@@ -17,6 +17,7 @@
package androidx.transition
import android.os.Build
+import android.view.View
import android.window.BackEvent
import androidx.activity.BackEventCompat
import androidx.lifecycle.Lifecycle
@@ -132,12 +133,16 @@
}
var startedEnter = false
val fragment1 = TransitionFragment(R.layout.scene1)
+ val transitionEndCountDownLatch = CountDownLatch(1)
fragment1.setReenterTransition(Fade().apply {
duration = 300
addListener(object : TransitionListenerAdapter() {
override fun onTransitionStart(transition: Transition) {
startedEnter = true
}
+ override fun onTransitionEnd(transition: Transition) {
+ transitionEndCountDownLatch.countDown()
+ }
})
})
@@ -199,6 +204,8 @@
// Make sure the original fragment was correctly readded to the container
assertThat(fragment2.requireView()).isNotNull()
+
+ assertThat(transitionEndCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
}
}
@@ -217,6 +224,8 @@
}
})
})
+ fragment1.sharedElementEnterTransition = null
+ fragment1.sharedElementReturnTransition = null
fm1.beginTransaction()
.replace(R.id.fragmentContainer, fragment1, "1")
@@ -236,6 +245,8 @@
}
})
})
+ fragment2.sharedElementEnterTransition = null
+ fragment2.sharedElementReturnTransition = null
fm1.beginTransaction()
.replace(R.id.fragmentContainer, fragment2, "2")
@@ -257,6 +268,8 @@
}
})
})
+ fragment3.sharedElementEnterTransition = null
+ fragment3.sharedElementReturnTransition = null
fm1.beginTransaction()
.replace(R.id.fragmentContainer, fragment3, "3")
@@ -563,4 +576,61 @@
assertThat(resumedAfterOnBackStarted).isTrue()
}
}
+
+ @Test
+ fun GestureBackWithNonSeekableSharedElement() {
+ withUse(ActivityScenario.launch(FragmentTransitionTestActivity::class.java)) {
+ val fm1 = withActivity { supportFragmentManager }
+
+ val fragment1 = StrictViewFragment(R.layout.scene1)
+
+ fm1.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment1, "1")
+ .setReorderingAllowed(true)
+ .addToBackStack(null)
+ .commit()
+ waitForExecution()
+
+ val fragment2 = TransitionFragment(R.layout.scene6)
+ fragment2.setEnterTransition(Fade())
+ fragment2.setReturnTransition(Fade())
+
+ val greenSquare = fragment1.requireView().findViewById<View>(R.id.greenSquare)
+
+ fm1.beginTransaction()
+ .replace(R.id.fragmentContainer, fragment2, "2")
+ .addSharedElement(greenSquare, "green")
+ .setReorderingAllowed(true)
+ .addToBackStack(null)
+ .commit()
+ waitForExecution()
+
+ fragment2.waitForTransition()
+
+ val dispatcher = withActivity { onBackPressedDispatcher }
+ withActivity {
+ dispatcher.dispatchOnBackStarted(
+ BackEventCompat(
+ 0.1F,
+ 0.1F,
+ 0.1F,
+ BackEvent.EDGE_LEFT
+ )
+ )
+ }
+ executePendingTransactions()
+
+ withActivity {
+ dispatcher.onBackPressed()
+ }
+ executePendingTransactions()
+
+ assertThat(fragment2.isAdded).isFalse()
+ assertThat(fm1.findFragmentByTag("2"))
+ .isEqualTo(null)
+
+ // Make sure the original fragment was correctly readded to the container
+ assertThat(fragment1.requireView().parent).isNotNull()
+ }
+ }
}
diff --git a/tv/samples/src/main/java/androidx/tv/samples/MaterialThemeMapping.kt b/tv/samples/src/main/java/androidx/tv/samples/MaterialThemeMapping.kt
index a6b0c73..da4047e 100644
--- a/tv/samples/src/main/java/androidx/tv/samples/MaterialThemeMapping.kt
+++ b/tv/samples/src/main/java/androidx/tv/samples/MaterialThemeMapping.kt
@@ -24,6 +24,7 @@
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun mapColorScheme(tvColorScheme: TvColorScheme): ColorScheme {
+ @Suppress("Deprecation")
return ColorScheme(
primary = tvColorScheme.primary,
onPrimary = tvColorScheme.onPrimary,
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index a20cb7d..0acef239 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -416,27 +416,6 @@
method @androidx.compose.runtime.Composable public static void Icon(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
}
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ImmersiveListBackgroundScope implements androidx.compose.foundation.layout.BoxScope {
- method @androidx.compose.runtime.Composable public void AnimatedContent(int targetState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<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);
- }
-
- @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ImmersiveListDefaults {
- method public androidx.compose.animation.EnterTransition getEnterTransition();
- method public androidx.compose.animation.ExitTransition getExitTransition();
- property public final androidx.compose.animation.EnterTransition EnterTransition;
- property public final androidx.compose.animation.ExitTransition ExitTransition;
- field public static final androidx.tv.material3.ImmersiveListDefaults INSTANCE;
- }
-
- public final class ImmersiveListKt {
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ImmersiveList(kotlin.jvm.functions.Function3<? super androidx.tv.material3.ImmersiveListBackgroundScope,? super java.lang.Integer,? super java.lang.Boolean,kotlin.Unit> background, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment listAlignment, kotlin.jvm.functions.Function1<? super androidx.tv.material3.ImmersiveListScope,kotlin.Unit> list);
- }
-
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ImmersiveListScope {
- method public androidx.compose.ui.Modifier immersiveListItem(androidx.compose.ui.Modifier, int index);
- }
-
@SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class InputChipDefaults {
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.SelectableChipBorder border(boolean hasAvatar, 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.SelectableChipColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long selectedContainerColor, optional long selectedContentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long focusedSelectedContainerColor, optional long focusedSelectedContentColor, optional long pressedSelectedContainerColor, optional long pressedSelectedContentColor);
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index a20cb7d..0acef239 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -416,27 +416,6 @@
method @androidx.compose.runtime.Composable public static void Icon(androidx.compose.ui.graphics.vector.ImageVector imageVector, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
}
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ImmersiveListBackgroundScope implements androidx.compose.foundation.layout.BoxScope {
- method @androidx.compose.runtime.Composable public void AnimatedContent(int targetState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.AnimatedContentTransitionScope<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);
- }
-
- @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ImmersiveListDefaults {
- method public androidx.compose.animation.EnterTransition getEnterTransition();
- method public androidx.compose.animation.ExitTransition getExitTransition();
- property public final androidx.compose.animation.EnterTransition EnterTransition;
- property public final androidx.compose.animation.ExitTransition ExitTransition;
- field public static final androidx.tv.material3.ImmersiveListDefaults INSTANCE;
- }
-
- public final class ImmersiveListKt {
- method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ImmersiveList(kotlin.jvm.functions.Function3<? super androidx.tv.material3.ImmersiveListBackgroundScope,? super java.lang.Integer,? super java.lang.Boolean,kotlin.Unit> background, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment listAlignment, kotlin.jvm.functions.Function1<? super androidx.tv.material3.ImmersiveListScope,kotlin.Unit> list);
- }
-
- @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ImmersiveListScope {
- method public androidx.compose.ui.Modifier immersiveListItem(androidx.compose.ui.Modifier, int index);
- }
-
@SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class InputChipDefaults {
method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.SelectableChipBorder border(boolean hasAvatar, 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.SelectableChipColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long selectedContainerColor, optional long selectedContentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long focusedSelectedContainerColor, optional long focusedSelectedContentColor, optional long pressedSelectedContainerColor, optional long pressedSelectedContentColor);
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
index c6569aa..d240598 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
@@ -76,7 +76,7 @@
import kotlin.math.abs
import kotlinx.coroutines.delay
import org.junit.Rule
-import org.junit.Test
+// import org.junit.Test
private const val delayBetweenItems = 2500L
private const val animationTime = 900L
@@ -86,7 +86,7 @@
@get:Rule
val rule = createComposeRule()
- @Test
+ // @Test
fun carousel_autoScrolls() {
rule.setContent {
SampleCarousel {
@@ -103,7 +103,7 @@
rule.onNodeWithText("Text 3").assertIsDisplayed()
}
- @Test
+ // @Test
fun carousel_onFocus_stopsScroll() {
rule.setContent {
SampleCarousel {
@@ -124,7 +124,7 @@
rule.onNodeWithText("Text 1").onParent().assertIsFocused()
}
- @Test
+ // @Test
fun carousel_onUserTriggeredPause_stopsScroll() {
rule.setContent {
val carouselState = rememberCarouselState()
@@ -143,7 +143,7 @@
rule.onNodeWithText("Text 1").assertIsDisplayed()
}
- @Test
+ // @Test
fun carousel_onUserTriggeredPauseAndResume_resumeScroll() {
var pauseHandle: ScrollPauseHandle? = null
rule.setContent {
@@ -176,7 +176,7 @@
rule.onNodeWithText("Text 2").assertIsDisplayed()
}
- @Test
+ // @Test
fun carousel_onMultipleUserTriggeredPauseAndResume_resumesScroll() {
var pauseHandle1: ScrollPauseHandle? = null
var pauseHandle2: ScrollPauseHandle? = null
@@ -220,7 +220,7 @@
rule.onNodeWithText("Text 2").assertIsDisplayed()
}
- @Test
+ // @Test
fun carousel_onRepeatedResumesOnSamePauseHandle_ignoresSubsequentResumeCalls() {
var pauseHandle1: ScrollPauseHandle? = null
rule.setContent {
@@ -259,7 +259,7 @@
rule.onNodeWithText("Text 1").assertIsDisplayed()
}
- @Test
+ // @Test
fun carousel_outOfFocus_resumesScroll() {
rule.setContent {
Column {
@@ -282,7 +282,7 @@
rule.onNodeWithText("Text 2").assertIsDisplayed()
}
- @Test
+ // @Test
fun carousel_pagerIndicatorDisplayed() {
rule.setContent {
SampleCarousel {
@@ -293,7 +293,7 @@
rule.onNodeWithTag("indicator").assertIsDisplayed()
}
- @Test
+ // @Test
fun carousel_withAnimatedContent_successfulTransition() {
rule.setContent {
SampleCarousel {
@@ -313,7 +313,7 @@
rule.onNodeWithText("PLAY").assertIsDisplayed()
}
- @Test
+ // @Test
fun carousel_withAnimatedContent_successfulFocusIn() {
rule.setContent {
SampleCarousel {
@@ -335,7 +335,7 @@
.assertIsFocused()
}
- @Test
+ // @Test
fun carousel_parentContainerGainsFocus_onBackPress() {
rule.setContent {
Box(modifier = Modifier
@@ -370,7 +370,7 @@
rule.onNodeWithTag("box-container").assertIsFocused()
}
- @Test
+ // @Test
fun carousel_withCarouselItem_parentContainerGainsFocusOnBackPress() {
rule.setContent {
Box(modifier = Modifier
@@ -407,7 +407,7 @@
rule.onNodeWithTag("box-container").assertIsFocused()
}
- @Test
+ // @Test
fun carousel_scrollToRegainFocus_checkBringIntoView() {
val focusRequester = FocusRequester()
rule.setContent {
@@ -500,7 +500,7 @@
assertThat(checkNodeCompletelyVisible(rule, "featured-carousel")).isTrue()
}
- @Test
+ // @Test
fun carousel_zeroItemCount_shouldNotCrash() {
val testTag = "emptyCarousel"
rule.setContent {
@@ -510,7 +510,7 @@
rule.onNodeWithTag(testTag).assertExists()
}
- @Test
+ // @Test
fun carousel_oneItemCount_shouldNotCrash() {
val testTag = "emptyCarousel"
rule.setContent {
@@ -520,7 +520,7 @@
rule.onNodeWithTag(testTag).assertExists()
}
- @Test
+ // @Test
fun carousel_manualScrollingWithFocusableItemsOnTop_focusStaysWithinCarousel() {
rule.setContent {
Column {
@@ -577,7 +577,7 @@
rule.onNodeWithText("Button-1").assertIsFocused()
}
- @Test
+ // @Test
fun carousel_manualScrollingFastMultipleKeyPresses_focusStaysWithinCarousel() {
val carouselState = CarouselState()
val tabs = listOf("Tab 1", "Tab 2", "Tab 3")
@@ -640,7 +640,7 @@
rule.onNodeWithText("Play ${finalItem + 3}", useUnmergedTree = true).assertIsFocused()
}
- @Test
+ // @Test
fun carousel_manualScrollingDpadLongPress_moveOnlyOneSlide() {
rule.setContent {
SampleCarousel(itemCount = 6) { index ->
@@ -685,7 +685,7 @@
rule.onNodeWithText("Button 1").assertIsFocused()
}
- @Test
+ // @Test
fun carousel_manualScrollingLtr_RightMovesToNextSlideLeftMovesToPrevSlide() {
rule.setContent {
SampleCarousel { index ->
@@ -731,7 +731,7 @@
rule.onNodeWithText("Button 1").assertIsDisplayed()
}
- @Test
+ // @Test
fun carousel_manualScrollingRtl_LeftMovesToNextSlideRightMovesToPrevSlide() {
rule.setContent {
CompositionLocalProvider(
@@ -780,7 +780,7 @@
rule.onNodeWithText("Button 1").assertIsDisplayed()
}
- @Test
+ // @Test
fun carousel_itemCountChangesDuringAnimation_shouldNotCrash() {
val itemDisplayDurationMs: Long = 100
var itemChanges = 0
@@ -813,7 +813,7 @@
rule.waitUntil(timeoutMillis = 5000) { itemChanges > minSuccessfulItemChanges }
}
- @Test
+ // @Test
fun carousel_slideWithTwoButtonsInARow_focusMovesWithinSlideAndChangesSlideOnlyOnFocusExit() {
rule.setContent {
// No AutoScrolling
@@ -834,7 +834,7 @@
rule.onNodeWithText("Left Button 2").assertIsFocused()
}
- @Test
+ // @Test
fun carousel_manualScrollingLtr_loopsAroundWhenNoAdjacentFocusableItemsArePresent() {
rule.setContent {
// No AutoScrolling
@@ -860,7 +860,7 @@
}
@OptIn(ExperimentalComposeUiApi::class)
- @Test
+ // @Test
fun carousel_manualScrollingLtr_focusMovesToAdjacentItemsOutsideCarousel() {
rule.setContent {
val focusRequester = remember { FocusRequester() }
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/ImmersiveListTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/ImmersiveListTest.kt
deleted file mode 100644
index d44e625..0000000
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/ImmersiveListTest.kt
+++ /dev/null
@@ -1,300 +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.tv.material3
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.text.BasicText
-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.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.input.key.NativeKeyEvent
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsFocused
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.test.requestFocus
-import androidx.compose.ui.unit.dp
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.tv.foundation.ExperimentalTvFoundationApi
-import androidx.tv.foundation.lazy.list.TvLazyColumn
-import androidx.tv.foundation.lazy.list.TvLazyRow
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-
-@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalTvFoundationApi::class)
-class ImmersiveListTest {
- @get:Rule
- val rule = createComposeRule()
-
- @Test
- fun immersiveList_scroll_backgroundChanges() {
- rule.setContent {
- ImmersiveList(
- background = { index, _ ->
- AnimatedContent(targetState = index) {
- Box(
- modifier = Modifier
- .testTag("background-$it")
- .size(200.dp)
- ) {
- BasicText("background-$it")
- }
- }
- }) {
- TvLazyRow {
- items(3) { index ->
- var isFocused by remember { mutableStateOf(false) }
-
- Box(
- modifier = Modifier
- .size(100.dp)
- .testTag("card-$index")
- .background(if (isFocused) Color.Red else Color.Transparent)
- .size(100.dp)
- .onFocusChanged { isFocused = it.isFocused }
- .immersiveListItem(index)
- .focusable(true)
- ) {
- BasicText("card-$index")
- }
- }
- }
- }
- }
-
- rule.waitForIdle()
- rule.onNodeWithTag("card-0").requestFocus()
- rule.waitForIdle()
-
- rule.onNodeWithTag("card-0").assertIsFocused()
- rule.onNodeWithTag("background-0").assertIsDisplayed()
- rule.onNodeWithTag("background-1").assertDoesNotExist()
- rule.onNodeWithTag("background-2").assertDoesNotExist()
-
- rule.waitForIdle()
- keyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
-
- rule.onNodeWithTag("card-1").assertIsFocused()
- rule.onNodeWithTag("background-1").assertIsDisplayed()
- rule.onNodeWithTag("background-0").assertDoesNotExist()
- rule.onNodeWithTag("background-2").assertDoesNotExist()
-
- rule.waitForIdle()
- keyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
-
- rule.onNodeWithTag("card-0").assertIsFocused()
- rule.onNodeWithTag("background-0").assertIsDisplayed()
- rule.onNodeWithTag("background-1").assertDoesNotExist()
- rule.onNodeWithTag("background-2").assertDoesNotExist()
- }
-
- @Test
- fun immersiveList_scrollToRegainFocusInLazyColumn_checkBringIntoView() {
- val focusRequesterList = mutableListOf<FocusRequester>()
- for (item in 0..2) { focusRequesterList.add(FocusRequester()) }
- setupContent(focusRequesterList)
-
- // Initially first focusable element would be focused
- rule.waitForIdle()
- rule.onNodeWithTag("test-card-0").assertIsFocused()
-
- // Scroll down to the Immersive List's first card
- keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
- rule.waitForIdle()
- rule.onNodeWithTag("list-card-0").assertIsFocused()
- rule.onNodeWithTag("immersive-list").assertIsDisplayed()
- assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
-
- // Scroll down to last element, making sure the immersive list is partially visible
- keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
- rule.waitForIdle()
- rule.onNodeWithTag("test-card-4").assertIsFocused()
- rule.onNodeWithTag("immersive-list").assertIsDisplayed()
-
- // Scroll back to the immersive list to check if it's brought into view on regaining focus
- keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
- rule.waitForIdle()
- rule.onNodeWithTag("immersive-list").assertIsDisplayed()
- assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
- }
-
- @Test
- fun immersiveList_scrollToRegainFocusInTvLazyColumn_checkBringIntoView() {
- val focusRequesterList = mutableListOf<FocusRequester>()
- for (item in 0..2) { focusRequesterList.add(FocusRequester()) }
- setupTvContent(focusRequesterList)
-
- // Initially first focusable element would be focused
- rule.waitForIdle()
- rule.onNodeWithTag("test-card-0").assertIsFocused()
-
- // Scroll down to the Immersive List's first card
- keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
- rule.waitForIdle()
- rule.onNodeWithTag("list-card-0").assertIsFocused()
- rule.onNodeWithTag("immersive-list").assertIsDisplayed()
- assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
-
- // Scroll down to last element, making sure the immersive list is partially visible
- keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
- rule.waitForIdle()
- rule.onNodeWithTag("test-card-4").assertIsFocused()
- rule.onNodeWithTag("immersive-list").assertIsDisplayed()
-
- // Scroll back to the immersive list to check if it's brought into view on regaining focus
- keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
- rule.waitForIdle()
- rule.onNodeWithTag("immersive-list").assertIsDisplayed()
- assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
- }
-
- private fun checkNodeCompletelyVisible(tag: String): Boolean {
- rule.waitForIdle()
-
- val rootRect = rule.onRoot().getUnclippedBoundsInRoot()
- val itemRect = rule.onNodeWithTag(tag).getUnclippedBoundsInRoot()
-
- return itemRect.left >= rootRect.left &&
- itemRect.right <= rootRect.right &&
- itemRect.top >= rootRect.top &&
- itemRect.bottom <= rootRect.bottom
- }
-
- private fun setupContent(focusRequesterList: List<FocusRequester>) {
- val focusRequester = FocusRequester()
- rule.setContent {
- LazyColumn {
- items(3) {
- val modifier =
- if (it == 0) Modifier.focusRequester(focusRequester)
- else Modifier
- BasicText(
- text = "test-card-$it",
- modifier = modifier
- .testTag("test-card-$it")
- .size(200.dp)
- .focusable()
- )
- }
- item { TestImmersiveList(focusRequesterList) }
- items(2) {
- BasicText(
- text = "test-card-${it + 3}",
- modifier = Modifier
- .testTag("test-card-${it + 3}")
- .size(200.dp)
- .focusable()
- )
- }
- }
- }
- rule.runOnIdle { focusRequester.requestFocus() }
- }
-
- private fun setupTvContent(focusRequesterList: List<FocusRequester>) {
- val focusRequester = FocusRequester()
- rule.setContent {
- TvLazyColumn {
- items(3) {
- val modifier =
- if (it == 0) Modifier.focusRequester(focusRequester)
- else Modifier
- BasicText(
- text = "test-card-$it",
- modifier = modifier
- .testTag("test-card-$it")
- .size(200.dp)
- .focusable()
- )
- }
- item { TestImmersiveList(focusRequesterList) }
- items(2) {
- BasicText(
- text = "test-card-${it + 3}",
- modifier = Modifier
- .testTag("test-card-${it + 3}")
- .size(200.dp)
- .focusable()
- )
- }
- }
- }
- rule.runOnIdle { focusRequester.requestFocus() }
- }
-
- @Composable
- private fun TestImmersiveList(focusRequesterList: List<FocusRequester>) {
- val frList = remember { focusRequesterList }
- ImmersiveList(
- background = { index, _ ->
- AnimatedContent(targetState = index) {
- Box(
- Modifier
- .testTag("background-$it")
- .fillMaxWidth()
- .height(400.dp)
- .border(2.dp, Color.Black, RectangleShape)
- ) {
- BasicText("background-$it")
- }
- }
- },
- modifier = Modifier.testTag("immersive-list")
- ) {
- TvLazyRow {
- items(frList.count()) { index ->
- var modifier = Modifier
- .testTag("list-card-$index")
- .size(50.dp)
- for (item in frList) {
- modifier = modifier.focusRequester(frList[index])
- }
- Box(
- modifier
- .immersiveListItem(index)
- .focusable(true)) {
- BasicText("list-card-$index")
- }
- }
- }
- }
- }
-
- private fun keyPress(keyCode: Int, numberOfPresses: Int = 1) {
- for (index in 0 until numberOfPresses)
- InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode)
- }
-}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/BringIntoViewIfChildrenAreFocused.kt b/tv/tv-material/src/main/java/androidx/tv/material3/BringIntoViewIfChildrenAreFocused.kt
index b9decd5..572fbfe 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/BringIntoViewIfChildrenAreFocused.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/BringIntoViewIfChildrenAreFocused.kt
@@ -16,39 +16,28 @@
package androidx.tv.material3
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.relocation.BringIntoViewResponder
-import androidx.compose.foundation.relocation.bringIntoViewResponder
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.debugInspectorInfo
-
-@OptIn(ExperimentalFoundationApi::class)
-internal fun Modifier.bringIntoViewIfChildrenAreFocused(): Modifier = composed(
- inspectorInfo = debugInspectorInfo { name = "bringIntoViewIfChildrenAreFocused" },
- factory = {
- var myRect: Rect = Rect.Zero
- this
- .onSizeChanged {
- myRect = Rect(Offset.Zero, Offset(it.width.toFloat(), it.height.toFloat()))
- }
- .bringIntoViewResponder(
- remember {
- object : BringIntoViewResponder {
- // return the current rectangle and ignoring the child rectangle received.
- @ExperimentalFoundationApi
- override fun calculateRectForParent(localRect: Rect): Rect = myRect
-
- // The container is not expected to be scrollable. Hence the child is
- // already in view with respect to the container.
- @ExperimentalFoundationApi
- override suspend fun bringChildIntoView(localRect: () -> Rect?) {}
- }
- }
- )
- }
-)
+// @OptIn(ExperimentalFoundationApi::class)
+// internal fun Modifier.bringIntoViewIfChildrenAreFocused(): Modifier = composed(
+// inspectorInfo = debugInspectorInfo { name = "bringIntoViewIfChildrenAreFocused" },
+// factory = {
+// var myRect: Rect = Rect.Zero
+// this
+// .onSizeChanged {
+// myRect = Rect(Offset.Zero, Offset(it.width.toFloat(), it.height.toFloat()))
+// }
+// .bringIntoViewResponder(
+// remember {
+// object : BringIntoViewResponder {
+// // return the current rectangle and ignoring the child rectangle received.
+// @ExperimentalFoundationApi
+// override fun calculateRectForParent(localRect: Rect): Rect = myRect
+//
+// // The container is not expected to be scrollable. Hence the child is
+// // already in view with respect to the container.
+// @ExperimentalFoundationApi
+// override suspend fun bringChildIntoView(localRect: () -> Rect?) {}
+// }
+// }
+// )
+// }
+// )
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
index 1cc9e95..9aec264 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
@@ -22,7 +22,6 @@
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ContentTransform
-import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -49,7 +48,6 @@
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusDirection.Companion.Left
@@ -86,6 +84,10 @@
/**
* Composes a hero card rotator to highlight a piece of content.
*
+ * Note: The animations and focus management features have been dropped temporarily due to
+ * some technical challenges. If you need them, consider using the previous version of the
+ * library (1.0.0-alpha10) or kindly wait until the next alpha version (1.1.0-alpha01).
+ *
* Examples:
* @sample androidx.tv.samples.SimpleCarousel
* @sample androidx.tv.samples.CarouselIndicatorWithRectangleShape
@@ -102,7 +104,7 @@
* @param carouselIndicator indicator showing the position of the current item among all items.
* @param content defines the items for a given index.
*/
-@OptIn(ExperimentalComposeUiApi::class)
+// @OptIn(ExperimentalComposeUiApi::class)
@ExperimentalTvMaterial3Api
@Composable
fun Carousel(
@@ -144,14 +146,14 @@
Box(modifier = modifier
.carouselSemantics(itemCount = itemCount, state = carouselState)
- .bringIntoViewIfChildrenAreFocused()
+ // .bringIntoViewIfChildrenAreFocused()
.focusRequester(carouselOuterBoxFocusRequester)
.onFocusChanged {
focusState = it
// When the carousel gains focus for the first time
- if (it.isFocused && isAutoScrollActive) {
- focusManager.moveFocus(FocusDirection.Enter)
- }
+// if (it.isFocused && isAutoScrollActive) {
+// focusManager.moveFocus(FocusDirection.Enter)
+// }
}
.handleKeyEvents(
carouselState = carouselState,
@@ -181,7 +183,7 @@
// Outer box is focused
if (!isAutoScrollActive && focusState?.isFocused == true) {
carouselOuterBoxFocusRequester.requestFocus()
- focusManager.moveFocus(FocusDirection.Enter)
+// focusManager.moveFocus(FocusDirection.Enter)
}
}
}
@@ -209,9 +211,9 @@
return !accessibilityManager.isEnabled && !(carouselIsFocused || carouselHasFocus)
}
-@OptIn(ExperimentalAnimationApi::class)
+// @OptIn(ExperimentalAnimationApi::class)
private suspend fun AnimatedVisibilityScope.onAnimationCompletion(action: suspend () -> Unit) {
- snapshotFlow { transition.currentState == transition.targetState }.first { it }
+// snapshotFlow { transition.currentState == transition.targetState }.first { it }
action.invoke()
}
@@ -246,7 +248,10 @@
onAutoScrollChange(doAutoScroll)
}
-@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalComposeUiApi::class)
+@OptIn(
+ ExperimentalTvMaterial3Api::class,
+// ExperimentalComposeUiApi::class
+)
private fun Modifier.handleKeyEvents(
carouselState: CarouselState,
outerBoxFocusRequester: FocusRequester,
@@ -289,7 +294,7 @@
}
!focusManager.moveFocus(direction) &&
- currentCarouselBoxFocusState()?.hasFocus == true -> {
+ currentCarouselBoxFocusState()?.hasFocus == true -> {
// if focus search was unsuccessful, interpret as input for slide change
updateItemBasedOnLayout(direction, isLtr)
KeyEventPropagation.StopPropagation
@@ -302,7 +307,7 @@
// Ignore KeyUp action type
it.type == KeyUp -> KeyEventPropagation.ContinuePropagation
it.key == Key.Back -> {
- focusManager.moveFocus(FocusDirection.Exit)
+// focusManager.moveFocus(FocusDirection.Exit)
KeyEventPropagation.ContinuePropagation
}
@@ -313,14 +318,14 @@
}
}.focusProperties {
// allow exit along horizontal axis only for first and last slide.
- exit = {
- when {
- shouldFocusExitCarousel(it, carouselState, itemCount, isLtr) ->
- FocusRequester.Default
-
- else -> FocusRequester.Cancel
- }
- }
+// exit = {
+// when {
+// shouldFocusExitCarousel(it, carouselState, itemCount, isLtr) ->
+// FocusRequester.Default
+//
+// else -> FocusRequester.Cancel
+// }
+// }
}
@OptIn(ExperimentalTvMaterial3Api::class)
@@ -490,9 +495,9 @@
* Transition applied when bringing it into view and removing it from the view
*/
val contentTransform: ContentTransform
- @Composable get() =
- fadeIn(animationSpec = tween(100))
- .togetherWith(fadeOut(animationSpec = tween(100)))
+ @Composable get() =
+ fadeIn(animationSpec = tween(100))
+ .togetherWith(fadeOut(animationSpec = tween(100)))
/**
* An indicator showing the position of the current active item among the items of the
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ImmersiveList.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ImmersiveList.kt
deleted file mode 100644
index 2720e90..0000000
--- a/tv/tv-material/src/main/java/androidx/tv/material3/ImmersiveList.kt
+++ /dev/null
@@ -1,197 +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.tv.material3
-
-import androidx.compose.animation.AnimatedContentTransitionScope
-import androidx.compose.animation.AnimatedVisibilityScope
-import androidx.compose.animation.ContentTransform
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-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.focus.FocusDirection
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.platform.LocalFocusManager
-
-/**
- * Immersive List consists of a list with multiple items and a background that displays content
- * based on the item in focus.
- * To animate the background's entry and exit, use [ImmersiveListBackgroundScope.AnimatedContent].
- * To display the background only when the list is in focus, use
- * [ImmersiveListBackgroundScope.AnimatedVisibility].
- *
- * @param background Composable defining the background to be displayed for a given item's
- * index. `listHasFocus` argument can be used to hide the background when the list is not in focus
- * @param modifier applied to Immersive List.
- * @param listAlignment Alignment of the List with respect to the Immersive List.
- * @param list composable defining the list of items that has to be rendered.
- */
-@OptIn(ExperimentalComposeUiApi::class)
-@ExperimentalTvMaterial3Api
-@Composable
-fun ImmersiveList(
- background:
- @Composable ImmersiveListBackgroundScope.(index: Int, listHasFocus: Boolean) -> Unit,
- modifier: Modifier = Modifier,
- listAlignment: Alignment = Alignment.BottomEnd,
- list: @Composable ImmersiveListScope.() -> Unit,
-) {
- var currentItemIndex by remember { mutableIntStateOf(0) }
- var listHasFocus by remember { mutableStateOf(false) }
-
- Box(modifier.bringIntoViewIfChildrenAreFocused()) {
- ImmersiveListBackgroundScope(this).background(currentItemIndex, listHasFocus)
-
- val focusManager = LocalFocusManager.current
-
- Box(Modifier.align(listAlignment).onFocusChanged { listHasFocus = it.hasFocus }) {
- ImmersiveListScope {
- currentItemIndex = it
- focusManager.moveFocus(FocusDirection.Enter)
- }.list()
- }
- }
-}
-
-@ExperimentalTvMaterial3Api
-object ImmersiveListDefaults {
- /**
- * Default transition used to bring the background content into view
- */
- val EnterTransition: EnterTransition = fadeIn(animationSpec = tween(300))
-
- /**
- * Default transition used to remove the background content from view
- */
- val ExitTransition: ExitTransition = fadeOut(animationSpec = tween(300))
-}
-
-@Immutable
-@ExperimentalTvMaterial3Api
-public class ImmersiveListBackgroundScope internal constructor(boxScope: BoxScope) : BoxScope
-by boxScope {
-
- /**
- * [ImmersiveListBackgroundScope.AnimatedVisibility] composable animates the appearance and
- * disappearance of its content, as [visible] value changes. Different [EnterTransition]s and
- * [ExitTransition]s can be defined in [enter] and [exit] for the appearance and disappearance
- * animation.
- *
- * @param visible defines whether the content should be visible
- * @param modifier modifier for the Layout created to contain the [content]
- * @param enter EnterTransition(s) used for the appearing animation, fading in by default
- * @param exit ExitTransition(s) used for the disappearing animation, fading out by default
- * @param label helps differentiate from other animations in Android Studio
- * @param content Content to appear or disappear based on the value of [visible]
- *
- * @link androidx.compose.animation.AnimatedVisibility
- * @see androidx.compose.animation.AnimatedVisibility
- * @see EnterTransition
- * @see ExitTransition
- * @see AnimatedVisibilityScope
- */
- @Composable
- fun AnimatedVisibility(
- visible: Boolean,
- modifier: Modifier = Modifier,
- enter: EnterTransition = ImmersiveListDefaults.EnterTransition,
- exit: ExitTransition = ImmersiveListDefaults.ExitTransition,
- label: String = "AnimatedVisibility",
- content: @Composable AnimatedVisibilityScope.() -> Unit
- ) {
- androidx.compose.animation.AnimatedVisibility(
- visible,
- modifier,
- enter,
- exit,
- label,
- content
- )
- }
-
- /**
- * [ImmersiveListBackgroundScope.AnimatedContent] is a container that automatically animates its
- * content when [targetState] changes. Its [content] for different target states is defined in a
- * mapping between a target state and a composable function.
- *
- * @param targetState defines the key to choose the content to be displayed
- * @param modifier modifier for the Layout created to contain the [content]
- * @param transitionSpec defines the EnterTransition(s) and ExitTransition(s) used to display
- * and remove the content, fading in and fading out by default
- * @param contentAlignment specifies how the background content should be aligned in the
- * container
- * @param content Content to appear or disappear based on the value of [targetState]
- *
- * @link androidx.compose.animation.AnimatedContent
- * @see androidx.compose.animation.AnimatedContent
- * @see ContentTransform
- * @see AnimatedContentTransitionScope
- */
- @Composable
- fun AnimatedContent(
- targetState: Int,
- modifier: Modifier = Modifier,
- transitionSpec: AnimatedContentTransitionScope<Int>.() -> ContentTransform = {
- ImmersiveListDefaults.EnterTransition.togetherWith(ImmersiveListDefaults.ExitTransition)
- },
- contentAlignment: Alignment = Alignment.TopStart,
- content: @Composable AnimatedVisibilityScope.(targetState: Int) -> Unit
- ) {
- androidx.compose.animation.AnimatedContent(
- targetState,
- modifier,
- transitionSpec,
- contentAlignment,
- content = content
- )
- }
-}
-
-@Immutable
-@ExperimentalTvMaterial3Api
-public class ImmersiveListScope internal constructor(private val onFocused: (Int) -> Unit) {
- /**
- * Modifier to be added to each of the items of the list within ImmersiveList to inform the
- * ImmersiveList of the index of the item in focus
- *
- * > **NOTE**: This modifier needs to be paired with either the "focusable" or the "clickable"
- * modifier for it to work
- *
- * @param index index of the item within the list
- */
- fun Modifier.immersiveListItem(index: Int): Modifier {
- return onFocusChanged {
- if (it.isFocused) {
- onFocused(index)
- }
- }
- }
-}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/BasicSwipeToDismissBox.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/BasicSwipeToDismissBox.kt
index 9bfc687..009334f 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/BasicSwipeToDismissBox.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/BasicSwipeToDismissBox.kt
@@ -353,6 +353,7 @@
edgeSwipeState: State<EdgeSwipeState>
): NestedScrollConnection =
object : NestedScrollConnection {
+ @Suppress("DEPRECATION") // b/327155912
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.x
// If swipeState = SwipeState.SWIPING_TO_DISMISS - perform swipeToDismiss
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
index 94200e9..e344c9f 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
@@ -471,6 +471,7 @@
*
* @return The delta the consumed by the [SwipeableV2State]
*/
+ @Suppress("DEPRECATION") // b/327155912
fun dispatchRawDelta(delta: Float): Float {
var remainingDelta = delta
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index bc3520f9..bd9565b 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -347,6 +347,7 @@
ctor public PlatformDataValues.Builder();
method public androidx.wear.protolayout.expression.PlatformDataValues build();
method public <T extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType> androidx.wear.protolayout.expression.PlatformDataValues.Builder put(androidx.wear.protolayout.expression.PlatformDataKey<T!>, androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue<T!>);
+ method public androidx.wear.protolayout.expression.PlatformDataValues.Builder putAll(androidx.wear.protolayout.expression.PlatformDataValues);
}
public class PlatformHealthSources {
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index bc3520f9..bd9565b 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -347,6 +347,7 @@
ctor public PlatformDataValues.Builder();
method public androidx.wear.protolayout.expression.PlatformDataValues build();
method public <T extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType> androidx.wear.protolayout.expression.PlatformDataValues.Builder put(androidx.wear.protolayout.expression.PlatformDataKey<T!>, androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue<T!>);
+ method public androidx.wear.protolayout.expression.PlatformDataValues.Builder putAll(androidx.wear.protolayout.expression.PlatformDataValues);
}
public class PlatformHealthSources {
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/PlatformDataValues.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/PlatformDataValues.java
index 18dbb6c..dfe1c6c 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/PlatformDataValues.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/PlatformDataValues.java
@@ -19,8 +19,6 @@
import static java.util.Collections.unmodifiableMap;
import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
import androidx.collection.ArrayMap;
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicType;
import androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue;
@@ -51,8 +49,6 @@
*/
@NonNull
@SuppressWarnings("BuilderSetStyle") // Map-style builder, getter is generic get().
- @VisibleForTesting
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public Builder putAll(@NonNull PlatformDataValues other) {
data.putAll(other.data);
return this;
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
index ca14d6b..0751da3 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
@@ -69,6 +69,7 @@
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.View;
+import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewOutlineProvider;
@@ -2481,16 +2482,7 @@
if (spacer.getWidth().hasLinearDimension()) {
handleProp(
spacer.getWidth().getLinearDimension(),
- width -> {
- LayoutParams lp = view.getLayoutParams();
- if (lp == null) {
- Log.e(TAG, "LayoutParams was null when updating spacer width");
- return;
- }
-
- lp.width = safeDpToPx(width);
- view.requestLayout();
- },
+ widthDp -> updateLayoutWidthParam(view, widthDp),
posId,
pipelineMaker);
}
@@ -2498,16 +2490,7 @@
if (spacer.getHeight().hasLinearDimension()) {
handleProp(
spacer.getHeight().getLinearDimension(),
- height -> {
- LayoutParams lp = view.getLayoutParams();
- if (lp == null) {
- Log.e(TAG, "LayoutParams was null when updating spacer height");
- return;
- }
-
- lp.height = safeDpToPx(height);
- view.requestLayout();
- },
+ heightDp -> updateLayoutHeightParam(view, heightDp),
posId,
pipelineMaker);
}
@@ -2517,45 +2500,14 @@
if (spacer.getWidth().hasLinearDimension()) {
handleProp(
spacer.getWidth().getLinearDimension(),
- width -> {
- // Update minimum width first, because LayoutParams could be null.
- // This calls requestLayout.
- int widthPx = safeDpToPx(width);
- view.setMinimumWidth(widthPx);
-
- // We still need to update layout params in case other dimension is
- // expand, so 0 could
- // be miss interpreted.
- LayoutParams lp = view.getLayoutParams();
- if (lp == null) {
- Log.e(TAG, "LayoutParams was null when updating spacer width");
- return;
- }
-
- lp.width = widthPx;
- },
+ widthDp -> updateLayoutWidthParam(view, widthDp),
posId,
pipelineMaker);
}
if (spacer.getHeight().hasLinearDimension()) {
handleProp(
spacer.getHeight().getLinearDimension(),
- height -> {
- // Update minimum height first, because LayoutParams could be null.
- // This calls requestLayout.
- int heightPx = safeDpToPx(height);
- view.setMinimumHeight(heightPx);
-
- // We still need to update layout params in case other dimension is
- // expand, so 0 could be miss interpreted.
- LayoutParams lp = view.getLayoutParams();
- if (lp == null) {
- Log.e(TAG, "LayoutParams was null when updating spacer height");
- return;
- }
-
- lp.height = heightPx;
- },
+ heightDp -> updateLayoutHeightParam(view, heightDp),
posId,
pipelineMaker);
}
@@ -2576,6 +2528,49 @@
}
}
+ private void updateLayoutWidthParam(@NonNull View view, float widthDp) {
+ scheduleLayoutParamsUpdate(
+ view,
+ () -> {
+ checkNotNull(view.getLayoutParams()).width = safeDpToPx(widthDp);
+ view.requestLayout();
+ });
+ }
+
+ private void updateLayoutHeightParam(@NonNull View view, float heightDp) {
+ scheduleLayoutParamsUpdate(
+ view,
+ () -> {
+ checkNotNull(view.getLayoutParams()).height = safeDpToPx(heightDp);
+ view.requestLayout();
+ });
+ }
+
+ private void scheduleLayoutParamsUpdate(@NonNull View view, Runnable layoutParamsUpdater) {
+ if (view.getLayoutParams() != null) {
+ layoutParamsUpdater.run();
+ return;
+ }
+
+ // View#getLayoutParams() returns null if this view is not attached to a parent ViewGroup.
+ // And once the view is attached to a parent ViewGroup, it guarantees a non-null return
+ // value. Thus, we use the listener to do the update of the layout param the moment that the
+ // view is attached to window.
+ view.addOnAttachStateChangeListener(
+ new View.OnAttachStateChangeListener() {
+
+ @Override
+ public void onViewAttachedToWindow(@NonNull View v) {
+ layoutParamsUpdater.run();
+ v.removeOnAttachStateChangeListener(this);
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(@NonNull View v) {
+ }
+ });
+ }
+
@Nullable
private InflatedView inflateArcSpacer(
ParentViewWrapper parentViewWrapper,
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
index 04e3f2b..d0fb91c 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
@@ -708,7 +708,7 @@
// This tests that minimum dimension is correctly set.
// Dimensions are in DP, but the density is currently 1 in the tests, so this is fine.
- expect.that(tv.getMinimumWidth()).isEqualTo(width);
+ expect.that(tv.getMeasuredWidth()).isEqualTo(width);
expect.that(tv.getMeasuredHeight()).isEqualTo(height);
}
@@ -741,13 +741,9 @@
ViewGroup boxAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0);
View spacerAfterMutation = boxAfterMutation.getChildAt(0);
- // Dimensions are in DP, but the density is currently 1 in the tests, so this is fine.
- expect.that(spacerAfterMutation.getMeasuredWidth()).isEqualTo(0);
- expect.that(spacerAfterMutation.getMeasuredHeight()).isEqualTo(0);
-
- // This tests that minimum dimension is correctly set.
- expect.that(spacerAfterMutation.getMinimumWidth()).isEqualTo(newWidth);
- expect.that(spacerAfterMutation.getMinimumHeight()).isEqualTo(newHeight);
+ // This tests that the layout dimension is correctly set.
+ expect.that(spacerAfterMutation.getMeasuredWidth()).isEqualTo(newWidth);
+ expect.that(spacerAfterMutation.getMeasuredHeight()).isEqualTo(newHeight);
}
@Test
@@ -799,7 +795,7 @@
Renderer renderer = renderer(layout1);
ViewGroup inflatedViewParent = renderer.inflate();
- Layout layout2 = layoutBoxWithSpacer(newHeight, newWidth, modifiers);
+ Layout layout2 = layoutBoxWithSpacer(newWidth, newHeight, modifiers);
// Compute the mutation.
ViewGroupMutation mutation =
@@ -815,8 +811,8 @@
View spacerAfterMutation = boxAfterMutation.getChildAt(0);
// Dimensions are in DP, but the density is currently 1 in the tests, so this is fine.
- expect.that(spacerAfterMutation.getMeasuredWidth()).isEqualTo(0);
- expect.that(spacerAfterMutation.getMeasuredHeight()).isEqualTo(0);
+ expect.that(spacerAfterMutation.getMeasuredWidth()).isEqualTo(newWidth);
+ expect.that(spacerAfterMutation.getMeasuredHeight()).isEqualTo(newHeight);
}
@Test
diff --git a/wear/protolayout/protolayout/api/current.txt b/wear/protolayout/protolayout/api/current.txt
index 1e0271b..24afe14 100644
--- a/wear/protolayout/protolayout/api/current.txt
+++ b/wear/protolayout/protolayout/api/current.txt
@@ -1155,11 +1155,11 @@
@androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static final class ModifiersBuilders.Transformation {
method public androidx.wear.protolayout.DimensionBuilders.PivotDimension? getPivotX();
method public androidx.wear.protolayout.DimensionBuilders.PivotDimension? getPivotY();
- method public androidx.wear.protolayout.DimensionBuilders.DegreesProp? getRotation();
- method public androidx.wear.protolayout.TypeBuilders.FloatProp? getScaleX();
- method public androidx.wear.protolayout.TypeBuilders.FloatProp? getScaleY();
- method public androidx.wear.protolayout.DimensionBuilders.DpProp? getTranslationX();
- method public androidx.wear.protolayout.DimensionBuilders.DpProp? getTranslationY();
+ method public androidx.wear.protolayout.DimensionBuilders.DegreesProp getRotation();
+ method public androidx.wear.protolayout.TypeBuilders.FloatProp getScaleX();
+ method public androidx.wear.protolayout.TypeBuilders.FloatProp getScaleY();
+ method public androidx.wear.protolayout.DimensionBuilders.DpProp getTranslationX();
+ method public androidx.wear.protolayout.DimensionBuilders.DpProp getTranslationY();
}
public static final class ModifiersBuilders.Transformation.Builder {
diff --git a/wear/protolayout/protolayout/api/restricted_current.txt b/wear/protolayout/protolayout/api/restricted_current.txt
index 1e0271b..24afe14 100644
--- a/wear/protolayout/protolayout/api/restricted_current.txt
+++ b/wear/protolayout/protolayout/api/restricted_current.txt
@@ -1155,11 +1155,11 @@
@androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static final class ModifiersBuilders.Transformation {
method public androidx.wear.protolayout.DimensionBuilders.PivotDimension? getPivotX();
method public androidx.wear.protolayout.DimensionBuilders.PivotDimension? getPivotY();
- method public androidx.wear.protolayout.DimensionBuilders.DegreesProp? getRotation();
- method public androidx.wear.protolayout.TypeBuilders.FloatProp? getScaleX();
- method public androidx.wear.protolayout.TypeBuilders.FloatProp? getScaleY();
- method public androidx.wear.protolayout.DimensionBuilders.DpProp? getTranslationX();
- method public androidx.wear.protolayout.DimensionBuilders.DpProp? getTranslationY();
+ method public androidx.wear.protolayout.DimensionBuilders.DegreesProp getRotation();
+ method public androidx.wear.protolayout.TypeBuilders.FloatProp getScaleX();
+ method public androidx.wear.protolayout.TypeBuilders.FloatProp getScaleY();
+ method public androidx.wear.protolayout.DimensionBuilders.DpProp getTranslationX();
+ method public androidx.wear.protolayout.DimensionBuilders.DpProp getTranslationY();
}
public static final class ModifiersBuilders.Transformation.Builder {
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ModifiersBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ModifiersBuilders.java
index 09e5e075..a6bb9fe 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ModifiersBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ModifiersBuilders.java
@@ -1337,12 +1337,12 @@
* Gets the horizontal offset of this element relative to the location where the element's
* layout placed it.
*/
- @Nullable
+ @NonNull
public DpProp getTranslationX() {
if (mImpl.hasTranslationX()) {
return DpProp.fromProto(mImpl.getTranslationX());
} else {
- return null;
+ return new DpProp.Builder(0f).build();
}
}
@@ -1350,12 +1350,12 @@
* Gets the vertical offset of this element in addition to the location where the element's
* layout placed it.
*/
- @Nullable
+ @NonNull
public DpProp getTranslationY() {
if (mImpl.hasTranslationY()) {
return DpProp.fromProto(mImpl.getTranslationY());
} else {
- return null;
+ return new DpProp.Builder(0f).build();
}
}
@@ -1364,12 +1364,12 @@
* Gets the scale of this element in the x direction around the pivot point, as a proportion
* of the element's unscaled width.
*/
- @Nullable
+ @NonNull
public FloatProp getScaleX() {
if (mImpl.hasScaleX()) {
return FloatProp.fromProto(mImpl.getScaleX());
} else {
- return null;
+ return new FloatProp.Builder(1f).build();
}
}
@@ -1377,24 +1377,24 @@
* Gets the scale of this element in the y direction around the pivot point, as a proportion
* of the element's unscaled height.
*/
- @Nullable
+ @NonNull
public FloatProp getScaleY() {
if (mImpl.hasScaleY()) {
return FloatProp.fromProto(mImpl.getScaleY());
} else {
- return null;
+ return new FloatProp.Builder(1f).build();
}
}
/**
* Gets the clockwise Degrees that the element is rotated around the pivot point.
*/
- @Nullable
+ @NonNull
public DegreesProp getRotation() {
if (mImpl.hasRotation()) {
return DegreesProp.fromProto(mImpl.getRotation());
} else {
- return null;
+ return new DegreesProp.Builder(0f).build();
}
}
diff --git a/wear/tiles/tiles-renderer/api/current.txt b/wear/tiles/tiles-renderer/api/current.txt
index 8b0c98e..1d336ea 100644
--- a/wear/tiles/tiles-renderer/api/current.txt
+++ b/wear/tiles/tiles-renderer/api/current.txt
@@ -46,25 +46,21 @@
ctor @Deprecated public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
ctor @Deprecated public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
ctor public TileRenderer(android.content.Context, java.util.concurrent.Executor, java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!>);
- ctor public TileRenderer(androidx.wear.tiles.renderer.TileRenderer.Config);
- method @Deprecated public android.view.View? inflate(android.view.ViewGroup);
- method public com.google.common.util.concurrent.ListenableFuture<android.view.View!> inflateAsync(androidx.wear.protolayout.LayoutElementBuilders.Layout, androidx.wear.protolayout.ResourceBuilders.Resources, android.view.ViewGroup);
- method public void setState(java.util.Map<androidx.wear.protolayout.expression.AppDataKey<?>!,androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue<?>!>);
- }
-
- public static final class TileRenderer.Config {
method public java.util.concurrent.Executor getLoadActionExecutor();
method public java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!> getLoadActionListener();
method public java.util.Map<androidx.wear.protolayout.expression.pipeline.PlatformDataProvider!,java.util.Set<androidx.wear.protolayout.expression.PlatformDataKey<?>!>!> getPlatformDataProviders();
method public int getTilesTheme();
method public android.content.Context getUiContext();
+ method @Deprecated public android.view.View? inflate(android.view.ViewGroup);
+ method public com.google.common.util.concurrent.ListenableFuture<android.view.View!> inflateAsync(androidx.wear.protolayout.LayoutElementBuilders.Layout, androidx.wear.protolayout.ResourceBuilders.Resources, android.view.ViewGroup);
+ method public void setState(java.util.Map<androidx.wear.protolayout.expression.AppDataKey<?>!,androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue<?>!>);
}
- public static final class TileRenderer.Config.Builder {
- ctor public TileRenderer.Config.Builder(android.content.Context, java.util.concurrent.Executor, java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!>);
- method public androidx.wear.tiles.renderer.TileRenderer.Config.Builder addPlatformDataProvider(androidx.wear.protolayout.expression.pipeline.PlatformDataProvider, androidx.wear.protolayout.expression.PlatformDataKey<?>!...);
- method public androidx.wear.tiles.renderer.TileRenderer.Config build();
- method public androidx.wear.tiles.renderer.TileRenderer.Config.Builder setTilesTheme(@StyleRes int);
+ public static final class TileRenderer.Builder {
+ ctor public TileRenderer.Builder(android.content.Context, java.util.concurrent.Executor, java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!>);
+ method public androidx.wear.tiles.renderer.TileRenderer.Builder addPlatformDataProvider(androidx.wear.protolayout.expression.pipeline.PlatformDataProvider, androidx.wear.protolayout.expression.PlatformDataKey<?>!...);
+ method public androidx.wear.tiles.renderer.TileRenderer build();
+ method public androidx.wear.tiles.renderer.TileRenderer.Builder setTilesTheme(@StyleRes int);
}
@Deprecated public static interface TileRenderer.LoadActionListener {
diff --git a/wear/tiles/tiles-renderer/api/restricted_current.txt b/wear/tiles/tiles-renderer/api/restricted_current.txt
index 8b0c98e..1d336ea 100644
--- a/wear/tiles/tiles-renderer/api/restricted_current.txt
+++ b/wear/tiles/tiles-renderer/api/restricted_current.txt
@@ -46,25 +46,21 @@
ctor @Deprecated public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
ctor @Deprecated public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
ctor public TileRenderer(android.content.Context, java.util.concurrent.Executor, java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!>);
- ctor public TileRenderer(androidx.wear.tiles.renderer.TileRenderer.Config);
- method @Deprecated public android.view.View? inflate(android.view.ViewGroup);
- method public com.google.common.util.concurrent.ListenableFuture<android.view.View!> inflateAsync(androidx.wear.protolayout.LayoutElementBuilders.Layout, androidx.wear.protolayout.ResourceBuilders.Resources, android.view.ViewGroup);
- method public void setState(java.util.Map<androidx.wear.protolayout.expression.AppDataKey<?>!,androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue<?>!>);
- }
-
- public static final class TileRenderer.Config {
method public java.util.concurrent.Executor getLoadActionExecutor();
method public java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!> getLoadActionListener();
method public java.util.Map<androidx.wear.protolayout.expression.pipeline.PlatformDataProvider!,java.util.Set<androidx.wear.protolayout.expression.PlatformDataKey<?>!>!> getPlatformDataProviders();
method public int getTilesTheme();
method public android.content.Context getUiContext();
+ method @Deprecated public android.view.View? inflate(android.view.ViewGroup);
+ method public com.google.common.util.concurrent.ListenableFuture<android.view.View!> inflateAsync(androidx.wear.protolayout.LayoutElementBuilders.Layout, androidx.wear.protolayout.ResourceBuilders.Resources, android.view.ViewGroup);
+ method public void setState(java.util.Map<androidx.wear.protolayout.expression.AppDataKey<?>!,androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue<?>!>);
}
- public static final class TileRenderer.Config.Builder {
- ctor public TileRenderer.Config.Builder(android.content.Context, java.util.concurrent.Executor, java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!>);
- method public androidx.wear.tiles.renderer.TileRenderer.Config.Builder addPlatformDataProvider(androidx.wear.protolayout.expression.pipeline.PlatformDataProvider, androidx.wear.protolayout.expression.PlatformDataKey<?>!...);
- method public androidx.wear.tiles.renderer.TileRenderer.Config build();
- method public androidx.wear.tiles.renderer.TileRenderer.Config.Builder setTilesTheme(@StyleRes int);
+ public static final class TileRenderer.Builder {
+ ctor public TileRenderer.Builder(android.content.Context, java.util.concurrent.Executor, java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!>);
+ method public androidx.wear.tiles.renderer.TileRenderer.Builder addPlatformDataProvider(androidx.wear.protolayout.expression.pipeline.PlatformDataProvider, androidx.wear.protolayout.expression.PlatformDataKey<?>!...);
+ method public androidx.wear.tiles.renderer.TileRenderer build();
+ method public androidx.wear.tiles.renderer.TileRenderer.Builder setTilesTheme(@StyleRes int);
}
@Deprecated public static interface TileRenderer.LoadActionListener {
diff --git a/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java b/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
index 763c021..c8c66c4 100644
--- a/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
+++ b/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
@@ -41,7 +41,6 @@
import androidx.wear.protolayout.proto.ResourceProto.Resources;
import androidx.wear.protolayout.protobuf.ByteString;
import androidx.wear.tiles.renderer.TileRenderer;
-import androidx.wear.tiles.renderer.TileRenderer.Config;
import com.google.protobuf.TextFormat;
@@ -236,12 +235,11 @@
LayoutElement rootElement = LayoutElement.parseFrom(contents);
TileRenderer renderer =
- new TileRenderer(
- new Config.Builder(
- appContext,
- ContextCompat.getMainExecutor(getApplicationContext()),
- i -> {})
- .build());
+ new TileRenderer.Builder(
+ appContext,
+ ContextCompat.getMainExecutor(getApplicationContext()),
+ i -> {})
+ .build();
View firstChild =
renderer.inflateAsync(
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 01a5cab..6bcd95b 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
@@ -81,6 +81,16 @@
void onClick(@NonNull androidx.wear.tiles.StateBuilders.State nextState);
}
+ @NonNull private final Context mUiContext;
+ @NonNull private final Executor mLoadActionExecutor;
+ @NonNull private final Consumer<StateBuilders.State> mLoadActionListener;
+
+ @StyleRes int mTilesTheme = 0; // Default theme.
+
+ @NonNull
+ private final Map<PlatformDataProvider, Set<PlatformDataKey<?>>> mPlatformDataProviders =
+ new ArrayMap<>();
+
@NonNull private final ProtoLayoutViewInstance mInstance;
@Nullable private final LayoutElementProto.Layout mLayout;
@Nullable private final ResourceProto.Resources mResources;
@@ -95,7 +105,7 @@
* @param resources The resources for the Tile.
* @param loadActionExecutor Executor for {@code loadActionListener}.
* @param loadActionListener Listener for clicks that will cause the contents to be reloaded.
- * @deprecated Use {@link #TileRenderer(Config)} which accepts Layout and Resources in {@link
+ * @deprecated Use {@link TileRenderer.Builder} which accepts Layout and Resources in {@link
* #inflateAsync(LayoutElementBuilders.Layout, ResourceBuilders.Resources, ViewGroup)}
* method.
*/
@@ -126,7 +136,7 @@
* @param resources The resources for the Tile.
* @param loadActionExecutor Executor for {@code loadActionListener}.
* @param loadActionListener Listener for clicks that will cause the contents to be reloaded.
- * @deprecated Use {@link #TileRenderer(Config)} which accepts Layout and Resources in {@link
+ * @deprecated Use {@link TileRenderer.Builder} which accepts Layout and Resources in {@link
* #inflateAsync(LayoutElementBuilders.Layout, ResourceBuilders.Resources, ViewGroup)}
* method.
*/
@@ -151,7 +161,7 @@
/**
* Constructor for {@link TileRenderer}.
*
- * <p>It is recommended to use the new {@link #TileRenderer(Config)} constructor instead.
+ * <p>It is recommended to use the new {@link TileRenderer.Builder} instead.
*
* @param uiContext A {@link Context} suitable for interacting with the UI.
* @param loadActionExecutor Executor for {@code loadActionListener}.
@@ -171,22 +181,6 @@
/* platformDataProviders */ null);
}
- /**
- * Constructor for {@link TileRenderer}.
- *
- * @param config A {@link Config} to create a {@link TileRenderer} instance.
- */
- public TileRenderer(@NonNull Config config) {
- this(
- config.getUiContext(),
- config.getTilesTheme(),
- config.getLoadActionExecutor(),
- config.getLoadActionListener(),
- /* layout= */ null,
- /* resources= */ null,
- config.getPlatformDataProviders());
- }
-
private TileRenderer(
@NonNull Context uiContext,
@StyleRes int tilesTheme,
@@ -196,6 +190,10 @@
@Nullable ResourceProto.Resources resources,
@Nullable Map<PlatformDataProvider, Set<PlatformDataKey<?>>> platformDataProviders) {
+ this.mUiContext = uiContext;
+ this.mTilesTheme = tilesTheme;
+ this.mLoadActionExecutor = loadActionExecutor;
+ this.mLoadActionListener = loadActionListener;
this.mLayout = layout;
this.mResources = resources;
this.mUiExecutor = MoreExecutors.newDirectExecutorService();
@@ -219,8 +217,8 @@
if (platformDataProviders != null) {
for (Map.Entry<PlatformDataProvider, Set<PlatformDataKey<?>>> entry :
platformDataProviders.entrySet()) {
- config.addPlatformDataProvider(entry.getKey(),
- entry.getValue().toArray(new PlatformDataKey[]{}));
+ config.addPlatformDataProvider(
+ entry.getKey(), entry.getValue().toArray(new PlatformDataKey[] {}));
}
}
this.mInstance = new ProtoLayoutViewInstance(config.build());
@@ -309,126 +307,103 @@
return FluentFuture.from(result).transform(ignored -> parent.getChildAt(0), mUiExecutor);
}
- /** Config class for {@link TileRenderer}. */
- public static final class Config {
+ /** Returns the {@link Context} suitable for interacting with the UI. */
+ @NonNull
+ public Context getUiContext() {
+ return mUiContext;
+ }
+
+ /** Returns the {@link Executor} for {@code loadActionListener}. */
+ @NonNull
+ public Executor getLoadActionExecutor() {
+ return mLoadActionExecutor;
+ }
+
+ /** Returns the Listener for clicks that will cause the contents to be reloaded. */
+ @NonNull
+ public Consumer<StateBuilders.State> getLoadActionListener() {
+ return mLoadActionListener;
+ }
+
+ /**
+ * Returns the theme to use for this Tile instance. This can be used to customise things like
+ * the default font family. Defaults to zero (default theme) if not specified by {@link
+ * Builder#setTilesTheme(int)}.
+ */
+ public int getTilesTheme() {
+ return mTilesTheme;
+ }
+
+ /** Returns the platform data providers that will be registered for this Tile instance. */
+ @NonNull
+ public Map<PlatformDataProvider, Set<PlatformDataKey<?>>> getPlatformDataProviders() {
+ return Collections.unmodifiableMap(mPlatformDataProviders);
+ }
+
+ /** Builder for {@link TileRenderer}. */
+ public static final class Builder {
@NonNull private final Context mUiContext;
@NonNull private final Executor mLoadActionExecutor;
@NonNull private final Consumer<StateBuilders.State> mLoadActionListener;
- @StyleRes int mTilesTheme;
+ @StyleRes int mTilesTheme = 0; // Default theme.
@NonNull
- private final Map<PlatformDataProvider, Set<PlatformDataKey<?>>> mPlatformDataProviders;
+ private final Map<PlatformDataProvider, Set<PlatformDataKey<?>>> mPlatformDataProviders =
+ new ArrayMap<>();
- Config(
+ /**
+ * Builder for the {@link TileRenderer} class.
+ *
+ * @param uiContext A {@link Context} suitable for interacting with the UI.
+ * @param loadActionExecutor Executor for {@code loadActionListener}.
+ * @param loadActionListener Listener for clicks that will cause the contents to be
+ * reloaded.
+ */
+ public Builder(
@NonNull Context uiContext,
@NonNull Executor loadActionExecutor,
- @NonNull Consumer<StateBuilders.State> loadActionListener,
- @StyleRes int tilesTheme,
- @NonNull Map<PlatformDataProvider, Set<PlatformDataKey<?>>> platformDataProviders) {
+ @NonNull Consumer<StateBuilders.State> loadActionListener) {
this.mUiContext = uiContext;
this.mLoadActionExecutor = loadActionExecutor;
this.mLoadActionListener = loadActionListener;
- this.mTilesTheme = tilesTheme;
- this.mPlatformDataProviders = platformDataProviders;
- }
-
- /** Returns the {@link Context} suitable for interacting with the UI. */
- @NonNull
- public Context getUiContext() {
- return mUiContext;
- }
-
- /** Returns the {@link Executor} for {@code loadActionListener}. */
- @NonNull
- public Executor getLoadActionExecutor() {
- return mLoadActionExecutor;
- }
-
- /** Returns the Listener for clicks that will cause the contents to be reloaded. */
- @NonNull
- public Consumer<StateBuilders.State> getLoadActionListener() {
- return mLoadActionListener;
}
/**
- * Returns the theme to use for this Tile instance. This can be used to customise things
- * like the default font family. Defaults to zero (default theme) if not specified by {@link
- * Builder#setTilesTheme(int)}.
+ * Sets the theme to use for this Tile instance. This can be used to customise things like
+ * the default font family. If not set, zero (default theme) will be used.
*/
- public int getTilesTheme() {
- return mTilesTheme;
- }
-
- /** Returns the platform data providers that will be registered for this Tile instance. */
@NonNull
- public Map<PlatformDataProvider, Set<PlatformDataKey<?>>> getPlatformDataProviders() {
- return Collections.unmodifiableMap(mPlatformDataProviders);
+ public Builder setTilesTheme(@StyleRes int tilesTheme) {
+ mTilesTheme = tilesTheme;
+ return this;
}
- /** Builder class for {@link Config}. */
- public static final class Builder {
- @NonNull private final Context mUiContext;
- @NonNull private final Executor mLoadActionExecutor;
- @NonNull private final Consumer<StateBuilders.State> mLoadActionListener;
+ /**
+ * Adds a {@link PlatformDataProvider} that will be registered for the given {@code
+ * supportedKeys}. Adding the same {@link PlatformDataProvider} several times will override
+ * previous entries instead of adding multiple entries.
+ */
+ @NonNull
+ public Builder addPlatformDataProvider(
+ @NonNull PlatformDataProvider platformDataProvider,
+ @NonNull PlatformDataKey<?>... supportedKeys) {
+ this.mPlatformDataProviders.put(
+ platformDataProvider, ImmutableSet.copyOf(supportedKeys));
+ return this;
+ }
- @StyleRes int mTilesTheme = 0; // Default theme.
-
- @NonNull
- private final Map<PlatformDataProvider, Set<PlatformDataKey<?>>>
- mPlatformDataProviders = new ArrayMap<>();
-
- /**
- * Builder for the {@link Config} class.
- *
- * @param uiContext A {@link Context} suitable for interacting with the UI.
- * @param loadActionExecutor Executor for {@code loadActionListener}.
- * @param loadActionListener Listener for clicks that will cause the contents to be
- * reloaded.
- */
- public Builder(
- @NonNull Context uiContext,
- @NonNull Executor loadActionExecutor,
- @NonNull Consumer<StateBuilders.State> loadActionListener) {
- this.mUiContext = uiContext;
- this.mLoadActionExecutor = loadActionExecutor;
- this.mLoadActionListener = loadActionListener;
- }
-
- /**
- * Sets the theme to use for this Tile instance. This can be used to customise things
- * like the default font family. If not set, zero (default theme) will be used.
- */
- @NonNull
- public Builder setTilesTheme(@StyleRes int tilesTheme) {
- mTilesTheme = tilesTheme;
- return this;
- }
-
- /**
- * Adds a {@link PlatformDataProvider} that will be registered for
- * the given {@code supportedKeys}. Adding the same {@link PlatformDataProvider} several
- * times will override previous entries instead of adding multiple entries.
- */
- @NonNull
- public Builder addPlatformDataProvider(
- @NonNull PlatformDataProvider platformDataProvider,
- @NonNull PlatformDataKey<?>... supportedKeys) {
- this.mPlatformDataProviders.put(
- platformDataProvider, ImmutableSet.copyOf(supportedKeys));
- return this;
- }
-
- /** Builds {@link Config} object. */
- @NonNull
- public Config build() {
- return new Config(
- mUiContext,
- mLoadActionExecutor,
- mLoadActionListener,
- mTilesTheme,
- mPlatformDataProviders);
- }
+ /** Builds {@link TileRenderer} object. */
+ @NonNull
+ public TileRenderer build() {
+ return new TileRenderer(
+ mUiContext,
+ mTilesTheme,
+ mLoadActionExecutor,
+ mLoadActionListener,
+ /* layout= */ null,
+ /* resources= */ null,
+ mPlatformDataProviders);
}
}
}
diff --git a/wear/tiles/tiles-tooling-preview/api/current.txt b/wear/tiles/tiles-tooling-preview/api/current.txt
index ea401ed..1b22f9e 100644
--- a/wear/tiles/tiles-tooling-preview/api/current.txt
+++ b/wear/tiles/tiles-tooling-preview/api/current.txt
@@ -19,12 +19,15 @@
}
public final class TilePreviewData {
+ ctor public TilePreviewData(optional kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.ResourcesRequest,androidx.wear.protolayout.ResourceBuilders.Resources> onTileResourceRequest, optional androidx.wear.protolayout.expression.PlatformDataValues? platformDataValues, kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> onTileRequest);
ctor public TilePreviewData(optional kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.ResourcesRequest,androidx.wear.protolayout.ResourceBuilders.Resources> onTileResourceRequest, kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> onTileRequest);
ctor public TilePreviewData(kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> onTileRequest);
method public kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> getOnTileRequest();
method public kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.ResourcesRequest,androidx.wear.protolayout.ResourceBuilders.Resources> getOnTileResourceRequest();
+ method public androidx.wear.protolayout.expression.PlatformDataValues? getPlatformDataValues();
property public final kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> onTileRequest;
property public final kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.ResourcesRequest,androidx.wear.protolayout.ResourceBuilders.Resources> onTileResourceRequest;
+ property public final androidx.wear.protolayout.expression.PlatformDataValues? platformDataValues;
}
public final class TilePreviewHelper {
diff --git a/wear/tiles/tiles-tooling-preview/api/restricted_current.txt b/wear/tiles/tiles-tooling-preview/api/restricted_current.txt
index ea401ed..1b22f9e 100644
--- a/wear/tiles/tiles-tooling-preview/api/restricted_current.txt
+++ b/wear/tiles/tiles-tooling-preview/api/restricted_current.txt
@@ -19,12 +19,15 @@
}
public final class TilePreviewData {
+ ctor public TilePreviewData(optional kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.ResourcesRequest,androidx.wear.protolayout.ResourceBuilders.Resources> onTileResourceRequest, optional androidx.wear.protolayout.expression.PlatformDataValues? platformDataValues, kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> onTileRequest);
ctor public TilePreviewData(optional kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.ResourcesRequest,androidx.wear.protolayout.ResourceBuilders.Resources> onTileResourceRequest, kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> onTileRequest);
ctor public TilePreviewData(kotlin.jvm.functions.Function1<? super androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> onTileRequest);
method public kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> getOnTileRequest();
method public kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.ResourcesRequest,androidx.wear.protolayout.ResourceBuilders.Resources> getOnTileResourceRequest();
+ method public androidx.wear.protolayout.expression.PlatformDataValues? getPlatformDataValues();
property public final kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.TileRequest,androidx.wear.tiles.TileBuilders.Tile> onTileRequest;
property public final kotlin.jvm.functions.Function1<androidx.wear.tiles.RequestBuilders.ResourcesRequest,androidx.wear.protolayout.ResourceBuilders.Resources> onTileResourceRequest;
+ property public final androidx.wear.protolayout.expression.PlatformDataValues? platformDataValues;
}
public final class TilePreviewHelper {
diff --git a/wear/tiles/tiles-tooling-preview/build.gradle b/wear/tiles/tiles-tooling-preview/build.gradle
index 3eda344..e668d30 100644
--- a/wear/tiles/tiles-tooling-preview/build.gradle
+++ b/wear/tiles/tiles-tooling-preview/build.gradle
@@ -33,8 +33,9 @@
dependencies {
implementation(libs.kotlinStdlib)
implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
- implementation(project(":wear:tiles:tiles"))
+ api(project(":wear:protolayout:protolayout-expression"))
+ api(project(":wear:tiles:tiles"))
api("androidx.wear:wear-tooling-preview:1.0.0")
api("androidx.annotation:annotation:1.6.0")
}
diff --git a/wear/tiles/tiles-tooling-preview/src/main/java/androidx/wear/tiles/tooling/preview/TilePreviewData.kt b/wear/tiles/tiles-tooling-preview/src/main/java/androidx/wear/tiles/tooling/preview/TilePreviewData.kt
index 7e8ecdc..3fa390a 100644
--- a/wear/tiles/tiles-tooling-preview/src/main/java/androidx/wear/tiles/tooling/preview/TilePreviewData.kt
+++ b/wear/tiles/tiles-tooling-preview/src/main/java/androidx/wear/tiles/tooling/preview/TilePreviewData.kt
@@ -17,9 +17,12 @@
package androidx.wear.tiles.tooling.preview
import androidx.wear.protolayout.ResourceBuilders.Resources
+import androidx.wear.protolayout.expression.PlatformDataKey
+import androidx.wear.protolayout.expression.PlatformDataValues
import androidx.wear.tiles.RequestBuilders.ResourcesRequest
import androidx.wear.tiles.RequestBuilders.TileRequest
import androidx.wear.tiles.TileBuilders
+import java.util.Objects
internal const val PERMANENT_RESOURCES_VERSION = "0"
private val defaultResources = Resources.Builder()
@@ -27,12 +30,14 @@
.build()
/**
- * Container class storing callbacks required to render previews for methods annotated with
- * [Preview].
+ * Container class storing data required to render previews for methods annotated with [Preview].
*
* @param onTileResourceRequest callback that provides a [Resources]. It will be called before
* rendering the preview of the [TileBuilders.Tile]. By default, this callback will return a
* [Resources] with the version "0".
+ * @param platformDataValues allows overriding platform data values for any [PlatformDataKey].
+ * Default platform data values will be set for all platform health sources that have not been
+ * overridden.
* @param onTileRequest callback that provides the [TileBuilders.Tile] to be previewed. It will be
* called before rendering the preview.
*
@@ -41,11 +46,12 @@
class TilePreviewData
@JvmOverloads constructor(
val onTileResourceRequest: (ResourcesRequest) -> Resources = { defaultResources },
+ val platformDataValues: PlatformDataValues? = null,
val onTileRequest: (TileRequest) -> TileBuilders.Tile,
) {
override fun toString(): String {
- return "TilePreviewData(onTileResourceRequest=$onTileResourceRequest," +
- " onTileRequest=$onTileRequest)"
+ return "TilePreviewData(onTileResourceRequest=$onTileResourceRequest, " +
+ "platformDataValues=$platformDataValues, onTileRequest=$onTileRequest)"
}
override fun equals(other: Any?): Boolean {
@@ -55,14 +61,15 @@
other as TilePreviewData
if (onTileResourceRequest != other.onTileResourceRequest) return false
+ if (platformDataValues != other.platformDataValues) return false
if (onTileRequest != other.onTileRequest) return false
return true
}
- override fun hashCode(): Int {
- var result = onTileResourceRequest.hashCode()
- result = 31 * result + onTileRequest.hashCode()
- return result
- }
+ override fun hashCode() = Objects.hash(
+ onTileResourceRequest,
+ platformDataValues,
+ onTileRequest
+ )
}
diff --git a/wear/tiles/tiles-tooling/build.gradle b/wear/tiles/tiles-tooling/build.gradle
index 73a238c..8e3f773 100644
--- a/wear/tiles/tiles-tooling/build.gradle
+++ b/wear/tiles/tiles-tooling/build.gradle
@@ -16,6 +16,7 @@
dependencies {
implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
+ implementation(project(":wear:protolayout:protolayout-expression-pipeline"))
implementation(project(":wear:tiles:tiles"))
implementation(project(":wear:tiles:tiles-renderer"))
implementation(project(":wear:tiles:tiles-tooling-preview"))
@@ -26,6 +27,8 @@
implementation("androidx.core:core:1.1.0")
testImplementation(libs.junit)
+ testImplementation(libs.mockitoCore4)
+ testImplementation(libs.mockitoKotlin4)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.testRunner)
diff --git a/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TestTilePreviews.java b/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TestTilePreviews.java
index 09a368e..4fd7b70 100644
--- a/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TestTilePreviews.java
+++ b/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TestTilePreviews.java
@@ -21,13 +21,20 @@
import android.content.Context;
+import androidx.wear.protolayout.LayoutElementBuilders;
import androidx.wear.protolayout.LayoutElementBuilders.FontStyle;
import androidx.wear.protolayout.LayoutElementBuilders.Layout;
import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
import androidx.wear.protolayout.LayoutElementBuilders.Text;
import androidx.wear.protolayout.ResourceBuilders.Resources;
+import androidx.wear.protolayout.TimelineBuilders;
import androidx.wear.protolayout.TimelineBuilders.Timeline;
import androidx.wear.protolayout.TimelineBuilders.TimelineEntry;
+import androidx.wear.protolayout.TypeBuilders;
+import androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue;
+import androidx.wear.protolayout.expression.PlatformDataValues;
+import androidx.wear.protolayout.expression.PlatformHealthSources;
+import androidx.wear.tiles.TileBuilders;
import androidx.wear.tiles.TileBuilders.Tile;
import androidx.wear.tiles.tooling.preview.Preview;
import androidx.wear.tiles.tooling.preview.TilePreviewData;
@@ -112,4 +119,48 @@
TilePreviewData nonStaticMethod() {
return new TilePreviewData((request) -> tile());
}
+
+ private static LayoutElementBuilders.Text heartRateText() {
+ return new LayoutElementBuilders.Text.Builder()
+ .setText(
+ new TypeBuilders.StringProp.Builder("--")
+ .setDynamicValue(PlatformHealthSources.heartRateBpm().format())
+ .build())
+ .setLayoutConstraintsForDynamicText(
+ new TypeBuilders.StringLayoutConstraint.Builder("XX")
+ .setAlignment(LayoutElementBuilders.TEXT_ALIGN_CENTER)
+ .build())
+ .setFontStyle(
+ new LayoutElementBuilders.FontStyle.Builder()
+ .setColor(argb(0xFF000000))
+ .build())
+ .build();
+ }
+
+ private static Tile tileWithPlatformData() {
+ return new TileBuilders.Tile.Builder()
+ .setResourcesVersion(RESOURCES_VERSION)
+ .setTileTimeline(new TimelineBuilders.Timeline.Builder()
+ .addTimelineEntry(new TimelineBuilders.TimelineEntry.Builder()
+ .setLayout(new LayoutElementBuilders.Layout.Builder()
+ .setRoot(heartRateText())
+ .build())
+ .build())
+ .build())
+ .build();
+ }
+
+ @Preview
+ static TilePreviewData tilePreviewWithDefaultPlatformData() {
+ return new TilePreviewData((request) -> tileWithPlatformData());
+ }
+
+ @Preview
+ static TilePreviewData tilePreviewWithOverriddenPlatformData() {
+ PlatformDataValues platformDataValues = PlatformDataValues.of(
+ PlatformHealthSources.Keys.HEART_RATE_BPM,
+ DynamicDataValue.fromFloat(180f));
+ return new TilePreviewData((request) -> RESOURCES, platformDataValues,
+ (request) -> tileWithPlatformData());
+ }
}
diff --git a/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TestTilePreviews.kt b/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TestTilePreviews.kt
index 46987d0..557b3dd 100644
--- a/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TestTilePreviews.kt
+++ b/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TestTilePreviews.kt
@@ -21,6 +21,10 @@
import androidx.wear.protolayout.LayoutElementBuilders
import androidx.wear.protolayout.ResourceBuilders
import androidx.wear.protolayout.TimelineBuilders
+import androidx.wear.protolayout.TypeBuilders
+import androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue
+import androidx.wear.protolayout.expression.PlatformDataValues
+import androidx.wear.protolayout.expression.PlatformHealthSources
import androidx.wear.tiles.TileBuilders
import androidx.wear.tiles.tooling.preview.Preview
import androidx.wear.tiles.tooling.preview.TilePreviewData
@@ -90,3 +94,50 @@
@Preview
fun nonStaticMethod() = TilePreviewData { tile() }
}
+
+private fun heartRateText() = LayoutElementBuilders.Text.Builder()
+ .setText(
+ TypeBuilders.StringProp.Builder("--")
+ .setDynamicValue(PlatformHealthSources.heartRateBpm().format())
+ .build()
+ )
+ .setLayoutConstraintsForDynamicText(
+ TypeBuilders.StringLayoutConstraint.Builder("XX")
+ .setAlignment(LayoutElementBuilders.TEXT_ALIGN_CENTER)
+ .build()
+ )
+ .setFontStyle(
+ LayoutElementBuilders.FontStyle.Builder()
+ .setColor(argb(0xFF000000.toInt()))
+ .build()
+ )
+ .build()
+
+private fun tileWithPlatformData() =
+ TileBuilders.Tile.Builder()
+ .setResourcesVersion(RESOURCES_VERSION)
+ .setTileTimeline(
+ TimelineBuilders.Timeline.Builder()
+ .addTimelineEntry(
+ TimelineBuilders.TimelineEntry.Builder()
+ .setLayout(
+ LayoutElementBuilders.Layout.Builder()
+ .setRoot(heartRateText())
+ .build()
+ )
+ .build()
+ )
+ .build()
+ )
+ .build()
+
+@Preview
+fun tilePreviewWithDefaultPlatformData() = TilePreviewData { tileWithPlatformData() }
+
+@Preview
+fun tilePreviewWithOverriddenPlatformData() = TilePreviewData(
+ platformDataValues = PlatformDataValues.of(
+ PlatformHealthSources.Keys.HEART_RATE_BPM,
+ DynamicDataValue.fromFloat(180f)
+ )
+) { tileWithPlatformData() }
diff --git a/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TileServiceViewAdapterTest.kt b/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TileServiceViewAdapterTest.kt
index eeb74fc..5d19992 100644
--- a/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TileServiceViewAdapterTest.kt
+++ b/wear/tiles/tiles-tooling/src/androidTest/java/androidx/wear/tiles/tooling/TileServiceViewAdapterTest.kt
@@ -125,7 +125,21 @@
assertThatTileHasInflatedSuccessfully()
}
- private fun assertThatTileHasInflatedSuccessfully() {
+ @Test
+ fun testTilePreviewWithDefaultPlatformData() {
+ initAndInflate("$testFile.tilePreviewWithDefaultPlatformData")
+
+ assertThatTileHasInflatedSuccessfully(expectedText = "80")
+ }
+
+ @Test
+ fun testTilePreviewWithOverriddenPlatformData() {
+ initAndInflate("$testFile.tilePreviewWithOverriddenPlatformData")
+
+ assertThatTileHasInflatedSuccessfully(expectedText = "180")
+ }
+
+ private fun assertThatTileHasInflatedSuccessfully(expectedText: String = "Hello world!") {
activityTestRule.runOnUiThread {
val textView = when (
val child = (tileServiceViewAdapter.getChildAt(0) as ViewGroup).getChildAt(0)
@@ -135,7 +149,7 @@
else -> (child as? FrameLayout)?.getChildAt(0) as? TextView
}
assertNotNull(textView)
- assertEquals("Hello world!", textView?.text.toString())
+ assertEquals(expectedText, textView?.text.toString())
}
}
diff --git a/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/StaticPlatformDataProvider.kt b/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/StaticPlatformDataProvider.kt
new file mode 100644
index 0000000..7e068ab
--- /dev/null
+++ b/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/StaticPlatformDataProvider.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.tooling
+
+import androidx.wear.protolayout.expression.PlatformDataValues
+import androidx.wear.protolayout.expression.pipeline.PlatformDataProvider
+import androidx.wear.protolayout.expression.pipeline.PlatformDataReceiver
+import java.util.concurrent.Executor
+
+/** A [PlatformDataProvider] that provides [values] as static data. */
+internal class StaticPlatformDataProvider(
+ private val values: PlatformDataValues
+) : PlatformDataProvider {
+
+ private var receiver: PlatformDataReceiver? = null
+
+ override fun setReceiver(executor: Executor, receiver: PlatformDataReceiver) {
+ this.receiver = receiver
+ executor.execute { receiver.onData(values) }
+ }
+
+ override fun clearReceiver() {
+ receiver?.onInvalidated(values.all.keys)
+ receiver = null
+ }
+}
diff --git a/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt b/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt
index d9e6909..b3ff5d6f 100644
--- a/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt
+++ b/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt
@@ -26,6 +26,11 @@
import androidx.wear.protolayout.LayoutElementBuilders
import androidx.wear.protolayout.StateBuilders
import androidx.wear.protolayout.TimelineBuilders
+import androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue
+import androidx.wear.protolayout.expression.PlatformDataValues
+import androidx.wear.protolayout.expression.PlatformHealthSources
+import androidx.wear.protolayout.expression.PlatformHealthSources.DynamicHeartRateAccuracy
+import androidx.wear.protolayout.expression.PlatformHealthSources.HEART_RATE_ACCURACY_MEDIUM
import androidx.wear.tiles.RequestBuilders
import androidx.wear.tiles.RequestBuilders.ResourcesRequest
import androidx.wear.tiles.renderer.TileRenderer
@@ -37,6 +42,18 @@
private const val TOOLS_NS_URI = "http://schemas.android.com/tools"
+private val defaultPlatformDataValues = PlatformDataValues.Builder()
+ .put(PlatformHealthSources.Keys.HEART_RATE_BPM, DynamicDataValue.fromFloat(80f))
+ .put(
+ PlatformHealthSources.Keys.HEART_RATE_ACCURACY,
+ DynamicHeartRateAccuracy.dynamicDataValueOf(HEART_RATE_ACCURACY_MEDIUM)
+ )
+ .put(PlatformHealthSources.Keys.DAILY_STEPS, DynamicDataValue.fromInt(4710))
+ .put(PlatformHealthSources.Keys.DAILY_FLOORS, DynamicDataValue.fromFloat(12.5f))
+ .put(PlatformHealthSources.Keys.DAILY_CALORIES, DynamicDataValue.fromFloat(245.3f))
+ .put(PlatformHealthSources.Keys.DAILY_DISTANCE_METERS, DynamicDataValue.fromFloat(3670.8f))
+ .build()
+
/**
* A method extending functionality of [Class.getDeclaredMethod] allowing to finding the methods
* (including non-public) declared in the superclasses as well.
@@ -49,7 +66,7 @@
while (currentClass != null) {
try {
return currentClass.getDeclaredMethod(name, *parameterTypes)
- } catch (_: NoSuchMethodException) { }
+ } catch (_: NoSuchMethodException) {}
currentClass = currentClass.superclass
}
val methodSignature = "$name(${parameterTypes.joinToString { ", " }})"
@@ -79,10 +96,18 @@
internal fun init(tilePreviewMethodFqn: String) {
val tilePreview = getTilePreview(tilePreviewMethodFqn) ?: return
+ val platformDataValues = getPlatformDataValues(tilePreview)
+
lateinit var tileRenderer: TileRenderer
- tileRenderer = TileRenderer(context, executor) { newState ->
+ tileRenderer = TileRenderer.Builder(context, executor) { newState ->
tileRenderer.previewTile(tilePreview, newState)
}
+ .addPlatformDataProvider(
+ StaticPlatformDataProvider(platformDataValues),
+ *platformDataValues.all.keys.toTypedArray()
+ )
+ .build()
+
tileRenderer.previewTile(tilePreview)
}
@@ -147,6 +172,17 @@
method.invoke(instance, *args) as? TilePreviewData
}
}
+
+ private fun getPlatformDataValues(tilePreview: TilePreviewData): PlatformDataValues {
+ return PlatformDataValues.Builder()
+ .putAll(defaultPlatformDataValues)
+ .apply {
+ tilePreview.platformDataValues?.let { platformDataValues ->
+ putAll(platformDataValues)
+ }
+ }
+ .build()
+ }
}
internal fun TimelineBuilders.Timeline?.getCurrentLayout(): LayoutElementBuilders.Layout? {
diff --git a/wear/tiles/tiles-tooling/src/test/java/androidx/wear/tiles/tooling/StaticPlatformDataProviderTest.kt b/wear/tiles/tiles-tooling/src/test/java/androidx/wear/tiles/tooling/StaticPlatformDataProviderTest.kt
new file mode 100644
index 0000000..35476c1
--- /dev/null
+++ b/wear/tiles/tiles-tooling/src/test/java/androidx/wear/tiles/tooling/StaticPlatformDataProviderTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.tooling
+
+import androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue
+import androidx.wear.protolayout.expression.PlatformDataValues
+import androidx.wear.protolayout.expression.PlatformHealthSources
+import androidx.wear.protolayout.expression.pipeline.PlatformDataReceiver
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicInteger
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoInteractions
+
+internal class StaticPlatformDataProviderTest {
+
+ private val platformDataValues = PlatformDataValues.Builder()
+ .put(PlatformHealthSources.Keys.DAILY_CALORIES, DynamicDataValue.fromFloat(1000f))
+ .put(PlatformHealthSources.Keys.DAILY_STEPS, DynamicDataValue.fromInt(256))
+ .build()
+
+ private val staticPlatformDataProvider = StaticPlatformDataProvider(platformDataValues)
+
+ private val receiver: PlatformDataReceiver = mock()
+
+ @Test
+ fun testReceiverReceivesPlatformDataValues() {
+ val executorInvocations = AtomicInteger(0)
+ val executor = Executor {
+ executorInvocations.getAndIncrement()
+ it.run()
+ }
+
+ staticPlatformDataProvider.setReceiver(executor, receiver)
+
+ verify(receiver).onData(platformDataValues)
+ assertEquals(1, executorInvocations.get())
+ }
+
+ @Test
+ fun testReceiverIsCleared() {
+ // the receiver hasn't been set yet
+ staticPlatformDataProvider.clearReceiver()
+ verifyNoInteractions(receiver)
+
+ // now set the receiver
+ staticPlatformDataProvider.setReceiver({ it.run() }, receiver)
+
+ // the receiver should only be cleared once, even with multiple calls to clearReceiver
+ staticPlatformDataProvider.clearReceiver()
+ staticPlatformDataProvider.clearReceiver()
+ verify(receiver, times(1)).onInvalidated(platformDataValues.all.keys)
+ }
+}
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index 57bcabe..5d3c6df 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -22,7 +22,6 @@
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
-import android.support.wearable.complications.ComplicationData as WireComplicationData
import androidx.annotation.ColorInt
import androidx.annotation.IntDef
import androidx.annotation.Px
@@ -50,6 +49,7 @@
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
+import java.util.ArrayList
import java.util.Objects
import kotlin.math.abs
import kotlin.math.atan2
@@ -1047,7 +1047,13 @@
* [complicationData] is selected.
*/
private var timelineComplicationData: ComplicationData = NoDataComplicationData()
- private var timelineEntries: List<WireComplicationData>? = null
+ private var timelineEntries: List<ApiTimelineEntry>? = null
+
+ private class ApiTimelineEntry(
+ val timelineStartEpochSecond: Long?,
+ val timelineEndEpochSecond: Long?,
+ val complicationData: ComplicationData
+ )
/**
* Sets the current [ComplicationData] and if it's a timeline, the correct override for
@@ -1098,7 +1104,15 @@
private fun setTimelineData(data: ComplicationData, instant: Instant) {
lastComplicationUpdate = instant
timelineComplicationData = data
- timelineEntries = data.asWireComplicationData().timelineEntries?.toList()
+ timelineEntries = data.asWireComplicationData()
+ .timelineEntries
+ ?.mapTo(ArrayList<ApiTimelineEntry>()) {
+ ApiTimelineEntry(
+ it.timelineStartEpochSecond,
+ it.timelineEndEpochSecond,
+ it.toApiComplicationData()
+ )
+ }
}
private fun loadData(data: ComplicationData, loadDrawablesAsynchronous: Boolean = false) {
@@ -1121,14 +1135,14 @@
// Select the shortest valid timeline entry.
timelineEntries?.let {
- for (wireEntry in it) {
- val start = wireEntry.timelineStartEpochSecond
- val end = wireEntry.timelineEndEpochSecond
+ for (entry in it) {
+ val start = entry.timelineStartEpochSecond
+ val end = entry.timelineEndEpochSecond
if (start != null && end != null && time >= start && time < end) {
val duration = end - start
if (duration < previousShortest) {
previousShortest = duration
- best = wireEntry.toApiComplicationData()
+ best = entry.complicationData
}
}
}
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 16c1bc4..9c34467 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
@@ -5681,6 +5681,44 @@
}
@Test
+ public fun selectComplicationDataForInstant_withTimelineDoesntUpdateWithNoChange() {
+ val complication =
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("A"))
+ .build()
+ complication.setTimelineEntryCollection(
+ listOf(
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("B"))
+ .build()
+ .apply {
+ timelineStartEpochSecond = 1000
+ timelineEndEpochSecond = 4000
+ },
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(WireComplicationText.plainText("C"))
+ .build()
+ .apply {
+ timelineStartEpochSecond = 2000
+ timelineEndEpochSecond = 3000
+ }
+ )
+ )
+ initWallpaperInteractiveWatchFaceInstance(complicationSlots = listOf(mockComplication))
+ engineWrapper.setComplicationDataList(
+ listOf(IdAndComplicationDataWireFormat(LEFT_COMPLICATION_ID, complication))
+ )
+ complicationSlotsManager.selectComplicationDataForInstant(Instant.ofEpochSecond(1000))
+ reset(mockCanvasComplication)
+
+ // Calling selectComplicationDataForInstant again with another time inside the same timeline
+ // entry should not result in a call to loadData.
+ complicationSlotsManager.selectComplicationDataForInstant(Instant.ofEpochSecond(1050))
+
+ verifyNoMoreInteractions(mockCanvasComplication)
+ }
+
+ @Test
@Config(sdk = [Build.VERSION_CODES.R])
public fun renderParameters_isScreenshot() {
initWallpaperInteractiveWatchFaceInstance(
diff --git a/wear/wear-core/api/current.txt b/wear/wear-core/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/wear-core/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/wear-core/api/res-current.txt b/wear/wear-core/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/wear-core/api/res-current.txt
diff --git a/wear/wear-core/api/restricted_current.txt b/wear/wear-core/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/wear-core/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/wear-core/build.gradle b/wear/wear-core/build.gradle
new file mode 100644
index 0000000..402a820
--- /dev/null
+++ b/wear/wear-core/build.gradle
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ // Add dependencies here
+}
+
+android {
+ namespace "androidx.wear.core"
+}
+
+androidx {
+ name = "Android Wear Core"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenVersion = LibraryVersions.WEAR_CORE
+ inceptionYear = "2024"
+ description = "Low-level utilities for building apps and libraries for Wear OS."
+}
diff --git a/wear/wear-core/src/main/java/androidx/wear/androidx-wear-wear-core-documentation.md b/wear/wear-core/src/main/java/androidx/wear/androidx-wear-wear-core-documentation.md
new file mode 100644
index 0000000..62b25e9
--- /dev/null
+++ b/wear/wear-core/src/main/java/androidx/wear/androidx-wear-wear-core-documentation.md
@@ -0,0 +1,7 @@
+# Module root
+
+androidx.wear wear-core
+
+# Package androidx.wear.core
+
+This package provides low level helpers and utilities for building Wear OS apps and libraries.
diff --git a/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java b/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java
index e45172f..3235df9 100644
--- a/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java
+++ b/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java
@@ -305,5 +305,10 @@
WebSettingsCompat.WEB_AUTHENTICATION_SUPPORT_APP);
Assert.assertEquals(WebSettingsCompat.WEB_AUTHENTICATION_SUPPORT_APP,
WebSettingsCompat.getWebAuthenticationSupport(settings));
+
+ WebSettingsCompat.setWebAuthenticationSupport(settings,
+ WebSettingsCompat.WEB_AUTHENTICATION_SUPPORT_BROWSER);
+ Assert.assertEquals(WebSettingsCompat.WEB_AUTHENTICATION_SUPPORT_BROWSER,
+ WebSettingsCompat.getWebAuthenticationSupport(settings));
}
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
index 413c0fc..a9eabaf 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
@@ -908,7 +908,8 @@
}
@IntDef({WEB_AUTHENTICATION_SUPPORT_NONE,
- WEB_AUTHENTICATION_SUPPORT_APP})
+ WEB_AUTHENTICATION_SUPPORT_APP,
+ WEB_AUTHENTICATION_SUPPORT_BROWSER})
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Retention(RetentionPolicy.SOURCE)
@interface WebAuthenticationSupport {
@@ -933,6 +934,15 @@
WebSettingsBoundaryInterface.WebauthnSupport.APP;
/**
+ * The support level that allows apps to make WebAuthn calls for any website. See
+ * <a href="https://developer.android.com/training/sign-in/privileged-apps">Privileged apps</a>
+ * to learn how to make WebAuthn calls for any website.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static final int WEB_AUTHENTICATION_SUPPORT_BROWSER =
+ WebSettingsBoundaryInterface.WebauthnSupport.BROWSER;
+
+ /**
* Sets the support level for the given {@link WebSettings}.
*
* <p>
@@ -944,6 +954,7 @@
* @param support The new support level which this WebView will use.
* @see #WEB_AUTHENTICATION_SUPPORT_NONE
* @see #WEB_AUTHENTICATION_SUPPORT_APP
+ * @see #WEB_AUTHENTICATION_SUPPORT_BROWSER
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresFeature(name = WebViewFeature.WEB_AUTHENTICATION,
@@ -972,6 +983,7 @@
* @see #setWebAuthenticationSupport(WebSettings, int)
* @see #WEB_AUTHENTICATION_SUPPORT_NONE
* @see #WEB_AUTHENTICATION_SUPPORT_APP
+ * @see #WEB_AUTHENTICATION_SUPPORT_BROWSER
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresFeature(name = WebViewFeature.WEB_AUTHENTICATION,
diff --git a/window/window-core/build.gradle b/window/window-core/build.gradle
index 703cd61..b406073 100644
--- a/window/window-core/build.gradle
+++ b/window/window-core/build.gradle
@@ -71,5 +71,4 @@
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2022"
description = "WindowManager Core Library."
- metalavaK2UastEnabled = true
}
diff --git a/window/window-java/src/main/java/androidx/window/java/core/CallbackToFlowAdapter.kt b/window/window-java/src/main/java/androidx/window/java/core/CallbackToFlowAdapter.kt
index 175c1ada..5e641c9 100644
--- a/window/window-java/src/main/java/androidx/window/java/core/CallbackToFlowAdapter.kt
+++ b/window/window-java/src/main/java/androidx/window/java/core/CallbackToFlowAdapter.kt
@@ -16,6 +16,7 @@
package androidx.window.java.core
+import androidx.annotation.GuardedBy
import androidx.core.util.Consumer
import java.util.concurrent.Executor
import java.util.concurrent.locks.ReentrantLock
@@ -31,7 +32,9 @@
*/
internal class CallbackToFlowAdapter {
- private val lock = ReentrantLock()
+ private val globalLock = ReentrantLock()
+
+ @GuardedBy("globalLock")
private val consumerToJobMap = mutableMapOf<Consumer<*>, Job>()
/**
@@ -39,7 +42,7 @@
* Registering the same [Consumer] is a no-op.
*/
fun <T : Any> connect(executor: Executor, consumer: Consumer<T>, flow: Flow<T>) {
- lock.withLock {
+ globalLock.withLock {
if (consumerToJobMap[consumer] == null) {
val scope = CoroutineScope(executor.asCoroutineDispatcher())
consumerToJobMap[consumer] = scope.launch {
@@ -56,7 +59,7 @@
* no-op.
*/
fun disconnect(consumer: Consumer<*>) {
- lock.withLock {
+ globalLock.withLock {
consumerToJobMap[consumer]?.cancel()
consumerToJobMap.remove(consumer)
}
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt
index 1446a39..afb37df 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt
@@ -36,14 +36,15 @@
private val consumerAdapter: ConsumerAdapter
) : WindowBackend {
- private val extensionWindowBackendLock = ReentrantLock()
- @GuardedBy("lock")
+ private val globalLock = ReentrantLock()
+
+ @GuardedBy("globalLock")
private val contextToListeners = mutableMapOf<Context, MulticastConsumer>()
- @GuardedBy("lock")
+ @GuardedBy("globalLock")
private val listenerToContext = mutableMapOf<Consumer<WindowLayoutInfo>, Context>()
- @GuardedBy("lock")
+ @GuardedBy("globalLock")
private val consumerToToken = mutableMapOf<MulticastConsumer, ConsumerAdapter.Subscription>()
/**
@@ -60,7 +61,7 @@
executor: Executor,
callback: Consumer<WindowLayoutInfo>
) {
- extensionWindowBackendLock.withLock {
+ globalLock.withLock {
contextToListeners[context]?.let { listener ->
listener.addListener(callback)
listenerToContext[callback] = context
@@ -100,7 +101,7 @@
* @param callback a listener that may have been registered
*/
override fun unregisterLayoutChangeCallback(callback: Consumer<WindowLayoutInfo>) {
- extensionWindowBackendLock.withLock {
+ globalLock.withLock {
val context = listenerToContext[callback] ?: return
val multicastListener = contextToListeners[context] ?: return
multicastListener.removeListener(callback)
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt
index 1c60c33..bc6fb57 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt
@@ -33,11 +33,12 @@
private val component: WindowLayoutComponent
) : WindowBackend {
- private val extensionWindowBackendLock = ReentrantLock()
- @GuardedBy("lock")
+ private val globalLock = ReentrantLock()
+
+ @GuardedBy("globalLock")
private val contextToListeners = mutableMapOf<Context, MulticastConsumer>()
- @GuardedBy("lock")
+ @GuardedBy("globalLock")
private val listenerToContext = mutableMapOf<Consumer<WindowLayoutInfo>, Context>()
/**
@@ -55,7 +56,7 @@
executor: Executor,
callback: Consumer<WindowLayoutInfo>
) {
- extensionWindowBackendLock.withLock {
+ globalLock.withLock {
contextToListeners[context]?.let { listener ->
listener.addListener(callback)
listenerToContext[callback] = context
@@ -77,7 +78,7 @@
* @param callback a listener that may have been registered
*/
override fun unregisterLayoutChangeCallback(callback: Consumer<WindowLayoutInfo>) {
- extensionWindowBackendLock.withLock {
+ globalLock.withLock {
val context = listenerToContext[callback] ?: return
val multicastListener = contextToListeners[context] ?: return
multicastListener.removeListener(callback)
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/MulticastConsumer.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/MulticastConsumer.kt
index a5d2cc1..e122e1c 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/MulticastConsumer.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/MulticastConsumer.kt
@@ -31,14 +31,15 @@
internal class MulticastConsumer(
private val context: Context
) : Consumer<OEMWindowLayoutInfo>, OEMConsumer<OEMWindowLayoutInfo> {
- private val multicastConsumerLock = ReentrantLock()
- @GuardedBy("lock")
+ private val globalLock = ReentrantLock()
+
+ @GuardedBy("globalLock")
private var lastKnownValue: WindowLayoutInfo? = null
- @GuardedBy("lock")
+ @GuardedBy("globalLock")
private val registeredListeners = mutableSetOf<Consumer<WindowLayoutInfo>>()
override fun accept(value: OEMWindowLayoutInfo) {
- multicastConsumerLock.withLock {
+ globalLock.withLock {
val newValue = ExtensionsWindowLayoutInfoAdapter.translate(context, value)
lastKnownValue = newValue
registeredListeners.forEach { consumer -> consumer.accept(newValue) }
@@ -46,14 +47,14 @@
}
fun addListener(listener: Consumer<WindowLayoutInfo>) {
- multicastConsumerLock.withLock {
+ globalLock.withLock {
lastKnownValue?.let { value -> listener.accept(value) }
registeredListeners.add(listener)
}
}
fun removeListener(listener: Consumer<WindowLayoutInfo>) {
- multicastConsumerLock.withLock {
+ globalLock.withLock {
registeredListeners.remove(listener)
}
}
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarCompat.kt b/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarCompat.kt
index 1e7dec9..fdaa0c2 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarCompat.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarCompat.kt
@@ -371,19 +371,19 @@
private class DistinctElementCallback(
private val callbackInterface: ExtensionCallbackInterface
) : ExtensionCallbackInterface {
- private val lock = ReentrantLock()
+ private val globalLock = ReentrantLock()
/**
* A map from [Activity] to the last computed [WindowLayoutInfo] for the
* given activity. A [WeakHashMap] is used to avoid retaining the [Activity].
*/
- @GuardedBy("mLock")
+ @GuardedBy("globalLock")
private val activityWindowLayoutInfo = WeakHashMap<Activity, WindowLayoutInfo>()
override fun onWindowLayoutChanged(
activity: Activity,
newLayout: WindowLayoutInfo
) {
- lock.withLock {
+ globalLock.withLock {
val lastInfo = activityWindowLayoutInfo[activity]
if (newLayout == lastInfo) {
return
@@ -394,7 +394,7 @@
}
fun clearWindowLayoutInfo(activity: Activity) {
- lock.withLock {
+ globalLock.withLock {
activityWindowLayoutInfo[activity] = null
}
}
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt b/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt
index d3ba19b1..325c701 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt
@@ -130,7 +130,7 @@
* Checks if there are no more registered callbacks left for the activity and inform
* extension if needed.
*/
- @GuardedBy("sLock")
+ @GuardedBy("globalLock")
private fun callbackRemovedForActivity(activity: Activity) {
val hasRegisteredCallback = windowLayoutChangeCallbacks.any { wrapper ->
wrapper.activity == activity
diff --git a/window/window/src/testUtil/java/androidx/window/layout/adapter/sidecar/SwitchOnUnregisterExtensionInterfaceCompat.kt b/window/window/src/testUtil/java/androidx/window/layout/adapter/sidecar/SwitchOnUnregisterExtensionInterfaceCompat.kt
index d902e75..69d82ee3 100644
--- a/window/window/src/testUtil/java/androidx/window/layout/adapter/sidecar/SwitchOnUnregisterExtensionInterfaceCompat.kt
+++ b/window/window/src/testUtil/java/androidx/window/layout/adapter/sidecar/SwitchOnUnregisterExtensionInterfaceCompat.kt
@@ -27,7 +27,6 @@
import androidx.window.layout.HardwareFoldingFeature.Type.Companion.HINGE
import androidx.window.layout.WindowLayoutInfo
import androidx.window.layout.adapter.sidecar.ExtensionInterfaceCompat.ExtensionCallbackInterface
-import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
@@ -37,11 +36,11 @@
* unregister then register again.
*/
internal class SwitchOnUnregisterExtensionInterfaceCompat : ExtensionInterfaceCompat {
- private val lock: Lock = ReentrantLock()
+ private val globalLock = ReentrantLock()
private val foldBounds = Rect(0, 100, 200, 100)
- @GuardedBy("mLock")
+ @GuardedBy("globalLock")
private var callback: ExtensionCallbackInterface = EmptyExtensionCallbackInterface()
- @GuardedBy("mLock")
+ @GuardedBy("globalLock")
private var state = FLAT
override fun validateExtensionInterface(): Boolean {
@@ -49,15 +48,15 @@
}
override fun setExtensionCallback(extensionCallback: ExtensionCallbackInterface) {
- lock.withLock { callback = extensionCallback }
+ globalLock.withLock { callback = extensionCallback }
}
override fun onWindowLayoutChangeListenerAdded(activity: Activity) {
- lock.withLock { callback.onWindowLayoutChanged(activity, currentWindowLayoutInfo()) }
+ globalLock.withLock { callback.onWindowLayoutChanged(activity, currentWindowLayoutInfo()) }
}
override fun onWindowLayoutChangeListenerRemoved(activity: Activity) {
- lock.withLock { state = toggleState(state) }
+ globalLock.withLock { state = toggleState(state) }
}
fun currentWindowLayoutInfo(): WindowLayoutInfo {
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.kt b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.kt
index 1349a9f..fd32739 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.kt
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.kt
@@ -57,6 +57,7 @@
import androidx.work.integration.testapp.RemoteService.Companion.updateUniquePeriodicIntent
import androidx.work.integration.testapp.imageprocessing.ImageProcessingActivity
import androidx.work.integration.testapp.sherlockholmes.AnalyzeSherlockHolmesActivity
+import androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_CLASS_NAME
import androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME
import androidx.work.multiprocess.RemoteWorkerService
import androidx.work.workDataOf
@@ -234,7 +235,8 @@
workManager.enqueue(request)
}
findViewById<View>(R.id.run_constraint_tracking_worker).setOnClickListener {
- val inputData = workDataOf(ARGUMENT_CLASS_NAME to ForegroundWorker::class.java.name)
+ val inputData = workDataOf(
+ CONSTRAINT_WORKER_ARGUMENT_CLASS_NAME to ForegroundWorker::class.java.name)
val request = OneTimeWorkRequest.Builder(ConstraintTrackingWorker::class.java)
.setConstraints(Constraints(requiredNetworkType = NetworkType.CONNECTED))
@@ -403,5 +405,5 @@
private const val UNIQUE_WORK_NAME = "importantUniqueWork"
private const val REPLACE_COMPLETED_WORK = "replaceCompletedWork"
private const val NUM_WORKERS = 150
-private const val ARGUMENT_CLASS_NAME =
+private const val CONSTRAINT_WORKER_ARGUMENT_CLASS_NAME =
"androidx.work.impl.workers.ConstraintTrackingWorker.ARGUMENT_CLASS_NAME"
diff --git a/work/work-multiprocess/src/androidTest/AndroidManifest.xml b/work/work-multiprocess/src/androidTest/AndroidManifest.xml
index 733b8f7..e8e40b8 100644
--- a/work/work-multiprocess/src/androidTest/AndroidManifest.xml
+++ b/work/work-multiprocess/src/androidTest/AndroidManifest.xml
@@ -17,6 +17,11 @@
<application android:name="androidx.multidex.MultiDexApplication">
<service android:name="androidx.work.multiprocess.RemoteWorkerService" />
+
+ <service
+ android:name="androidx.work.multiprocess.RemoteWorkerService2"
+ android:exported="false"
+ android:process=":worker2"/>
</application>
</manifest>
diff --git a/lifecycle/lifecycle-viewmodel/src/jvmMain/kotlin/androidx/lifecycle/viewmodel/internal/Lock.jvm.kt b/work/work-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteWorkerService2.kt
similarity index 68%
copy from lifecycle/lifecycle-viewmodel/src/jvmMain/kotlin/androidx/lifecycle/viewmodel/internal/Lock.jvm.kt
copy to work/work-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteWorkerService2.kt
index 9d20353..4afc83a 100644
--- a/lifecycle/lifecycle-viewmodel/src/jvmMain/kotlin/androidx/lifecycle/viewmodel/internal/Lock.jvm.kt
+++ b/work/work-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteWorkerService2.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 The Android Open Source Project
+ * 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.
@@ -14,9 +14,6 @@
* limitations under the License.
*/
-package androidx.lifecycle.viewmodel.internal
+package androidx.work.multiprocess
-internal actual class Lock actual constructor() {
- actual inline fun <T> withLock(crossinline block: () -> T): T =
- synchronized(lock = this, block)
-}
+class RemoteWorkerService2 : RemoteWorkerService()
diff --git a/work/work-multiprocess/src/androidTest/java/androidx/work/multiprocess/WorkerinRemoteProcessTest.kt b/work/work-multiprocess/src/androidTest/java/androidx/work/multiprocess/WorkerinRemoteProcessTest.kt
new file mode 100644
index 0000000..d3bb6a6
--- /dev/null
+++ b/work/work-multiprocess/src/androidTest/java/androidx/work/multiprocess/WorkerinRemoteProcessTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess
+
+import android.content.ComponentName
+import android.content.Context
+import android.util.Log
+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.work.Configuration
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkInfo
+import androidx.work.impl.WorkManagerImpl
+import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor
+import androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_CLASS_NAME
+import androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME
+import androidx.work.workDataOf
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class WorkerinRemoteProcessTest {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+
+ val configuration = Configuration.Builder()
+ .setMinimumLoggingLevel(Log.VERBOSE)
+ .build()
+ val taskExecutor = WorkManagerTaskExecutor(configuration.taskExecutor)
+ val workManager = WorkManagerImpl(
+ context = context,
+ configuration = configuration,
+ workTaskExecutor = taskExecutor,
+ )
+
+ init {
+ WorkManagerImpl.setDelegate(workManager)
+ }
+
+ @SdkSuppress(minSdkVersion = 27)
+ @MediumTest
+ @Test
+ fun runWorker() = runBlocking {
+ val componentName = ComponentName("androidx.work.multiprocess.test",
+ RemoteWorkerService2::class.java.canonicalName!!)
+ val workRequest = OneTimeWorkRequestBuilder<RemoteSuccessWorker>()
+ .setInputData(
+ workDataOf(
+ ARGUMENT_PACKAGE_NAME to componentName.packageName,
+ ARGUMENT_CLASS_NAME to componentName.className,
+ )
+ )
+ .build()
+ workManager.enqueue(workRequest)
+ val finished = workManager.getWorkInfoByIdFlow(workRequest.id).filter {
+ it?.state?.isFinished ?: false
+ }.first()
+ assertThat(finished!!.state).isEqualTo(WorkInfo.State.SUCCEEDED)
+ }
+}
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteExecute.kt b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteExecute.kt
index ffd9cb2..076129b 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteExecute.kt
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/RemoteExecute.kt
@@ -63,8 +63,7 @@
}
deathRecipient = localRecipient
binder.linkToDeath(localRecipient, 0)
- dispatcher.execute(iInterface, object : IWorkManagerImplCallback {
- override fun asBinder() = binder
+ dispatcher.execute(iInterface, object : IWorkManagerImplCallback.Stub() {
override fun onSuccess(response: ByteArray) = continuation.resume(response)
diff --git a/work/work-runtime/api/current.txt b/work/work-runtime/api/current.txt
index 5af96fc..8ff2147 100644
--- a/work/work-runtime/api/current.txt
+++ b/work/work-runtime/api/current.txt
@@ -31,12 +31,14 @@
method public androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo>? getWorkerExecutionExceptionHandler();
method public androidx.work.WorkerFactory getWorkerFactory();
method public androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo>? getWorkerInitializationExceptionHandler();
+ method @SuppressCompatibility @androidx.work.ExperimentalConfigurationApi public boolean isMarkingJobsAsImportantWhileForeground();
property public final androidx.work.Clock clock;
property public final int contentUriTriggerWorkersLimit;
property public final String? defaultProcessName;
property public final java.util.concurrent.Executor executor;
property public final androidx.core.util.Consumer<java.lang.Throwable>? initializationExceptionHandler;
property public final androidx.work.InputMergerFactory inputMergerFactory;
+ property @SuppressCompatibility @androidx.work.ExperimentalConfigurationApi public final boolean isMarkingJobsAsImportantWhileForeground;
property public final int maxJobSchedulerId;
property public final int minJobSchedulerId;
property public final androidx.work.RunnableScheduler runnableScheduler;
@@ -60,6 +62,7 @@
method public androidx.work.Configuration.Builder setInitializationExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable> exceptionHandler);
method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory inputMergerFactory);
method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int minJobSchedulerId, int maxJobSchedulerId);
+ method @SuppressCompatibility @androidx.work.ExperimentalConfigurationApi public androidx.work.Configuration.Builder setMarkingJobsAsImportantWhileForeground(boolean markAsImportant);
method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int maxSchedulerLimit);
method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int loggingLevel);
method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler runnableScheduler);
@@ -218,6 +221,9 @@
enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
}
+ @SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.WARNING) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.CLASS}) public @interface ExperimentalConfigurationApi {
+ }
+
public final class ForegroundInfo {
ctor public ForegroundInfo(int, android.app.Notification);
ctor public ForegroundInfo(int, android.app.Notification, int);
diff --git a/work/work-runtime/api/restricted_current.txt b/work/work-runtime/api/restricted_current.txt
index 5af96fc..8ff2147 100644
--- a/work/work-runtime/api/restricted_current.txt
+++ b/work/work-runtime/api/restricted_current.txt
@@ -31,12 +31,14 @@
method public androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo>? getWorkerExecutionExceptionHandler();
method public androidx.work.WorkerFactory getWorkerFactory();
method public androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo>? getWorkerInitializationExceptionHandler();
+ method @SuppressCompatibility @androidx.work.ExperimentalConfigurationApi public boolean isMarkingJobsAsImportantWhileForeground();
property public final androidx.work.Clock clock;
property public final int contentUriTriggerWorkersLimit;
property public final String? defaultProcessName;
property public final java.util.concurrent.Executor executor;
property public final androidx.core.util.Consumer<java.lang.Throwable>? initializationExceptionHandler;
property public final androidx.work.InputMergerFactory inputMergerFactory;
+ property @SuppressCompatibility @androidx.work.ExperimentalConfigurationApi public final boolean isMarkingJobsAsImportantWhileForeground;
property public final int maxJobSchedulerId;
property public final int minJobSchedulerId;
property public final androidx.work.RunnableScheduler runnableScheduler;
@@ -60,6 +62,7 @@
method public androidx.work.Configuration.Builder setInitializationExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable> exceptionHandler);
method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory inputMergerFactory);
method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int minJobSchedulerId, int maxJobSchedulerId);
+ method @SuppressCompatibility @androidx.work.ExperimentalConfigurationApi public androidx.work.Configuration.Builder setMarkingJobsAsImportantWhileForeground(boolean markAsImportant);
method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int maxSchedulerLimit);
method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int loggingLevel);
method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler runnableScheduler);
@@ -218,6 +221,9 @@
enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
}
+ @SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.WARNING) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.CLASS}) public @interface ExperimentalConfigurationApi {
+ }
+
public final class ForegroundInfo {
ctor public ForegroundInfo(int, android.app.Notification);
ctor public ForegroundInfo(int, android.app.Notification, int);
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 5265543..8dd4953 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
@@ -73,7 +73,7 @@
@Before
public void setUp() {
mConverter = new SystemJobInfoConverter(
- ApplicationProvider.getApplicationContext(), new SystemClock());
+ ApplicationProvider.getApplicationContext(), new SystemClock(), true);
}
@Test
@@ -241,6 +241,18 @@
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 29)
+ public void testConvert_setImportantWhileForeground_respectFlag() {
+ mConverter = new SystemJobInfoConverter(
+ ApplicationProvider.getApplicationContext(), new SystemClock(), false);
+ WorkSpec workSpec = getTestWorkSpecWithConstraints(new Constraints.Builder().build());
+ workSpec.lastEnqueueTime = System.currentTimeMillis();
+ JobInfo jobInfo = mConverter.convert(workSpec, JOB_ID);
+ assertThat(jobInfo.isImportantWhileForeground(), is(false));
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 29)
public void testConvert_setImportantWhileForeground_withTimingConstraints() {
WorkSpec workSpec = new WorkSpec("id", TestWorker.class.getName());
workSpec.setPeriodic(TEST_INTERVAL_DURATION, TEST_FLEX_DURATION);
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
index 3715173..be32fc7 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
@@ -126,7 +126,9 @@
workDatabase,
configuration,
mJobScheduler,
- new SystemJobInfoConverter(context, configuration.getClock())));
+ new SystemJobInfoConverter(context, configuration.getClock(),
+ configuration.isMarkingJobsAsImportantWhileForeground()
+ )));
doNothing().when(mSystemJobScheduler).scheduleInternal(any(WorkSpec.class), anyInt());
}
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 2224980..3465a5b 100644
--- a/work/work-runtime/src/main/java/androidx/work/Configuration.kt
+++ b/work/work-runtime/src/main/java/androidx/work/Configuration.kt
@@ -43,6 +43,7 @@
*
* To set a custom Configuration for WorkManager, see [WorkManager.initialize].
*/
+@OptIn(ExperimentalConfigurationApi::class)
class Configuration internal constructor(builder: Builder) {
/**
* The [Executor] used by [WorkManager] to execute [Worker]s.
@@ -160,6 +161,16 @@
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
val isUsingDefaultTaskExecutor: Boolean
+ /**
+ * Specifies whether WorkManager automatically set
+ * [android.app.job.JobInfo.Builder.setImportantWhileForeground] for workers that
+ * are eligible to run immediately.
+ */
+ @get:ExperimentalConfigurationApi
+ @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+ @property:ExperimentalConfigurationApi
+ val isMarkingJobsAsImportantWhileForeground: Boolean
+
init {
val builderWorkerDispatcher = builder.workerContext
@@ -198,6 +209,7 @@
workerExecutionExceptionHandler = builder.workerExecutionExceptionHandler
defaultProcessName = builder.defaultProcessName
contentUriTriggerWorkersLimit = builder.contentUriTriggerWorkersLimit
+ isMarkingJobsAsImportantWhileForeground = builder.markJobsAsImportantWhileForeground
}
/**
@@ -221,6 +233,7 @@
internal var maxJobSchedulerId: Int = Int.MAX_VALUE
internal var maxSchedulerLimit: Int = MIN_SCHEDULER_LIMIT
internal var contentUriTriggerWorkersLimit: Int = DEFAULT_CONTENT_URI_TRIGGERS_WORKERS_LIMIT
+ internal var markJobsAsImportantWhileForeground: Boolean = true
/**
* Creates a new [Configuration.Builder].
@@ -515,6 +528,22 @@
}
/**
+ * Regulates whether WorkManager should automatically set
+ * [android.app.job.JobInfo.Builder.setImportantWhileForeground] for workers that
+ * are eligible to run immediately.
+ *
+ * It will have effects only on API levels >= 23.
+ *
+ * @param markAsImportant whether to mark jobs as important
+ * @return This [Builder] instance
+ */
+ @ExperimentalConfigurationApi
+ fun setMarkingJobsAsImportantWhileForeground(markAsImportant: Boolean): Builder {
+ this.markJobsAsImportantWhileForeground = markAsImportant
+ return this
+ }
+
+ /**
* Builds a [Configuration] object.
*
* @return A [Configuration] object with this [Builder]'s parameters.
diff --git a/work/work-runtime/src/main/java/androidx/work/ExperimentalConfigurationApi.kt b/work/work-runtime/src/main/java/androidx/work/ExperimentalConfigurationApi.kt
new file mode 100644
index 0000000..efaf1e5
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/ExperimentalConfigurationApi.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work
+
+/**
+ * Annotation indicating experimental API for new WorkManager's Configuration APIs.
+ *
+ * These APIs allow fine grained tuning WorkManager's behavior. However, full effects of these
+ * flags on OS health and WorkManager's throughput aren't fully known and currently are being
+ * explored. After the research either the best default value for a flag will be chosen and then
+ * associated API will be removed or the guidance on how to choose a value depending on app's
+ * specifics will developed and then associated API will be promoted to stable.
+ *
+ * As a result these APIs annotated with `ExperimentalConfigurationApi` requires opt-in
+ */
+@Retention(AnnotationRetention.BINARY)
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY,
+ AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.CLASS)
+@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
+annotation class ExperimentalConfigurationApi
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
index 9f1d6c1..dc462d6 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
@@ -54,11 +54,14 @@
private final ComponentName mWorkServiceComponent;
private final Clock mClock;
+ private final boolean mMarkImportantWhileForeground;
- SystemJobInfoConverter(@NonNull Context context, Clock clock) {
+ SystemJobInfoConverter(@NonNull Context context,
+ Clock clock, boolean markImportantWhileForeground) {
mClock = clock;
Context appContext = context.getApplicationContext();
mWorkServiceComponent = new ComponentName(appContext, SystemJobService.class);
+ mMarkImportantWhileForeground = markImportantWhileForeground;
}
/**
@@ -107,7 +110,7 @@
if (offset > 0) {
// Only set a minimum latency when applicable.
builder.setMinimumLatency(offset);
- } else if (!workSpec.expedited) {
+ } else if (!workSpec.expedited && mMarkImportantWhileForeground) {
// Only set this if the workSpec is not expedited.
builder.setImportantWhileForeground(true);
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
index b186c71..f3301ee 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
@@ -80,7 +80,8 @@
workDatabase,
configuration,
getWmJobScheduler(context),
- new SystemJobInfoConverter(context, configuration.getClock())
+ new SystemJobInfoConverter(context, configuration.getClock(),
+ configuration.isMarkingJobsAsImportantWhileForeground())
);
}