Merge "Start generating Kotlin for FieldReadWriteWriter.readFromCursor()" into androidx-main
diff --git a/appcompat/appcompat/build.gradle b/appcompat/appcompat/build.gradle
index ea61343..c188aea 100644
--- a/appcompat/appcompat/build.gradle
+++ b/appcompat/appcompat/build.gradle
@@ -14,7 +14,7 @@
}
api("androidx.annotation:annotation:1.3.0")
- api(projectOrArtifact(":core:core"))
+ api("androidx.core:core:1.9.0")
// Required to make activity 1.5.0-rc01 dependencies resolve.
implementation("androidx.core:core-ktx:1.8.0")
diff --git a/benchmark/benchmark-common/build.gradle b/benchmark/benchmark-common/build.gradle
index c68112e..d015492 100644
--- a/benchmark/benchmark-common/build.gradle
+++ b/benchmark/benchmark-common/build.gradle
@@ -56,8 +56,6 @@
api("androidx.annotation:annotation:1.1.0")
api("androidx.annotation:annotation-experimental:1.0.0")
implementation("androidx.tracing:tracing-ktx:1.0.0")
- implementation(project(":tracing:tracing-perfetto"))
- implementation(project(":tracing:tracing-perfetto-binary"))
implementation(project(":tracing:tracing-perfetto-common"))
implementation(libs.testMonitor)
implementation(libs.wireRuntime)
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
index b1890bc..04756ad 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
@@ -149,7 +149,11 @@
RESULT_CODE_ERROR_BINARY_MISSING ->
"Perfetto SDK binary dependencies missing. " +
"Required version: ${response.requiredVersion}. " +
- "Error: ${response.message}."
+ "Error: ${response.message}.\n" +
+ "To fix, declare the following dependency in your" +
+ " *benchmark* project (i.e. not the app under benchmark): " +
+ "\nandroidTestImplementation(" +
+ "\"androidx.tracing:tracing-perfetto-binary:${response.requiredVersion}\")"
RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH ->
"Perfetto SDK binary mismatch. " +
"Required version: ${response.requiredVersion}. " +
diff --git a/benchmark/benchmark-macro/build.gradle b/benchmark/benchmark-macro/build.gradle
index 158b519..e555101 100644
--- a/benchmark/benchmark-macro/build.gradle
+++ b/benchmark/benchmark-macro/build.gradle
@@ -38,6 +38,11 @@
SupportConfigKt.getPrebuiltsRoot(project),
"androidx/traceprocessor/testdata"
)
+ // PerfettoHandshake supports both AAR and APK as libtracing_perfetto.so sources:
+ // - AAR is used when e.g. tooling downloads tracing-perfetto-binary AAR files from Maven
+ // - APK is when an APK that contains tracing binaries is used
+ //
+ // Adding the AAR as a test asset below allows us to test the AAR scenario
androidTest.assets.srcDirs += new File(
SupportConfigKt.getPrebuiltsRoot(project),
"androidx/internal/androidx/tracing/tracing-perfetto-binary/" +
@@ -65,6 +70,7 @@
androidTestImplementation(project(":internal-testutils-ktx"))
androidTestImplementation("androidx.activity:activity-ktx:1.3.1")
androidTestImplementation(project(":tracing:tracing-perfetto-common"))
+ androidTestImplementation(project(":tracing:tracing-perfetto-binary"))
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
index fbd021b..972ed5f 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
@@ -342,6 +342,10 @@
"Perfetto SDK binary dependencies missing" +
".*UnsatisfiedLinkError.*libtracing_perfetto.so"
)
+ assertThat(response).contains(
+ "androidTestImplementation(" +
+ "\"androidx.tracing:tracing-perfetto-binary:$tracingPerfettoVersion\")"
+ )
}
private fun assertPackageAlive(expected: Boolean) =
diff --git a/benchmark/benchmark/build.gradle b/benchmark/benchmark/build.gradle
index 9e2a189..eb6c7f2 100644
--- a/benchmark/benchmark/build.gradle
+++ b/benchmark/benchmark/build.gradle
@@ -27,6 +27,7 @@
androidTestImplementation(project(":benchmark:benchmark-junit4"))
androidTestImplementation(project(":tracing:tracing-ktx"))
androidTestImplementation(project(":tracing:tracing-perfetto"))
+ androidTestImplementation(project(":tracing:tracing-perfetto-binary"))
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testExtJunit)
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/dependencyTracker/BuildPropParserTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/dependencyTracker/BuildPropParserTest.kt
index ca23588..d5aa82c 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/dependencyTracker/BuildPropParserTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/dependencyTracker/BuildPropParserTest.kt
@@ -35,7 +35,6 @@
val repoProps = tmpFolder.newFile("repo.prop")
repoProps.writeText(
"""
- platform/external/doclava 798edee4d01239248ed33585a42b19ca93c31f56
platform/external/flatbuffers b5c9b8e12b47d0597cd29b217e5765d325957d8a
platform/external/noto-fonts d20a289eaebf2e371064dacc306b57d37b733f7c
platform/frameworks/support fc9030edfe7e0a85a5545a342c7efbf93283f62f
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/doclava/ChecksConfig.kt b/buildSrc/private/src/main/kotlin/androidx/build/doclava/ChecksConfig.kt
deleted file mode 100644
index d56030e..0000000
--- a/buildSrc/private/src/main/kotlin/androidx/build/doclava/ChecksConfig.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build.doclava
-
-import java.io.Serializable
-
-data class ChecksConfig(
- /**
- * List of Doclava error codes to treat as errors.
- * <p>
- * See {@link com.google.doclava.Errors} for a complete list of error codes.
- */
- val errors: List<Int>,
- /**
- * List of Doclava error codes to treat as warnings.
- * <p>
- * See {@link com.google.doclava.Errors} for a complete list of error codes.
- */
- val warnings: List<Int>,
- /**
- * List of Doclava error codes to ignore.
- * <p>
- * See {@link com.google.doclava.Errors} for a complete list of error codes.
- */
- val hidden: List<Int>,
- /** Message to display on check failure. */
- val onFailMessage: String? = null
-) : Serializable
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/doclava/Doclava.kt b/buildSrc/private/src/main/kotlin/androidx/build/doclava/Doclava.kt
deleted file mode 100644
index 1e88b2ec..0000000
--- a/buildSrc/private/src/main/kotlin/androidx/build/doclava/Doclava.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build.doclava
-
-import androidx.build.SupportConfig
-import androidx.build.getAndroidJar
-import androidx.build.getSdkPath
-import org.gradle.api.Project
-import org.gradle.api.artifacts.Configuration
-import org.gradle.api.tasks.TaskProvider
-import org.gradle.api.tasks.util.PatternSet
-import java.io.File
-
-data class DacOptions(val libraryroot: String, val dataname: String)
-
-/**
- * Creates a task to generate an API file from the platform SDK's source and stub JARs.
- * <p>
- * This is useful for federating docs against the platform SDK when no API XML file is available.
- */
-internal fun createGenerateSdkApiTask(
- project: Project,
- doclavaConfig: Configuration,
- annotationConfig: Configuration,
- destination: File
-): TaskProvider<DoclavaTask> =
- project.tasks.register("generateSdkApi", DoclavaTask::class.java) { task ->
- task.apply {
- dependsOn(doclavaConfig)
- dependsOn(annotationConfig)
- description = "Generates API files for the current SDK."
- setDocletpath(doclavaConfig)
- destinationDir = destination
- // Strip the androidx.annotation classes injected by Metalava. They are not accessible.
- classpath = project.getAndroidJar()
- .filter { it.path.contains("androidx/annotation") }
- .plus(annotationConfig)
- source(
- project.zipTree(androidSrcJarFile(project))
- .matching(PatternSet().include("**/*.java"))
- )
- apiFile = File(destination, "release/sdk_current.txt")
- generateDocs = false
- extraArgumentsBuilder.apply({
- addStringOption("stubpackages", "android.*")
- addStringOption("-release", "8")
- })
- }
- }
-
-/**
- * List of Doclava checks that should be ignored when generating documentation.
- */
-private val GENERATEDOCS_HIDDEN = listOf(101, 105, 106, 107, 111, 112, 113, 115, 116, 121)
-
-/**
- * Doclava checks configuration for use in generating documentation.
- */
-internal val GENERATE_DOCS_CONFIG = ChecksConfig(
- warnings = emptyList(),
- hidden = GENERATEDOCS_HIDDEN + DEFAULT_DOCLAVA_CONFIG.hidden,
- errors = ((101..122) - GENERATEDOCS_HIDDEN)
-)
-
-/**
- * @return the project's Android SDK stub source JAR as a File.
- */
-private fun androidSrcJarFile(project: Project): File = File(
- project.getSdkPath(),
- "platforms/${SupportConfig.COMPILE_SDK_VERSION}/android-stubs-src.jar"
-)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/doclava/DoclavaTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/doclava/DoclavaTask.kt
deleted file mode 100644
index fc1d9a3..0000000
--- a/buildSrc/private/src/main/kotlin/androidx/build/doclava/DoclavaTask.kt
+++ /dev/null
@@ -1,305 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build.doclava
-
-import org.gradle.api.DefaultTask
-import org.gradle.api.file.FileCollection
-import org.gradle.api.provider.ListProperty
-import org.gradle.api.tasks.CacheableTask
-import org.gradle.api.tasks.Classpath
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.Internal
-import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.OutputDirectory
-import org.gradle.api.tasks.OutputFile
-import org.gradle.api.tasks.PathSensitive
-import org.gradle.api.tasks.PathSensitivity
-import org.gradle.api.tasks.TaskAction
-import org.gradle.process.ExecOperations
-import org.gradle.workers.WorkAction
-import org.gradle.workers.WorkParameters
-import org.gradle.workers.WorkerExecutor
-import java.io.File
-import javax.inject.Inject
-
-// external/doclava/src/com/google/doclava/Errors.java
-val DEFAULT_DOCLAVA_CONFIG = ChecksConfig(
- errors = listOf(
- 101, // unresolved link
- 103, // unknown tag
- 104 // unknown param name
- ),
- warnings = listOf(121 /* hidden type param */),
- hidden = listOf(
- 111, // hidden super class
- 113 // @deprecation mismatch
- )
-)
-
-@CacheableTask()
-abstract class DoclavaTask @Inject constructor(
- private val workerExecutor: WorkerExecutor
-) : DefaultTask() {
-
- // All lowercase name to match MinimalJavadocOptions#docletpath
- @Classpath
- private lateinit var docletpath: FileCollection
-
- @Input
- var checksConfig: ChecksConfig = DEFAULT_DOCLAVA_CONFIG
-
- /**
- * If non-null, the list of packages that will be treated as if they were
- * marked with {@literal @hide}.<br>
- * Packages names will be matched exactly; sub-packages are not automatically recognized.
- */
- @Optional
- @Input
- var hiddenPackages: Collection<String>? = null
-
- /**
- * If non-null and not-empty, the inclusion list of packages that will be present in the
- * generated stubs; if null or empty, then all packages have stubs generated.<br>
- * Wildcards are accepted.
- */
- @Optional
- @Input
- var stubPackages: Set<String>? = null
-
- @Input
- var generateDocs = true
-
- /**
- * If non-null, the location of where to place the generated api file.
- * If this is non-null, then {@link #removedApiFile} must be non-null as well.
- */
- @Optional
- @OutputFile
- var apiFile: File? = null
-
- /**
- * If non-null, the location of where to place the generated removed api file.
- */
- @Optional
- @OutputFile
- var removedApiFile: File? = null
-
- /**
- * If non-null, the location to put the generated stub sources.
- */
- @Optional
- @OutputDirectory
- var stubsDir: File? = null
-
- init {
- // If none of generateDocs, apiFile, or stubJarsDir are true, then there is
- // no work to do.
- onlyIf({ generateDocs || apiFile != null || stubsDir != null })
- }
-
- /**
- * The doclet path which has the {@code com.google.doclava.Doclava} class.
- * This option will override any doclet path set in this instance's
- * {@link #options JavadocOptions}.
- * @see MinimalJavadocOptions#getDocletpath()
- */
- @InputFiles
- fun getDocletpath(): List<File> {
- return docletpath.files.toList()
- }
-
- /**
- * Sets the doclet path which has the {@code com.gogole.doclava.Doclava} class.
- * This option will override any doclet path set in this instance's
- * {@link #options JavadocOptions}.
- * @see MinimalJavadocOptions#setDocletpath(java.util.List)
- */
- fun setDocletpath(docletpath: FileCollection) {
- this.docletpath = docletpath
- }
-
- @OutputDirectory
- var destinationDir: File? = null
-
- @InputFiles @Classpath
- var classpath: FileCollection? = null
-
- @[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
- val sources = mutableListOf<FileCollection>()
-
- fun source(files: FileCollection) {
- sources.add(files)
- }
-
- /**
- * Builder containing extra arguments
- */
- @Internal
- val extraArgumentsBuilder = DoclavaArgumentBuilder()
-
- @Input
- val extraArguments = extraArgumentsBuilder.build()
-
- private fun computeArguments(): List<String> {
- val args = DoclavaArgumentBuilder()
-
- // classpath
- val classpathFile = File.createTempFile("doclavaClasspath", ".txt")
- classpathFile.deleteOnExit()
- classpathFile.bufferedWriter().use { writer ->
- val classpathString = classpath!!.files.map({ f -> f.toString() }).joinToString(":")
- writer.write(classpathString)
- }
- args.addStringOption("cp", "@$classpathFile")
- args.addStringOption("doclet", "com.google.doclava.Doclava")
- args.addStringOption("docletpath", "@$classpathFile")
-
- args.addOption("quiet")
- args.addStringOption("encoding", "UTF-8")
-
- // configure doclava error/warning/hide levels
- args.addRepeatableOption("hide", checksConfig.hidden)
- args.addRepeatableOption("warning", checksConfig.warnings)
- args.addRepeatableOption("error", checksConfig.errors)
-
- if (hiddenPackages != null) {
- args.addRepeatableOption("hidePackage", hiddenPackages!!)
- }
-
- if (!generateDocs) {
- args.addOption("nodocs")
- }
-
- // If requested, generate the API files.
- if (apiFile != null) {
- args.addFileOption("api", apiFile!!)
- if (removedApiFile != null) {
- args.addFileOption("removedApi", removedApiFile!!)
- }
- }
-
- // If requested, generate stubs.
- if (stubsDir != null) {
- args.addFileOption("stubs", stubsDir!!)
- val stubs = stubPackages
- if (stubs != null) {
- args.addStringOption("stubpackages", stubs.joinToString(":"))
- }
- }
- // Always treat this as an Android docs task.
- args.addOption("android")
-
- // destination directory
- args.addFileOption("d", destinationDir!!)
-
- // source files
- val tmpArgs = File.createTempFile("doclavaSourceArgs", ".txt")
- tmpArgs.deleteOnExit()
- tmpArgs.bufferedWriter().use { writer ->
- for (source in sources) {
- for (file in source) {
- val arg = file.toString()
- // Doclava does not know how to parse Kotlin files
- if (!arg.endsWith(".kt")) {
- writer.write(arg)
- writer.newLine()
- }
- }
- }
- }
- args.add("@$tmpArgs")
-
- return args.build() + extraArgumentsBuilder.build()
- }
-
- @TaskAction
- fun generate() {
- val args = computeArguments()
- runDoclavaWithArgs(getDocletpath(), args, workerExecutor)
- }
-}
-
-class DoclavaArgumentBuilder {
- fun add(value: String) {
- args.add(value)
- }
-
- fun addOption(name: String) {
- args.add("-" + name)
- }
-
- fun addStringOption(name: String, value: String) {
- addOption(name)
- args.add(value)
- }
-
- fun addBooleanOption(name: String, value: Boolean) {
- addStringOption(name, value.toString())
- }
-
- fun addFileOption(name: String, value: File) {
- addStringOption(name, value.toString())
- }
-
- fun addRepeatableOption(name: String, values: Collection<*>) {
- for (value in values) {
- addStringOption(name, value.toString())
- }
- }
-
- fun addStringOption(name: String, values: Collection<String>) {
- args.add("-" + name)
- for (value in values) {
- args.add(value)
- }
- }
-
- fun build(): List<String> {
- return args
- }
-
- private val args = mutableListOf<String>()
-}
-
-interface DoclavaParams : WorkParameters {
- fun getClasspath(): ListProperty<File>
- fun getArgs(): ListProperty<String>
-}
-
-fun runDoclavaWithArgs(classpath: List<File>, args: List<String>, workerExecutor: WorkerExecutor) {
- val workQueue = workerExecutor.noIsolation()
- workQueue.submit(DoclavaWorkAction::class.java) { parameters ->
- parameters.getArgs().set(args)
- parameters.getClasspath().set(classpath)
- }
-}
-
-abstract class DoclavaWorkAction @Inject constructor(
- private val execOperations: ExecOperations
-) : WorkAction<DoclavaParams> {
- override fun execute() {
- val args = getParameters().getArgs().get()
- val classpath = getParameters().getClasspath().get()
-
- execOperations.javaexec {
- it.classpath(classpath)
- it.mainClass.set("com.google.doclava.Doclava")
- it.args = args
- }
- }
-}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/doclava/OWNERS b/buildSrc/private/src/main/kotlin/androidx/build/doclava/OWNERS
deleted file mode 100644
index b26dde4..0000000
--- a/buildSrc/private/src/main/kotlin/androidx/build/doclava/OWNERS
+++ /dev/null
@@ -1,3 +0,0 @@
-asfalcone@google.com
-fsladkey@google.com
-owengray@google.com
\ No newline at end of file
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 9bd950b..373d480 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -20,15 +20,10 @@
import androidx.build.dackka.DackkaTask
import androidx.build.dackka.GenerateMetadataTask
import androidx.build.dependencies.KOTLIN_VERSION
-import androidx.build.doclava.DacOptions
-import androidx.build.doclava.DoclavaTask
-import androidx.build.doclava.GENERATE_DOCS_CONFIG
-import androidx.build.doclava.createGenerateSdkApiTask
import androidx.build.dokka.Dokka
import androidx.build.enforceKtlintVersion
import androidx.build.getAndroidJar
import androidx.build.getBuildId
-import androidx.build.getCheckoutRoot
import androidx.build.getDistributionDirectory
import androidx.build.getKeystore
import androidx.build.getLibraryByName
@@ -154,13 +149,6 @@
dependencyClasspath,
buildOnServer
)
- configureDoclava(
- project,
- unzippedDocsSources,
- unzipDocsTask,
- dependencyClasspath,
- buildOnServer
- )
}
/**
@@ -395,9 +383,7 @@
excludedPackagesForJava = hiddenPackagesJava
excludedPackagesForKotlin = emptySet()
libraryMetadataFile.set(getMetadataRegularFile(project))
-
- // TODO(b/223712700): change to `true` once bug is resolved
- showLibraryMetadata = false
+ showLibraryMetadata = true
}
}
@@ -493,110 +479,6 @@
buildOnServer.configure { it.dependsOn(zipTask) }
}
- private fun configureDoclava(
- project: Project,
- unzippedDocsSources: File,
- unzipDocsTask: TaskProvider<Sync>,
- dependencyClasspath: FileCollection,
- buildOnServer: TaskProvider<*>
- ) {
- // Hack to force tools.jar (required by com.sun.javadoc) to be available on the Doclava
- // run-time classpath. Note this breaks the ability to use JDK 9+ for compilation.
- val doclavaConfiguration = project.configurations.create("doclava")
- doclavaConfiguration.dependencies.add(project.dependencies.create(DOCLAVA_DEPENDENCY))
- doclavaConfiguration.dependencies.add(
- project.dependencies.create(
- project.files(System.getenv("JAVA_TOOLS_JAR"))
- )
- )
-
- val annotationConfiguration = project.configurations.create("annotation")
- annotationConfiguration.dependencies.add(
- project.dependencies.project(
- mapOf("path" to ":fakeannotations")
- )
- )
-
- val generatedSdk = File(project.buildDir, "generatedsdk")
- val generateSdkApiTask = createGenerateSdkApiTask(
- project, doclavaConfiguration, annotationConfiguration, generatedSdk
- )
-
- val destDir = File(project.buildDir, "javadoc")
- val offlineOverride = project.findProject("offlineDocs") as String?
- val offline = if (offlineOverride != null) { offlineOverride == "true" } else false
- val dacOptions = DacOptions("androidx", "ANDROIDX_DATA")
-
- val doclavaTask = project.tasks.register("doclavaDocs", DoclavaTask::class.java) {
- it.apply {
- dependsOn(unzipDocsTask)
- dependsOn(generateSdkApiTask)
- group = JavaBasePlugin.DOCUMENTATION_GROUP
- description = "Generates Java documentation in the style of d.android.com. To " +
- "generate offline docs use \'-PofflineDocs=true\' parameter. Places the " +
- "documentation in $destDir"
- dependsOn(doclavaConfiguration)
- setDocletpath(doclavaConfiguration)
- destinationDir = destDir
- classpath = project.getAndroidJar() + dependencyClasspath
- checksConfig = GENERATE_DOCS_CONFIG
- extraArgumentsBuilder.apply {
- addStringOption(
- "templatedir",
- "${project.getCheckoutRoot()}/external/doclava/res/assets/templates-sdk"
- )
- // Note, this is pointing to the root checkout directory.
- addStringOption(
- "samplesdir",
- "${project.rootDir}/samples"
- )
- addStringOption(
- "federate",
- listOf("Android", "https://developer.android.com")
- )
- addStringOption(
- "federationapi",
- listOf(
- "Android",
- generateSdkApiTask.get().apiFile?.absolutePath.toString()
- )
- )
- addStringOption("hdf", listOf("android.whichdoc", "online"))
- addStringOption("hdf", listOf("android.hasSamples", "true"))
- addStringOption("hdf", listOf("dac", "true"))
-
- // Specific to reference docs.
- if (!offline) {
- addStringOption("toroot", "/")
- addOption("devsite")
- addOption("yamlV2")
- addStringOption("dac_libraryroot", dacOptions.libraryroot)
- addStringOption("dac_dataname", dacOptions.dataname)
- }
- }
- it.source(project.fileTree(unzippedDocsSources))
- }
- }
- val zipTask = project.tasks.register("zipDoclavaDocs", Zip::class.java) {
- it.apply {
- it.dependsOn(doclavaTask)
- from(doclavaTask.map { it.destinationDir!! })
- val baseName = "doclava-$docsType-docs"
- val buildId = getBuildId()
- archiveBaseName.set(baseName)
- archiveVersion.set(buildId)
- destinationDirectory.set(project.getDistributionDirectory())
- group = JavaBasePlugin.DOCUMENTATION_GROUP
- val filePath = "${project.getDistributionDirectory().canonicalPath}/"
- val fileName = "$baseName-$buildId.zip"
- val destinationFile = filePath + fileName
- description = "Zips Java documentation (generated via Doclava in the " +
- "style of d.android.com) into $destinationFile"
- }
- }
- buildOnServer.configure { it.dependsOn(zipTask) }
- }
-
/**
* Replace all tests etc with empty task, so we don't run anything
* it is more effective then task.enabled = false, because we avoid executing deps as well
@@ -638,7 +520,6 @@
fun getRequiredFiles(): List<File> {
return listOf(
File(distributionDirectory, "dackka-$docsType-docs-$buildId.zip"),
- File(distributionDirectory, "doclava-$docsType-docs-$buildId.zip"),
File(distributionDirectory, "dokka-$docsType-docs-$buildId.zip")
)
}
@@ -688,8 +569,6 @@
private fun getMetadataRegularFile(project: Project): Provider<RegularFile> =
project.layout.buildDirectory.file("AndroidXLibraryMetadata.json")
-private const val DOCLAVA_DEPENDENCY = "com.android:doclava:1.0.6"
-
// List of packages to exclude from both Java and Kotlin refdoc generation
private val hiddenPackages = listOf(
"androidx.camera.camera2.impl",
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt b/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
index 0d1f592..6c905f1 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
@@ -117,11 +117,6 @@
":hilt:hilt-navigation-compose:kaptGenerateStubsReleaseKotlin",
":lint-checks:integration-tests:copyDebugAndroidLintReports",
- // https://github.com/gradle/gradle/issues/17262
- ":doclava:compileJava",
- ":doclava:processResources",
- ":doclava:jar",
-
// https://youtrack.jetbrains.com/issue/KT-49933
"generateProjectStructureMetadata"
)
@@ -138,9 +133,6 @@
"zipDokkaDocs",
"dackkaDocs",
- // Flakily not up-to-date, b/176120659
- "doclavaDocs",
-
// We know that these tasks are never up to date due to maven-metadata.xml changing
// https://github.com/gradle/gradle/issues/11203
"partiallyDejetifyArchive",
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt b/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
index f4446fb..f162a39 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
@@ -17,7 +17,6 @@
package androidx.build
import androidx.build.SupportConfig.COMPILE_SDK_VERSION
-import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.file.FileCollection
import java.io.File
@@ -51,15 +50,7 @@
}
fun Project.getExternalProjectPath(): File {
- val path = if (System.getenv("COMPOSE_DESKTOP_GITHUB_BUILD") != null)
- File(System.getenv("OUT_DIR")).also {
- if (!File(it, "doclava").isDirectory()) {
- throw GradleException("Please checkout doclava to $it")
- }
- }
- else
- File(rootProject.projectDir, "../../external")
- return path.getCanonicalFile()
+ return File(rootProject.projectDir, "../../external").canonicalFile
}
fun Project.getKeystore(): File {
diff --git a/busytown/impl/verify_no_caches_in_source_repo.sh b/busytown/impl/verify_no_caches_in_source_repo.sh
index bcc5c14..056f7e4 100755
--- a/busytown/impl/verify_no_caches_in_source_repo.sh
+++ b/busytown/impl/verify_no_caches_in_source_repo.sh
@@ -26,7 +26,7 @@
# Paths that are still expected to be generated and that we have to allow
# If you need add or remove an exemption here, update cleanBuild.sh too
- EXEMPT_PATHS=".gradle buildSrc/.gradle local.properties reports build"
+ EXEMPT_PATHS=".gradle placeholder/.gradle buildSrc/.gradle local.properties reports build"
# put "./" in front of each path to match the output from 'find'
EXEMPT_PATHS="$(echo " $EXEMPT_PATHS" | sed 's| | ./|g')"
# build a `find` argument for skipping descending into the exempt paths
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 5b22093..2457f15 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -279,6 +279,8 @@
@FlashMode
private static final int DEFAULT_FLASH_MODE = FLASH_MODE_OFF;
+ boolean mUseProcessingPipeline = false;
+
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
static final ExifRotationAvailability EXIF_ROTATION_AVAILABILITY =
new ExifRotationAvailability();
@@ -1885,7 +1887,41 @@
@MainThread
private boolean isNodeEnabled() {
checkMainThread();
- return false;
+ ImageCaptureConfig config = (ImageCaptureConfig) getCurrentConfig();
+ if (config.getImageReaderProxyProvider() != null) {
+ // Use old pipeline for custom ImageReader.
+ return false;
+ }
+ if (isSessionProcessorEnabledInCurrentCamera()) {
+ // Use old pipeline for advanced Extensions.
+ return false;
+ }
+ if (mCaptureProcessor != null) {
+ // Use old pipeline for basic Extensions.
+ return false;
+ }
+ if (getCaptureStageSize(config) > 1) {
+ // Use old pipeline for multiple stages capture.
+ return false;
+ }
+ if (requireNonNull(config.retrieveOption(OPTION_INPUT_FORMAT, ImageFormat.JPEG))
+ != ImageFormat.JPEG) {
+ // Use old pipeline for non-JPEG output format.
+ return false;
+ }
+ return mUseProcessingPipeline;
+ }
+
+ private int getCaptureStageSize(@NonNull ImageCaptureConfig config) {
+ CaptureBundle captureBundle = config.getCaptureBundle(null);
+ if (captureBundle == null) {
+ return 1;
+ }
+ List<CaptureStage> captureStages = captureBundle.getCaptureStages();
+ if (captureStages == null) {
+ return 1;
+ }
+ return captureStages.size();
}
/**
@@ -2017,6 +2053,11 @@
input -> null, CameraXExecutors.directExecutor());
}
+ @VisibleForTesting
+ boolean isProcessingPipelineEnabled() {
+ return mImagePipeline != null && mTakePictureManager != null;
+ }
+
// ===== New architecture end =====
/**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java
index fc6d05e..3199bcb 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java
@@ -64,7 +64,8 @@
// TODO: we might need to calculate this number dynamically based on on many frames are
// needed by the post-processing. e.g. night mode might need to merge 10+ frames. 4 images
// should be enough for now.
- private static final int MAX_IMAGES = 4;
+ @VisibleForTesting
+ static final int MAX_IMAGES = 4;
@NonNull
private final Set<Integer> mPendingStageIds = new HashSet<>();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
index 044158d..af12fa8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
@@ -31,6 +31,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.ForwardingImageProxy;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.impl.CaptureBundle;
import androidx.camera.core.impl.CaptureConfig;
@@ -70,7 +71,7 @@
@NonNull
private final SingleBundlingNode mBundlingNode;
@NonNull
- private ProcessingNode mProcessingNode;
+ private final ProcessingNode mProcessingNode;
@NonNull
private final CaptureNode.In mPipelineIn;
@@ -121,6 +122,26 @@
mProcessingNode.release();
}
+ /**
+ * Returns the number of empty slots in the queue.
+ */
+ @MainThread
+ public int getCapacity() {
+ checkMainThread();
+ return mCaptureNode.getCapacity();
+ }
+
+ /**
+ * Sets a listener for close calls on this image.
+ * @param listener to set
+ */
+ @MainThread
+ public void setOnImageCloseListener(
+ @NonNull ForwardingImageProxy.OnImageCloseListener listener) {
+ checkMainThread();
+ mCaptureNode.setOnImageCloseListener(listener);
+ }
+
// ===== protected methods =====
/**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
index 87953af..2ecfcd5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
@@ -33,8 +33,10 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.ImageProxy;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.core.util.Pair;
@@ -59,7 +61,7 @@
* <p>The thread safety is guaranteed by using the main thread.
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class TakePictureManager {
+public class TakePictureManager implements OnImageCloseListener {
private static final String TAG = "TakePictureManager";
@@ -88,6 +90,7 @@
checkMainThread();
mImageCaptureControl = imageCaptureControl;
mImagePipeline = imagePipeline;
+ mImagePipeline.setOnImageCloseListener(this);
}
/**
@@ -160,6 +163,10 @@
Log.d(TAG, "The class is paused.");
return;
}
+ if (mImagePipeline.getCapacity() == 0) {
+ Log.d(TAG, "Too many acquire images. Close image to be able to process next.");
+ return;
+ }
TakePictureRequest request = mNewRequests.poll();
if (request == null) {
Log.d(TAG, "No new request.");
@@ -228,4 +235,9 @@
boolean hasInFlightRequest() {
return mInFlightRequest != null;
}
+
+ @Override
+ public void onImageClose(@NonNull ImageProxy image) {
+ mainThreadExecutor().execute(this::issueNextRequest);
+ }
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index 283218b..a7f51e4 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -25,12 +25,15 @@
import android.os.Looper.getMainLooper
import android.util.Pair
import android.util.Rational
+import android.util.Size
import android.view.Surface
import androidx.camera.core.ImageCapture.ImageCaptureRequest
import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor
import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor.ImageCaptor
import androidx.camera.core.impl.CameraFactory
import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.impl.CaptureProcessor
+import androidx.camera.core.impl.ImageProxyBundle
import androidx.camera.core.impl.SessionConfig
import androidx.camera.core.impl.TagBundle
import androidx.camera.core.impl.UseCaseConfig
@@ -189,11 +192,72 @@
}
@Test
+ fun processingPipelineOffByDefault() {
+ assertThat(
+ bindImageCapture(
+ useProcessingPipeline = false,
+ bufferFormat = ImageFormat.JPEG,
+ ).isProcessingPipelineEnabled
+ ).isFalse()
+ }
+
+ @Test
+ fun processingPipelineOn_pipelineEnabled() {
+ assertThat(
+ bindImageCapture(
+ useProcessingPipeline = true,
+ bufferFormat = ImageFormat.JPEG,
+ ).isProcessingPipelineEnabled
+ ).isTrue()
+ }
+
+ @Test
+ fun useImageReaderProvider_pipelineDisabled() {
+ assertThat(
+ bindImageCapture(
+ useProcessingPipeline = true,
+ bufferFormat = ImageFormat.JPEG,
+ imageReaderProxyProvider = getImageReaderProxyProvider(),
+ ).isProcessingPipelineEnabled
+ ).isFalse()
+ }
+
+ @Test
+ fun yuvFormat_pipelineDisabled() {
+ assertThat(
+ bindImageCapture(
+ useProcessingPipeline = true,
+ bufferFormat = ImageFormat.YUV_420_888,
+ ).isProcessingPipelineEnabled
+ ).isFalse()
+ }
+
+ @Test
+ fun extensionIsOn_pipelineDisabled() {
+ assertThat(
+ bindImageCapture(
+ useProcessingPipeline = true,
+ captureProcessor = object : CaptureProcessor {
+ override fun onOutputSurface(surface: Surface, imageFormat: Int) {
+ }
+
+ override fun process(bundle: ImageProxyBundle) {
+ }
+
+ override fun onResolutionUpdate(size: Size) {
+ }
+ }
+ ).isProcessingPipelineEnabled
+ ).isFalse()
+ }
+
+ @Test
fun captureImageWithViewPort_isSet() {
// Arrange
val imageCapture = bindImageCapture(
ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
- ViewPort.Builder(Rational(1, 1), Surface.ROTATION_0).build()
+ ViewPort.Builder(Rational(1, 1), Surface.ROTATION_0).build(),
+ imageReaderProxyProvider = getImageReaderProxyProvider()
)
// Act
@@ -241,7 +305,10 @@
@Test
fun capturedImageSize_isEqualToSurfaceSize() {
// Act/arrange.
- val imageCapture = bindImageCapture(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+ val imageCapture = bindImageCapture(
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ imageReaderProxyProvider = getImageReaderProxyProvider()
+ )
// Act
imageCapture.takePicture(executor, onImageCapturedCallback)
@@ -495,10 +562,20 @@
viewPort: ViewPort? = null,
// Set non jpg format so it doesn't trigger the exif code path.
bufferFormat: Int = ImageFormat.YUV_420_888,
- imageReaderProxyProvider: ImageReaderProxyProvider? = null
+ imageReaderProxyProvider: ImageReaderProxyProvider? = null,
+ useProcessingPipeline: Boolean? = null,
+ captureProcessor: CaptureProcessor? = null
): ImageCapture {
// Arrange.
- val imageCapture = createImageCapture(captureMode, bufferFormat, imageReaderProxyProvider)
+ val imageCapture = createImageCapture(
+ captureMode,
+ bufferFormat,
+ imageReaderProxyProvider,
+ captureProcessor
+ )
+ if (useProcessingPipeline != null) {
+ imageCapture.mUseProcessingPipeline = useProcessingPipeline
+ }
cameraUseCaseAdapter = CameraUtil.createCameraUseCaseAdapter(
ApplicationProvider
@@ -515,16 +592,26 @@
captureMode: Int = ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
// Set non jpg format by default so it doesn't trigger the exif code path.
bufferFormat: Int = ImageFormat.YUV_420_888,
- imageReaderProxyProvider: ImageReaderProxyProvider? = null
- ) = ImageCapture.Builder()
- .setBufferFormat(bufferFormat)
- .setTargetRotation(Surface.ROTATION_0)
- .setCaptureMode(captureMode)
- .setFlashMode(ImageCapture.FLASH_MODE_OFF)
- .setCaptureOptionUnpacker { _: UseCaseConfig<*>?, _: CaptureConfig.Builder? -> }
- .setImageReaderProxyProvider(imageReaderProxyProvider ?: getImageReaderProxyProvider())
- .setSessionOptionUnpacker { _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
- .build()
+ imageReaderProxyProvider: ImageReaderProxyProvider? = null,
+ captureProcessor: CaptureProcessor? = null
+ ): ImageCapture {
+ val builder = ImageCapture.Builder()
+ .setTargetRotation(Surface.ROTATION_0)
+ .setCaptureMode(captureMode)
+ .setFlashMode(ImageCapture.FLASH_MODE_OFF)
+ .setCaptureOptionUnpacker { _: UseCaseConfig<*>?, _: CaptureConfig.Builder? -> }
+ .setSessionOptionUnpacker { _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
+
+ if (captureProcessor != null) {
+ builder.setCaptureProcessor(captureProcessor)
+ } else {
+ builder.setBufferFormat(bufferFormat)
+ }
+ if (imageReaderProxyProvider != null) {
+ builder.setImageReaderProxyProvider(imageReaderProxyProvider)
+ }
+ return builder.build()
+ }
private fun getImageReaderProxyProvider(): ImageReaderProxyProvider {
return ImageReaderProxyProvider { width, height, imageFormat, queueDepth, usage ->
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImagePipeline.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImagePipeline.kt
index 8253d4d..93750c6 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImagePipeline.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImagePipeline.kt
@@ -18,6 +18,7 @@
import android.util.Size
import androidx.annotation.MainThread
+import androidx.camera.core.imagecapture.CaptureNode.MAX_IMAGES
import androidx.camera.core.imagecapture.Utils.createEmptyImageCaptureConfig
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.ImageCaptureConfig
@@ -33,6 +34,7 @@
private var responseMap: MutableMap<TakePictureRequest,
Pair<CameraRequest, ProcessingRequest>> = mutableMapOf()
var captureConfigMap: MutableMap<TakePictureRequest, List<CaptureConfig>> = mutableMapOf()
+ var queueCapacity: Int = MAX_IMAGES
constructor() : this(
createEmptyImageCaptureConfig(),
@@ -64,4 +66,9 @@
internal fun getProcessingRequest(takePictureRequest: TakePictureRequest): ProcessingRequest {
return responseMap[takePictureRequest]!!.second!!
}
+
+ @MainThread
+ override fun getCapacity(): Int {
+ return queueCapacity
+ }
}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
index 9baec2a..8362800 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
@@ -26,6 +26,9 @@
import androidx.camera.core.ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
import androidx.camera.core.ImageCapture.CaptureMode
import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.SafeCloseImageReaderProxy
+import androidx.camera.core.imagecapture.CaptureNode.MAX_IMAGES
import androidx.camera.core.imagecapture.ImagePipeline.JPEG_QUALITY_MAX_QUALITY
import androidx.camera.core.imagecapture.ImagePipeline.JPEG_QUALITY_MIN_LATENCY
import androidx.camera.core.imagecapture.Utils.CROP_RECT
@@ -46,6 +49,7 @@
import androidx.camera.testing.TestImageUtil.createJpegBytes
import androidx.camera.testing.TestImageUtil.createJpegFakeImageProxy
import androidx.camera.testing.fakes.FakeImageInfo
+import androidx.camera.testing.fakes.FakeImageReaderProxy
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -251,4 +255,25 @@
// Assert: the image is received by TakePictureCallback.
assertThat(CALLBACK.inMemoryResult!!.planes).isEqualTo(image.planes)
}
+
+ @Test
+ fun acquireImageProxy_capacityIsUpdated() {
+ // Arrange.
+ val images = ArrayDeque<ImageProxy>()
+ val imageReaderProxy = FakeImageReaderProxy(MAX_IMAGES)
+ imagePipeline.captureNode.mSafeCloseImageReaderProxy =
+ SafeCloseImageReaderProxy(imageReaderProxy)
+
+ // Act.
+ // Exhaust outstanding image quota.
+ for (i in 0 until MAX_IMAGES) {
+ val imageInfo = FakeImageInfo()
+ imageReaderProxy.triggerImageAvailable(imageInfo.tagBundle, 0)
+ imagePipeline.captureNode.mSafeCloseImageReaderProxy.acquireNextImage()
+ ?.let { images.add(it) }
+ }
+
+ // Assert: the capacity of queue is 0.
+ assertThat(imagePipeline.capacity).isEqualTo(0)
+ }
}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
index 1a068e8..5335ee6 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
@@ -299,4 +299,31 @@
// Assert.
assertThat(request.imageReceived).isEqualTo(image)
}
+
+ @Test
+ fun takePictureManager_unableToProcessNextWhenOverMaxImages() {
+ // Arrange.
+ imagePipeline.queueCapacity = 0
+
+ // Act: send the request.
+ val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
+ takePictureManager.offerRequest(request)
+
+ // Assert: the request is blocked.
+ assertThat(takePictureManager.mNewRequests.size).isEqualTo(1)
+ assertThat(imageCaptureControl.actions).isEmpty()
+
+ // Act: increase the capacity and invoke image closed.
+ imagePipeline.queueCapacity = 1
+ takePictureManager.onImageClose(FakeImageProxy(FakeImageInfo()))
+ shadowOf(getMainLooper()).idle()
+
+ // Assert: the request is sent.
+ assertThat(takePictureManager.mNewRequests.size).isEqualTo(0)
+ assertThat(imageCaptureControl.actions).containsExactly(
+ FakeImageCaptureControl.Action.LOCK_FLASH,
+ FakeImageCaptureControl.Action.SUBMIT_REQUESTS,
+ FakeImageCaptureControl.Action.UNLOCK_FLASH,
+ ).inOrder()
+ }
}
\ No newline at end of file
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/CameraPipeConfigTestRule.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/CameraPipeConfigTestRule.kt
index 07a627c..b7fbf24 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/CameraPipeConfigTestRule.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/CameraPipeConfigTestRule.kt
@@ -75,6 +75,11 @@
log("finished: ${description.displayName}")
}
} else {
+ if (Log.isLoggable(CAMERA2_TEST_DISABLE, Log.DEBUG)) {
+ throw AssumptionViolatedException(
+ "Ignore Camera2 tests since CAMERA2_TEST_DISABLE flag is turned on."
+ )
+ }
base.evaluate()
}
}
@@ -101,6 +106,7 @@
}
companion object {
+ private const val CAMERA2_TEST_DISABLE = "CAMERA2_TEST_DISABLE"
private const val CAMERA_PIPE_TEST_FLAG = "CAMERA_PIPE_TESTING"
private const val CAMERA_PIPE_MH_FLAG = "CameraPipeMH"
private const val LOG_TAG = "CameraPipeTest"
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 94d1746..c250892 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -110,6 +110,7 @@
import androidx.camera.video.internal.encoder.VideoEncoderConfig;
import androidx.camera.video.internal.encoder.VideoEncoderInfo;
import androidx.camera.video.internal.encoder.VideoEncoderInfoImpl;
+import androidx.camera.video.internal.workaround.VideoEncoderInfoWrapper;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.core.util.Preconditions;
import androidx.core.util.Supplier;
@@ -890,13 +891,21 @@
if (mVideoEncoderInfo != null) {
return mVideoEncoderInfo;
}
+
+ VideoEncoderInfo videoEncoderInfo = resolveVideoEncoderInfo(videoEncoderInfoFinder,
+ videoCapabilities, timebase, mediaSpec, resolution, targetFps);
+ if (videoEncoderInfo == null) {
+ return null;
+ }
+
+ videoEncoderInfo = VideoEncoderInfoWrapper.from(videoEncoderInfo, resolution);
+
// Cache the VideoEncoderInfo as it should be the same when recreating the pipeline.
// This avoids recreating the MediaCodec instance to get encoder information.
// Note: We should clear the cache if the MediaSpec changes at any time, especially when
// the Encoder-related content in the VideoSpec changes. i.e. when we need to observe the
// MediaSpec Observable.
- return mVideoEncoderInfo = resolveVideoEncoderInfo(videoEncoderInfoFinder,
- videoCapabilities, timebase, mediaSpec, resolution, targetFps);
+ return mVideoEncoderInfo = videoEncoderInfo;
}
@Nullable
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/VideoEncoderInfoWrapper.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/VideoEncoderInfoWrapper.java
new file mode 100644
index 0000000..e7cf9a2
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/VideoEncoderInfoWrapper.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.workaround;
+
+import android.media.MediaCodecInfo;
+import android.util.Range;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.Logger;
+import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.video.internal.compat.quirk.MediaCodecInfoReportIncorrectInfoQuirk;
+import androidx.camera.video.internal.encoder.VideoEncoderInfo;
+import androidx.core.util.Preconditions;
+
+/**
+ * Workaround to wrap the VideoEncoderInfo in order to fix the wrong information provided by
+ * {@link MediaCodecInfo}.
+ *
+ * <p>One use case is VideoCapture resizing the crop to a size valid for the encoder.
+ *
+ * @see MediaCodecInfoReportIncorrectInfoQuirk
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class VideoEncoderInfoWrapper implements VideoEncoderInfo {
+ private static final String TAG = "VideoEncoderInfoWrapper";
+
+ // The resolution of CamcorderProfile.QUALITY_4KDCI
+ private static final int WIDTH_4KDCI = 4096;
+ private static final int HEIGHT_4KDCI = 2160;
+
+ private final VideoEncoderInfo mVideoEncoderInfo;
+ private final Range<Integer> mSupportedWidths;
+ private final Range<Integer> mSupportedHeights;
+
+ /**
+ * Check and wrap an input VideoEncoderInfo
+ *
+ * <p>The input VideoEncoderInfo will be wrapped when
+ * <ul>
+ * <li>The device is a quirk device determined in
+ * {@link MediaCodecInfoReportIncorrectInfoQuirk}.</li>
+ * <li>The input {@code validSizeToCheck} is not supported by input VideoEncoderInfo.</li>
+ * </ul>
+ * Otherwise, the input VideoEncoderInfo will be returned.
+ *
+ * @param videoEncoderInfo the input VideoEncoderInfo.
+ * @param validSizeToCheck a valid size to check.
+ * @return a wrapped VideoEncoderInfo or the input VideoEncoderInfo.
+ */
+ @NonNull
+ public static VideoEncoderInfo from(@NonNull VideoEncoderInfo videoEncoderInfo,
+ @NonNull Size validSizeToCheck) {
+ boolean toWrap = false;
+ if (DeviceQuirks.get(MediaCodecInfoReportIncorrectInfoQuirk.class) != null) {
+ toWrap = true;
+ } else if (!isSizeSupported(videoEncoderInfo, validSizeToCheck)) {
+ // If the device does not support a size that should be valid, assume the device
+ // reports incorrect information. This is used to detect devices that we haven't
+ // discovered incorrect information yet.
+ Logger.w(TAG, String.format(
+ "Detected that the device does not support a size %s that should be valid"
+ + " in widths/heights = %s/%s", validSizeToCheck,
+ videoEncoderInfo.getSupportedWidths(),
+ videoEncoderInfo.getSupportedHeights()));
+ toWrap = true;
+ }
+ return toWrap ? new VideoEncoderInfoWrapper(videoEncoderInfo) : videoEncoderInfo;
+ }
+
+ VideoEncoderInfoWrapper(@NonNull VideoEncoderInfo videoEncoderInfo) {
+ mVideoEncoderInfo = videoEncoderInfo;
+
+ // Ideally we should find out supported widths/heights for each problematic device.
+ // As a workaround, simply return a big enough size for video encoding. i.e.
+ // CamcorderProfile.QUALITY_4KDCI. The size still need to follow the multiple of alignment.
+ int widthAlignment = videoEncoderInfo.getWidthAlignment();
+ int maxWidth = (int) Math.ceil((double) WIDTH_4KDCI / widthAlignment) * widthAlignment;
+ mSupportedWidths = Range.create(widthAlignment, maxWidth);
+ int heightAlignment = videoEncoderInfo.getHeightAlignment();
+ int maxHeight = (int) Math.ceil((double) HEIGHT_4KDCI / heightAlignment) * heightAlignment;
+ mSupportedHeights = Range.create(heightAlignment, maxHeight);
+ }
+
+ @NonNull
+ @Override
+ public String getName() {
+ return mVideoEncoderInfo.getName();
+ }
+
+ @NonNull
+ @Override
+ public Range<Integer> getSupportedWidths() {
+ return mSupportedWidths;
+ }
+
+ @NonNull
+ @Override
+ public Range<Integer> getSupportedHeights() {
+ return mSupportedHeights;
+ }
+
+ @NonNull
+ @Override
+ public Range<Integer> getSupportedWidthsFor(int height) {
+ Preconditions.checkArgument(mSupportedHeights.contains(height),
+ "Not supported height: " + height + " in " + mSupportedHeights);
+ return mSupportedWidths;
+ }
+
+ @NonNull
+ @Override
+ public Range<Integer> getSupportedHeightsFor(int width) {
+ Preconditions.checkArgument(mSupportedWidths.contains(width),
+ "Not supported width: " + width + " in " + mSupportedWidths);
+ return mSupportedHeights;
+ }
+
+ @Override
+ public int getWidthAlignment() {
+ return mVideoEncoderInfo.getWidthAlignment();
+ }
+
+ @Override
+ public int getHeightAlignment() {
+ return mVideoEncoderInfo.getHeightAlignment();
+ }
+
+ private static boolean isSizeSupported(@NonNull VideoEncoderInfo videoEncoderInfo,
+ @NonNull Size size) {
+ if (!videoEncoderInfo.getSupportedWidths().contains(size.getWidth())
+ || !videoEncoderInfo.getSupportedHeights().contains(size.getHeight())) {
+ return false;
+ }
+ try {
+ if (!videoEncoderInfo.getSupportedHeightsFor(size.getWidth()).contains(size.getHeight())
+ || !videoEncoderInfo.getSupportedWidthsFor(size.getHeight()).contains(
+ size.getWidth())) {
+ return false;
+ }
+ } catch (IllegalArgumentException e) {
+ Logger.w(TAG, "size is not supported", e);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index af83bd4..df60502 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -659,8 +659,9 @@
videoEncoderInfo = createVideoEncoderInfo(
widthAlignment = 8,
heightAlignment = 8,
- supportedWidths = Range(80, 800),
- supportedHeights = Range(100, 800),
+ // 1280x720 is a valid size
+ supportedWidths = Range(80, 1600),
+ supportedHeights = Range(100, 1600),
),
cropRect = Rect(8, 8, 48, 48), // 40x40
expectedCropRect = Rect(0, 0, 80, 100),
@@ -668,6 +669,21 @@
}
@Test
+ fun adjustCropRect_notValidSize_ignoreSupportedSizeAndClampByWorkaroundSize() {
+ testAdjustCropRectToValidSize(
+ videoEncoderInfo = createVideoEncoderInfo(
+ widthAlignment = 8,
+ heightAlignment = 8,
+ // 1280x720 is not a valid size, workaround size is [8-4096], [8-2160]
+ supportedWidths = Range(80, 80),
+ supportedHeights = Range(80, 80),
+ ),
+ cropRect = Rect(0, 0, 4, 4), // 4x4
+ expectedCropRect = Rect(0, 0, 8, 8), // 8x8
+ )
+ }
+
+ @Test
fun adjustCropRect_toSmallestDimensionChange() {
testAdjustCropRectToValidSize(
videoEncoderInfo = createVideoEncoderInfo(widthAlignment = 8, heightAlignment = 8),
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java b/camera/camera-video/src/test/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java
new file mode 100644
index 0000000..9f668ea
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.compat.quirk;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.List;
+
+/**
+ * Tests version of main/.../DeviceQuirks.java, which provides device specific quirks, used for
+ * device specific workarounds.
+ * <p>
+ * In main/.../DeviceQuirks, Device quirks are loaded the first time a device workaround is
+ * encountered, and remain in memory until the process is killed. When running tests, this means
+ * that the same device quirks are used for all the tests. This causes an issue when tests modify
+ * device properties (using Robolectric for instance). Instead of force-reloading the device
+ * quirks in every test that uses a device workaround, this class internally reloads the quirks
+ * every time a device workaround is needed.
+ */
+public class DeviceQuirks {
+
+ private DeviceQuirks() {
+ }
+
+ /**
+ * Retrieves a specific device {@link Quirk} instance given its type.
+ *
+ * @param quirkClass The type of device quirk to retrieve.
+ * @return A device {@link Quirk} instance of the provided type, or {@code null} if it isn't
+ * found.
+ */
+ @SuppressWarnings("unchecked")
+ @Nullable
+ public static <T extends Quirk> T get(@NonNull final Class<T> quirkClass) {
+ final List<Quirk> quirks = DeviceQuirksLoader.loadQuirks();
+ for (final Quirk quirk : quirks) {
+ if (quirk.getClass() == quirkClass) {
+ return (T) quirk;
+ }
+ }
+ return null;
+ }
+}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/internal/workaround/VideoEncoderInfoWrapperTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/internal/workaround/VideoEncoderInfoWrapperTest.kt
new file mode 100644
index 0000000..8a04d49
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/internal/workaround/VideoEncoderInfoWrapperTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.workaround
+
+import android.os.Build
+import android.util.Range
+import android.util.Size
+import androidx.camera.video.internal.encoder.FakeVideoEncoderInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class VideoEncoderInfoWrapperTest(
+ private val brand: String,
+ private val model: String,
+ private val sizeToCheck: Size,
+ private val expectedSupportedWidths: Range<Int>,
+ private val expectedSupportedHeights: Range<Int>,
+) {
+
+ companion object {
+ private const val WIDTH_ALIGNMENT = 2
+ private const val HEIGHT_ALIGNMENT = 2
+ private val SUPPORTED_WIDTHS = Range.create(WIDTH_ALIGNMENT, 640)
+ private val SUPPORTED_HEIGHTS = Range.create(HEIGHT_ALIGNMENT, 480)
+ private val VALID_SIZE = Size(320, 240)
+ private val INVALID_SIZE = Size(1920, 1080)
+
+ private const val WIDTH_4KDCI = 4096
+ private const val HEIGHT_4KDCI = 2160
+ private val OVERRIDE_SUPPORTED_WIDTHS = Range.create(WIDTH_ALIGNMENT, WIDTH_4KDCI)
+ private val OVERRIDE_SUPPORTED_HEIGHTS = Range.create(HEIGHT_ALIGNMENT, HEIGHT_4KDCI)
+ private const val NONE_QUIRK_BRAND = "NoneQuirkBrand"
+ private const val NONE_QUIRK_MODEL = "NoneQuirkModel"
+
+ @JvmStatic
+ @ParameterizedRobolectricTestRunner.Parameters(
+ name = "brand={0}, model={1}, sizeToCheck={2}" +
+ ", expectedSupportedWidths={3}, expectedSupportedHeights={4}"
+ )
+ fun data() = mutableListOf<Array<Any?>>().apply {
+ add(
+ arrayOf(
+ NONE_QUIRK_BRAND,
+ NONE_QUIRK_MODEL,
+ VALID_SIZE,
+ SUPPORTED_WIDTHS,
+ SUPPORTED_HEIGHTS,
+ )
+ )
+ add(
+ arrayOf(
+ NONE_QUIRK_BRAND,
+ NONE_QUIRK_MODEL,
+ INVALID_SIZE,
+ OVERRIDE_SUPPORTED_WIDTHS,
+ OVERRIDE_SUPPORTED_HEIGHTS,
+ )
+ )
+ add(
+ arrayOf(
+ "Nokia",
+ "Nokia 1",
+ VALID_SIZE,
+ OVERRIDE_SUPPORTED_WIDTHS,
+ OVERRIDE_SUPPORTED_HEIGHTS,
+ )
+ )
+ add(
+ arrayOf(
+ "motorola",
+ "moto c",
+ VALID_SIZE,
+ OVERRIDE_SUPPORTED_WIDTHS,
+ OVERRIDE_SUPPORTED_HEIGHTS,
+ )
+ )
+ // No necessary to test all models.
+ }
+ }
+
+ private val baseVideoEncoderInfo = FakeVideoEncoderInfo(
+ _supportedWidths = SUPPORTED_WIDTHS,
+ _supportedHeights = SUPPORTED_HEIGHTS,
+ _widthAlignment = WIDTH_ALIGNMENT,
+ _heightAlignment = HEIGHT_ALIGNMENT,
+ )
+
+ @Before
+ fun setup() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", brand)
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", model)
+ }
+
+ @Test
+ fun from() {
+ val videoEncoderInfo = VideoEncoderInfoWrapper.from(baseVideoEncoderInfo, sizeToCheck)
+
+ assertThat(videoEncoderInfo.supportedWidths).isEqualTo(expectedSupportedWidths)
+ assertThat(videoEncoderInfo.supportedHeights).isEqualTo(expectedSupportedHeights)
+ }
+}
diff --git a/camera/camera-viewfinder/build.gradle b/camera/camera-viewfinder/build.gradle
index feb5a537..e06571c 100644
--- a/camera/camera-viewfinder/build.gradle
+++ b/camera/camera-viewfinder/build.gradle
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import androidx.build.LibraryType
+import androidx.build.Publish
import androidx.build.RunApiTasks
plugins {
@@ -78,7 +78,8 @@
androidx {
name = "androidx.camera:camera-viewfinder"
- type = LibraryType.PUBLISHED_LIBRARY
+ publish = Publish.SNAPSHOT_AND_RELEASE
+ runApiTasks = new RunApiTasks.Yes("Need to track API surface before moving to publish")
mavenGroup = LibraryGroups.CAMERA
inceptionYear = "2022"
description = "Standalone Viewfinder for Camera2 and CameraX"
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
index cfec7b8..ab9edeb 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
@@ -68,6 +68,7 @@
Logger.i(TAG, "playState before stopped: ${audioTrack!!.playState}")
Logger.i(TAG, "playbackHeadPosition before stopped: ${audioTrack!!.playbackHeadPosition}")
audioTrack!!.stop()
+ audioTrack!!.reloadStaticData()
}
suspend fun initial(
diff --git a/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml b/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
index 5de58b5..1c8e0e8 100644
--- a/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
@@ -15,8 +15,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
- android:maxSdkVersion="32"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature android:name="android.hardware.camera" />
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index 773eb0e..b9f5851 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -41,7 +41,6 @@
import android.hardware.display.DisplayManager;
import android.media.MediaScannerConnection;
import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
@@ -155,25 +154,12 @@
*/
public class CameraXActivity extends AppCompatActivity {
private static final String TAG = "CameraXActivity";
- private static final String[] REQUIRED_PERMISSIONS;
-
- static {
-
- //WRITE_EXTERNAL_STORAGE permission is not needed for SDK 33 or later to store media
- if (Build.VERSION.SDK_INT >= 33) {
- REQUIRED_PERMISSIONS = new String[]{
- Manifest.permission.CAMERA,
- Manifest.permission.RECORD_AUDIO
- };
- } else {
- REQUIRED_PERMISSIONS = new String[]{
+ private static final String[] REQUIRED_PERMISSIONS =
+ new String[]{
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
- }
- }
-
// Possible values for this intent key: "backward" or "forward".
private static final String INTENT_EXTRA_CAMERA_DIRECTION = "camera_direction";
// Possible values for this intent key: "switch_test_case", "preview_test_case" or
@@ -646,7 +632,7 @@
} else if (outputOptions instanceof FileOutputOptions) {
videoFilePath = ((FileOutputOptions) outputOptions).getFile().getPath();
MediaScannerConnection.scanFile(this,
- new String[]{videoFilePath}, null,
+ new String[] { videoFilePath }, null,
(path, uri1) -> {
Log.i(TAG, "Scanned " + path + " -> uri= " + uri1);
updateVideoSavedSessionData(uri1);
@@ -1409,8 +1395,8 @@
for (String permission : REQUIRED_PERMISSIONS) {
if (!Objects.requireNonNull(result.get(permission))) {
Toast.makeText(getApplicationContext(),
- "Camera permission denied.",
- Toast.LENGTH_SHORT)
+ "Camera permission denied.",
+ Toast.LENGTH_SHORT)
.show();
finish();
return;
@@ -1595,10 +1581,10 @@
cameraInfo.getZoomState().removeObservers(this);
cameraInfo.getZoomState().observe(this,
state -> {
- String str = String.format("%.2fx", state.getZoomRatio());
- mZoomRatioLabel.setText(str);
- mZoomSeekBar.setProgress((int) (MAX_SEEKBAR_VALUE * state.getLinearZoom()));
- });
+ String str = String.format("%.2fx", state.getZoomRatio());
+ mZoomRatioLabel.setText(str);
+ mZoomSeekBar.setProgress((int) (MAX_SEEKBAR_VALUE * state.getLinearZoom()));
+ });
}
private boolean is2XZoomSupported() {
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/RequestPermissionScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/RequestPermissionScreen.java
index 95db90e..73840e7 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/RequestPermissionScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/RequestPermissionScreen.java
@@ -139,6 +139,7 @@
getCarContext(),
String.format("Approved: %s Rejected: %s", approved, rejected),
CarToast.LENGTH_LONG).show();
+ invalidate();
});
if (!getCarContext().getPackageManager().hasSystemFeature(FEATURE_AUTOMOTIVE)) {
CarToast.makeText(getCarContext(),
@@ -168,6 +169,7 @@
getCarContext().startActivity(
new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK));
+ invalidate();
if (!getCarContext().getPackageManager().hasSystemFeature(
FEATURE_AUTOMOTIVE)) {
CarToast.makeText(getCarContext(),
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionScreen.java
index 91f6583..e179d34 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionScreen.java
@@ -144,6 +144,7 @@
getCarContext(),
String.format("Approved: %s Rejected: %s", approved, rejected),
CarToast.LENGTH_LONG).show();
+ invalidate();
});
if (!getCarContext().getPackageManager().hasSystemFeature(FEATURE_AUTOMOTIVE)) {
CarToast.makeText(getCarContext(),
@@ -173,6 +174,7 @@
getCarContext().startActivity(
new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK));
+ invalidate();
if (!getCarContext().getPackageManager().hasSystemFeature(
FEATURE_AUTOMOTIVE)) {
CarToast.makeText(getCarContext(),
diff --git a/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt b/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt
index 514019a..89bff07 100644
--- a/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt
+++ b/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt
@@ -228,6 +228,7 @@
@androidx.car.app.annotations.RequiresCarApi(2) public class ConstraintManager implements androidx.car.app.managers.Manager {
method public int getContentLimit(int);
+ method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public boolean isAppDrivenRefreshEnabled();
field public static final int CONTENT_LIMIT_TYPE_GRID = 1; // 0x1
field public static final int CONTENT_LIMIT_TYPE_LIST = 0; // 0x0
field public static final int CONTENT_LIMIT_TYPE_PANE = 4; // 0x4
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 514019a..89bff07 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -228,6 +228,7 @@
@androidx.car.app.annotations.RequiresCarApi(2) public class ConstraintManager implements androidx.car.app.managers.Manager {
method public int getContentLimit(int);
+ method @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public boolean isAppDrivenRefreshEnabled();
field public static final int CONTENT_LIMIT_TYPE_GRID = 1; // 0x1
field public static final int CONTENT_LIMIT_TYPE_LIST = 0; // 0x0
field public static final int CONTENT_LIMIT_TYPE_PANE = 4; // 0x4
diff --git a/car/app/app/src/main/aidl/androidx/car/app/constraints/IConstraintHost.aidl b/car/app/app/src/main/aidl/androidx/car/app/constraints/IConstraintHost.aidl
index 3f1b7d4..c1fb5eb 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/constraints/IConstraintHost.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/constraints/IConstraintHost.aidl
@@ -24,4 +24,9 @@
* Queries the host for the limit for a content type.
*/
int getContentLimit(int contentType) = 1;
+
+ /**
+ * Queries the host for the ability to support App Driven Refresh.
+ */
+ boolean isAppDrivenRefreshEnabled() = 2;
}
diff --git a/car/app/app/src/main/java/androidx/car/app/constraints/ConstraintManager.java b/car/app/app/src/main/java/androidx/car/app/constraints/ConstraintManager.java
index 4e025f3..c8fcccd 100644
--- a/car/app/app/src/main/java/androidx/car/app/constraints/ConstraintManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/constraints/ConstraintManager.java
@@ -31,6 +31,7 @@
import androidx.car.app.HostDispatcher;
import androidx.car.app.HostException;
import androidx.car.app.R;
+import androidx.car.app.annotations.ExperimentalCarApi;
import androidx.car.app.annotations.RequiresCarApi;
import androidx.car.app.managers.Manager;
import androidx.car.app.utils.LogTags;
@@ -137,6 +138,36 @@
return mCarContext.getResources().getInteger(getResourceIdForContentType(contentLimitType));
}
+
+ /**
+ * Determines if the hosts supports App Driven Refresh.
+ * This enables applications to refresh lists content without being counted towards a step.
+ *
+ * If this function returns false the app should return a template that is of the same
+ * type and contains the same main content as the previous template, the new template will
+ * not be counted against the quota.
+ *
+ */
+ @RequiresCarApi(6)
+ @ExperimentalCarApi
+ public boolean isAppDrivenRefreshEnabled() {
+ Boolean result;
+ try {
+ // TODO(b/185805900): consider caching these values if performance is a concern.
+ result = mHostDispatcher.dispatchForResult(
+ CarContext.CONSTRAINT_SERVICE,
+ "isAppDrivenRefreshEnabled", IConstraintHost::isAppDrivenRefreshEnabled
+ );
+ return Boolean.TRUE.equals(result);
+ } catch (RemoteException e) {
+ // The host is dead, don't crash the app, just log.
+ Log.w(LogTags.TAG,
+ "Failed to retrieve if the host supports appDriven Refresh, using defaults", e);
+ }
+ // Returns default values as documented if host call failed.
+ return false;
+ }
+
@IntegerRes
private int getResourceIdForContentType(@ContentLimitType int contentType) {
switch (contentType) {
diff --git a/car/app/app/src/test/java/androidx/car/app/constraints/ConstraintManagerTest.java b/car/app/app/src/test/java/androidx/car/app/constraints/ConstraintManagerTest.java
index f152848..4a905e5 100644
--- a/car/app/app/src/test/java/androidx/car/app/constraints/ConstraintManagerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/constraints/ConstraintManagerTest.java
@@ -72,6 +72,11 @@
public int getContentLimit(int contentType) throws RemoteException {
return mMockConstraintHost.getContentLimit(contentType);
}
+
+ @Override
+ public boolean isAppDrivenRefreshEnabled() throws RemoteException {
+ return mMockConstraintHost.isAppDrivenRefreshEnabled();
+ }
};
when(mMockCarHost.getHost(any())).thenReturn(hostStub.asBinder());
mHostDispatcher.setCarHost(mMockCarHost);
@@ -104,4 +109,18 @@
assertThat(mConstraintManager.getContentLimit(CONTENT_LIMIT_TYPE_ROUTE_LIST)).isEqualTo(4);
assertThat(mConstraintManager.getContentLimit(CONTENT_LIMIT_TYPE_PANE)).isEqualTo(5);
}
+
+ @Test
+ public void host_throwsException_returnsDefault() throws RemoteException {
+ when(mMockConstraintHost.isAppDrivenRefreshEnabled()).thenThrow(new RemoteException());
+
+ assertThat(mConstraintManager.isAppDrivenRefreshEnabled()).isFalse();
+ }
+
+ @Test
+ public void host_returAppDrivenRefreshEnabled() throws RemoteException {
+ when(mMockConstraintHost.isAppDrivenRefreshEnabled()).thenReturn(true);
+
+ assertThat(mConstraintManager.isAppDrivenRefreshEnabled()).isTrue();
+ }
}
diff --git a/compose/compiler/compiler-hosted/integration-tests/build.gradle b/compose/compiler/compiler-hosted/integration-tests/build.gradle
index 82135c6..220690d 100644
--- a/compose/compiler/compiler-hosted/integration-tests/build.gradle
+++ b/compose/compiler/compiler-hosted/integration-tests/build.gradle
@@ -59,6 +59,7 @@
testImplementation("androidx.core:core-ktx:1.1.0")
testImplementation("androidx.activity:activity-ktx:1.2.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.3.4")
+ testImplementation("com.google.dagger:dagger:2.40.1")
}
afterEvaluate {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
index bf771fb..a597c21 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
@@ -303,6 +303,23 @@
)
@Test
+ fun testDaggerLazyIsStableIfItsTypeIs() = assertStability(
+ """
+ class Foo<T>(val x: dagger.Lazy<T>)
+ """,
+ "Parameter(T)"
+ )
+
+ @Test
+ fun testDaggerLazyOfCrossModuleTypeIsRuntimeStable() = assertStability(
+ """
+ class A
+ """,
+ "class Foo(val x: dagger.Lazy<A>)",
+ "Runtime(A)"
+ )
+
+ @Test
fun testVarPropDelegateWithCrossModuleStableDelegateTypeIsStable() = assertStability(
"""
@Stable
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index 0b1b031..d85520f 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -92,7 +92,7 @@
8300 to "1.3.0-beta01",
8400 to "1.3.0-beta02",
8500 to "1.3.0-beta03",
- 8600 to "1.3.0-beta04",
+ 8600 to "1.3.0-rc01",
)
/**
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/analysis/Stability.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/analysis/Stability.kt
index 91fef70..1118e3e 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/analysis/Stability.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/analysis/Stability.kt
@@ -264,6 +264,8 @@
"kotlinx.collections.immutable.ImmutableList" to 0b1,
"kotlinx.collections.immutable.ImmutableSet" to 0b1,
"kotlinx.collections.immutable.ImmutableMap" to 0b11,
+ // Dagger
+ "dagger.Lazy" to 0b1,
)
// TODO: buildList, buildMap, buildSet, etc.
diff --git a/compose/foundation/foundation/api/public_plus_experimental_1.3.0-beta04.txt b/compose/foundation/foundation/api/public_plus_experimental_1.3.0-beta04.txt
index eb3a5ef..1bcf19f 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_1.3.0-beta04.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_1.3.0-beta04.txt
@@ -358,12 +358,12 @@
}
@androidx.compose.foundation.ExperimentalFoundationApi public final class SnapFlingBehavior implements androidx.compose.foundation.gestures.FlingBehavior {
- ctor public SnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider snapLayoutInfoProvider, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> approachAnimationSpec, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.ui.unit.Density density, optional float shortSnapVelocityThreshold);
+ ctor public SnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider snapLayoutInfoProvider, androidx.compose.animation.core.AnimationSpec<java.lang.Float> lowVelocityAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> highVelocityAnimationSpec, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.ui.unit.Density density, optional float shortSnapVelocityThreshold);
method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float>);
}
public final class SnapFlingBehaviorKt {
- method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.gestures.snapping.SnapFlingBehavior rememberSnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider snapLayoutInfoProvider, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> approachAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec);
+ method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.gestures.snapping.SnapFlingBehavior rememberSnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider snapLayoutInfoProvider);
}
@androidx.compose.foundation.ExperimentalFoundationApi public interface SnapLayoutInfoProvider {
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index eb3a5ef..1bcf19f 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -358,12 +358,12 @@
}
@androidx.compose.foundation.ExperimentalFoundationApi public final class SnapFlingBehavior implements androidx.compose.foundation.gestures.FlingBehavior {
- ctor public SnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider snapLayoutInfoProvider, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> approachAnimationSpec, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.ui.unit.Density density, optional float shortSnapVelocityThreshold);
+ ctor public SnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider snapLayoutInfoProvider, androidx.compose.animation.core.AnimationSpec<java.lang.Float> lowVelocityAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> highVelocityAnimationSpec, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.ui.unit.Density density, optional float shortSnapVelocityThreshold);
method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float>);
}
public final class SnapFlingBehaviorKt {
- method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.gestures.snapping.SnapFlingBehavior rememberSnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider snapLayoutInfoProvider, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> approachAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec);
+ method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.foundation.gestures.snapping.SnapFlingBehavior rememberSnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider snapLayoutInfoProvider);
}
@androidx.compose.foundation.ExperimentalFoundationApi public interface SnapLayoutInfoProvider {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
index b3d3b3d..047223a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
@@ -19,17 +19,20 @@
import androidx.compose.animation.SplineBasedFloatDecayAnimationSpec
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.core.DecayAnimationSpec
import androidx.compose.animation.core.FloatDecayAnimationSpec
-import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.TwoWayConverter
import androidx.compose.animation.core.VectorizedAnimationSpec
import androidx.compose.animation.core.generateDecayAnimationSpec
import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
import androidx.compose.foundation.gestures.snapping.NoVelocity
+import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
import androidx.compose.foundation.gestures.snapping.findClosestOffset
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
@@ -54,6 +57,7 @@
val rule = createComposeRule()
private val inspectSpringAnimationSpec = InspectSpringAnimationSpec(spring())
+ private val inspectTweenAnimationSpec = InspectSpringAnimationSpec(tween(easing = LinearEasing))
private val density: Density
get() = rule.density
@@ -123,7 +127,7 @@
}
@Test
- fun approach_cannotDecay_justSnap() {
+ fun approach_cannotDecay_useLowVelocityApproachAndSnap() {
val testLayoutInfoProvider = TestLayoutInfoProvider(approachOffset = SnapStep * 5)
var inspectSplineAnimationSpec: InspectSplineAnimationSpec? = null
rule.setContent {
@@ -132,18 +136,22 @@
}
val testFlingBehavior = rememberSnapFlingBehavior(
snapLayoutInfoProvider = testLayoutInfoProvider,
- approachAnimationSpec = splineAnimationSpec.generateDecayAnimationSpec()
+ highVelocityApproachSpec = splineAnimationSpec.generateDecayAnimationSpec(),
+ lowVelocityApproachSpec = inspectTweenAnimationSpec,
+ snapAnimationSpec = inspectSpringAnimationSpec
)
VelocityEffect(testFlingBehavior, calculateVelocityThreshold() * 2)
}
rule.runOnIdle {
assertEquals(0, inspectSplineAnimationSpec?.animationWasExecutions)
+ assertEquals(1, inspectTweenAnimationSpec.animationWasExecutions)
+ assertEquals(1, inspectSpringAnimationSpec.animationWasExecutions)
}
}
@Test
- fun approach_canDecayWithoutHalfStep_decayAndSnap() {
+ fun approach_canDecay_decayAndSnap() {
val testLayoutInfoProvider = TestLayoutInfoProvider(maxOffset = 100f)
var inspectSplineAnimationSpec: InspectSplineAnimationSpec? = null
rule.setContent {
@@ -152,7 +160,8 @@
}
val testFlingBehavior = rememberSnapFlingBehavior(
snapLayoutInfoProvider = testLayoutInfoProvider,
- approachAnimationSpec = splineAnimationSpec.generateDecayAnimationSpec(),
+ highVelocityApproachSpec = splineAnimationSpec.generateDecayAnimationSpec(),
+ lowVelocityApproachSpec = inspectTweenAnimationSpec,
snapAnimationSpec = inspectSpringAnimationSpec
)
VelocityEffect(testFlingBehavior, calculateVelocityThreshold() * 5)
@@ -161,29 +170,7 @@
rule.runOnIdle {
assertEquals(1, inspectSplineAnimationSpec?.animationWasExecutions)
assertEquals(1, inspectSpringAnimationSpec.animationWasExecutions)
- }
- }
-
- @Test
- fun approach_canDecayWithHalfStep_doubleDecayAndSnap() {
- val testLayoutInfoProvider = TestLayoutInfoProvider(maxOffset = 300f, snapStep = 400f)
- var inspectSplineAnimationSpec: InspectSplineAnimationSpec? = null
- rule.setContent {
- val splineAnimationSpec = rememberInspectSplineAnimationSpec().also {
- inspectSplineAnimationSpec = it
- }
- val testFlingBehavior = rememberSnapFlingBehavior(
- snapLayoutInfoProvider = testLayoutInfoProvider,
- approachAnimationSpec = splineAnimationSpec.generateDecayAnimationSpec(),
- snapAnimationSpec = inspectSpringAnimationSpec
- )
-
- VelocityEffect(testFlingBehavior, calculateVelocityThreshold() * 3)
- }
-
- rule.runOnIdle {
- assertEquals(2, inspectSplineAnimationSpec?.animationWasExecutions)
- assertEquals(1, inspectSpringAnimationSpec.animationWasExecutions)
+ assertEquals(0, inspectTweenAnimationSpec.animationWasExecutions)
}
}
}
@@ -201,8 +188,8 @@
}
private class InspectSpringAnimationSpec(
- private val springSpec: SpringSpec<Float>
-) : AnimationSpec<Float> by springSpec {
+ private val animation: AnimationSpec<Float>
+) : AnimationSpec<Float> {
var animationWasExecutions = 0
@@ -210,7 +197,7 @@
converter: TwoWayConverter<Float, V>
): VectorizedAnimationSpec<V> {
animationWasExecutions++
- return springSpec.vectorize(converter)
+ return animation.vectorize(converter)
}
}
@@ -282,4 +269,27 @@
calculateApproachOffsetCount++
return approachOffset
}
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun rememberSnapFlingBehavior(
+ snapLayoutInfoProvider: SnapLayoutInfoProvider,
+ highVelocityApproachSpec: DecayAnimationSpec<Float>,
+ lowVelocityApproachSpec: AnimationSpec<Float>,
+ snapAnimationSpec: AnimationSpec<Float>
+): FlingBehavior {
+ val density = LocalDensity.current
+ return remember(
+ snapLayoutInfoProvider,
+ highVelocityApproachSpec
+ ) {
+ SnapFlingBehavior(
+ snapLayoutInfoProvider = snapLayoutInfoProvider,
+ lowVelocityAnimationSpec = lowVelocityApproachSpec,
+ highVelocityAnimationSpec = highVelocityApproachSpec,
+ snapAnimationSpec = snapAnimationSpec,
+ density = density
+ )
+ }
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
index 665cfe5..9d9672b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
@@ -58,7 +59,6 @@
import org.junit.Test
import org.junit.runners.Parameterized
import kotlin.math.roundToInt
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.runBlocking
import org.junit.runner.RunWith
@@ -273,22 +273,22 @@
@Test
fun itemSizeChangeAnimatesNextItems() {
- var height by mutableStateOf(itemSizeDp)
+ var size by mutableStateOf(itemSizeDp)
rule.setContent {
LazyGrid(1, minSize = itemSizeDp * 5, maxSize = itemSizeDp * 5) {
items(listOf(0, 1, 2, 3), key = { it }) {
- Item(it, height = if (it == 1) height else itemSizeDp)
+ Item(it, size = if (it == 1) size else itemSizeDp)
}
}
}
rule.runOnIdle {
- height = itemSizeDp * 2
+ size = itemSizeDp * 2
}
rule.mainClock.advanceTimeByFrame()
rule.onNodeWithTag("1")
- .assertMainAxisSizeIsEqualTo(height)
+ .assertMainAxisSizeIsEqualTo(size)
onAnimationFrame { fraction ->
if (!reverseLayout) {
@@ -745,7 +745,7 @@
if (it % 2 == 0) itemSizeDp else itemSize3Dp / 2
}
}
- Item(it, height = height)
+ Item(it, size = height)
}
}
}
@@ -823,7 +823,7 @@
if (it % 2 == 0) itemSizeDp else itemSize3Dp / 2
}
}
- Item(it, height = height)
+ Item(it, size = height)
}
}
}
@@ -1143,6 +1143,432 @@
}
}
+ @Test
+ fun noAnimationWhenScrollForwardBySmallOffset() {
+ rule.setContent {
+ LazyGrid(1, maxSize = itemSizeDp * 3) {
+ items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(itemSize / 2f)
+ }
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to AxisIntOffset(0, -itemSize / 2),
+ 1 to AxisIntOffset(0, itemSize / 2),
+ 2 to AxisIntOffset(0, itemSize * 3 / 2),
+ 3 to AxisIntOffset(0, itemSize * 5 / 2),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun noAnimationWhenScrollBackwardBySmallOffset() {
+ rule.setContent {
+ LazyGrid(1, maxSize = itemSizeDp * 3, startIndex = 2) {
+ items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-itemSize / 2f)
+ }
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 1 to AxisIntOffset(0, -itemSize / 2),
+ 2 to AxisIntOffset(0, itemSize / 2),
+ 3 to AxisIntOffset(0, itemSize * 3 / 2),
+ 4 to AxisIntOffset(0, itemSize * 5 / 2),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun noAnimationWhenScrollForwardByLargeOffset() {
+ rule.setContent {
+ LazyGrid(1, maxSize = itemSizeDp * 3) {
+ items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(itemSize * 2.5f)
+ }
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 2 to AxisIntOffset(0, -itemSize / 2),
+ 3 to AxisIntOffset(0, itemSize / 2),
+ 4 to AxisIntOffset(0, itemSize * 3 / 2),
+ 5 to AxisIntOffset(0, itemSize * 5 / 2),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun noAnimationWhenScrollBackwardByLargeOffset() {
+ rule.setContent {
+ LazyGrid(1, maxSize = itemSizeDp * 3, startIndex = 3) {
+ items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-itemSize * 2.5f)
+ }
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to AxisIntOffset(0, -itemSize / 2),
+ 1 to AxisIntOffset(0, itemSize / 2),
+ 2 to AxisIntOffset(0, itemSize * 3 / 2),
+ 3 to AxisIntOffset(0, itemSize * 5 / 2),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun noAnimationWhenScrollForwardByLargeOffset_differentSizes() {
+ rule.setContent {
+ LazyGrid(1, maxSize = itemSizeDp * 3) {
+ items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
+ Item(it, size = if (it % 2 == 0) itemSizeDp else itemSize2Dp)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(itemSize + itemSize2 + itemSize / 2f)
+ }
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 2 to AxisIntOffset(0, -itemSize / 2),
+ 3 to AxisIntOffset(0, itemSize / 2),
+ 4 to AxisIntOffset(0, itemSize2 + itemSize / 2),
+ 5 to AxisIntOffset(0, itemSize2 + itemSize * 3 / 2),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun noAnimationWhenScrollBackwardByLargeOffset_differentSizes() {
+ rule.setContent {
+ LazyGrid(1, maxSize = itemSizeDp * 3, startIndex = 3) {
+ items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
+ Item(it, size = if (it % 2 == 0) itemSizeDp else itemSize2Dp)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-(itemSize + itemSize2 + itemSize / 2f))
+ }
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to AxisIntOffset(0, -itemSize / 2),
+ 1 to AxisIntOffset(0, itemSize / 2),
+ 2 to AxisIntOffset(0, itemSize2 + itemSize / 2),
+ 3 to AxisIntOffset(0, itemSize2 + itemSize * 3 / 2),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun noAnimationWhenScrollForwardByLargeOffset_multipleCells() {
+ rule.setContent {
+ LazyGrid(3, maxSize = itemSizeDp * 2) {
+ items(List(20) { it }, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 0 to AxisIntOffset(0, 0),
+ 1 to AxisIntOffset(itemSize, 0),
+ 2 to AxisIntOffset(itemSize * 2, 0),
+ 3 to AxisIntOffset(0, itemSize),
+ 4 to AxisIntOffset(itemSize, itemSize),
+ 5 to AxisIntOffset(itemSize * 2, itemSize)
+ )
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(itemSize * 2.5f)
+ }
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 6 to AxisIntOffset(0, -itemSize / 2),
+ 7 to AxisIntOffset(itemSize, -itemSize / 2),
+ 8 to AxisIntOffset(itemSize * 2, -itemSize / 2),
+ 9 to AxisIntOffset(0, itemSize / 2),
+ 10 to AxisIntOffset(itemSize, itemSize / 2),
+ 11 to AxisIntOffset(itemSize * 2, itemSize / 2),
+ 12 to AxisIntOffset(0, itemSize * 3 / 2),
+ 13 to AxisIntOffset(itemSize, itemSize * 3 / 2),
+ 14 to AxisIntOffset(itemSize * 2, itemSize * 3 / 2),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun noAnimationWhenScrollBackwardByLargeOffset_multipleCells() {
+ rule.setContent {
+ LazyGrid(3, maxSize = itemSizeDp * 2, startIndex = 9) {
+ items(List(20) { it }, key = { it }) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 9 to AxisIntOffset(0, 0),
+ 10 to AxisIntOffset(itemSize, 0),
+ 11 to AxisIntOffset(itemSize * 2, 0),
+ 12 to AxisIntOffset(0, itemSize),
+ 13 to AxisIntOffset(itemSize, itemSize),
+ 14 to AxisIntOffset(itemSize * 2, itemSize)
+ )
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-itemSize * 2.5f)
+ }
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to AxisIntOffset(0, -itemSize / 2),
+ 1 to AxisIntOffset(itemSize, -itemSize / 2),
+ 2 to AxisIntOffset(itemSize * 2, -itemSize / 2),
+ 3 to AxisIntOffset(0, itemSize / 2),
+ 4 to AxisIntOffset(itemSize, itemSize / 2),
+ 5 to AxisIntOffset(itemSize * 2, itemSize / 2),
+ 6 to AxisIntOffset(0, itemSize * 3 / 2),
+ 7 to AxisIntOffset(itemSize, itemSize * 3 / 2),
+ 8 to AxisIntOffset(itemSize * 2, itemSize * 3 / 2),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun noAnimationWhenScrollForwardByLargeOffset_differentSpans() {
+ rule.setContent {
+ LazyGrid(3, maxSize = itemSizeDp * 2) {
+ items(
+ List(20) { it },
+ key = { it },
+ span = { GridItemSpan(if (it == 9) 3 else if (it == 10) 2 else 1) }
+ ) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 0 to AxisIntOffset(0, 0),
+ 1 to AxisIntOffset(itemSize, 0),
+ 2 to AxisIntOffset(itemSize * 2, 0),
+ 3 to AxisIntOffset(0, itemSize),
+ 4 to AxisIntOffset(itemSize, itemSize),
+ 5 to AxisIntOffset(itemSize * 2, itemSize)
+ )
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(itemSize * 2.5f)
+ }
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 6 to AxisIntOffset(0, -itemSize / 2),
+ 7 to AxisIntOffset(itemSize, -itemSize / 2),
+ 8 to AxisIntOffset(itemSize * 2, -itemSize / 2),
+ 9 to AxisIntOffset(0, itemSize / 2), // 3 spans
+ 10 to AxisIntOffset(0, itemSize * 3 / 2), // 2 spans
+ 11 to AxisIntOffset(itemSize * 2, itemSize * 3 / 2),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun noAnimationWhenScrollBackwardByLargeOffset_differentSpans() {
+ rule.setContent {
+ LazyGrid(3, maxSize = itemSizeDp * 2, startIndex = 6) {
+ items(
+ List(20) { it },
+ key = { it },
+ span = { GridItemSpan(if (it == 3) 3 else if (it == 4) 2 else 1) }
+ ) {
+ Item(it)
+ }
+ }
+ }
+
+ assertPositions(
+ 6 to AxisIntOffset(0, 0),
+ 7 to AxisIntOffset(itemSize, 0),
+ 8 to AxisIntOffset(itemSize * 2, 0),
+ 9 to AxisIntOffset(0, itemSize),
+ 10 to AxisIntOffset(itemSize, itemSize),
+ 11 to AxisIntOffset(itemSize * 2, itemSize)
+ )
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-itemSize * 2.5f)
+ }
+ }
+
+ onAnimationFrame { fraction ->
+ assertPositions(
+ 0 to AxisIntOffset(0, -itemSize / 2),
+ 1 to AxisIntOffset(itemSize, -itemSize / 2),
+ 2 to AxisIntOffset(itemSize * 2, -itemSize / 2),
+ 3 to AxisIntOffset(0, itemSize / 2), // 3 spans
+ 4 to AxisIntOffset(0, itemSize * 3 / 2), // 2 spans
+ 5 to AxisIntOffset(itemSize * 2, itemSize * 3 / 2),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun noAnimationWhenScrollForwardByLargeOffset_differentSpansAndDifferentSizes() {
+ rule.setContent {
+ LazyGrid(3, maxSize = itemSizeDp * 2) {
+ items(
+ List(20) { it },
+ key = { it },
+ span = { GridItemSpan(if (it == 9) 3 else if (it == 10) 2 else 1) }
+ ) {
+ Item(
+ it, size = when (it) {
+ in 6..8 -> itemSize2Dp
+ 9 -> itemSize3Dp
+ else -> itemSizeDp
+ }
+ )
+ }
+ }
+ }
+
+ assertPositions(
+ 0 to AxisIntOffset(0, 0),
+ 1 to AxisIntOffset(itemSize, 0),
+ 2 to AxisIntOffset(itemSize * 2, 0),
+ 3 to AxisIntOffset(0, itemSize),
+ 4 to AxisIntOffset(itemSize, itemSize),
+ 5 to AxisIntOffset(itemSize * 2, itemSize)
+ )
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(itemSize * 2.5f)
+ }
+ }
+
+ onAnimationFrame { fraction ->
+ val startOffset = -itemSize / 2
+ assertPositions(
+ 6 to AxisIntOffset(0, startOffset),
+ 7 to AxisIntOffset(itemSize, startOffset),
+ 8 to AxisIntOffset(itemSize * 2, startOffset),
+ 9 to AxisIntOffset(0, startOffset + itemSize2), // 3 spans
+ 10 to AxisIntOffset(0, startOffset + itemSize2 + itemSize3), // 2 spans
+ 11 to AxisIntOffset(itemSize * 2, startOffset + itemSize2 + itemSize3),
+ fraction = fraction
+ )
+ }
+ }
+
+ @Test
+ fun noAnimationWhenScrollBackwardByLargeOffset_differentSpansAndDifferentSizes() {
+ rule.setContent {
+ LazyGrid(3, maxSize = itemSizeDp * 2, startIndex = 6) {
+ items(
+ List(20) { it },
+ key = { it },
+ span = { GridItemSpan(if (it == 3) 3 else if (it == 4) 2 else 1) }
+ ) {
+ Item(
+ it, size = when (it) {
+ in 0..2 -> itemSize2Dp
+ 3 -> itemSize3Dp
+ else -> itemSizeDp
+ }
+ )
+ }
+ }
+ }
+
+ assertPositions(
+ 6 to AxisIntOffset(0, 0),
+ 7 to AxisIntOffset(itemSize, 0),
+ 8 to AxisIntOffset(itemSize * 2, 0),
+ 9 to AxisIntOffset(0, itemSize),
+ 10 to AxisIntOffset(itemSize, itemSize),
+ 11 to AxisIntOffset(itemSize * 2, itemSize)
+ )
+
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-itemSize - itemSize3 - itemSize2 / 2f)
+ }
+ }
+
+ onAnimationFrame { fraction ->
+ val startOffset = -itemSize2 / 2
+ assertPositions(
+ 0 to AxisIntOffset(0, startOffset),
+ 1 to AxisIntOffset(itemSize, startOffset),
+ 2 to AxisIntOffset(itemSize * 2, startOffset),
+ 3 to AxisIntOffset(0, startOffset + itemSize2), // 3 spans
+ 4 to AxisIntOffset(0, startOffset + itemSize2 + itemSize3), // 2 spans
+ 5 to AxisIntOffset(itemSize * 2, startOffset + itemSize2 + itemSize3),
+ fraction = fraction
+ )
+ }
+ }
+
private fun AxisIntOffset(crossAxis: Int, mainAxis: Int) =
if (isVertical) IntOffset(crossAxis, mainAxis) else IntOffset(mainAxis, crossAxis)
@@ -1249,7 +1675,7 @@
@Composable
private fun LazyGrid(
- columns: Int,
+ cells: Int,
arrangement: Arrangement.HorizontalOrVertical? = null,
minSize: Dp = 0.dp,
maxSize: Dp = containerSizeDp,
@@ -1261,10 +1687,10 @@
state = rememberLazyGridState(startIndex)
if (isVertical) {
LazyVerticalGrid(
- GridCells.Fixed(columns),
+ GridCells.Fixed(cells),
Modifier
.requiredHeightIn(minSize, maxSize)
- .requiredWidth(itemSizeDp * columns)
+ .requiredWidth(itemSizeDp * cells)
.testTag(ContainerTag),
state = state,
verticalArrangement = arrangement as? Arrangement.Vertical
@@ -1275,10 +1701,10 @@
)
} else {
LazyHorizontalGrid(
- GridCells.Fixed(columns),
+ GridCells.Fixed(cells),
Modifier
.requiredWidthIn(minSize, maxSize)
- .requiredHeight(itemSizeDp * columns)
+ .requiredHeight(itemSizeDp * cells)
.testTag(ContainerTag),
state = state,
horizontalArrangement = arrangement as? Arrangement.Horizontal
@@ -1293,16 +1719,16 @@
@Composable
private fun LazyGridItemScope.Item(
tag: Int,
- height: Dp = itemSizeDp,
+ size: Dp = itemSizeDp,
animSpec: FiniteAnimationSpec<IntOffset>? = AnimSpec
) {
Box(
Modifier
.then(
if (isVertical) {
- Modifier.requiredHeight(height)
+ Modifier.requiredHeight(size)
} else {
- Modifier.requiredWidth(height)
+ Modifier.requiredWidth(size)
}
)
.testTag(tag.toString())
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
index 3bdcfcb..0d9cfc7 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
@@ -22,12 +22,14 @@
import androidx.compose.animation.core.AnimationVector
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDecay
import androidx.compose.animation.core.animateTo
import androidx.compose.animation.core.calculateTargetValue
import androidx.compose.animation.core.copy
import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.FlingBehavior
@@ -52,20 +54,23 @@
*
* When long snapping, you can use [SnapLayoutInfoProvider.calculateApproachOffset] to
* indicate that snapping should happen after this offset. If the velocity generated by the
- * fling is high enough to get there, we'll use [approachAnimationSpec] to get to that offset and
- * then we'll snap to the next bound calculated by
+ * fling is high enough to get there, we'll use [highVelocityAnimationSpec] to get to that offset
+ * and then we'll snap to the next bound calculated by
* [SnapLayoutInfoProvider.calculateSnappingOffsetBounds] in the direction of the fling using
* [snapAnimationSpec].
*
- * If the velocity is not high enough, we'll perform the same algorithm, but use [snapAnimationSpec]
- * to do so.
+ * If the velocity is not high enough, we'll use [lowVelocityAnimationSpec] to approach and then
+ * use [snapAnimationSpec] to snap into place.
*
* Please refer to the sample to learn how to use this API.
* @sample androidx.compose.foundation.samples.SnapFlingBehaviorSimpleSample
* @sample androidx.compose.foundation.samples.SnapFlingBehaviorCustomizedSample
*
* @param snapLayoutInfoProvider The information about the layout being snapped.
- * @param approachAnimationSpec The animation spec used to approach the target offset.
+ * @param lowVelocityAnimationSpec The animation spec used to approach the target offset. When
+ * the fling velocity is not large enough. Large enough means large enough to naturally decay.
+ * @param highVelocityAnimationSpec The animation spec used to approach the target offset. When
+ * the fling velocity is large enough. Large enough means large enough to naturally decay.
* @param snapAnimationSpec The animation spec used to finally snap to the correct bound.
* @param density The screen [Density]
* @param shortSnapVelocityThreshold Use the given velocity to determine if it's a
@@ -75,7 +80,8 @@
@ExperimentalFoundationApi
class SnapFlingBehavior(
private val snapLayoutInfoProvider: SnapLayoutInfoProvider,
- private val approachAnimationSpec: DecayAnimationSpec<Float>,
+ private val lowVelocityAnimationSpec: AnimationSpec<Float>,
+ private val highVelocityAnimationSpec: DecayAnimationSpec<Float>,
private val snapAnimationSpec: AnimationSpec<Float>,
private val density: Density,
private val shortSnapVelocityThreshold: Dp = MinFlingVelocityDp
@@ -117,9 +123,13 @@
val animation =
if (isDecayApproachPossible(offset = initialTargetOffset, velocity = initialVelocity)) {
- DecayApproachAnimation(approachAnimationSpec, snapLayoutInfoProvider, density)
+ HighVelocityApproachAnimation(highVelocityAnimationSpec)
} else {
- SnapApproachAnimation(snapAnimationSpec, snapLayoutInfoProvider, density)
+ LowVelocityApproachAnimation(
+ lowVelocityAnimationSpec,
+ snapLayoutInfoProvider,
+ density
+ )
}
return approach(
@@ -132,21 +142,21 @@
}
/**
- * If we can approach the target and still have velocity left to run 1 step's worth of animation
+ * If we can approach the target and still have velocity left
*/
private fun isDecayApproachPossible(
offset: Float,
velocity: Float
): Boolean {
- val decayOffset = approachAnimationSpec.calculateTargetValue(NoDistance, velocity)
- val stepSize = with(snapLayoutInfoProvider) { density.snapStepSize() }
- return abs(decayOffset) > stepSize && abs(decayOffset) - stepSize > abs(offset)
+ val decayOffset = highVelocityAnimationSpec.calculateTargetValue(NoDistance, velocity)
+ return abs(decayOffset) > abs(offset)
}
override fun equals(other: Any?): Boolean {
return if (other is SnapFlingBehavior) {
other.snapAnimationSpec == this.snapAnimationSpec &&
- other.approachAnimationSpec == this.approachAnimationSpec &&
+ other.highVelocityAnimationSpec == this.highVelocityAnimationSpec &&
+ other.lowVelocityAnimationSpec == this.lowVelocityAnimationSpec &&
other.snapLayoutInfoProvider == this.snapLayoutInfoProvider &&
other.density == this.density &&
other.shortSnapVelocityThreshold == this.shortSnapVelocityThreshold
@@ -157,7 +167,8 @@
override fun hashCode(): Int = 0
.let { 31 * it + snapAnimationSpec.hashCode() }
- .let { 31 * it + approachAnimationSpec.hashCode() }
+ .let { 31 * it + highVelocityAnimationSpec.hashCode() }
+ .let { 31 * it + lowVelocityAnimationSpec.hashCode() }
.let { 31 * it + snapLayoutInfoProvider.hashCode() }
.let { 31 * it + density.hashCode() }
.let { 31 * it + shortSnapVelocityThreshold.hashCode() }
@@ -166,27 +177,25 @@
/**
* Creates and remember a [FlingBehavior] that performs snapping.
* @param snapLayoutInfoProvider The information about the layout that will do snapping
- * @param approachAnimationSpec The animation spec to use for approaching the target item
- * @param snapAnimationSpec The animation spec to use for snapping to the final position
*/
@ExperimentalFoundationApi
@Composable
fun rememberSnapFlingBehavior(
- snapLayoutInfoProvider: SnapLayoutInfoProvider,
- approachAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
- snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow)
+ snapLayoutInfoProvider: SnapLayoutInfoProvider
): SnapFlingBehavior {
val density = LocalDensity.current
+ val highVelocityApproachSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
return remember(
snapLayoutInfoProvider,
- approachAnimationSpec,
- snapAnimationSpec
+ highVelocityApproachSpec,
+ density
) {
SnapFlingBehavior(
- snapLayoutInfoProvider,
- approachAnimationSpec,
- snapAnimationSpec,
- density
+ snapLayoutInfoProvider = snapLayoutInfoProvider,
+ lowVelocityAnimationSpec = tween(easing = LinearEasing),
+ highVelocityAnimationSpec = highVelocityApproachSpec,
+ snapAnimationSpec = spring(stiffness = Spring.StiffnessMediumLow),
+ density = density
)
}
}
@@ -212,20 +221,12 @@
density: Density
): ApproachStepResult {
- var currentAnimationState =
+ val currentAnimationState =
animation.approachAnimation(this, initialTargetOffset, initialVelocity)
- var remainingOffset =
+ val remainingOffset =
findClosestOffset(currentAnimationState.velocity, snapLayoutInfoProvider, density)
- val currentHalfStep = snapLayoutInfoProvider.halfStep(density)
- if (abs(remainingOffset) > currentHalfStep) {
- currentAnimationState =
- animation.halfStepAnimation(this, remainingOffset, currentAnimationState)
- remainingOffset =
- (abs(remainingOffset) - currentHalfStep) * sign(currentAnimationState.velocity)
- }
-
// will snap the remainder
return ApproachStepResult(remainingOffset, currentAnimationState)
}
@@ -352,11 +353,6 @@
return if (target > 0) coerceAtMost(target) else coerceAtLeast(target)
}
-@OptIn(ExperimentalFoundationApi::class)
-private fun SnapLayoutInfoProvider.halfStep(density: Density): Float {
- return density.snapStepSize() / 2f
-}
-
/**
* The animations used to approach offset and approach half a step offset.
*/
@@ -366,17 +362,11 @@
offset: T,
velocity: T
): AnimationState<T, V>
-
- suspend fun halfStepAnimation(
- scope: ScrollScope,
- offset: T,
- previousAnimationState: AnimationState<T, V>
- ): AnimationState<T, V>
}
@OptIn(ExperimentalFoundationApi::class)
-private class SnapApproachAnimation(
- private val snapAnimationSpec: AnimationSpec<Float>,
+private class LowVelocityApproachAnimation(
+ private val lowVelocityAnimationSpec: AnimationSpec<Float>,
private val layoutInfoProvider: SnapLayoutInfoProvider,
private val density: Density
) : ApproachAnimation<Float, AnimationVector1D> {
@@ -393,34 +383,14 @@
targetOffset = targetOffset,
cancelOffset = offset,
animationState = animationState,
- snapAnimationSpec = snapAnimationSpec,
- )
- }
- }
-
- override suspend fun halfStepAnimation(
- scope: ScrollScope,
- offset: Float,
- previousAnimationState: AnimationState<Float, AnimationVector1D>
- ): AnimationState<Float, AnimationVector1D> {
- val animationState = previousAnimationState.copy(NoDistance)
-
- return with(scope) {
- animateSnap(
- targetOffset = offset,
- cancelOffset = layoutInfoProvider.halfStep(density) * sign(animationState.velocity),
- animationState = animationState,
- snapAnimationSpec = snapAnimationSpec
+ snapAnimationSpec = lowVelocityAnimationSpec,
)
}
}
}
-@OptIn(ExperimentalFoundationApi::class)
-private class DecayApproachAnimation(
- private val decayAnimationSpec: DecayAnimationSpec<Float>,
- private val snapLayoutInfoProvider: SnapLayoutInfoProvider,
- private val density: Density
+private class HighVelocityApproachAnimation(
+ private val decayAnimationSpec: DecayAnimationSpec<Float>
) : ApproachAnimation<Float, AnimationVector1D> {
override suspend fun approachAnimation(
scope: ScrollScope,
@@ -432,21 +402,6 @@
animateDecay(offset, animationState, decayAnimationSpec)
}
}
-
- override suspend fun halfStepAnimation(
- scope: ScrollScope,
- offset: Float,
- previousAnimationState: AnimationState<Float, AnimationVector1D>
- ): AnimationState<Float, AnimationVector1D> {
- val animationState = previousAnimationState.copy(value = NoDistance)
- return with(scope) {
- animateDecay(
- snapLayoutInfoProvider.halfStep(density) * sign(animationState.velocity),
- animationState,
- decayAnimationSpec
- )
- }
- }
}
internal val MinFlingVelocityDp = 400.dp
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index 5e37cbc..79c1683 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -336,7 +336,6 @@
measuredLineProvider = measuredLineProvider,
measuredItemProvider = measuredItemProvider,
mainAxisAvailableSize = mainAxisAvailableSize,
- slotsPerLine = resolvedSlotSizesSums.size,
beforeContentPadding = beforeContentPadding,
afterContentPadding = afterContentPadding,
spaceBetweenLines = spaceBetweenLines,
@@ -350,6 +349,7 @@
reverseLayout = reverseLayout,
density = this,
placementAnimator = placementAnimator,
+ spanLayoutProvider = itemProvider.spanLayoutProvider,
layout = { width, height, placement ->
layout(
containerConstraints.constrainWidth(width + totalHorizontalPadding),
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
index 6f5957a..78679fe 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
@@ -22,7 +22,6 @@
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.spring
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -31,7 +30,6 @@
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
-import kotlin.math.absoluteValue
import kotlin.math.max
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@@ -44,13 +42,10 @@
* This class is responsible for detecting when item position changed, figuring our start/end
* offsets and starting the animations.
*/
-@OptIn(ExperimentalFoundationApi::class)
internal class LazyGridItemPlacementAnimator(
private val scope: CoroutineScope,
private val isVertical: Boolean
) {
- private var slotsPerLine = 0
-
// state containing an animation and all relevant info for each item.
private val keyToItemInfoMap = mutableMapOf<Any, ItemInfo>()
@@ -75,10 +70,10 @@
consumedScroll: Int,
layoutWidth: Int,
layoutHeight: Int,
- slotsPerLine: Int,
reverseLayout: Boolean,
positionedItems: MutableList<LazyGridPositionedItem>,
measuredItemProvider: LazyMeasuredItemProvider,
+ spanLayoutProvider: LazyGridSpanLayoutProvider
) {
if (!positionedItems.fastAny { it.hasAnimations }) {
// no animations specified - no work needed
@@ -86,8 +81,6 @@
return
}
- this.slotsPerLine = slotsPerLine
-
val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
// the consumed scroll is considered as a delta we don't need to animate
@@ -171,7 +164,9 @@
scrolledBy = notAnimatableDelta,
fallback = fallback,
reverseLayout = reverseLayout,
- mainAxisLayoutSize = mainAxisLayoutSize
+ mainAxisLayoutSize = mainAxisLayoutSize,
+ visibleItems = positionedItems,
+ spanLayoutProvider = spanLayoutProvider
)
}
val targetPlaceableOffset = if (isVertical) {
@@ -273,7 +268,9 @@
scrolledBy = notAnimatableDelta,
fallback = mainAxisLayoutSize,
reverseLayout = reverseLayout,
- mainAxisLayoutSize = mainAxisLayoutSize
+ mainAxisLayoutSize = mainAxisLayoutSize,
+ visibleItems = positionedItems,
+ spanLayoutProvider = spanLayoutProvider
)
val targetOffset = if (reverseLayout) {
mainAxisLayoutSize - absoluteTargetOffset - measuredItem.mainAxisSize
@@ -354,27 +351,54 @@
scrolledBy: IntOffset,
reverseLayout: Boolean,
mainAxisLayoutSize: Int,
- fallback: Int
+ fallback: Int,
+ visibleItems: List<LazyGridPositionedItem>,
+ spanLayoutProvider: LazyGridSpanLayoutProvider
): Int {
- require(slotsPerLine != 0)
- val beforeViewportStart =
- if (!reverseLayout) viewportEndItemIndex < index else viewportEndItemIndex > index
val afterViewportEnd =
+ if (!reverseLayout) viewportEndItemIndex < index else viewportEndItemIndex > index
+ val beforeViewportStart =
if (!reverseLayout) viewportStartItemIndex > index else viewportStartItemIndex < index
return when {
- beforeViewportStart -> {
- val diff = ((index - viewportEndItemIndex).absoluteValue + slotsPerLine - 1) /
- slotsPerLine
- mainAxisLayoutSize + viewportEndItemNotVisiblePartSize +
- averageLineMainAxisSize * (diff - 1) +
- scrolledBy.mainAxis
- }
afterViewportEnd -> {
- val diff = ((viewportStartItemIndex - index).absoluteValue + slotsPerLine - 1) /
- slotsPerLine
- viewportStartItemNotVisiblePartSize - mainAxisSizeWithSpacings -
- averageLineMainAxisSize * (diff - 1) +
- scrolledBy.mainAxis
+ val fromIndex = if (!reverseLayout) {
+ // viewportEndItemIndex is the last item in the line already
+ viewportEndItemIndex + 1
+ } else {
+ spanLayoutProvider.firstIndexInNextLineAfter(index)
+ }
+ val toIndex = spanLayoutProvider.lastIndexInPreviousLineBefore(
+ if (!reverseLayout) index else viewportEndItemIndex
+ )
+ mainAxisLayoutSize + viewportEndItemNotVisiblePartSize + scrolledBy.mainAxis +
+ // add sizes of the lines between the last visible one and this one.
+ spanLayoutProvider.getLinesMainAxisSizesSum(
+ fromIndex = fromIndex,
+ toIndex = toIndex,
+ averageLineMainAxisSize = averageLineMainAxisSize,
+ visibleItems = visibleItems
+ )
+ }
+ beforeViewportStart -> {
+ val fromIndex = spanLayoutProvider.firstIndexInNextLineAfter(
+ if (!reverseLayout) index else viewportStartItemIndex
+ )
+ val toIndex = if (!reverseLayout) {
+ // viewportStartItemIndex is the first item in the line already
+ viewportStartItemIndex - 1
+ } else {
+ spanLayoutProvider.lastIndexInPreviousLineBefore(index)
+ }
+ viewportStartItemNotVisiblePartSize + scrolledBy.mainAxis +
+ // minus the size of this item as we are looking for the start offset of it.
+ -mainAxisSizeWithSpacings +
+ // minus sizes of the lines between the first visible one and this one.
+ -spanLayoutProvider.getLinesMainAxisSizesSum(
+ fromIndex = fromIndex,
+ toIndex = toIndex,
+ averageLineMainAxisSize = averageLineMainAxisSize,
+ visibleItems = visibleItems
+ )
}
else -> {
fallback
@@ -460,3 +484,51 @@
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold
)
+
+private fun LazyGridSpanLayoutProvider.lastIndexInPreviousLineBefore(index: Int): Int {
+ val lineIndex = getLineIndexOfItem(index)
+ val lineConfiguration = getLineConfiguration(lineIndex.value)
+ return lineConfiguration.firstItemIndex - 1
+}
+
+private fun LazyGridSpanLayoutProvider.firstIndexInNextLineAfter(index: Int): Int {
+ val lineIndex = getLineIndexOfItem(index)
+ val lineConfiguration = getLineConfiguration(lineIndex.value)
+ return lineConfiguration.firstItemIndex + lineConfiguration.spans.size
+}
+
+private fun LazyGridSpanLayoutProvider.getLinesMainAxisSizesSum(
+ fromIndex: Int,
+ toIndex: Int,
+ averageLineMainAxisSize: Int,
+ visibleItems: List<LazyGridPositionedItem>
+): Int {
+ var index = fromIndex
+ var sizes = 0
+ while (index <= toIndex) {
+ val lastItemInTheLine = firstIndexInNextLineAfter(index) - 1
+ if (lastItemInTheLine <= toIndex) {
+ sizes += visibleItems.getLineSize(lastItemInTheLine, averageLineMainAxisSize)
+ }
+ index = lastItemInTheLine + 1
+ }
+ return sizes
+}
+
+private fun List<LazyGridPositionedItem>.getLineSize(itemIndex: Int, fallback: Int): Int {
+ if (isEmpty() || itemIndex < first().index || itemIndex > last().index) return fallback
+ if ((itemIndex - first().index) < (last().index - itemIndex)) {
+ for (index in indices) {
+ val item = get(index)
+ if (item.index == itemIndex) return item.lineMainAxisSizeWithSpacings
+ if (item.index > itemIndex) break
+ }
+ } else {
+ for (index in lastIndex downTo 0) {
+ val item = get(index)
+ if (item.index == itemIndex) return item.lineMainAxisSizeWithSpacings
+ if (item.index < itemIndex) break
+ }
+ }
+ return fallback
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index c8757b7..b1ebdd0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -37,13 +37,11 @@
* Measures and calculates the positions for the currently visible items. The result is produced
* as a [LazyGridMeasureResult] which contains all the calculations.
*/
-@OptIn(ExperimentalFoundationApi::class)
internal fun measureLazyGrid(
itemsCount: Int,
measuredLineProvider: LazyMeasuredLineProvider,
measuredItemProvider: LazyMeasuredItemProvider,
mainAxisAvailableSize: Int,
- slotsPerLine: Int,
beforeContentPadding: Int,
afterContentPadding: Int,
spaceBetweenLines: Int,
@@ -57,6 +55,7 @@
reverseLayout: Boolean,
density: Density,
placementAnimator: LazyGridItemPlacementAnimator,
+ spanLayoutProvider: LazyGridSpanLayoutProvider,
layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
): LazyGridMeasureResult {
require(beforeContentPadding >= 0)
@@ -243,10 +242,10 @@
consumedScroll = consumedScroll.toInt(),
layoutWidth = layoutWidth,
layoutHeight = layoutHeight,
- slotsPerLine = slotsPerLine,
reverseLayout = reverseLayout,
positionedItems = positionedItems,
- measuredItemProvider = measuredItemProvider
+ measuredItemProvider = measuredItemProvider,
+ spanLayoutProvider = spanLayoutProvider
)
return LazyGridMeasureResult(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt
index 173c217..5445569 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt
@@ -28,6 +28,7 @@
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.width
import androidx.compose.material3.tokens.SliderTokens
import androidx.compose.runtime.Composable
@@ -457,6 +458,28 @@
}
@Test
+ fun slider_sizes_within_row() {
+ val rowWidth = 100.dp
+ val spacerWidth = 10.dp
+
+ rule.setMaterialContent(lightColorScheme()) {
+ Row(modifier = Modifier.requiredWidth(rowWidth)) {
+ Spacer(Modifier.width(spacerWidth))
+ Slider(
+ modifier = Modifier.testTag(tag).weight(1f),
+ value = 0f,
+ onValueChange = {}
+ )
+ Spacer(Modifier.width(spacerWidth))
+ }
+ }
+
+ rule.onNodeWithTag(tag)
+ .assertWidthIsEqualTo(rowWidth - spacerWidth.times(2))
+ .assertHeightIsEqualTo(SliderTokens.HandleHeight)
+ }
+
+ @Test
fun slider_min_size() {
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.requiredSize(0.dp)) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
index 61299ed..c9f2656 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt
@@ -44,6 +44,7 @@
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSizeIn
@@ -738,7 +739,10 @@
val activeTrackColor = colors.trackColor(enabled, active = true)
val inactiveTickColor = colors.tickColor(enabled, active = false)
val activeTickColor = colors.tickColor(enabled, active = true)
- Canvas(modifier.fillMaxWidth()) {
+ Canvas(modifier
+ .fillMaxWidth()
+ .height(TrackHeight)
+ ) {
val isRtl = layoutDirection == LayoutDirection.Rtl
val sliderLeft = Offset(0f, center.y)
val sliderRight = Offset(size.width, center.y)
@@ -879,6 +883,10 @@
},
modifier = modifier
.minimumTouchTargetSize()
+ .requiredSizeIn(
+ minWidth = SliderTokens.HandleWidth,
+ minHeight = SliderTokens.HandleHeight
+ )
.sliderSemantics(
value,
enabled,
@@ -892,29 +900,31 @@
.then(drag)
) { measurables, constraints ->
- val thumbPlaceable = measurables.first { it.layoutId == SliderComponents.THUMB }.measure(
- constraints
+ val thumbPlaceable = measurables.first {
+ it.layoutId == SliderComponents.THUMB
+ }.measure(constraints)
+
+ val maxTrackWidth = constraints.maxWidth - thumbPlaceable.width
+ val trackPlaceable = measurables.first {
+ it.layoutId == SliderComponents.TRACK
+ }.measure(
+ constraints.copy(
+ minWidth = 0,
+ maxWidth = maxTrackWidth,
+ minHeight = 0
+ )
)
- val maxTrackWidth = max(constraints.minWidth, constraints.maxWidth - thumbPlaceable.width)
- val trackPlaceable = measurables.first { it.layoutId == SliderComponents.TRACK }.measure(
- constraints.copy(maxWidth = maxTrackWidth)
- )
-
- val sliderWidth = max(SliderTokens.HandleWidth.roundToPx(), constraints.maxWidth)
- val sliderHeight = maxOf(
- SliderTokens.HandleHeight.roundToPx(),
- trackPlaceable.height,
- thumbPlaceable.height
- )
+ val sliderWidth = thumbPlaceable.width + trackPlaceable.width
+ val sliderHeight = max(trackPlaceable.height, thumbPlaceable.height)
thumbWidth.value = thumbPlaceable.width.toFloat()
totalWidth.value = sliderWidth
val trackOffsetX = thumbPlaceable.width / 2
val thumbOffsetX = ((trackPlaceable.width) * positionFraction).roundToInt()
- val trackOffsetY = sliderHeight / 2 - trackPlaceable.height / 2
- val thumbOffsetY = sliderHeight / 2 - thumbPlaceable.height / 2
+ val trackOffsetY = (sliderHeight - trackPlaceable.height) / 2
+ val thumbOffsetY = (sliderHeight - thumbPlaceable.height) / 2
layout(
sliderWidth,
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
index bd58e67..f7af201 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
@@ -1028,7 +1028,7 @@
)
for (i in 0 until paragraph.lineCount) {
- assertThat(paragraph.isEllipsisApplied(i)).isFalse()
+ assertThat(paragraph.isLineEllipsized(i)).isFalse()
}
}
}
@@ -1051,7 +1051,7 @@
width = paragraphWidth
)
- assertThat(paragraph.isEllipsisApplied(0)).isTrue()
+ assertThat(paragraph.isLineEllipsized(0)).isTrue()
}
}
@@ -1074,7 +1074,7 @@
)
for (i in 0 until paragraph.lineCount) {
- assertThat(paragraph.isEllipsisApplied(i)).isFalse()
+ assertThat(paragraph.isLineEllipsized(i)).isFalse()
}
}
}
@@ -1096,7 +1096,7 @@
)
for (i in 0 until paragraph.lineCount) {
- assertThat(paragraph.isEllipsisApplied(i)).isFalse()
+ assertThat(paragraph.isLineEllipsized(i)).isFalse()
}
}
}
@@ -1118,7 +1118,7 @@
)
assertThat(paragraph.lineCount).isEqualTo(2)
- assertThat(paragraph.isEllipsisApplied(paragraph.lineCount - 1)).isTrue()
+ assertThat(paragraph.isLineEllipsized(paragraph.lineCount - 1)).isTrue()
}
}
@@ -1139,7 +1139,7 @@
)
for (i in 0 until paragraph.lineCount) {
- assertThat(paragraph.isEllipsisApplied(i)).isFalse()
+ assertThat(paragraph.isLineEllipsized(i)).isFalse()
}
}
}
@@ -1162,7 +1162,7 @@
)
assertThat(paragraph.lineCount).isEqualTo(2)
- assertThat(paragraph.isEllipsisApplied(paragraph.lineCount - 1)).isTrue()
+ assertThat(paragraph.isLineEllipsized(paragraph.lineCount - 1)).isTrue()
}
}
@@ -1184,7 +1184,7 @@
)
assertThat(paragraph.lineCount).isEqualTo(2)
- assertThat(paragraph.isEllipsisApplied(paragraph.lineCount - 1)).isTrue()
+ assertThat(paragraph.isLineEllipsized(paragraph.lineCount - 1)).isTrue()
}
}
@@ -1206,7 +1206,7 @@
assertThat(paragraph.didExceedMaxLines).isTrue()
assertThat(paragraph.lineCount).isEqualTo(1)
- assertThat(paragraph.isEllipsisApplied(paragraph.lineCount - 1)).isTrue()
+ assertThat(paragraph.isLineEllipsized(paragraph.lineCount - 1)).isTrue()
}
}
@@ -1230,7 +1230,7 @@
)
assertThat(paragraph.lineCount).isEqualTo(1)
- assertThat(paragraph.isEllipsisApplied(paragraph.lineCount - 1)).isTrue()
+ assertThat(paragraph.isLineEllipsized(paragraph.lineCount - 1)).isTrue()
}
}
@Test
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
index 47bd978..298347f 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
@@ -434,13 +434,6 @@
ResolvedTextDirection.Ltr
}
- /**
- * @return true if the given line is ellipsized, else false.
- */
- @VisibleForTesting
- internal fun isEllipsisApplied(lineIndex: Int): Boolean =
- layout.isEllipsisApplied(lineIndex)
-
private fun TextLayout.getShaderBrushSpans(): Array<ShaderBrushSpan> {
if (text !is Spanned) return emptyArray()
val brushSpans = (text as Spanned).getSpans(
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
index e4a7eb7..3780431 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
@@ -733,10 +733,10 @@
}
/**
- * Returns true if ellipsis happens on the given line, otherwise returns false
+ * Returns true if the given line is ellipsized, otherwise returns false.
*
* @param lineIndex a 0 based line index
- * @return true if ellipsis happens on the given line, otherwise false
+ * @return true if the given line is ellipsized, otherwise false
*/
fun isLineEllipsized(lineIndex: Int): Boolean {
requireLineIndexInRange(lineIndex)
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
index 7003493..2da5fdb 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
@@ -144,10 +144,10 @@
fun getLineEnd(lineIndex: Int, visibleEnd: Boolean = false): Int
/**
- * Returns true if ellipsis happens on the given line, otherwise returns false
+ * Returns true if the given line is ellipsized, otherwise returns false.
*
* @param lineIndex a 0 based line index
- * @return true if ellipsis happens on the given line, otherwise false
+ * @return true if the given line is ellipsized, otherwise false
*/
fun isLineEllipsized(lineIndex: Int): Boolean
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextLayoutResult.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextLayoutResult.kt
index 5f32551..cc822d5 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextLayoutResult.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextLayoutResult.kt
@@ -366,7 +366,16 @@
val lineCount: Int get() = multiParagraph.lineCount
/**
- * Returns the end offset of the given line, inclusive.
+ * Returns the start offset of the given line, inclusive.
+ *
+ * The start offset represents a position in text before the first character in the given line.
+ * For example, `getLineStart(1)` will return 4 for the text below
+ * <pre>
+ * ┌────┐
+ * │abcd│
+ * │efg │
+ * └────┘
+ * </pre>
*
* @param lineIndex the line number
* @return the start offset of the line
@@ -374,10 +383,19 @@
fun getLineStart(lineIndex: Int): Int = multiParagraph.getLineStart(lineIndex)
/**
- * Returns the end offset of the given line
+ * Returns the end offset of the given line.
+ *
+ * The end offset represents a position in text after the last character in the given line.
+ * For example, `getLineEnd(0)` will return 4 for the text below
+ * <pre>
+ * ┌────┐
+ * │abcd│
+ * │efg │
+ * └────┘
+ * </pre>
*
* Characters being ellipsized are treated as invisible characters. So that if visibleEnd is
- * false, it will return line end including the ellipsized characters and vice verse.
+ * false, it will return line end including the ellipsized characters and vice versa.
*
* @param lineIndex the line number
* @param visibleEnd if true, the returned line end will not count trailing whitespaces or
@@ -389,10 +407,10 @@
multiParagraph.getLineEnd(lineIndex, visibleEnd)
/**
- * Returns true if ellipsis happens on the given line, otherwise returns false
+ * Returns true if the given line is ellipsized, otherwise returns false.
*
* @param lineIndex a 0 based line index
- * @return true if ellipsis happens on the given line, otherwise false
+ * @return true if the given line is ellipsized, otherwise false
*/
fun isLineEllipsized(lineIndex: Int): Boolean = multiParagraph.isLineEllipsized(lineIndex)
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index c26b7d4..cfd04cb 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -46,7 +46,7 @@
api(project(":compose:ui:ui-graphics"))
api(project(":compose:ui:ui-text"))
api(project(":compose:ui:ui-unit"))
- api("androidx.annotation:annotation:1.5.0-beta01")
+ api("androidx.annotation:annotation:1.5.0")
// This has stub APIs for access to legacy Android APIs, so we don't want
// any dependency on this module.
@@ -148,7 +148,7 @@
// This has stub APIs for access to legacy Android APIs, so we don't want
// any dependency on this module.
compileOnly(project(":compose:ui:ui-android-stubs"))
- api("androidx.annotation:annotation:1.5.0-beta01")
+ api("androidx.annotation:annotation:1.5.0")
implementation("androidx.autofill:autofill:1.0.0")
implementation(libs.kotlinCoroutinesAndroid)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt
new file mode 100644
index 0000000..c1577e0
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.node
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalComposeUiApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DelegatableNodeTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun visitChildren_noChildren() {
+ // Arrange.
+ val testNode = object : Modifier.Node() {}
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(modifier = modifierElementOf { testNode })
+ }
+
+ // Act.
+ rule.runOnIdle {
+ testNode.visitChildren(Nodes.Any) {
+ visitedChildren.add(it)
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).isEmpty()
+ }
+
+ @Test
+ fun visitChildWithinCurrentLayoutNode_immediateChild() {
+ // Arrange.
+ val (node1, node2, node3) = List(3) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(
+ modifier = modifierElementOf { node1 }
+ .then(modifierElementOf { node2 })
+ .then(modifierElementOf { node3 })
+ )
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node1.visitChildren(Nodes.Any) {
+ visitedChildren.add(it)
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(node2)
+ }
+
+ @Test
+ fun visitChildWithinCurrentLayoutNode_nonContiguousChild() {
+ // Arrange.
+ val (node1, node2) = List(2) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(
+ modifier = modifierElementOf { node1 }
+ .otherModifier()
+ .then(modifierElementOf { node2 })
+ )
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node1.visitChildren(Nodes.Any) {
+ visitedChildren.add(it)
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(node2)
+ }
+
+ @Test
+ fun visitChildrenInOtherLayoutNodes() {
+ // Arrange.
+ val (node1, node2, node3, node4, node5) = List(5) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(modifier = modifierElementOf { node1 }) {
+ Box(modifier = modifierElementOf { node2 }) {
+ Box(modifier = modifierElementOf { node3 })
+ }
+ Box(modifier = modifierElementOf { node4 }) {
+ Box(modifier = modifierElementOf { node5 })
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node1.visitChildren(Nodes.Any) {
+ visitedChildren.add(it)
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(node2, node4).inOrder()
+ }
+
+ @Test
+ fun visitAncestorWithinCurrentLayoutNode_immediateParent() {
+ // Arrange.
+ val (node1, node2) = List(2) { object : Modifier.Node() {} }
+ val visitedAncestors = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(
+ modifier = modifierElementOf { node1 }
+ .then(modifierElementOf { node2 })
+ )
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node2.visitAncestors(Nodes.Any) {
+ visitedAncestors.add(it)
+ }
+ }
+
+ // Assert.
+ assertThat(visitedAncestors.first()).isEqualTo(node1)
+ }
+
+ @Test
+ fun visitAncestorWithinCurrentLayoutNode_nonContiguousAncestor() {
+ // Arrange.
+ val (node1, node2) = List(2) { object : Modifier.Node() {} }
+ val visitedAncestors = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(
+ modifier = modifierElementOf { node1 }
+ .border(10.dp, Color.Red)
+ .then(modifierElementOf { node2 })
+ )
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node2.visitAncestors(Nodes.Any) {
+ visitedAncestors.add(it)
+ }
+ }
+
+ // Assert.
+ assertThat(visitedAncestors).contains(node1)
+ }
+
+ @Test
+ fun visitAncestorsInOtherLayoutNodes() {
+ // Arrange.
+ val (node1, node2, node3, node4, node5) = List(5) { object : Modifier.Node() {} }
+ val visitedAncestors = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(modifier = modifierElementOf { node1 }) {
+ Box(
+ modifier = Modifier
+ .then(modifierElementOf { node2 })
+ .then(modifierElementOf { node3 })
+ ) {
+ Box {
+ Box(
+ modifier = Modifier
+ .then(modifierElementOf { node4 })
+ .then(modifierElementOf { node5 })
+ )
+ }
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node5.visitAncestors(Nodes.Any) {
+ visitedAncestors.add(it)
+ }
+ }
+
+ // Assert.
+ assertThat(visitedAncestors)
+ .containsAtLeastElementsIn(arrayOf(node4, node3, node2, node1))
+ .inOrder()
+ }
+
+ @Test
+ fun nearestAncestorWithinCurrentLayoutNode_immediateParent() {
+ // Arrange.
+ val (node1, node2) = List(2) { object : Modifier.Node() {} }
+ rule.setContent {
+ Box(
+ modifier = modifierElementOf { node1 }
+ .then(modifierElementOf { node2 })
+ )
+ }
+
+ // Act.
+ val parent = rule.runOnIdle {
+ node2.nearestAncestor(Nodes.Any)
+ }
+
+ // Assert.
+ assertThat(parent).isEqualTo(node1)
+ }
+
+ @Test
+ fun nearestAncestorWithinCurrentLayoutNode_nonContiguousAncestor() {
+ // Arrange.
+ val (node1, node2) = List(2) { object : Modifier.Node() {} }
+ rule.setContent {
+ Box(
+ modifier = modifierElementOf { node1 }
+ .otherModifier()
+ .then(modifierElementOf { node2 })
+ )
+ }
+
+ // Act.
+ val parent = rule.runOnIdle {
+ node2.nearestAncestor(Nodes.Any)
+ }
+
+ // Assert.
+ assertThat(parent).isEqualTo(node1)
+ }
+
+ @Test
+ fun nearestAncestorInDifferentLayoutNode_immediateParentLayoutNode() {
+ // Arrange.
+ val (node1, node2) = List(2) { object : Modifier.Node() {} }
+ rule.setContent {
+ Box(modifier = modifierElementOf { node1 }) {
+ Box(modifier = modifierElementOf { node2 })
+ }
+ }
+
+ // Act.
+ val parent = rule.runOnIdle {
+ node2.nearestAncestor(Nodes.Any)
+ }
+
+ // Assert.
+ assertThat(parent).isEqualTo(node1)
+ }
+
+ @Test
+ fun nearestAncestorInDifferentLayoutNode_nonContiguousParentLayoutNode() {
+ // Arrange.
+ val (node1, node2) = List(2) { object : Modifier.Node() {} }
+ rule.setContent {
+ Box(modifier = modifierElementOf { node1 }) {
+ Box {
+ Box(modifier = modifierElementOf { node2 })
+ }
+ }
+ }
+
+ // Act.
+ val parent = rule.runOnIdle {
+ node2.nearestAncestor(Nodes.Any)
+ }
+
+ // Assert.
+ assertThat(parent).isEqualTo(node1)
+ }
+
+ private fun Modifier.otherModifier(): Modifier = this.then(Modifier)
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
index 2c7edff..b26f320 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
@@ -135,10 +135,10 @@
}
/**
- * [Layout] is the main core component for layout for "leaf" nodes. It can be used to measure and
- * position zero children.
+ * [Layout] is the main core component for layout. It can be used to measure and position
+ * zero or more layout children.
*
- * This overload accepts a list of multiple composable content lambdas, which allows to threat
+ * This overload accepts a list of multiple composable content lambdas, which allows treating
* measurables put into different content lambdas differently - measure policy will provide
* a list of lists of Measurables, not just a single list. Such list has the same size
* as the list of contents passed into [Layout] and contains the list of measurables
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index d31045a..b212c78 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -135,7 +135,7 @@
@OptIn(ExperimentalComposeUiApi::class)
private fun MutableVector<Modifier.Node>.addLayoutNodeChildren(node: Modifier.Node) {
- node.requireLayoutNode()._children.forEach {
+ node.requireLayoutNode()._children.forEachReversed {
add(it.nodes.head)
}
}
@@ -150,7 +150,7 @@
else
branches.add(child)
while (branches.isNotEmpty()) {
- val branch = branches.removeAt(branches.size)
+ val branch = branches.removeAt(branches.lastIndex)
if (branch.aggregateChildKindSet and mask == 0) {
branches.addLayoutNodeChildren(branch)
// none of these nodes match the mask, so don't bother traversing them
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index 7a0e7b3..ae0ffee 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -56,21 +56,22 @@
@OptIn(ExperimentalComposeUiApi::class)
internal object Nodes {
- val Layout = NodeKind<LayoutModifierNode>(0b1 shl 0)
- val Draw = NodeKind<DrawModifierNode>(0b1 shl 1)
- val Semantics = NodeKind<SemanticsModifierNode>(0b1 shl 2)
- val PointerInput = NodeKind<PointerInputModifierNode>(0b1 shl 3)
- val Locals = NodeKind<ModifierLocalNode>(0b1 shl 6)
- val ParentData = NodeKind<ParentDataModifierNode>(0b1 shl 7)
- val LayoutAware = NodeKind<LayoutAwareModifierNode>(0b1 shl 8)
- val GlobalPositionAware = NodeKind<GlobalPositionAwareModifierNode>(0b1 shl 9)
- val IntermediateMeasure = NodeKind<IntermediateLayoutModifierNode>(0b1 shl 10)
+ val Any = NodeKind<Modifier.Node>(0b1)
+ val Layout = NodeKind<LayoutModifierNode>(0b1 shl 1)
+ val Draw = NodeKind<DrawModifierNode>(0b1 shl 2)
+ val Semantics = NodeKind<SemanticsModifierNode>(0b1 shl 3)
+ val PointerInput = NodeKind<PointerInputModifierNode>(0b1 shl 4)
+ val Locals = NodeKind<ModifierLocalNode>(0b1 shl 5)
+ val ParentData = NodeKind<ParentDataModifierNode>(0b1 shl 6)
+ val LayoutAware = NodeKind<LayoutAwareModifierNode>(0b1 shl 7)
+ val GlobalPositionAware = NodeKind<GlobalPositionAwareModifierNode>(0b1 shl 8)
+ val IntermediateMeasure = NodeKind<IntermediateLayoutModifierNode>(0b1 shl 9)
// ...
}
@OptIn(ExperimentalComposeUiApi::class)
internal fun calculateNodeKindSetFrom(element: Modifier.Element): Int {
- var mask = 0
+ var mask = Nodes.Any.mask
if (element is LayoutModifier) {
mask = mask or Nodes.Layout
}
@@ -113,7 +114,7 @@
@OptIn(ExperimentalComposeUiApi::class)
internal fun calculateNodeKindSetFrom(node: Modifier.Node): Int {
- var mask = 0
+ var mask = Nodes.Any.mask
if (node is LayoutModifierNode) {
mask = mask or Nodes.Layout
}
@@ -142,4 +143,4 @@
mask = mask or Nodes.IntermediateMeasure
}
return mask
-}
\ No newline at end of file
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/FederatedCredentialJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/FederatedCredentialJavaTest.java
new file mode 100644
index 0000000..b5365a2
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/FederatedCredentialJavaTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class FederatedCredentialJavaTest {
+ @Test
+ public void hasCorrectTypeConstantValue() {
+ assertThat(FederatedCredential.TYPE_FEDERATED_CREDENTIAL)
+ .isEqualTo("type.federated_credential");
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/FederatedCredentialTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/FederatedCredentialTest.kt
new file mode 100644
index 0000000..080e629
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/FederatedCredentialTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class FederatedCredentialTest {
+ @Test
+ fun hasCorrectTypeConstantValue() {
+ assertThat(FederatedCredential.TYPE_FEDERATED_CREDENTIAL)
+ .isEqualTo("type.federated_credential")
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/FederatedCredential.kt b/credentials/credentials/src/main/java/androidx/credentials/FederatedCredential.kt
new file mode 100644
index 0000000..dfc262c
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/FederatedCredential.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials
+
+/**
+ * A federated credential fetched from a federated identity provider (FedCM).
+ *
+ * Note: the FedCM proposal is still under public discussion and its constructor will be exposed
+ * after the proposal is final.
+ *
+ * @hide
+ */
+class FederatedCredential private constructor() {
+ companion object {
+ /** The type value for federated credential related operations. */
+ const val TYPE_FEDERATED_CREDENTIAL: String = "type.federated_credential"
+ }
+}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreMultiProcessTest.kt b/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreMultiProcessTest.kt
index 31de4e8..699976c 100644
--- a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreMultiProcessTest.kt
+++ b/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreMultiProcessTest.kt
@@ -459,6 +459,13 @@
signalService(connection)
assertThat(dataStore.data.first()).isEqualTo(FOO_WITH_TEXT)
+
+ // version file should be ready at this point
+ val sharedCounter = SharedCounter.create(/* enableMlock = */ false) {
+ File(testFile.absolutePath + ".version")
+ }
+ // only 1 write should be done to handle the corruption, so version is incremented by 1
+ assertThat(sharedCounter.getValue()).isEqualTo(1)
}
class InterleavedHandlerUpdateDataService(
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
index 07cc6edc..3dcb1cb 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
@@ -373,19 +373,19 @@
}
}
} catch (ex: CorruptionException) {
- val newData: T = corruptionHandler.handleCorruption(ex)
+ var newData: T = corruptionHandler.handleCorruption(ex)
var version: Int = INVALID_VERSION // should be overridden if write successfully
try {
- // TODO(b/241286493): acquire the write lock and confirm the data is still corrupted
- // before overwriting to avoid race condition
- if (hasWriteFileLock) {
- version = writeData(newData)
- } else {
- getWriteFileLock {
+ doWithWriteFileLock(hasWriteFileLock) {
+ // Confirms the file is still corrupted before overriding
+ try {
+ newData = readDataFromFileOrDefault()
+ version = sharedCounter.getValue()
+ } catch (ignoredEx: CorruptionException) {
version = writeData(newData)
- newData
}
+ newData
}
} catch (writeEx: IOException) {
// If we fail to write the handled data, add the new exception as a suppressed
@@ -399,6 +399,10 @@
}
}
+ private suspend fun doWithWriteFileLock(hasWriteFileLock: Boolean, block: suspend () -> T) {
+ if (hasWriteFileLock) block() else getWriteFileLock { block() }
+ }
+
// It handles the read when the current state is Data
private suspend fun readData(): T {
// Check if the cached version matches with shared memory counter
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 966172a..6a4626c 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -266,6 +266,8 @@
WARN: Unable to find what is referred to by
# > Task :docs-tip-of-tree:dackkaDocs
in DClass ComplicationSlot
+Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of ComplicationSlot in file \$OUT_DIR\/androidx\/docs\-public\/build\/unzippedSourcesForDackka\/androidx\/wear\/watchface\/ComplicationSlot\.kt at line [0-9]+\.
+Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of ComplicationSlot in file \$OUT_DIR\/androidx\/docs\-tip\-of\-tree\/build\/unzippedSourcesForDackka\/androidx\/wear\/watchface\/ComplicationSlot\.kt at line [0-9]+\.
@param id,
@param canvasComplicationFactory,
@param supportedTypes,
@@ -275,8 +277,12 @@
\@param defaultPolicy
@param complicationTapFilter
in DClass UserStyle
+Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of UserStyle in file \$OUT_DIR\/androidx\/docs\-public\/build\/unzippedSourcesForDackka\/androidx\/wear\/watchface\/style\/CurrentUserStyleRepository\.kt at line [0-9]+\.
+Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of UserStyle in file \$OUT_DIR\/androidx\/docs\-tip\-of\-tree\/build\/unzippedSourcesForDackka\/androidx\/wear\/watchface\/style\/CurrentUserStyleRepository\.kt at line [0-9]+\.
@param copySelectedOptions
in DClass Builder
+Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of Builder in file \$OUT_DIR\/androidx\/docs\-public\/build\/unzippedSourcesForDackka\/androidx\/wear\/watchface\/ComplicationSlot\.kt at line [0-9]+\.
+Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of Builder in file \$OUT_DIR\/androidx\/docs\-tip\-of\-tree\/build\/unzippedSourcesForDackka\/androidx\/wear\/watchface\/ComplicationSlot\.kt at line [0-9]+\.
Did you make a typo\? Are you trying to refer to something not visible to users\?
WARNING: do not use 'an' before the exception type in an @throws statement\. This is against jdoc spec, will be an error in the next version of dackka, and your exception is not being linked and looks bad\. This was observed in Throws\(root=CustomDocTag\(children=\[P\(children=\[DocumentationLink\(dri=java\.lang/IllegalArgumentException///PointingToDeclaration/, children=\[Text\(body=IllegalArgumentException, children=\[\], params=\{\}\)\], params=\{\}\), Text\(body= if the property name or index is invalid\., children=\[\], params=\{\}\)\], params=\{\}\)\], params=\{\}, name=MARKDOWN_FILE\), name=an, exceptionAddress=null\)\.
WARNING: do not use 'an' before the exception type in an @throws statement\. This is against jdoc spec, will be an error in the next version of dackka, and your exception is not being linked and looks bad\. This was observed in Throws\(root=CustomDocTag\(children=\[P\(children=\[DocumentationLink\(dri=kotlin/IllegalArgumentException///PointingToDeclaration/, children=\[Text\(body=IllegalArgumentException, children=\[\], params=\{\}\)\], params=\{href=\[IllegalArgumentException\]\}\), Text\(body= if address is invalid\., children=\[\], params=\{\}\)\], params=\{\}\)\], params=\{\}, name=MARKDOWN_FILE\), name=an, exceptionAddress=null\)\.
@@ -496,4 +502,6 @@
w\: \$SUPPORT\/graphics\/graphics\-path\/src\/main\/java\/androidx\/graphics\/path\/Paths\.kt\: \([0-9]+\, [0-9]+\)\: Extension is shadowed by a member\: public open fun iterator\(\)\: PathIterator
# > Task :core:core-splashscreen:core-splashscreen-samples:lintReportDebug
Warning: Lint will treat :annotation:annotation as an external dependency and not analyze it\.
-\* Recommended Action: Apply the 'com\.android\.lint' plugin to java library project :annotation:annotation\. to enable lint to analyze those sources\.
\ No newline at end of file
+Did you make a typo\? Are you trying to refer to something not visible to users\?
+\* Recommended Action: Apply the 'com\.android\.lint' plugin to java library project :annotation:annotation\. to enable lint to analyze those sources\.
+WARN: Sources for .+ is empty
diff --git a/development/offlinifyDocs/offlinify_docs.py b/development/offlinifyDocs/offlinify_docs.py
deleted file mode 100755
index 9db9f8e..0000000
--- a/development/offlinifyDocs/offlinify_docs.py
+++ /dev/null
@@ -1,176 +0,0 @@
-#!/usr/bin/python3
-
-import argparse
-from pathlib import Path
-import os
-import re
-import shutil
-import sys
-import urllib
-
-SCRIPT_PATH = Path(__file__).parent.absolute()
-DEFAULT_DIR = os.path.abspath(os.path.join(SCRIPT_PATH, '../../../../out/androidx/docs-tip-of-tree/build/javadoc'))
-
-STYLE_FNAME = 'style.css'
-CSS_SOURCE_PATH = os.path.abspath(os.path.join(SCRIPT_PATH, STYLE_FNAME))
-
-VERBOSE = False
-
-def check_env():
- """
- Error early if any system setup is missing
- """
- try:
- from bs4 import BeautifulSoup
- except ModuleNotFoundError:
- print("ERROR: This script requires beatifulsoup module `bs4` to run. Please install with pip or another package manager.")
- sys.exit(-1)
-
-def parse_args():
- parser = argparse.ArgumentParser()
- parser.add_argument('--path', '-p', required=False)
- parser.add_argument('--quiet', '-q', required=False, action="store_true")
- parser.set_defaults(format=False)
- args = parser.parse_args()
- global VERBOSE
- VERBOSE = not args.quiet # just update the global, sorry
- return args
-
-def log(msg, end = '\n', flush=False):
- if (VERBOSE):
- print(msg, end=end, flush=flush)
-
-def sanitize_destination(fpath):
- """
- Ensure that destination always points to a Javadoc folder as this is the main param to script
- """
- if fpath is None:
- fpath = DEFAULT_DIR
- if (os.path.isdir(fpath)):
- return fpath
- else:
- print("Unable to find javadoc directory, ensure it's been created by running")
- print(" ./gradlew doclavaDocs -PofflineDocs=true")
- sys.exit(-1)
-
- # convert files to directories
- if (os.path.isfile(fpath)):
- result = Path(fpath).parent.absolute()
- log(f"Provided path: {fpath}")
- log(f"Using directory: {result}")
- fpath = result
-
- if (os.path.isdir(fpath)):
- if VERBOSE:
- print(f"Confirm that directory \033[4m{os.path.abspath(fpath)}\033[0m points to the root of generated javadoc files [Y/N]:", end=' ', flush=True)
- result = sys.stdin.readline().rstrip()
- valid_responses = ['Y', 'N']
- while result.upper() not in valid_responses:
- print("Please enter [Y/N]:", end=' ', flush=True)
- result = sys.stdin.readline().rstrip()
- if result.upper() == "N":
- sys.exit(-1)
- return os.path.abspath(fpath)
- else:
- print(f"Invalid path {fpath}, please specify the generated javadoc root directory")
- sys.exit(-1)
-
-def copy_css_to_root(javadocroot):
- """
- Drop our style sheet into the root dir.
- """
- log(f"Copying {os.path.relpath(CSS_SOURCE_PATH)}", end = " to ", flush=True)
- dest_path = os.path.join(javadocroot, STYLE_FNAME)
- shutil.copy(CSS_SOURCE_PATH, dest_path)
- log(f"{os.path.relpath(dest_path)} ✅")
- return dest_path
-
-def fix_css(soup, relative_css):
- """
- Replace any css links with a correct link
- """
- for tag in soup.find_all("link", rel="stylesheet"):
- tag.extract()
-
- new_tag = soup.new_tag("link", rel="stylesheet", href=relative_css)
- soup.head.append(new_tag)
-
-def fix_links(soup, rootdir, file_loc, last_root):
- """
- Fix any in-javadoc links to be relative instead of absolute so they can be opened from the filesystem.
- """
- for atag in soup.find_all('a'):
- generated_path = atag.get('href')
- if generated_path is None:
- continue
-
- parsed_url = urllib.parse.urlparse(generated_path.lstrip('/'))
- non_root_generated = parsed_url.path
-
- # see if we can just fix it quick
- if last_root is not None and os.path.isfile(os.path.join(last_root, non_root_generated)):
- new_path = generate_relative_link(os.path.join(last_root, non_root_generated), file_loc)
- atag['href'] = urllib.parse.urlunparse(parsed_url._replace(path=new_path))
- continue
- else:
- # ¯\_(ツ)_/¯
- # walk back from file_loc to rootdir and try to append generated_path
- # this will catch situations where rootdir is above the expected path
- current_path = Path(file_loc).parent.absolute()
- last_current_path = None
- while current_path != last_current_path: # if there's a better way to detect root swap it
- test_path = os.path.join(current_path.as_posix(), non_root_generated)
- if os.path.isfile(test_path):
- last_root = current_path.as_posix()
- new_path = generate_relative_link(test_path, file_loc)
- new_path_abs = Path(file_loc).parent.joinpath(new_path).as_posix()
- if (os.path.commonprefix([rootdir, new_path_abs]) == rootdir):
- # if the found file is inside the rootdir, we'll update the url to work in the browser
- atag['href'] = urllib.parse.urlunparse(parsed_url._replace(path=new_path))
- else:
- log(f"not updating path {generated_path} to {new_path} because it points above {rootdir}")
- break
- else:
- last_current_path = current_path
- current_path = Path(current_path).parent
- return last_root
-
-def generate_relative_link(destination, source):
- """
- Generate a relative link in a form that a web browser likes
- """
- absdest = os.path.abspath(destination)
- abssource = os.path.abspath(source)
- if os.path.isfile(abssource):
- abssource = Path(abssource).parent.absolute()
- return os.path.relpath(absdest, start=abssource)
-
-def fix_html(javadocroot):
- """
- Inject css link and fix all <a href to work on the local file system for all files under javadocroot
- """
- from bs4 import BeautifulSoup
-
- css_path = copy_css_to_root(javadocroot)
- last_relative_root = None
- for html_file in list(Path(javadocroot).glob('**/*.html')):
- relative_css_path = os.path.relpath(css_path, Path(html_file).parent)
- with html_file.open() as fd:
- parsed_html = BeautifulSoup(fd, "html.parser")
-
- fix_css(parsed_html, relative_css_path)
- last_relative_root = fix_links(parsed_html, javadocroot, html_file, last_relative_root)
-
- # replace the file
- html_file.write_text(str(parsed_html))
- log(f"{os.path.relpath(html_file)} ✅", flush=False)
-
-def main(args=None):
- check_env()
- args = parse_args()
- javadocpath = sanitize_destination(args.path)
- log(f"Javadoc root path: {os.path.relpath(javadocpath)} ✅")
- fix_html(javadocpath)
-
-if __name__ == '__main__':
- main()
diff --git a/development/offlinifyDocs/style.css b/development/offlinifyDocs/style.css
deleted file mode 100644
index dfe2156..0000000
--- a/development/offlinifyDocs/style.css
+++ /dev/null
@@ -1 +0,0 @@
-@charset "UTF-8";body[theme=android-theme][layout=docs] .devsite-banner-announcement{-webkit-border-radius:0;border-radius:0}body[theme=android-theme] .devsite-book-nav-bg:after,body[theme=android-theme][layout=docs]{background-color:#e8eaed}body[theme=android-theme][layout=docs] .devsite-article{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);-webkit-border-radius:2px;border-radius:2px}body[theme=android-theme][layout=docs] .devsite-landing-row:first-child{-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0}body[theme=android-theme][layout=docs] .devsite-landing-row:last-child{-webkit-border-radius:0 0 2px 2px;border-radius:0 0 2px 2px}body[theme=android-theme] devsite-header .devsite-top-logo-row,body[theme=android-theme] devsite-header .devsite-top-logo-row-wrapper-wrapper,body[theme=android-theme] devsite-header .devsite-top-logo-row-wrapper-wrapper:before,body[theme=android-theme] devsite-header cloudx-tabs-nav.upper-tabs .devsite-tabs-wrapper,body[theme=android-theme] devsite-header devsite-tabs.upper-tabs .devsite-tabs-wrapper,body[theme=android-theme] devsite-header devsite-tabs.upper-tabs tab[overflow-tab]:after{background:#fff}body[theme=android-theme] devsite-header .devsite-header-billboard{background-color:#f7f9fa}body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-guillemet,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-link,body[theme=android-theme] devsite-header .devsite-breadcrumb-guillemet,body[theme=android-theme] devsite-header .devsite-breadcrumb-link{color:rgba(0,0,0,.65);fill:rgba(0,0,0,.65)}body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-guillemet:focus,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-guillemet:hover,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-link:focus,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-link:hover,body[theme=android-theme] devsite-header .devsite-breadcrumb-guillemet:focus,body[theme=android-theme] devsite-header .devsite-breadcrumb-guillemet:hover,body[theme=android-theme] devsite-header .devsite-breadcrumb-link:focus,body[theme=android-theme] devsite-header .devsite-breadcrumb-link:hover{color:rgba(0,0,0,.87)}body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-guillemet:focus .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-guillemet:hover .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-link:focus .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-link:hover .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-header .devsite-breadcrumb-guillemet:focus .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-header .devsite-breadcrumb-guillemet:hover .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-header .devsite-breadcrumb-link:focus .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-header .devsite-breadcrumb-link:hover .devsite-google-wordmark-svg-path{fill:rgba(0,0,0,.87)}body[theme=android-theme] devsite-header .devsite-product-description-row{color:#202124}body[theme=android-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link{color:#202124;fill:#202124}body[theme=android-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link:focus,body[theme=android-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link:hover,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet:focus,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet:hover,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link:focus,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link:hover{color:rgba(32,33,36,.7);fill:rgba(32,33,36,.7)}body[theme=android-theme] devsite-header .devsite-header-icon-button{color:rgba(0,0,0,.65)}body[theme=android-theme] devsite-header .devsite-header-icon-button:active,body[theme=android-theme] devsite-header .devsite-header-icon-button:focus,body[theme=android-theme] devsite-header .devsite-header-icon-button:hover{color:rgba(0,0,0,.87)}body[theme=android-theme] devsite-header .devsite-top-button{color:rgba(32,33,36,.7)}body[theme=android-theme] devsite-header .devsite-top-button:focus,body[theme=android-theme] devsite-header .devsite-top-button:hover{background-color:hsla(0,0%,80%,.15);color:#202124}body[theme=android-theme] devsite-header .devsite-top-button:active{background-color:hsla(0,0%,80%,.3);color:#202124}body[theme=android-theme] devsite-header .devsite-top-logo-row .devsite-top-button,body[theme=android-theme] devsite-header devsite-user #devsite-signin-btn{background:transparent;color:#1a73e8}body[theme=android-theme] devsite-header .devsite-top-logo-row .devsite-top-button:active,body[theme=android-theme] devsite-header .devsite-top-logo-row .devsite-top-button:focus,body[theme=android-theme] devsite-header .devsite-top-logo-row .devsite-top-button:hover,body[theme=android-theme] devsite-header devsite-user #devsite-signin-btn:active,body[theme=android-theme] devsite-header devsite-user #devsite-signin-btn:focus,body[theme=android-theme] devsite-header devsite-user #devsite-signin-btn:hover{background:transparent;border:0;-webkit-box-shadow:none;box-shadow:none;color:#1967d2}body[theme=android-theme] devsite-header .devsite-header-link,body[theme=android-theme] devsite-header .devsite-header-link:visited,body[theme=android-theme] devsite-header .devsite-settings-kabob,body[theme=android-theme] devsite-header .devsite-settings-kabob:visited{color:#1a73e8}body[theme=android-theme] devsite-header .devsite-header-link:focus,body[theme=android-theme] devsite-header .devsite-header-link:hover,body[theme=android-theme] devsite-header .devsite-settings-kabob:focus,body[theme=android-theme] devsite-header .devsite-settings-kabob:hover{color:#1967d2}body[theme=android-theme] #app-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-1,body[theme=android-theme] #app-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-3,body[theme=android-theme] devsite-header .devsite-header--loading:after,body[theme=android-theme] devsite-header .devsite-header--loading span:after{background-color:#3ddc84}body[theme=android-theme] devsite-toc>.devsite-nav-list{border-color:#3ddc84}body[theme=android-theme] .devsite-landing-row-cta:not([background]){background-color:#3ddc84;color:#fff}body[theme=android-theme] .devsite-feedback-item-icon-color{background-color:#3ddc84}body[theme=android-theme] .devsite-landing-row-cta.devsite-landing-row-large-headings .devsite-landing-row-item-description h3,body[theme=android-theme] .devsite-landing-row-cta.devsite-landing-row h2{color:#fff}body[theme=android-theme] .devsite-search-project .devsite-product-logo-container,body[theme=android-theme] devsite-header .devsite-product-logo-container{color:#3ddc84}body[theme=android-theme] .devsite-search-project .devsite-product-logo-container[background],body[theme=android-theme] devsite-header .devsite-product-logo-container[background]{background:#3ddc84;color:#fff}body[theme=android-theme] devsite-header .devsite-product-logo{color:inherit}body[theme=android-theme] .devsite-landing-row-item-icon-container[background][foreground=theme],body[theme=android-theme] .devsite-landing-row-item-icon-container[foreground=theme],body[theme=android-theme] .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=theme],body[theme=android-theme] .devsite-landing-row :focus .devsite-landing-row-item-icon-container[background][foreground=theme],body[theme=android-theme] .devsite-landing-row :link .devsite-landing-row-item-icon-container[background][foreground=theme]:hover,body[theme=android-theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[background][foreground=theme],body[theme=android-theme] :link .devsite-landing-row-item-list-item-icon-container[background][foreground=theme]:hover{color:#3ddc84}body[theme=android-theme] .devsite-collapsible-section{background-color:#f7f9fa}body[theme=android-theme] cloudx-tabs-nav.lower-tabs a,body[theme=android-theme] devsite-tabs.lower-tabs a{color:rgba(32,33,36,.7)}body[theme=android-theme] cloudx-tabs-nav.lower-tabs a:focus,body[theme=android-theme] cloudx-tabs-nav.lower-tabs a:hover,body[theme=android-theme] cloudx-tabs-nav.lower-tabs tab[active]>a,body[theme=android-theme] devsite-tabs.lower-tabs a:focus,body[theme=android-theme] devsite-tabs.lower-tabs a:hover,body[theme=android-theme] devsite-tabs.lower-tabs tab[active]>a{color:#202124}body[theme=android-theme] cloudx-tabs-nav.lower-tabs tab[active]>a:after,body[theme=android-theme] devsite-tabs.lower-tabs tab[active]>a:after{background:#3ddc84}body[theme=android-theme] cloudx-tabs-nav tab[dropdown] .devsite-nav-item-description,body[theme=android-theme] cloudx-tabs-nav tab[dropdown] .devsite-nav-title,body[theme=android-theme] devsite-tabs tab[dropdown] .devsite-nav-item-description,body[theme=android-theme] devsite-tabs tab[dropdown] .devsite-nav-title{color:#5f6368}body[theme=android-theme] cloudx-tabs-nav .devsite-tabs-dropdown a,body[theme=android-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:visited,body[theme=android-theme] devsite-tabs .devsite-tabs-dropdown a,body[theme=android-theme] devsite-tabs .devsite-tabs-dropdown a:visited{color:#202124}body[theme=android-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:focus,body[theme=android-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:hover,body[theme=android-theme] devsite-tabs .devsite-tabs-dropdown a:focus,body[theme=android-theme] devsite-tabs .devsite-tabs-dropdown a:hover{color:#1a73e8}body[theme=android-theme] cloudx-tabs-nav.upper-tabs tab>a,body[theme=android-theme] devsite-tabs.upper-tabs tab>a{color:#5f6368}body[theme=android-theme] cloudx-tabs-nav.upper-tabs tab[dropdown] .devsite-tabs-dropdown-content,body[theme=android-theme] cloudx-tabs-nav.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu,body[theme=android-theme] devsite-tabs.upper-tabs tab[dropdown] .devsite-tabs-dropdown-content,body[theme=android-theme] devsite-tabs.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu{-webkit-border-radius:0;border-radius:0}body[theme=android-theme] cloudx-tabs-nav.upper-tabs>div>tab>a:focus,body[theme=android-theme] cloudx-tabs-nav.upper-tabs>div>tab>a:hover,body[theme=android-theme] cloudx-tabs-nav.upper-tabs>div>tab[active]>a,body[theme=android-theme] devsite-tabs.upper-tabs>div>tab>a:focus,body[theme=android-theme] devsite-tabs.upper-tabs>div>tab>a:hover,body[theme=android-theme] devsite-tabs.upper-tabs>div>tab[active]>a{color:#202124}body[theme=android-theme] cloudx-tabs-nav.upper-tabs>div>tab[active]>a:after,body[theme=android-theme] devsite-tabs.upper-tabs>div>tab[active]>a:after{background:#3ddc84}body[theme=android-theme] devsite-user .devsite-user-dialog-signin .devsite-user-dialog-letter,body[theme=android-theme] devsite-user .devsite-user-dialog .devsite-user-dialog-photo{background:#3ddc84;color:#fff}body[theme=android-theme] .devsite-landing-row-item-custom-image:not([background]),body[theme=android-theme] [background=theme]{background-color:#3ddc84}body[theme=android-theme] .devsite-landing-row-item[foreground=theme] :link h2,body[theme=android-theme] .devsite-landing-row-item[foreground=theme] :link h3,body[theme=android-theme] [foreground=theme] .button,body[theme=android-theme] [foreground=theme] :focus>:not(.material-icons),body[theme=android-theme] [foreground=theme] :link>:not(.material-icons):hover,body[theme=android-theme] [foreground=theme] a:not(.button),body[theme=android-theme] [foreground=theme] a:not(.button) h2,body[theme=android-theme] [foreground=theme] a:not(.button) h3{color:#3ddc84}body[theme=android-theme] [foreground=theme] .button:active,body[theme=android-theme] [foreground=theme] .button:focus,body[theme=android-theme] [foreground=theme] .button:hover{background:#ecfcf3;color:#689f38}body[theme=android-theme] [foreground=theme] .button-primary{background:#3ddc84;color:#fff}body[theme=android-theme] [foreground=theme] .button-primary:active,body[theme=android-theme] [foreground=theme] .button-primary:focus,body[theme=android-theme] [foreground=theme] .button-primary:hover{background:#689f38;color:#fff}body[theme=android-theme] [background=theme] .devsite-landing-row-description,body[theme=android-theme] [background=theme] .devsite-landing-row-item-list-item-icon-container:not([foreground]),body[theme=android-theme] [background=theme] :link .devsite-landing-row-item-list-item-description h4+p,body[theme=android-theme] [background=theme] :link:not(.button),body[theme=android-theme] [background=theme]:not(.devsite-landing-row-cards) .button-white,body[theme=android-theme] [background=theme]:not(.devsite-landing-row-cards) h3,body[theme=android-theme] [background=theme]:not([foreground]):not(.devsite-landing-row-cards),body[theme=android-theme] [background=theme] :visited:not(.button),body[theme=android-theme] [background=theme] h2,body[theme=android-theme] [background]:not([background=grey]):not(.devsite-landing-row-cards) .button-white{color:#fff}body[theme=android-theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=theme],body[theme=android-theme] :link .devsite-landing-row-item-list-item-icon-container[foreground=theme]:hover,body[theme=android-theme] [background=theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,body[theme=android-theme] [background=theme] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:hsla(0,0%,100%,.7)}body[theme=android-theme] devsite-content .devsite-404-header h3,body[theme=android-theme] devsite-content .devsite-offline-header h3{color:#689f38}body[theme=android-theme] devsite-header .devsite-search-background,body[theme=android-theme] devsite-header devsite-search .devsite-searchbox:before{background:#fff}body[theme=android-theme] devsite-header .devsite-header-billboard-search .devsite-search-background,body[theme=android-theme] devsite-header .devsite-header-billboard-search devsite-search .devsite-searchbox:before{background:#f7f9fa}body[theme=android-theme] devsite-header .devsite-search-background:after,body[theme=android-theme] devsite-header[search-active] .devsite-search-background:after{background:#f1f3f4}body[theme=android-theme] devsite-header devsite-search .devsite-search-field,body[theme=android-theme] devsite-header devsite-search .devsite-search-image,body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-image{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field{background:#f1f3f4}body[theme=android-theme] devsite-header devsite-search .devsite-search-field::-ms-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field::placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field::-webkit-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field::-moz-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field:-ms-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field:hover{background:#e8eaed}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field::-ms-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field::placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field::-webkit-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field::-moz-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field:-ms-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field,body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field:hover{background:#f1f3f4;color:#202124}body[theme=android-theme] devsite-book-nav .devsite-mobile-header{background:#fff;border-bottom:1px solid #dadce0}@media screen and (max-width:840px){body[theme=android-theme][layout=docs] .devsite-banner-announcement{-webkit-border-radius:0;border-radius:0}body[theme=android-theme] devsite-header .devsite-search-background,body[theme=android-theme] devsite-header .devsite-search-background:after,body[theme=android-theme] devsite-header[search-active] .devsite-search-background:after,body[theme=android-theme] devsite-header devsite-search .devsite-search-field,body[theme=android-theme] devsite-header devsite-search .devsite-search-field:hover{background:0}body[theme=android-theme][layout=docs] .devsite-article{-webkit-border-radius:0;border-radius:0}}body[theme=android-ndk-theme][layout=docs] .devsite-banner-announcement{-webkit-border-radius:0;border-radius:0}body[theme=android-ndk-theme] .devsite-book-nav-bg:after,body[theme=android-ndk-theme][layout=docs]{background-color:#e8eaed}body[theme=android-ndk-theme][layout=docs] .devsite-article{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);-webkit-border-radius:2px;border-radius:2px}body[theme=android-ndk-theme][layout=docs] .devsite-landing-row:first-child{-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0}body[theme=android-ndk-theme][layout=docs] .devsite-landing-row:last-child{-webkit-border-radius:0 0 2px 2px;border-radius:0 0 2px 2px}body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row,body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row-wrapper-wrapper,body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row-wrapper-wrapper:before,body[theme=android-ndk-theme] devsite-header cloudx-tabs-nav.upper-tabs .devsite-tabs-wrapper,body[theme=android-ndk-theme] devsite-header devsite-tabs.upper-tabs .devsite-tabs-wrapper,body[theme=android-ndk-theme] devsite-header devsite-tabs.upper-tabs tab[overflow-tab]:after{background:#fff}body[theme=android-ndk-theme] devsite-header .devsite-header-billboard{background-color:#24c1e0}body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-guillemet,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-link,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-guillemet,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-link{color:rgba(0,0,0,.65);fill:rgba(0,0,0,.65)}body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-guillemet:focus,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-guillemet:hover,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-link:focus,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-link:hover,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-guillemet:focus,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-guillemet:hover,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-link:focus,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-link:hover{color:rgba(0,0,0,.87)}body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-guillemet:focus .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-guillemet:hover .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-link:focus .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-link:hover .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-guillemet:focus .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-guillemet:hover .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-link:focus .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-link:hover .devsite-google-wordmark-svg-path{fill:rgba(0,0,0,.87)}body[theme=android-ndk-theme] devsite-header .devsite-product-description-row{color:#fff}body[theme=android-ndk-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link{color:#fff;fill:#fff}body[theme=android-ndk-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link:focus,body[theme=android-ndk-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link:hover,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet:focus,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet:hover,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link:focus,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link:hover{color:hsla(0,0%,100%,.7);fill:hsla(0,0%,100%,.7)}body[theme=android-ndk-theme] devsite-header .devsite-header-icon-button{color:rgba(0,0,0,.65)}body[theme=android-ndk-theme] devsite-header .devsite-header-icon-button:active,body[theme=android-ndk-theme] devsite-header .devsite-header-icon-button:focus,body[theme=android-ndk-theme] devsite-header .devsite-header-icon-button:hover{color:rgba(0,0,0,.87)}body[theme=android-ndk-theme] devsite-header .devsite-top-button{color:hsla(0,0%,100%,.7)}body[theme=android-ndk-theme] devsite-header .devsite-top-button:focus,body[theme=android-ndk-theme] devsite-header .devsite-top-button:hover{background-color:hsla(0,0%,80%,.15);color:#fff}body[theme=android-ndk-theme] devsite-header .devsite-top-button:active{background-color:hsla(0,0%,80%,.3);color:#fff}body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row .devsite-top-button,body[theme=android-ndk-theme] devsite-header devsite-user #devsite-signin-btn{background:transparent;color:rgba(0,0,0,.65)}body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row .devsite-top-button:active,body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row .devsite-top-button:focus,body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row .devsite-top-button:hover,body[theme=android-ndk-theme] devsite-header devsite-user #devsite-signin-btn:active,body[theme=android-ndk-theme] devsite-header devsite-user #devsite-signin-btn:focus,body[theme=android-ndk-theme] devsite-header devsite-user #devsite-signin-btn:hover{background:transparent;border:0;-webkit-box-shadow:none;box-shadow:none;color:rgba(0,0,0,.87)}body[theme=android-ndk-theme] devsite-header .devsite-header-link,body[theme=android-ndk-theme] devsite-header .devsite-header-link:visited,body[theme=android-ndk-theme] devsite-header .devsite-settings-kabob,body[theme=android-ndk-theme] devsite-header .devsite-settings-kabob:visited{color:rgba(0,0,0,.65)}body[theme=android-ndk-theme] devsite-header .devsite-header-link:focus,body[theme=android-ndk-theme] devsite-header .devsite-header-link:hover,body[theme=android-ndk-theme] devsite-header .devsite-settings-kabob:focus,body[theme=android-ndk-theme] devsite-header .devsite-settings-kabob:hover{color:rgba(0,0,0,.87)}body[theme=android-ndk-theme] #app-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-1,body[theme=android-ndk-theme] #app-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-3,body[theme=android-ndk-theme] devsite-header .devsite-header--loading:after,body[theme=android-ndk-theme] devsite-header .devsite-header--loading span:after{background-color:#24c1e0}body[theme=android-ndk-theme] devsite-toc>.devsite-nav-list{border-color:#24c1e0}body[theme=android-ndk-theme] .devsite-landing-row-cta:not([background]){background-color:#24c1e0;color:#202124}body[theme=android-ndk-theme] .devsite-feedback-item-icon-color{background-color:#24c1e0}body[theme=android-ndk-theme] .devsite-landing-row-cta.devsite-landing-row-large-headings .devsite-landing-row-item-description h3,body[theme=android-ndk-theme] .devsite-landing-row-cta.devsite-landing-row h2{color:#202124}body[theme=android-ndk-theme] .devsite-search-project .devsite-product-logo-container,body[theme=android-ndk-theme] devsite-header .devsite-product-logo-container{color:#24c1e0}body[theme=android-ndk-theme] .devsite-search-project .devsite-product-logo-container[background],body[theme=android-ndk-theme] devsite-header .devsite-product-logo-container[background]{background:#24c1e0;color:#fff}body[theme=android-ndk-theme] devsite-header .devsite-product-logo{color:inherit}body[theme=android-ndk-theme] .devsite-landing-row-item-icon-container[background][foreground=theme],body[theme=android-ndk-theme] .devsite-landing-row-item-icon-container[foreground=theme],body[theme=android-ndk-theme] .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=theme],body[theme=android-ndk-theme] .devsite-landing-row :focus .devsite-landing-row-item-icon-container[background][foreground=theme],body[theme=android-ndk-theme] .devsite-landing-row :link .devsite-landing-row-item-icon-container[background][foreground=theme]:hover,body[theme=android-ndk-theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[background][foreground=theme],body[theme=android-ndk-theme] :link .devsite-landing-row-item-list-item-icon-container[background][foreground=theme]:hover{color:#24c1e0}body[theme=android-ndk-theme] .devsite-collapsible-section{background-color:#24c1e0}body[theme=android-ndk-theme] cloudx-tabs-nav.lower-tabs a,body[theme=android-ndk-theme] devsite-tabs.lower-tabs a{color:hsla(0,0%,100%,.7)}body[theme=android-ndk-theme] cloudx-tabs-nav.lower-tabs a:focus,body[theme=android-ndk-theme] cloudx-tabs-nav.lower-tabs a:hover,body[theme=android-ndk-theme] cloudx-tabs-nav.lower-tabs tab[active]>a,body[theme=android-ndk-theme] devsite-tabs.lower-tabs a:focus,body[theme=android-ndk-theme] devsite-tabs.lower-tabs a:hover,body[theme=android-ndk-theme] devsite-tabs.lower-tabs tab[active]>a{color:#fff}body[theme=android-ndk-theme] cloudx-tabs-nav.lower-tabs tab[active]>a:after,body[theme=android-ndk-theme] devsite-tabs.lower-tabs tab[active]>a:after{background:#fff}body[theme=android-ndk-theme] cloudx-tabs-nav tab[dropdown] .devsite-nav-item-description,body[theme=android-ndk-theme] cloudx-tabs-nav tab[dropdown] .devsite-nav-title,body[theme=android-ndk-theme] devsite-tabs tab[dropdown] .devsite-nav-item-description,body[theme=android-ndk-theme] devsite-tabs tab[dropdown] .devsite-nav-title{color:#5f6368}body[theme=android-ndk-theme] cloudx-tabs-nav .devsite-tabs-dropdown a,body[theme=android-ndk-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:visited,body[theme=android-ndk-theme] devsite-tabs .devsite-tabs-dropdown a,body[theme=android-ndk-theme] devsite-tabs .devsite-tabs-dropdown a:visited{color:#202124}body[theme=android-ndk-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:focus,body[theme=android-ndk-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:hover,body[theme=android-ndk-theme] devsite-tabs .devsite-tabs-dropdown a:focus,body[theme=android-ndk-theme] devsite-tabs .devsite-tabs-dropdown a:hover{color:#1a73e8}body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs tab>a,body[theme=android-ndk-theme] devsite-tabs.upper-tabs tab>a{color:#5f6368}body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs tab[dropdown] .devsite-tabs-dropdown-content,body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu,body[theme=android-ndk-theme] devsite-tabs.upper-tabs tab[dropdown] .devsite-tabs-dropdown-content,body[theme=android-ndk-theme] devsite-tabs.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu{-webkit-border-radius:0;border-radius:0}body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs>div>tab>a:focus,body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs>div>tab>a:hover,body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs>div>tab[active]>a,body[theme=android-ndk-theme] devsite-tabs.upper-tabs>div>tab>a:focus,body[theme=android-ndk-theme] devsite-tabs.upper-tabs>div>tab>a:hover,body[theme=android-ndk-theme] devsite-tabs.upper-tabs>div>tab[active]>a{color:#202124}body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs>div>tab[active]>a:after,body[theme=android-ndk-theme] devsite-tabs.upper-tabs>div>tab[active]>a:after{background:#129eaf}body[theme=android-ndk-theme] devsite-user .devsite-user-dialog-signin .devsite-user-dialog-letter,body[theme=android-ndk-theme] devsite-user .devsite-user-dialog .devsite-user-dialog-photo{background:#78d9ec;color:#202124}body[theme=android-ndk-theme] .devsite-landing-row-item-custom-image:not([background]),body[theme=android-ndk-theme] [background=theme]{background-color:#78d9ec}body[theme=android-ndk-theme] .devsite-landing-row-item[foreground=theme] :link h2,body[theme=android-ndk-theme] .devsite-landing-row-item[foreground=theme] :link h3,body[theme=android-ndk-theme] [foreground=theme] .button,body[theme=android-ndk-theme] [foreground=theme] :focus>:not(.material-icons),body[theme=android-ndk-theme] [foreground=theme] :link>:not(.material-icons):hover,body[theme=android-ndk-theme] [foreground=theme] a:not(.button),body[theme=android-ndk-theme] [foreground=theme] a:not(.button) h2,body[theme=android-ndk-theme] [foreground=theme] a:not(.button) h3{color:#24c1e0}body[theme=android-ndk-theme] [foreground=theme] .button:active,body[theme=android-ndk-theme] [foreground=theme] .button:focus,body[theme=android-ndk-theme] [foreground=theme] .button:hover{background:#e9f9fc;color:#129eaf}body[theme=android-ndk-theme] [foreground=theme] .button-primary{background:#24c1e0;color:#202124}body[theme=android-ndk-theme] [foreground=theme] .button-primary:active,body[theme=android-ndk-theme] [foreground=theme] .button-primary:focus,body[theme=android-ndk-theme] [foreground=theme] .button-primary:hover{background:#129eaf;color:#202124}body[theme=android-ndk-theme] [background=theme] .devsite-landing-row-description,body[theme=android-ndk-theme] [background=theme] .devsite-landing-row-item-list-item-icon-container:not([foreground]),body[theme=android-ndk-theme] [background=theme] :link .devsite-landing-row-item-list-item-description h4+p,body[theme=android-ndk-theme] [background=theme] :link:not(.button),body[theme=android-ndk-theme] [background=theme]:not(.devsite-landing-row-cards) .button-white,body[theme=android-ndk-theme] [background=theme]:not(.devsite-landing-row-cards) h3,body[theme=android-ndk-theme] [background=theme]:not([foreground]):not(.devsite-landing-row-cards),body[theme=android-ndk-theme] [background=theme] :visited:not(.button),body[theme=android-ndk-theme] [background=theme] h2,body[theme=android-ndk-theme] [background]:not([background=grey]):not(.devsite-landing-row-cards) .button-white{color:#202124}body[theme=android-ndk-theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=theme],body[theme=android-ndk-theme] :link .devsite-landing-row-item-list-item-icon-container[foreground=theme]:hover,body[theme=android-ndk-theme] [background=theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,body[theme=android-ndk-theme] [background=theme] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}body[theme=android-ndk-theme] devsite-content .devsite-404-header h3,body[theme=android-ndk-theme] devsite-content .devsite-offline-header h3{color:#129eaf}body[theme=android-ndk-theme] devsite-header .devsite-search-background,body[theme=android-ndk-theme] devsite-header devsite-search .devsite-searchbox:before{background:#fff}body[theme=android-ndk-theme] devsite-header .devsite-header-billboard-search .devsite-search-background,body[theme=android-ndk-theme] devsite-header .devsite-header-billboard-search devsite-search .devsite-searchbox:before{background:#24c1e0}body[theme=android-ndk-theme] devsite-header .devsite-search-background:after,body[theme=android-ndk-theme] devsite-header[search-active] .devsite-search-background:after{background:#f1f3f4}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field,body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-image,body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-image{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field{background:#f1f3f4}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field::-ms-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field::placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field::-webkit-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field::-moz-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field:-ms-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field:hover{background:#e8eaed}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field::-ms-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field::placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field::-webkit-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field::-moz-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field:-ms-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field,body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field:hover{background:#f1f3f4;color:#202124}body[theme=android-ndk-theme] devsite-book-nav .devsite-mobile-header{background:#fff;border-bottom:1px solid #dadce0}@media screen and (max-width:840px){body[theme=android-ndk-theme][layout=docs] .devsite-banner-announcement{-webkit-border-radius:0;border-radius:0}body[theme=android-ndk-theme] devsite-header .devsite-search-background,body[theme=android-ndk-theme] devsite-header .devsite-search-background:after,body[theme=android-ndk-theme] devsite-header[search-active] .devsite-search-background:after,body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field,body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field:hover{background:0}body[theme=android-ndk-theme][layout=docs] .devsite-article{-webkit-border-radius:0;border-radius:0}}.devsite-site-logo{width:134px}body,dd,div,dl,figure,form,img,input,menu{margin:0;padding:0}body[no-overflow]{overflow:hidden}iframe{border:0}iframe:not([src]){display:none}.caution>:first-child,.dogfood>:first-child,.key-point>:first-child,.key-term>:first-child,.note>:first-child,.objective>:first-child,.quickstart-left>:first-child,.quickstart-wide>:first-child,.special>:first-child,.success>:first-child,.warning>:first-child,aside>:first-child,blockquote>:first-child,dd>:first-child,li>p:first-child,td>.devsite-table-wrapper:first-child>table,td>.expandable:first-child>:nth-child(2),td>:first-child,td>:first-child>li:first-child{margin-top:0}.caution>:last-child,.dogfood>:last-child,.key-point>:last-child,.key-term>:last-child,.note>:last-child,.objective>:last-child,.quickstart-left>:last-child,.quickstart-wide>:last-child,.special>:last-child,.success>:last-child,.warning>:last-child,aside>:last-child,blockquote>:last-child,dd>:last-child,li>p:last-child,td>.devsite-table-wrapper:last-child>table,td>.expandable:last-child>:last-child,td>:last-child,td>:last-child>li:last-child{margin-bottom:0}html{-webkit-box-sizing:border-box;box-sizing:border-box}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}.clearfix:after,.quickstart-step:after{clear:both;content:"";display:table;height:0;visibility:hidden}body,html{color:#202124;font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;height:100%;margin:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}body[sitemask--active]{overflow:hidden}p{margin:16px 0;padding:0}img,video{border:0;height:auto;max-width:100%}table img{max-width:272px}:link,:visited{color:#1a73e8;outline:0;text-decoration:none}a:focus{text-decoration:underline}a:focus img{-webkit-filter:brightness(75%);filter:brightness(75%)}.devsite-toast-content :link,.devsite-toast-content :visited{color:#fff;text-decoration:underline}.devsite-toast-content a:focus{background:hsla(0,0%,100%,.3);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}sup{line-height:1}dd,ol,ul{margin:0;padding-left:40px}td>dl>dd,td>ol,td>ul{padding-left:20px}ol{list-style:decimal outside}ol ol{list-style-type:lower-alpha}ol ol ol{list-style-type:lower-roman}ol.upper-alpha{list-style-type:upper-alpha}ul{list-style:disc outside}li,li p{margin:12px 0;padding:0}dt{font:700 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}blockquote,dd,dt{margin:16px 0}blockquote{background:#f1f3f4;padding:8px;quotes:none}hr{background:#e8eaed;border:0;height:1px;margin:16px 0;width:100%}abbr,acronym{border-bottom:1px dotted #5f6368;cursor:help}address,cite,dfn,em{font-style:italic}strong{font-weight:700}[visually-hidden]{opacity:0!important;pointer-events:none!important;visibility:hidden!important}.hidden,[hidden]{display:none!important}[render-hidden]{display:inline!important;position:absolute!important;visibility:hidden!important}[no-scroll]{overflow:hidden}#app-progress{left:0;position:fixed;right:0;top:0;z-index:1011}.devsite-article .material-icons{vertical-align:bottom}.devsite-article-body .material-icons:not(:link),[type=landing] .devsite-article .material-icons:not(:link){cursor:default}.footnotes ol{padding-left:16px}.footnotes li{font:400 13px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}#qv-wrapper,#table-of-contents,#tb-wrapper,.inline-toc,div.toc:not(.class):not(.group):not(.type):not(.interface),h2#contents,h2.toc,h3#contents,h3.toc,ol.toc,section.toc,ul.toc{display:none}@media screen and (max-width:840px){#app-progress{z-index:1014}}.no-feedback devsite-feedback{display:none!important}.preserve-case{text-transform:none}a.external:after,a[href*=man7\.org]:after,a[href*=oracle\.com]:after{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;content:"open_in_new";font-size:18px;margin:0;vertical-align:text-bottom}[dir=ltr] a.external:after,[dir=ltr] a[href*=man7\.org]:after,[dir=ltr] a[href*=oracle\.com]:after{margin-left:4px}[dir=rtl] a.external:after,[dir=rtl] a[href*=man7\.org]:after,[dir=rtl] a[href*=oracle\.com]:after{margin-right:4px;-webkit-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}a.download:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;content:"file_download";display:inline-block;font-size:18px;margin:0;vertical-align:text-bottom}[dir=ltr] a.download:before{margin-right:4px}[dir=rtl] a.download:before{margin-left:4px}devsite-content{counter-reset:numbered}h2.numbered{line-height:48px;margin-top:60px;padding-bottom:19px}h2.numbered:before{background:#bdc1c6;-webkit-border-radius:50%;border-radius:50%;color:#fff;content:counter(numbered);counter-increment:numbered;display:inline-block;height:48px;line-height:48px;margin:0 20px 0 0;text-align:center;width:48px}[dir=rtl] h2.numbered:before{margin:0 0 0 20px}.compare-better,.compare-no,.compare-worse,.compare-yes{font-weight:700}.compare-better:before,.compare-blank:before,.compare-no:before,.compare-worse:before,.compare-yes:before{content:"";display:inline-block;font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;word-wrap:normal;margin:-4px 4px 0 0;text-transform:none;vertical-align:middle;width:24px}[dir=rtl] .compare-better:before,[dir=rtl] .compare-blank:before,[dir=rtl] .compare-no:before,[dir=rtl] .compare-worse:before,[dir=rtl] .compare-yes:before{margin:-4px 0 0 4px}.compare-better:before{color:#34a853;content:"thumb_up"}.compare-no:before{color:#dd2c00;content:"not_interested"}.compare-worse:before{color:#dd2c00;content:"thumb_down"}.compare-yes:before{color:#34a853;content:"check"}.align-center{text-align:center}.align-right{text-align:right}.hanging-indent,.members.function td:first-child{padding-left:25px;text-indent:-17px}[dir=rtl] .hanging-indent,[dir=rtl] .members.function td:first-child{padding-left:0;padding-right:25px}.bad-table{table-layout:fixed}.bad-table td,.bad-table tr{word-wrap:break-word}.bad-table pre{word-wrap:normal}.screenshot{border:1px solid #e8eaed;padding:3px}.columns td,.columns th,.columns tr{background:0;border:0;font:16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0}[dir=ltr] .columns td,[dir=ltr] .columns th{padding-right:20px}[dir=rtl] .columns td,[dir=rtl] .columns th{padding-left:20px}.columns th{color:#202124;font-weight:500}.columns code,.columns pre{background:#f1f3f4}.inline:not(.expandable){display:inline}.inline-block{display:inline-block}.block{display:block}img.inline-icon{height:1.2em;vertical-align:sub}.no-select{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.attempt-left,.attempt-right,aside.attempt-left,aside.attempt-right{max-width:-webkit-calc((100% - 40px)/2);max-width:calc((100% - 40px)/2)}.attempt-left,.video-wrapper-left,aside.attempt-left{float:left;margin:0 40px 40px 0}.attempt-right,.video-wrapper,[dir=rtl] .attempt-left,[dir=rtl] .video-wrapper-left,[dir=rtl] aside.attempt-left,aside.attempt-right{float:right;margin:0 0 40px 40px}[dir=rtl] .attempt-right,[dir=rtl] .video-wrapper,[dir=rtl] aside.attempt-right{float:left;margin:0 40px 40px 0}.attempt-left+.attempt-right,.attempt-left+.video-wrapper,.video-wrapper-left+.attempt-right,.video-wrapper-left+.video-wrapper,[dir=rtl] .attempt-left+.attempt-right,[dir=rtl] .attempt-left+.video-wrapper,[dir=rtl] .video-wrapper-left+.attempt-right,[dir=rtl] .video-wrapper-left+.video-wrapper{margin:0 0 40px}.video-wrapper,.video-wrapper-full-width{overflow:hidden;position:relative}.video-wrapper,.video-wrapper-left{width:-webkit-calc((100% - 40px)/2);width:calc((100% - 40px)/2)}.video-wrapper-full-width{margin:16px 0;width:100%}.video-wrapper-full-width embed,.video-wrapper-full-width iframe,.video-wrapper-full-width object,.video-wrapper-left embed,.video-wrapper-left iframe,.video-wrapper-left object,.video-wrapper embed,.video-wrapper iframe,.video-wrapper object{height:101%;left:-.5%;position:absolute;top:-.5%;width:101%}@media screen and (max-width:840px){.attempt-left,.attempt-right,aside.attempt-left,aside.attempt-right{display:block;max-width:100%}.attempt-left,.attempt-right,.video-wrapper,.video-wrapper-left,aside.attempt-left,aside.attempt-right{float:none;margin:16px 0;width:100%}}body[pending] #gc-wrapper{margin-top:0!important}body[ready] #gc-wrapper{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-wrapper{min-height:100vh}body[ready] .devsite-wrapper{min-height:100%;overflow:hidden}@supports ((display:-webkit-flex) or (display:flex)){body[ready] .devsite-wrapper{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}}.full-bleed{margin:0;padding:0}.devsite-book-nav-bg,devsite-book-nav{width:268px}body[pending] devsite-book-nav{position:absolute}@media screen and (max-width:840px){body[devsite-book-nav--open]{overflow:hidden}body[devsite-book-nav--open] devsite-book-nav[fixed]{-webkit-transform:translateZ(0)!important;transform:translateZ(0)!important}}body devsite-toc.devsite-toc{-ms-grid-column:5;grid-column:3;-ms-grid-row:1;grid-row:1;margin:24px 24px 0 0;min-width:0;width:auto}[dir=rtl] body devsite-toc.devsite-toc{margin:24px 0 0 24px}body devsite-toc>.devsite-nav-list{width:auto}.devsite-main-content{margin:0 auto;position:relative;width:100%;z-index:1003}#contain-402{z-index:1004!important}body[pending] .devsite-main-content{min-height:-webkit-calc(100vh - 456px);min-height:calc(100vh - 456px)}body[ready] .devsite-main-content{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;min-height:auto}body[layout=full] .devsite-main-content{max-width:1600px;padding:40px 80px}body[layout=full][type=error] .devsite-main-content{max-width:1600px;padding:0 80px}body[layout=full][type=landing] .devsite-main-content{max-width:none;padding:0}body[layout=docs] .devsite-main-content{display:-ms-grid;display:grid;grid-gap:24px;-ms-grid-columns:1fr 24px minmax(752px,936px) 24px 1fr;grid-template-columns:1fr minmax(752px,936px) 1fr;-ms-grid-rows:1fr;grid-template-rows:1fr}body[layout=docs] .devsite-main-content[has-toc]{-ms-grid-columns:1fr 24px minmax(752px,936px) 24px minmax(160px,1fr);grid-template-columns:1fr minmax(752px,936px) minmax(160px,1fr)}body[layout=docs] .devsite-main-content[has-book-nav]{-ms-grid-columns:minmax(268px,1fr) 24px minmax(752px,936px) 24px 1fr;grid-template-columns:minmax(268px,1fr) minmax(752px,936px) 1fr}body[layout=docs] .devsite-main-content[has-book-nav][has-toc]{-ms-grid-columns:minmax(268px,1fr) 24px minmax(752px,936px) 24px minmax(160px,1fr);grid-template-columns:minmax(268px,1fr) minmax(752px,936px) minmax(160px,1fr)}.devsite-main-content[has-book-nav]~.devsite-footer,.devsite-main-content[has-book-nav]~devsite-footer-promos,.devsite-main-content[has-book-nav]~devsite-footer-utility{margin:0 0 0 268px}[dir=rtl] .devsite-main-content[has-book-nav]~.devsite-footer,[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-promos,[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-utility{margin:0 268px 0 0}@media screen and (max-width:840px){.devsite-main-content[has-book-nav]~.devsite-footer,.devsite-main-content[has-book-nav]~devsite-footer-promos,.devsite-main-content[has-book-nav]~devsite-footer-utility{margin-left:0}[dir=rtl] .devsite-main-content[has-book-nav]~.devsite-footer,[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-promos,[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-utility{margin-right:0}}@media screen and (max-width:1252px){body[layout=docs] .devsite-main-content[has-toc]{-ms-grid-columns:1fr 24px minmax(752px,936px) 24px 1fr;grid-template-columns:1fr minmax(752px,936px) 1fr}body[layout=docs] .devsite-main-content[has-book-nav],body[layout=docs] .devsite-main-content[has-book-nav][has-toc]{-ms-grid-columns:268px 24px 1fr 24px;grid-template-columns:268px 1fr 0}}@media screen and (max-width:840px){body[layout=full] .devsite-main-content{padding:24px}body[layout=full][type=error] .devsite-main-content{padding:0 24px}body[layout=docs] .devsite-main-content{display:block;min-width:100%}devsite-content-footer{padding:0 24px}}@media screen and (max-width:600px){body[layout=full] .devsite-main-content{padding:16px}body[layout=full][type=error] .devsite-main-content,devsite-content-footer{padding:0 16px}}.devsite-icon:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal}.devsite-icon-arrow-drop-down:before{content:"arrow_drop_down"}.devsite-icon-code:before{content:"code"}.devsite-icon-code-dark:before,.devsite-icon-code-light:before{content:"brightness_medium"}.devsite-icon-copy:before{content:"content_copy"}aside{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] aside{padding-left:60px}[dir=rtl] aside{padding-right:60px}body[layout=full]:not([type=landing]) aside{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) aside{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) aside{padding-right:calc(50vw - 50% + 36px)}aside:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] aside:before{float:left;margin-left:-36px}[dir=rtl] aside:before{float:right;margin-right:-36px}aside :link,aside :visited{text-decoration:underline}aside a:focus,aside a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}aside pre{background:hsla(0,0%,100%,.75)}aside code{font-weight:700;padding:0}.caution{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .caution{padding-left:60px}[dir=rtl] .caution{padding-right:60px}body[layout=full]:not([type=landing]) .caution{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .caution{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .caution{padding-right:calc(50vw - 50% + 36px)}.caution:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .caution:before{float:left;margin-left:-36px}[dir=rtl] .caution:before{float:right;margin-right:-36px}.caution :link,.caution :visited{text-decoration:underline}.caution a:focus,.caution a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.caution pre{background:hsla(0,0%,100%,.75)}.caution code{font-weight:700;padding:0}.dogfood{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .dogfood{padding-left:60px}[dir=rtl] .dogfood{padding-right:60px}body[layout=full]:not([type=landing]) .dogfood{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .dogfood{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .dogfood{padding-right:calc(50vw - 50% + 36px)}.dogfood:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .dogfood:before{float:left;margin-left:-36px}[dir=rtl] .dogfood:before{float:right;margin-right:-36px}.dogfood :link,.dogfood :visited{text-decoration:underline}.dogfood a:focus,.dogfood a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.dogfood pre{background:hsla(0,0%,100%,.75)}.dogfood code{font-weight:700;padding:0}.key-point{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .key-point{padding-left:60px}[dir=rtl] .key-point{padding-right:60px}body[layout=full]:not([type=landing]) .key-point{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .key-point{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .key-point{padding-right:calc(50vw - 50% + 36px)}.key-point:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .key-point:before{float:left;margin-left:-36px}[dir=rtl] .key-point:before{float:right;margin-right:-36px}.key-point :link,.key-point :visited{text-decoration:underline}.key-point a:focus,.key-point a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.key-point pre{background:hsla(0,0%,100%,.75)}.key-point code{font-weight:700;padding:0}.key-term{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .key-term{padding-left:60px}[dir=rtl] .key-term{padding-right:60px}body[layout=full]:not([type=landing]) .key-term{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .key-term{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .key-term{padding-right:calc(50vw - 50% + 36px)}.key-term:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .key-term:before{float:left;margin-left:-36px}[dir=rtl] .key-term:before{float:right;margin-right:-36px}.key-term :link,.key-term :visited{text-decoration:underline}.key-term a:focus,.key-term a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.key-term pre{background:hsla(0,0%,100%,.75)}.key-term code{font-weight:700;padding:0}.note{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .note{padding-left:60px}[dir=rtl] .note{padding-right:60px}body[layout=full]:not([type=landing]) .note{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .note{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .note{padding-right:calc(50vw - 50% + 36px)}.note:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .note:before{float:left;margin-left:-36px}[dir=rtl] .note:before{float:right;margin-right:-36px}.note :link,.note :visited{text-decoration:underline}.note a:focus,.note a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.note pre{background:hsla(0,0%,100%,.75)}.note code{font-weight:700;padding:0}.objective{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .objective{padding-left:60px}[dir=rtl] .objective{padding-right:60px}body[layout=full]:not([type=landing]) .objective{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .objective{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .objective{padding-right:calc(50vw - 50% + 36px)}.objective:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .objective:before{float:left;margin-left:-36px}[dir=rtl] .objective:before{float:right;margin-right:-36px}.objective :link,.objective :visited{text-decoration:underline}.objective a:focus,.objective a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.objective pre{background:hsla(0,0%,100%,.75)}.objective code{font-weight:700;padding:0}.special{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .special{padding-left:60px}[dir=rtl] .special{padding-right:60px}body[layout=full]:not([type=landing]) .special{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .special{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .special{padding-right:calc(50vw - 50% + 36px)}.special:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .special:before{float:left;margin-left:-36px}[dir=rtl] .special:before{float:right;margin-right:-36px}.special :link,.special :visited{text-decoration:underline}.special a:focus,.special a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.special pre{background:hsla(0,0%,100%,.75)}.special code{font-weight:700;padding:0}.success{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .success{padding-left:60px}[dir=rtl] .success{padding-right:60px}body[layout=full]:not([type=landing]) .success{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .success{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .success{padding-right:calc(50vw - 50% + 36px)}.success:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .success:before{float:left;margin-left:-36px}[dir=rtl] .success:before{float:right;margin-right:-36px}.success :link,.success :visited{text-decoration:underline}.success a:focus,.success a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.success pre{background:hsla(0,0%,100%,.75)}.success code{font-weight:700;padding:0}.warning{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .warning{padding-left:60px}[dir=rtl] .warning{padding-right:60px}body[layout=full]:not([type=landing]) .warning{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .warning{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .warning{padding-right:calc(50vw - 50% + 36px)}.warning:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .warning:before{float:left;margin-left:-36px}[dir=rtl] .warning:before{float:right;margin-right:-36px}.warning :link,.warning :visited{text-decoration:underline}.warning a:focus,.warning a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.warning pre{background:hsla(0,0%,100%,.75)}.warning code{font-weight:700;padding:0}@media screen and (max-width:600px){body[layout=full] aside{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] aside{padding-left:76px}[dir=rtl] body[layout=full] aside{padding-right:76px}body[layout=full] .caution{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .caution{padding-left:76px}[dir=rtl] body[layout=full] .caution{padding-right:76px}body[layout=full] .dogfood{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .dogfood{padding-left:76px}[dir=rtl] body[layout=full] .dogfood{padding-right:76px}body[layout=full] .key-point{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .key-point{padding-left:76px}[dir=rtl] body[layout=full] .key-point{padding-right:76px}body[layout=full] .key-term{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .key-term{padding-left:76px}[dir=rtl] body[layout=full] .key-term{padding-right:76px}body[layout=full] .note{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .note{padding-left:76px}[dir=rtl] body[layout=full] .note{padding-right:76px}body[layout=full] .objective{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .objective{padding-left:76px}[dir=rtl] body[layout=full] .objective{padding-right:76px}body[layout=full] .special{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .special{padding-left:76px}[dir=rtl] body[layout=full] .special{padding-right:76px}body[layout=full] .success{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .success{padding-left:76px}[dir=rtl] body[layout=full] .success{padding-right:76px}body[layout=full] .warning{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .warning{padding-left:76px}[dir=rtl] body[layout=full] .warning{padding-right:76px}}aside var{background:inherit;font-weight:700;padding:0}.note,.note :link,.note :visited,.note code,.special,.special :link,.special :visited,.special code,aside,aside :link,aside :visited,aside code{background:#e1f5fe;color:#01579b}.note:before,.special:before,aside:before{content:"star"}.caution,.caution :link,.caution :visited,.caution code{background:#feefe3;color:#bf360c}.caution:before{content:"error"}.dogfood,.dogfood :link,.dogfood :visited,.dogfood code{background:#eceff1;color:#546e7a}.dogfood:before{content:"pets"}.key-point,.key-point :link,.key-point :visited,.key-point code{background:#e8eaf6;color:#3f51b5}.key-point:before{content:"lightbulb_outline"}.key-term,.key-term :link,.key-term :visited,.key-term code{background:#f3e8fd;color:#9334e6}.key-term:before{content:"font_download"}.objective,.objective :link,.objective :visited,.objective code,.success,.success :link,.success :visited,.success code{background:#e0f2f1;color:#00796b}.objective:before{content:"school"}.success:before{content:"check_circle"}.warning,.warning :link,.warning :visited,.warning code{background:#fce8e6;color:#d50000}.warning:before{content:"warning"}.caution :focus code,.caution :hover code,.dogfood :focus code,.dogfood :hover code,.key-point :focus code,.key-point :hover code,.key-term :focus code,.key-term :hover code,.note :focus code,.note :hover code,.objective :focus code,.objective :hover code,.special :focus code,.special :hover code,.success :focus code,.success :hover code,.warning :focus code,.warning :hover code,aside :focus code,aside :hover code{background:transparent}.devsite-no-page-title>.caution:first-child,.devsite-no-page-title>.dogfood:first-child,.devsite-no-page-title>.key-point:first-child,.devsite-no-page-title>.key-term:first-child,.devsite-no-page-title>.note:first-child,.devsite-no-page-title>.objective:first-child,.devsite-no-page-title>.special:first-child,.devsite-no-page-title>.success:first-child,.devsite-no-page-title>.warning:first-child,.devsite-no-page-title>aside:first-child{clear:right}.devsite-banner{font-size:14px}.devsite-banner :link,.devsite-banner :visited{text-decoration:underline}body[layout=full] .devsite-banner{margin:-40px calc(50% - 50vw) 40px}body[type=landing][layout] .devsite-banner{margin:0}body[layout=docs] .devsite-banner{margin:-40px -40px 40px}.devsite-banner-message{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin:0 auto;max-width:1520px;padding:20px 40px}body[layout=full] .devsite-banner-message,body[type=landing] .devsite-banner-message,body[type=landing][layout=docs] .devsite-banner-message{padding:20px 40px}[dir=ltr] .devsite-banner-message-text{margin-right:auto}[dir=rtl] .devsite-banner-message-text{margin-left:auto}.devsite-banner[background] a:not(.button):focus,.devsite-banner a:not(.button):focus{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.devsite-banner[background=white]{border-bottom:1px solid #dadce0}.devsite-banner-announcement,.devsite-banner-announcement :link,.devsite-banner-announcement :visited{background:#e1f5fe}.devsite-banner-announcement[background] :link,.devsite-banner-announcement[background] :visited{background:0}.devsite-banner-confidential{background:#feefe3;color:#bf360c}.devsite-banner-confidential .devsite-banner-message:before{content:"warning";font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal}[dir=ltr] .devsite-banner-confidential .devsite-banner-message:before{margin-right:16px}[dir=rtl] .devsite-banner-confidential .devsite-banner-message:before{margin-left:16px}.devsite-banner-translated{background:#f1f3f4;color:rgba(0,0,0,.65)}.devsite-banner-translated :link{text-decoration:none}.devsite-banner .button,.devsite-banner button{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;margin:-6px 0;text-decoration:none}[dir=ltr] .devsite-banner .button,[dir=ltr] .devsite-banner button{margin-left:16px}[dir=rtl] .devsite-banner .button,[dir=rtl] .devsite-banner button{margin-right:16px}.devsite-banner .material-icons{margin:-2px 0;vertical-align:middle}.devsite-banner-translated-image{margin:4px 0 -4px;width:122px}[dir=ltr] .devsite-banner-translated-image{margin-right:24px}[dir=rtl] .devsite-banner-translated-image{margin-left:24px}.devsite-banner-heading{font-weight:700}@media screen and (max-width:1252px){.devsite-banner-translated .devsite-banner-translated-text{display:block}}@media screen and (max-width:840px){body[layout=docs] .devsite-banner,body[layout=full] .devsite-banner{margin:-24px -24px 24px}.devsite-banner-message,body[layout] .devsite-banner-message,body[layout][type] .devsite-banner-message,body[type] .devsite-banner-message{padding:20px 24px}body[layout=full] .devsite-banner{margin-bottom:40px}}@media screen and (max-width:600px){body[layout=docs] .devsite-banner,body[layout=full] .devsite-banner{margin:-16px -16px 16px}body[layout=full] .devsite-banner{margin-bottom:40px}.devsite-banner-message,body[layout] .devsite-banner-message,body[layout][type] .devsite-banner-message,body[type] .devsite-banner-message{display:block;padding:16px}[dir] .devsite-banner .button,[dir] .devsite-banner button{margin:12px 0 0}}.devsite-card-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:-24px 0 0 -24px}[dir=rtl] .devsite-card-group{margin:-24px -24px 0 0}.devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 33.333%;flex:0 0 33.333%;min-width:0;padding:24px 0 0 24px}[dir=rtl] .devsite-card-wrapper{padding:24px 24px 0 0}.devsite-card-wrapper[hidden]{display:none}.devsite-card-list-link,.devsite-card h3{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:-.01em}.devsite-card h3{margin:0 0 10px}.devsite-card{background:#fff;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;height:100%;position:relative}.devsite-card-image-bg{background-position:50%;background-repeat:no-repeat;-o-background-size:cover;background-size:cover;padding:0 0 56.25%}.devsite-card-content-wrapper{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-card-content{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;padding:16px;word-break:break-word}.devsite-card-category{font:700 12px/22px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.3px;margin-top:0;text-transform:uppercase}.devsite-card-summary{-webkit-box-orient:vertical;-webkit-line-clamp:4;display:-webkit-box;font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:16px 0 0;max-height:96px;overflow:hidden}.devsite-card-author{border-top:1px solid #dadce0;-webkit-box-sizing:content-box;box-sizing:content-box;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;font-size:12px;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;line-height:16px;min-height:40px;position:relative}.devsite-card-author,.devsite-card-buttons{padding:16px}.devsite-card-author-date,.devsite-card-author-name{margin:0}.devsite-card-author-name+.devsite-card-author-date{margin-top:8px}.devsite-card-author+.devsite-card-buttons,.devsite-card-content+.devsite-card-buttons{padding-top:0}.devsite-card-buttons{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-webkit-flex-direction:row-reverse;flex-direction:row-reverse;margin:auto 0 0}.devsite-card-list{list-style:none;padding:0}.devsite-card-list-item{-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}.devsite-card-list-item:not(:last-child){margin:0 0 20px;padding:0}.devsite-card-list-link{-webkit-box-align:center;-webkit-align-items:center;align-items:center;color:#1a73e8;display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;padding:16px 20px;-webkit-transition:background .2s,color .2s;-o-transition:background .2s,color .2s;transition:background .2s,color .2s;width:100%}.devsite-card-list-link:focus,.devsite-card-list-link:hover{background:#e4eefc}.devsite-card-list-link:focus{text-decoration:none}.devsite-card-list-link:after{content:"arrow_forward";font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin-left:auto;opacity:0;-webkit-transform:translateX(-20px);-o-transform:translateX(-20px);transform:translateX(-20px);-webkit-transition:opacity .2s,-webkit-transform .2s;transition:opacity .2s,-webkit-transform .2s;-o-transition:opacity .2s,-o-transform .2s;transition:opacity .2s,transform .2s;transition:opacity .2s,transform .2s,-webkit-transform .2s,-o-transform .2s}.devsite-card-list-link:focus:after,.devsite-card-list-link:hover:after{opacity:1;-webkit-transform:translateX(0);-o-transform:translateX(0);transform:translateX(0)}@media screen and (max-width:840px){.devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%}}@media screen and (max-width:600px){.devsite-card-group{margin:-16px 0 0 -16px}[dir=rtl] .devsite-card-group{margin:-16px -16px 0 0}.devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;padding:16px 0 0 16px}[dir=rtl] .devsite-card-wrapper{padding:16px 16px 0 0}.devsite-card-summary{font-size:14px;line-height:20px;max-height:80px}}.pre-style,code,pre{background:#f1f3f4;color:#37474f;font:400 100%/1 Roboto Mono,monospace;padding:1px 4px}code{font:500 90%/1 Roboto Mono,monospace;word-break:break-word}.pre-style code,pre code,table code{font-weight:400;word-break:normal}.pre-style,pre{font:14px/20px Roboto Mono,monospace;margin:16px 0;overflow-x:auto;padding:24px;position:relative}.pre-style code,pre code{background:0;font-size:14px;padding:0}b code,strong code{font-weight:700}pre.devsite-code-highlight>span{opacity:.54}td>pre:only-child{padding:0}td>devsite-code:only-child pre,td>devsite-code pre.inline-code{padding:0 64px 0 0}td>devsite-code:not([dark-code]):only-child pre,td>devsite-code pre.inline-code{background:0}td>devsite-code:only-child pre~.devsite-code-buttons-container,td>devsite-code pre.inline-code~.devsite-code-buttons-container{top:-6px}h1 code,h2 code,h3 code,h4 code,h5 code,h6 code{background:0;color:#212121;padding:0}h1 code{color:#757575}a code,td a code{color:#1967d2}body[layout] .devsite-main-content var span,var,var code{color:#ec407a;-moz-osx-font-smoothing:auto;-webkit-font-smoothing:auto;font-weight:700}pre.clear-for-copy{all:initial;left:-99999px;position:absolute;top:-99999px;white-space:pre}pre.clear-for-copy *{all:unset;font-family:Roboto Mono,monospace;white-space:pre}fieldset{border:0;margin:0;padding:0}label{color:#5f6368;display:block;font-size:12px}input+label{color:#202124;display:inline;font-size:16px}label[for]{cursor:pointer}input[type=checkbox],input[type=radio]{-webkit-appearance:none;background:#fff;-webkit-border-radius:2px;border-radius:2px;cursor:pointer;font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;height:18px;margin:-2px 8px 2px 0;outline:0;position:relative;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;vertical-align:middle;width:18px}[dir=rtl] input[type=checkbox],[dir=rtl] input[type=radio]{margin:-2px 0 2px 8px}input[type=checkbox]{color:#5f6368}input[type=radio]{-webkit-border-radius:50%;border-radius:50%;color:#5f6368;-webkit-transition:none;-o-transition:none;transition:none}input[type=checkbox]:focus:before,input[type=radio]:focus:before{background:#e8eaed}input[type=checkbox]:checked,input[type=checkbox]:indeterminate,input[type=radio]:checked{color:#1a73e8}input[type=checkbox]:checked:focus:before,input[type=checkbox]:indeterminate:focus:before,input[type=radio]:focus:before{background:#d2e3fc}input[type=checkbox]:after,input[type=radio]:after{content:"check_box_outline_blank";position:relative;right:3px;top:-3px;z-index:1}[dir=rtl] input[type=checkbox]:after,[dir=rtl] input[type=radio]:after{left:3px;right:auto}input[type=checkbox]:checked:after{content:"check_box"}input[type=checkbox]:indeterminate:after{content:"indeterminate_check_box"}input[type=radio]:after{content:"radio_button_unchecked"}input[type=radio]:checked:after{content:"radio_button_checked"}input[type=checkbox]:before,input[type=radio]:before{-webkit-border-radius:50%;border-radius:50%;content:"";display:block;height:36px;left:-9px;position:absolute;top:-9px;-webkit-transition:background .2s;-o-transition:background .2s;transition:background .2s;width:36px}[dir=rtl] input[type=checkbox]:before,[dir=rtl] input[type=radio]:before{left:auto;right:-9px}input:disabled+label,input[type=checkbox]:disabled,input[type=radio]:disabled{color:#bdc1c6;cursor:default}input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select[multiple],select[size],textarea{border:1px solid #e8eaed;-webkit-border-radius:2px;border-radius:2px;color:#202124;font:16px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0;max-width:100%;outline:0;padding:7px;-webkit-transition:border-color .2s;-o-transition:border-color .2s;transition:border-color .2s;vertical-align:middle}input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus,select[multiple]:focus,select[size]:focus,textarea:focus{border-bottom:2px solid #1a73e8;padding-bottom:6px}input[type=date]:disabled,input[type=datetime-local]:disabled,input[type=datetime]:disabled,input[type=email]:disabled,input[type=month]:disabled,input[type=number]:disabled,input[type=password]:disabled,input[type=search]:disabled,input[type=tel]:disabled,input[type=text]:disabled,input[type=time]:disabled,input[type=url]:disabled,input[type=week]:disabled,select[multiple]:disabled,select[size]:disabled,textarea:disabled{background:#f1f3f4}body input[type=file]{height:auto;line-height:1;padding:8px 16px}select{border:1px solid #e8eaed;-webkit-border-radius:2px;border-radius:2px;padding:0 27px 0 7px;-moz-appearance:none;-webkit-appearance:none;background:#fff url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='4' viewBox='0 0 20 4'><path d='M0,0l4,4l4-4H0z' fill='%23212121'/></svg>") no-repeat 100%;-webkit-box-shadow:none;box-shadow:none;color:#202124;cursor:pointer;display:inline-block;font:500 14px/36px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:36px;line-height:34px;max-width:256px;min-width:72px;outline:0;overflow:hidden;text-align:left;text-indent:.01px;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:background-color .2s;-o-transition:background-color .2s;transition:background-color .2s;vertical-align:middle;white-space:nowrap}select:focus,select:hover{background-color:#f1f3f4}select:active{background-color:#e8eaed}select:disabled{background:#f1f3f4 url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='4' viewBox='0 0 20 4'><path d='M0,0l4,4l4-4H0z' fill='%23bdbdbd'/></svg>") no-repeat 100%;border-color:transparent;color:#bdc1c6;cursor:default}select::-ms-expand{display:none}devsite-book-nav .devsite-breadcrumb-list,devsite-content .devsite-breadcrumb-list,devsite-header .devsite-breadcrumb-list{-webkit-box-align:center;-webkit-align-items:center;align-items:center;padding:0;white-space:nowrap}.devsite-search-project .devsite-breadcrumb-list,devsite-book-nav .devsite-breadcrumb-list,devsite-header .devsite-breadcrumb-list{display:-webkit-box;display:-webkit-flex;display:flex}devsite-content .devsite-breadcrumb-list{display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-flex-wrap:wrap;flex-wrap:wrap;font-size:13px}body[layout=full] devsite-content .devsite-breadcrumb-list,body[type=landing] devsite-content .devsite-breadcrumb-list{display:none}devsite-book-nav .devsite-breadcrumb-item,devsite-content .devsite-breadcrumb-item,devsite-header .devsite-breadcrumb-item{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin:0}devsite-book-nav .devsite-breadcrumb-guillemet,devsite-header .devsite-breadcrumb-guillemet{font-size:24px;margin:0 4px;width:24px}devsite-content .devsite-breadcrumb-guillemet{color:#5f6368;font-size:18px;margin:0 4px;width:18px}devsite-book-nav .devsite-breadcrumb-guillemet:before,devsite-content .devsite-breadcrumb-guillemet:before,devsite-header .devsite-breadcrumb-guillemet:before{content:"chevron_right"}[dir=rtl] devsite-book-nav .devsite-breadcrumb-guillemet:before,[dir=rtl] devsite-content .devsite-breadcrumb-guillemet:before,[dir=rtl] devsite-header .devsite-breadcrumb-guillemet:before{content:"chevron_left"}devsite-book-nav .devsite-breadcrumb-link,devsite-content .devsite-breadcrumb-link,devsite-header .devsite-breadcrumb-link{display:inline-block;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-book-nav .devsite-breadcrumb-link:focus,devsite-book-nav .devsite-breadcrumb-link:hover,devsite-header .devsite-breadcrumb-link:focus,devsite-header .devsite-breadcrumb-link:hover{text-decoration:none}devsite-content .devsite-breadcrumb-link{color:#5f6368}devsite-content .devsite-breadcrumb-link:focus,devsite-content .devsite-breadcrumb-link:hover{color:#1a73e8;text-decoration:none}.devsite-nav{font-size:13px}.devsite-nav-list,.devsite-nav-responsive-tabs,.devsite-nav-section{list-style-type:none;padding:0}.devsite-nav-item{line-height:16px;margin:0}.devsite-nav-title{color:#202124;display:-webkit-box;display:-webkit-flex;display:flex;padding:4px 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.devsite-nav-title[href]:focus,.devsite-nav-title[href]:hover{color:#1a73e8;text-decoration:none}.devsite-nav-heading>.devsite-nav-title{color:rgba(0,0,0,.65);font-weight:700}.devsite-nav-active{font-weight:500}.devsite-nav-active,.devsite-nav-active.devsite-nav-title,.devsite-nav-active.devsite-nav-title>.devsite-nav-icon:before,.devsite-nav-deprecated .devsite-nav-active.devsite-nav-title{color:#1a73e8}.devsite-nav-text{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}.devsite-nav-text>span{pointer-events:none}.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-title-no-path:focus,.devsite-nav-title-no-path:focus{color:#1a73e8}.devsite-nav-icon{cursor:default;font-size:18px;margin:-1px 0 -1px 4px}[dir=rtl] .devsite-nav-icon{margin:-1px 4px -1px 0}.devsite-nav-icon:before{color:#5f6368;content:"info"}.devsite-nav-icon[data-icon=alpha]:before,.devsite-nav-icon[data-icon=beta]:before,.devsite-nav-icon[data-icon=experimental]:before{content:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 18 18'><path d='M15.78,13.39L11,7V4h2V2H5v2h2v3l-4.9,6.53c-0.34,0.47-0.39,1.1-0.12,1.62C2.24,15.67,2.77,16,3.36,16h11.28 c0.86,0,1.56-0.7,1.56-1.56C16.2,14.04,16.03,13.67,15.78,13.39z' fill='%2380868b'/></svg>")}.devsite-nav-deprecated.devsite-nav-accordion .devsite-nav-title,.devsite-nav-deprecated .devsite-nav-title,.devsite-nav-icon[data-icon=deprecated]:before{color:#bdc1c6}.devsite-nav-icon[data-icon=deprecated]:before{content:"not_interested"}.devsite-nav-icon[data-icon=external]:before{content:"open_in_new"}[dir=rtl] .devsite-nav-icon[data-icon=external]:before{display:inline-block;-webkit-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.devsite-nav-icon[data-icon=forward]:before{content:"arrow_forward";cursor:pointer}[dir=rtl] .devsite-nav-icon[data-icon=forward]:before{content:"arrow_back"}.devsite-nav-icon[data-icon=limited]:before{content:"verified_user"}.devsite-nav-icon[data-icon=new]:before{content:"new_releases"}.devsite-nav-icon[data-icon=nightly]:before{content:"nights_stay"}.button,.devsite-footer-utility-button>a,button,input[type=button],input[type=file],input[type=image],input[type=reset],input[type=submit]{-moz-appearance:none;-webkit-appearance:none;background:#fff;-webkit-box-sizing:border-box;box-sizing:border-box;color:#1a73e8;cursor:pointer;display:inline-block;height:36px;margin:0;min-width:36px;outline:0;overflow:hidden;text-decoration:none;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:background-color .2s,border .2s,-webkit-box-shadow .2s;transition:background-color .2s,border .2s,-webkit-box-shadow .2s;-o-transition:background-color .2s,border .2s,box-shadow .2s;transition:background-color .2s,border .2s,box-shadow .2s;transition:background-color .2s,border .2s,box-shadow .2s,-webkit-box-shadow .2s;vertical-align:middle;white-space:nowrap;border:0;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);font:500 14px/36px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0 16px;text-transform:uppercase}.button:focus,.button:hover,.devsite-footer-utility-button>a:focus,.devsite-footer-utility-button>a:hover,button:focus,button:hover,input[type=button]:focus,input[type=button]:hover,input[type=file]:focus,input[type=file]:hover,input[type=image]:focus,input[type=image]:hover,input[type=reset]:focus,input[type=reset]:hover,input[type=submit]:focus,input[type=submit]:hover{background:#e4eefc}.button:active,.devsite-footer-utility-button>a:active,button:active,input[type=button]:active,input[type=file]:active,input[type=image]:active,input[type=reset]:active,input[type=submit]:active{background:#c8ddf9;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}.button.button-disabled,.button.button-disabled:active,.button.button-disabled:focus,.button.button-disabled:hover,[foreground] .button.button-disabled,[foreground] .button.button-disabled:active,[foreground] .button.button-disabled:focus,[foreground] .button.button-disabled:hover,body[theme] [foreground] .button.button-disabled,body[theme] [foreground] .button.button-disabled:active,body[theme] [foreground] .button.button-disabled:focus,body[theme] [foreground] .button.button-disabled:hover,button[disabled],button[disabled]:active,button[disabled]:focus,button[disabled]:hover,input[type=button][disabled],input[type=button][disabled]:active,input[type=button][disabled]:focus,input[type=button][disabled]:hover,input[type=file][disabled],input[type=file][disabled]:active,input[type=file][disabled]:focus,input[type=file][disabled]:hover,input[type=image][disabled],input[type=image][disabled]:active,input[type=image][disabled]:focus,input[type=image][disabled]:hover,input[type=reset][disabled],input[type=reset][disabled]:active,input[type=reset][disabled]:focus,input[type=reset][disabled]:hover,input[type=submit][disabled],input[type=submit][disabled]:active,input[type=submit][disabled]:focus,input[type=submit][disabled]:hover{-webkit-box-shadow:none;box-shadow:none;cursor:default;pointer-events:none;background:#ddd;color:rgba(0,0,0,.26)}.button-blue,.button-green,.button-primary,.button-red,body devsite-footer-utility .devsite-footer-utility-button>a{background:#1a73e8;color:#fff}.button-blue:focus,.button-blue:hover,.button-green:focus,.button-green:hover,.button-primary:focus,.button-primary:hover,.button-red:focus,.button-red:hover,body devsite-footer-utility .devsite-footer-utility-button>a:focus,body devsite-footer-utility .devsite-footer-utility-button>a:hover{background:#1765cc;color:#fff}.button-blue:active,.button-green:active,.button-primary:active,.button-red:active,body devsite-footer-utility .devsite-footer-utility-button>a:active{background:#0277bd;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-primary{background:#fff;color:#1a73e8}[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-primary:focus,[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-primary:hover{background:#e4eefc}[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-primary:active{background:#c8ddf9}.button-white{background:0;color:#1a73e8;padding:0 8px}.button-white,.button-white:active,.button-white:focus,.button-white:hover{border:0;-webkit-box-shadow:none;box-shadow:none}.button-white.button-disabled,.button-white[disabled]{background:0}.button-raised{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}.button-raised:focus,.button-raised:hover{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}.button-raised:active{-webkit-box-shadow:0 1px 3px 0 rgba(60,64,67,.3),0 4px 8px 3px rgba(60,64,67,.15);box-shadow:0 1px 3px 0 rgba(60,64,67,.3),0 4px 8px 3px rgba(60,64,67,.15)}.button+.button,button+button,input[type=button]+input[type=button],input[type=file]+input[type=file],input[type=image]+input[type=image],input[type=reset]+input[type=reset],input[type=submit]+input[type=submit]{margin-left:16px}[dir=rtl] .button+.button,[dir=rtl] button+button,[dir=rtl] input[type=button]+input[type=button],[dir=rtl] input[type=file]+input[type=file],[dir=rtl] input[type=image]+input[type=image],[dir=rtl] input[type=reset]+input[type=reset],[dir=rtl] input[type=submit]+input[type=submit]{margin-left:0;margin-right:16px}.button-flat+.button-flat,.button-white+.button-white,button+.button{margin-left:8px}[dir=rtl] .button-flat+.button-flat,[dir=rtl] .button-white+.button-white,[dir=rtl] button+.button{margin-left:0;margin-right:8px}.button:focus{text-decoration:none}.button-flat{padding:0 8px}.button-flat,.button-flat:active,.button-flat:focus,.button-flat:hover{background:0;border:0;-webkit-box-shadow:none;box-shadow:none}.button-flat:disabled{background-color:transparent}.button-transparent{padding:0 8px}.button-transparent,.button-transparent:focus,.button-transparent:hover{background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.button-text-white{color:#fff}.button-text-blue{color:#1a73e8}.button-lowercase{text-transform:none}.button-unindented{margin-left:-8px}[dir=rtl] .button-unindented{margin-left:0;margin-right:-8px}.button-icon,.button>.material-icons,button>.material-icons{font-size:18px;height:18px;width:18px}.button.button-with-icon,.button.external{padding:0 16px}.button>.material-icons,button>.material-icons{margin:0 8px;position:relative;top:-2px;vertical-align:middle}.button>.button-icon,button>.button-icon{margin:0 8px}.button-with-icon>.button-icon,.button-with-icon>.material-icons{margin:0 8px 0 -4px}.button-with-icon>.icon-after,.button.external:not(.button-with-icon):after,[dir=rtl] .button-with-icon>.button-icon,[dir=rtl] .button-with-icon>.material-icons,button.external:not(.button-with-icon):after{margin:0 -4px 0 8px}[dir=rtl] .button-with-icon>.icon-after,[dir=rtl] .button.external:not(.button-with-icon):after,[dir=rtl] button.external:not(.button-with-icon):after{margin:0 8px 0 -4px}.button:not(.button-with-icon)>.material-icons:not(.icon-after){margin-left:-4px}.button:not(.button-with-icon)>.icon-after,[dir=rtl] .button:not(.button-with-icon)>.material-icons:not(.icon-after){margin-right:-4px}[dir=rtl] .button:not(.button-with-icon)>.icon-after{margin-left:-4px}.button-white:not(.button-with-icon)>.material-icons:not(.icon-after){margin-left:4px}.button-white:not(.button-with-icon)>.icon-after,[dir=rtl] .button-white:not(.button-with-icon)>.material-icons:not(.icon-after){margin-right:4px}[dir=rtl] .button-white:not(.button-with-icon)>.icon-after{margin-left:4px}[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-white:hover{background:rgba(154,160,166,.3)}[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-white:focus{background:rgba(154,160,166,.5)}.devsite-landing-row-item[foreground=grey] .button,[foreground=grey] .button{background:#5f6368}.devsite-landing-row-item[foreground=grey] .button:active,.devsite-landing-row-item[foreground=grey] .button:focus,.devsite-landing-row-item[foreground=grey] .button:hover,[foreground=grey] .button:active,[foreground=grey] .button:focus,[foreground=grey] .button:hover{background:#3c4043}devsite-header .button,devsite-header .button:active,devsite-header .button:focus,devsite-header .button:hover{-webkit-box-shadow:none;box-shadow:none}h1,h2,h3,h4,h5,h6{outline:0}[layout=docs] h1,[layout=docs] h2,[layout=docs] h3,[layout=docs] h4,[layout=docs] h5,[layout=docs] h6{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}.devsite-article h1:first-of-type{margin-top:0;position:relative;top:-4px}.devsite-landing-row-large-headings .devsite-landing-row-item-description h3,.devsite-landing-row h2,h1{color:#5f6368;font:300 34px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:-.01em;margin:40px 0 20px}.devsite-landing-row .devsite-catalog-alphabet-letter-heading h2{margin:20px 0}[layout=docs] h2{border-bottom:1px solid #e8eaed;padding-bottom:3px}.devsite-landing-row h3,h2{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:-.01em;margin:40px 0 20px}h3{margin:32px 0 16px}.devsite-landing-row-item-no-media h3,.devsite-landing-row h4,h3{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.devsite-landing-row-item-no-media h3,.devsite-landing-row h4{margin:32px 0 12px;padding:0}.devsite-landing-row-large-headings .devsite-landing-row-item-list h4{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:-.01em}h4,h5,h6{margin:32px 0 16px}h4{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}h5{font:700 14px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}h6{font:500 14px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}h1+dl>dt:first-child,h1+h1,h1+h2,h1+h3,h1+h4,h1+h5,h1+h6,h2+dl>dt:first-child,h2+h1,h2+h2,h2+h3,h2+h4,h2+h5,h2+h6,h3+dl>dt:first-child,h3+h1,h3+h2,h3+h3,h3+h4,h3+h5,h3+h6,h4+dl>dt:first-child,h4+h1,h4+h2,h4+h3,h4+h4,h4+h5,h4+h6,h5+dl>dt:first-child,h5+h1,h5+h2,h5+h3,h5+h4,h5+h5,h5+h6,h6+dl>dt:first-child,h6+h1,h6+h2,h6+h3,h6+h4,h6+h5,h6+h6{margin-top:0}@media screen and (max-width:600px){.devsite-landing-row-large-headings .devsite-landing-row-item-description h3,.devsite-landing-row h2,h1{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}table{border:0;border-collapse:collapse;border-spacing:0;font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:16px 0 15px;width:100%}caption{background:#f1f3f4;font-weight:500;padding:12px 8px;text-align:center}tr{border:0;border-bottom:1px solid #dadce0}tr:first-child{border-top:1px solid #dadce0}td,th{border:0;margin:0;text-align:left}[dir=rtl] td,[dir=rtl] th{text-align:right}th{height:48px;padding:8px;vertical-align:middle}th>devsite-heading>h2,th>devsite-heading>h3,th>h2,th>h3{border:0;font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0!important;padding:0!important}td>.expandable>h2.showalways,td>.expandable>h3.showalways,td>h2:only-child,td>h3:only-child{border:0;font:500 14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0;padding-bottom:0}td>.expandable>h2.showalways,td>.expandable>h3.showalways{line-height:24px}td b,td strong,th b,th strong{font-weight:500}td,td code{padding:7px 8px 8px}td code,th code{background:0;font:500 100%/1 Roboto Mono,monospace;-moz-osx-font-smoothing:auto;-webkit-font-smoothing:auto;padding:0}td pre code{color:#37474f;font-weight:400;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased}table.responsive td,table.responsive th{min-width:214px}table.responsive table:not(.responsive) td,table.responsive table:not(.responsive) th{min-width:120px}table.responsive td code,table.responsive th code{word-break:break-all;word-break:break-word}table.responsive td tr:not(.alt) td:first-child,table.responsive tr:not(.alt) td td:first-child,td{background:hsla(0,0%,100%,.95);vertical-align:top}.devsite-table-wrapper{margin:16px 0;overflow:auto}.devsite-table-wrapper .devsite-table-wrapper{margin:0;overflow:visible}.devsite-table-wrapper table{margin:0}.devsite-table-wrapper .devsite-table-wrapper table{margin:16px 0}table.responsive table.responsive{margin:0}table.responsive td tr:first-child td{padding-top:0}table.responsive td tr:last-child td{padding-bottom:0}[dir=ltr] table.responsive td td:first-child{padding-left:0}[dir=rtl] table.responsive td td:first-child{padding-right:0}table.responsive>*>tr>th:not(:first-child),table.responsive>tr>th:not(:first-child){display:none}table.columns tr{border:0}table table tr:first-child{border-top:0}devsite-selector .devsite-table-wrapper:last-child tr:last-child,table table tr:last-child{border-bottom:0}th,th code{background:#e8eaed;color:#202124;font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}table.vertical-rules tr>td:not(:last-child),table.vertical-rules tr>th:not(:last-child){border-right:1px solid #dadce0}.alt td,td.alt{background:rgba(241,243,244,.75)}table.responsive>*>tr:not(.alt) td:first-child,table.responsive>tr:not(.alt) td:first-child{background:hsla(0,0%,96.5%,.87)}table.blue tr{background:#e8eaf6;border-bottom-color:#c5cae9}table.cyan tr{background:#e4f7fb;border-bottom-color:#a1e4f2}table.green tr{background:#e0f2f1;border-bottom-color:#b2dfdb}table.orange tr{background:#feefe3;border-bottom-color:#fedfc8}table.pink tr{background:#fde7f3;border-bottom-color:#fdcfe8}table.purple tr{background:#f3e8fd;border-bottom-color:#e9d2fd}table.blue tr:first-child{border-top-color:#c5cae9}table.cyan tr:first-child{border-top-color:#a1e4f2}table.green tr:first-child{border-top-color:#b2dfdb}table.orange tr:first-child{border-top-color:#fedfc8}table.pink tr:first-child{border-top-color:#fdcfe8}table.purple tr:first-child{border-top-color:#e9d2fd}table.blue th,table.cyan th,table.green th,table.orange th,table.pink th,table.purple th{background:inherit}table.blue tr.alt td,table.responsive.blue tr:not(.alt) td:first-child{background:#f6f7fb}table.responsive.blue table td:first-child{background:#fff}table.cyan tr.alt td,table.responsive.cyan tr:not(.alt) td:first-child{background:#f4fcfd}table.responsive.cyan table td:first-child{background:#fff}table.green tr.alt td,table.responsive.green tr:not(.alt) td:first-child{background:#f3faf9}table.responsive.green table td:first-child{background:#fff}table.orange tr.alt td,table.responsive.orange tr:not(.alt) td:first-child{background:#fff9f4}table.responsive.orange table td:first-child{background:#fff}table.pink tr.alt td,table.responsive.pink tr:not(.alt) td:first-child{background:#fef5fa}table.responsive.pink table td:first-child{background:#fff}table.purple tr.alt td,table.responsive.purple tr:not(.alt) td:first-child{background:#faf6fe}table.responsive.purple table td:first-child{background:#fff}table.vertical-rules.blue tr>td:not(:last-child),table.vertical-rules.blue tr>th:not(:last-child){border-right:1px solid #c5cae9}table.vertical-rules.cyan tr>td:not(:last-child),table.vertical-rules.cyan tr>th:not(:last-child){border-right:1px solid #a1e4f2}table.vertical-rules.green tr>td:not(:last-child),table.vertical-rules.green tr>th:not(:last-child){border-right:1px solid #b2dfdb}table.vertical-rules.orange tr>td:not(:last-child),table.vertical-rules.orange tr>th:not(:last-child){border-right:1px solid #fedfc8}table.vertical-rules.pink tr>td:not(:last-child),table.vertical-rules.pink tr>th:not(:last-child){border-right:1px solid #fdcfe8}table.vertical-rules.purple tr>td:not(:last-child),table.vertical-rules.purple tr>th:not(:last-child){border-right:1px solid #e9d2fd}.devsite-article-body>.devsite-full-width-table,.devsite-article-body>table.full-width{margin:16px -40px}@media screen and (max-width:840px){.devsite-article-body>.devsite-full-width-table,.devsite-article-body>table.full-width{margin:16px -24px}}@media screen and (max-width:600px){.devsite-article-body>.devsite-full-width-table,.devsite-article-body>table.full-width{margin:16px -16px}}.devsite-article-body>.devsite-full-width-table td:first-child,.devsite-article-body>.devsite-full-width-table th:first-child,.devsite-article-body>table.full-width td:first-child,.devsite-article-body>table.full-width th:first-child{padding-left:40px}@media screen and (max-width:840px){.devsite-article-body>.devsite-full-width-table td:first-child,.devsite-article-body>.devsite-full-width-table th:first-child,.devsite-article-body>table.full-width td:first-child,.devsite-article-body>table.full-width th:first-child{padding-left:24px}}@media screen and (max-width:600px){.devsite-article-body>.devsite-full-width-table td:first-child,.devsite-article-body>.devsite-full-width-table th:first-child,.devsite-article-body>table.full-width td:first-child,.devsite-article-body>table.full-width th:first-child{padding-left:16px}}.devsite-article-body>.devsite-full-width-table td:last-child,.devsite-article-body>.devsite-full-width-table th:last-child,.devsite-article-body>table.full-width td:last-child,.devsite-article-body>table.full-width th:last-child{padding-right:40px}@media screen and (max-width:840px){.devsite-article-body>.devsite-full-width-table td:last-child,.devsite-article-body>.devsite-full-width-table th:last-child,.devsite-article-body>table.full-width td:last-child,.devsite-article-body>table.full-width th:last-child{padding-right:24px}}@media screen and (max-width:600px){.devsite-article-body>.devsite-full-width-table td:last-child,.devsite-article-body>.devsite-full-width-table th:last-child,.devsite-article-body>table.full-width td:last-child,.devsite-article-body>table.full-width th:last-child{padding-right:16px}}.devsite-full-width-table table table td:first-child,.devsite-full-width-table table table th:first-child{padding-left:0}@media screen and (max-width:840px){.devsite-full-width-table table table td:first-child,.devsite-full-width-table table table th:first-child{padding-right:0}}.devsite-full-width-table table table td:last-child,.devsite-full-width-table table table th:last-child{padding-right:0}@media screen and (max-width:840px){.devsite-full-width-table table table td:last-child,.devsite-full-width-table table table th:last-child{padding-left:0}}@media screen and (max-width:840px){table.responsive td,table.responsive th,table.responsive tr{display:block}table.responsive table:not(.responsive) tr{display:table-row}table.responsive table:not(.responsive) td,table.responsive table:not(.responsive) th{display:table-cell}table.responsive>*>th,table.responsive>th{height:auto;padding:14px 8px}}.devsite-book-nav::-webkit-scrollbar,.devsite-dialog::-webkit-scrollbar,.devsite-popout::-webkit-scrollbar,.devsite-table-wrapper::-webkit-scrollbar,.devsite-tabs-overflow-menu::-webkit-scrollbar,.devsite-toc::-webkit-scrollbar,[scrollbars]::-webkit-scrollbar,pre::-webkit-scrollbar{height:8px;width:8px}.devsite-book-nav::-webkit-scrollbar-thumb,.devsite-dialog::-webkit-scrollbar-thumb,.devsite-popout::-webkit-scrollbar-thumb,.devsite-table-wrapper::-webkit-scrollbar-thumb,.devsite-tabs-overflow-menu::-webkit-scrollbar-thumb,.devsite-toc::-webkit-scrollbar-thumb,[scrollbars]::-webkit-scrollbar-thumb,pre::-webkit-scrollbar-thumb{background:rgba(128,134,139,.26);-webkit-border-radius:8px;border-radius:8px}.devsite-doc-set-nav-row::-webkit-scrollbar,.devsite-header-upper-tabs::-webkit-scrollbar,[no-horizontal-scrollbars]::-webkit-scrollbar{height:0;width:0}.devsite-table-wrapper::-webkit-scrollbar-corner,[scrollbars]::-webkit-scrollbar-corner,pre::-webkit-scrollbar-corner{background:0}.devsite-cse-confidential-results{background:rgba(254,239,227,.5);margin:16px -40px;padding:0 40px 16px}.devsite-cse-confidential-results+aside{margin-top:-16px!important}.devsite-search-results-stats{margin-bottom:8px}.devsite-search-results-restricted .gs-title{font-weight:500}.devsite-search-results-restricted .gs-title:link,.devsite-search-results-restricted .gs-title:visited{color:#039be5}.devsite-search-results-restricted .gs-visibleUrl{color:#1e8e3e;font-size:14px}.devsite-result-item-link .devsite-result-item-confidential,.devsite-search-results-restricted .gs-title-label{background:#feefe3;-webkit-border-radius:4px;border-radius:4px;color:#bf360c;display:inline-block;font:500 11px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.8px;margin:0 8px;padding:5px 8px 3px;text-transform:uppercase}body[type=search] .gsc-webResult .gsc-result{border:none;margin:24px 0;padding:0}.devsite-search-page-controls{margin-top:8px}.devsite-search-project{border-bottom:1px solid #dadce0;margin-bottom:24px;padding-bottom:23px}.devsite-search-project .devsite-project-scoped-results-title{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin-bottom:8px}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:36px;margin:6px 0}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-link,.devsite-search-project .devsite-project-scoped-results-title .devsite-site-logo-link{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;opacity:1;-webkit-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-link:focus,.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-link:hover,.devsite-search-project .devsite-project-scoped-results-title .devsite-site-logo-link:focus{opacity:.7;text-decoration:none}.devsite-search-project .devsite-project-scoped-results-title .devsite-site-logo{height:32px}.devsite-search-project .devsite-project-scoped-results-title .devsite-has-google-wordmark>.devsite-breadcrumb-link,.devsite-search-project .devsite-project-scoped-results-title .devsite-has-google-wordmark>.devsite-product-name{direction:ltr}.devsite-search-project .devsite-project-scoped-results-title .devsite-google-wordmark{height:24px;margin:0 4px 0 0;position:relative;top:5px}.devsite-search-project .devsite-project-scoped-results-title .devsite-google-wordmark-svg-path{-webkit-transition:fill .2s;-o-transition:fill .2s;transition:fill .2s}.devsite-search-project .devsite-project-scoped-results-title .devsite-site-logo-link canvas{height:auto!important}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-logo-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;height:36px;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;width:36px}[dir=ltr] .devsite-search-project .devsite-project-scoped-results-title .devsite-product-logo-container{margin-right:4px}[dir=rtl] .devsite-search-project .devsite-project-scoped-results-title .devsite-product-logo-container{margin-left:4px}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-logo{font-size:32px;height:32px;max-width:32px;min-width:32px;overflow:hidden;white-space:nowrap}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-logo-container[background] .devsite-product-logo{font-size:28px;height:28px;max-width:28px;min-width:28px}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:0;margin:0;max-height:32px;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;white-space:nowrap}.devsite-search-project .devsite-project-scoped-results-title .devsite-site-logo:not([src*=\.svg]){height:auto;max-height:32px}.devsite-search-project .devsite-project-scoped-results-title .devsite-breadcrumb-link>.devsite-product-name{color:inherit}@media screen and (max-width:840px){.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;min-width:0}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-breadcrumb-item:not(:first-of-type),.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-site-logo-link+.devsite-product-name{display:none}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-breadcrumb-item,.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-breadcrumb-link,.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-breadcrumb-list,.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-product-name{width:100%}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-breadcrumb-link{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper{position:relative;margin-left:.3em}.devsite-search-project .devsite-breadcrumb-list,.devsite-search-project .devsite-project-scoped-results-title{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.devsite-search-project .devsite-breadcrumb-link,.devsite-search-project .devsite-breadcrumb-link:hover{color:#202124}.devsite-search-project .devsite-breadcrumb-link .devsite-google-wordmark{fill:currentColor}.devsite-search-title{margin:0;padding:0}.devsite-search-title .devsite-search-term{color:#202124;font-weight:500}.devsite-steps{padding:24px 0 40px;-webkit-flex-wrap:wrap;flex-wrap:wrap}.devsite-steps,.devsite-steps .steps-direction{display:-webkit-box;display:-webkit-flex;display:flex}.devsite-steps .steps-direction{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;font-weight:500;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;position:relative}.devsite-steps .steps-previous{margin-left:40px}.devsite-steps .steps-next{margin-right:40px;text-align:right}.devsite-steps .steps-link:focus{text-decoration:none}.devsite-steps .steps-link-direction{color:#1a73e8;display:block;font-size:14px}.devsite-steps .steps-link-title{color:#01579b;font-size:18px}.devsite-steps .steps-link:focus .steps-link-title{border-bottom:1px solid #01579b}.devsite-steps .steps-link-arrow{color:#1a73e8;position:absolute}.devsite-steps .steps-link-arrow-left{left:-40px}.devsite-steps .steps-link-arrow-right{right:-40px}@media screen and (max-width:840px){.devsite-steps .steps-link-title{font-size:14px}.devsite-steps .steps-previous{margin-left:24px}.devsite-steps .steps-next{margin-right:24px}.devsite-steps .steps-link-arrow-left{left:-24px}.devsite-steps .steps-link-arrow-right{right:-24px}.devsite-steps .steps-link-arrow{bottom:4px;font-size:16px}}@media screen and (max-width:600px){.devsite-steps{padding:8px 0 16px}.devsite-steps .steps-next,.devsite-steps .steps-previous{margin:0}.devsite-steps .steps-link-arrow{display:none}}.devsite-jsfiddle-hide,devsite-googler-buttons,devsite-playlist,devsite-quiz,devsite-search .devsite-popout,html[cached] .devsite-wrapper{display:none}.devsite-dialog:not([is-upgraded]),iframe.devsite-embedded-youtube-video:not([is-upgraded]){pointer-events:none;visibility:hidden}.code-sample,.data-sample,.ds-selector-dropdown,.ds-selector-tabs,.expandable,.kd-tabbed-horz,.kd-tabbed-vert{display:none}devsite-selector{pointer-events:none;visibility:hidden}devsite-search .devsite-searchbox{background:#f1f3f4;-webkit-border-radius:2px;border-radius:2px}devsite-page-rating[position=header]{-webkit-box-flex:0;-webkit-flex:0 0 120px;flex:0 0 120px;margin:0 0 0 16px;width:120px}[dir=rtl] devsite-page-rating[position=header]{margin:0 16px 0 0}google-codelab{display:none}iframe.framebox,iframe.inherit-locale{display:block;width:100%}[background]:not(.devsite-landing-row-cards),[background]:not(.devsite-landing-row-cards) h3,[background] h2{color:#fff}[background=grey]{background-color:#f1f3f4}[background=grey] h2{color:#5f6368}[background=grey]:not(.devsite-landing-row-cards),[background=grey]:not(.devsite-landing-row-cards) [background] h3,[background=grey]:not(.devsite-landing-row-cards) h3{color:inherit}[background] .devsite-landing-row-description{color:#fff}[background=grey] .devsite-landing-row-description{color:#202124}[background] :link:not(.button),[background] :visited:not(.button){color:#fff}[background=grey] :link:not(.button),[background=grey] :visited:not(.button),[background].devsite-landing-row-cards :link:not(.button),[background].devsite-landing-row-cards :visited:not(.button){color:#1a73e8}[background]:not([background=grey]) :focus>:not(.material-icons),[background]:not([background=grey]) :link>:not(.material-icons):hover,[background]:not([background=grey]) p>a:not(.button){text-decoration:underline}[background]:not([background=grey]) p>a:focus{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}[foreground] :focus>:not(.material-icons),[foreground] :link>:not(.material-icons):hover{text-decoration:underline}[foreground=blue-grey] .button{color:#607d8b}[foreground=blue-grey] .button:active,[foreground=blue-grey] .button:focus,[foreground=blue-grey] .button:hover{background:#eff2f3}[foreground=blue-grey] .button-primary{background:#607d8b;color:#fff}[foreground=blue-grey] .button-primary:active,[foreground=blue-grey] .button-primary:focus,[foreground=blue-grey] .button-primary:hover{background:#455a64}[background=blue-grey]{background-color:#607d8b}[foreground=blue-grey] :focus>:not(.material-icons),[foreground=blue-grey] :link>:not(.material-icons):hover,[foreground=blue-grey] a:not(.button) h2,[foreground=blue-grey] a:not(.button) h3{color:#607d8b}.devsite-landing-row[background=blue-grey]+.devsite-landing-row[background=blue-grey]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=blue-grey],.devsite-landing-row-item-icon-container[foreground=blue-grey],.devsite-landing-row-item-list-item-icon-container[background][foreground=blue-grey],.devsite-landing-row-item-list-item-icon-container[foreground=blue-grey],.devsite-landing-row-item[foreground=blue-grey] :link h2,.devsite-landing-row-item[foreground=blue-grey] :link h3{color:#607d8b}.devsite-landing-row-item-icon-container[background=blue-grey],.devsite-landing-row-item-list-item-icon-container[background=blue-grey]{background:#607d8b}[foreground=blue-grey-dark] .button{color:#455a64}[foreground=blue-grey-dark] .button:active,[foreground=blue-grey-dark] .button:focus,[foreground=blue-grey-dark] .button:hover{background:#eceff0}[foreground=blue-grey-dark] .button-primary{background:#455a64;color:#fff}[foreground=blue-grey-dark] .button-primary:active,[foreground=blue-grey-dark] .button-primary:focus,[foreground=blue-grey-dark] .button-primary:hover{background:#37474f}[background=blue-grey-dark]{background-color:#455a64}[foreground=blue-grey-dark] :focus>:not(.material-icons),[foreground=blue-grey-dark] :link>:not(.material-icons):hover,[foreground=blue-grey-dark] a:not(.button) h2,[foreground=blue-grey-dark] a:not(.button) h3{color:#455a64}.devsite-landing-row[background=blue-grey-dark]+.devsite-landing-row[background=blue-grey-dark]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=blue-grey-dark],.devsite-landing-row-item-icon-container[foreground=blue-grey-dark],.devsite-landing-row-item-list-item-icon-container[background][foreground=blue-grey-dark],.devsite-landing-row-item-list-item-icon-container[foreground=blue-grey-dark],.devsite-landing-row-item[foreground=blue-grey-dark] :link h2,.devsite-landing-row-item[foreground=blue-grey-dark] :link h3{color:#455a64}.devsite-landing-row-item-icon-container[background=blue-grey-dark],.devsite-landing-row-item-list-item-icon-container[background=blue-grey-dark]{background:#455a64}[foreground=deep-orange] .button{color:#ff5722}[foreground=deep-orange] .button:active,[foreground=deep-orange] .button:focus,[foreground=deep-orange] .button:hover{background:#ffeee9}[foreground=deep-orange] .button-primary{background:#ff5722;color:#fff}[foreground=deep-orange] .button-primary:active,[foreground=deep-orange] .button-primary:focus,[foreground=deep-orange] .button-primary:hover{background:#e64a19}[background=deep-orange]{background-color:#ff5722}[foreground=deep-orange] :focus>:not(.material-icons),[foreground=deep-orange] :link>:not(.material-icons):hover,[foreground=deep-orange] a:not(.button) h2,[foreground=deep-orange] a:not(.button) h3{color:#ff5722}.devsite-landing-row[background=deep-orange]+.devsite-landing-row[background=deep-orange]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=deep-orange],.devsite-landing-row-item-icon-container[foreground=deep-orange],.devsite-landing-row-item-list-item-icon-container[background][foreground=deep-orange],.devsite-landing-row-item-list-item-icon-container[foreground=deep-orange],.devsite-landing-row-item[foreground=deep-orange] :link h2,.devsite-landing-row-item[foreground=deep-orange] :link h3{color:#ff5722}.devsite-landing-row-item-icon-container[background=deep-orange],.devsite-landing-row-item-list-item-icon-container[background=deep-orange]{background:#ff5722}[foreground=deep-purple] .button{color:#673ab7}[foreground=deep-purple] .button:active,[foreground=deep-purple] .button:focus,[foreground=deep-purple] .button:hover{background:#f0ebf8}[foreground=deep-purple] .button-primary{background:#673ab7;color:#fff}[foreground=deep-purple] .button-primary:active,[foreground=deep-purple] .button-primary:focus,[foreground=deep-purple] .button-primary:hover{background:#512da8}[background=deep-purple]{background-color:#673ab7}[foreground=deep-purple] :focus>:not(.material-icons),[foreground=deep-purple] :link>:not(.material-icons):hover,[foreground=deep-purple] a:not(.button) h2,[foreground=deep-purple] a:not(.button) h3{color:#673ab7}.devsite-landing-row[background=deep-purple]+.devsite-landing-row[background=deep-purple]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=deep-purple],.devsite-landing-row-item-icon-container[foreground=deep-purple],.devsite-landing-row-item-list-item-icon-container[background][foreground=deep-purple],.devsite-landing-row-item-list-item-icon-container[foreground=deep-purple],.devsite-landing-row-item[foreground=deep-purple] :link h2,.devsite-landing-row-item[foreground=deep-purple] :link h3{color:#673ab7}.devsite-landing-row-item-icon-container[background=deep-purple],.devsite-landing-row-item-list-item-icon-container[background=deep-purple]{background:#673ab7}[foreground=google-blue] .button{color:#1a73e8}[foreground=google-blue] .button:active,[foreground=google-blue] .button:focus,[foreground=google-blue] .button:hover{background:#e8f1fd}[foreground=google-blue] .button-primary{background:#1a73e8;color:#fff}[foreground=google-blue] .button-primary:active,[foreground=google-blue] .button-primary:focus,[foreground=google-blue] .button-primary:hover{background:#185abc}[background=google-blue]{background-color:#1a73e8}[foreground=google-blue] :focus>:not(.material-icons),[foreground=google-blue] :link>:not(.material-icons):hover,[foreground=google-blue] a:not(.button) h2,[foreground=google-blue] a:not(.button) h3{color:#1a73e8}.devsite-landing-row[background=google-blue]+.devsite-landing-row[background=google-blue]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=google-blue],.devsite-landing-row-item-icon-container[foreground=google-blue],.devsite-landing-row-item-list-item-icon-container[background][foreground=google-blue],.devsite-landing-row-item-list-item-icon-container[foreground=google-blue],.devsite-landing-row-item[foreground=google-blue] :link h2,.devsite-landing-row-item[foreground=google-blue] :link h3{color:#1a73e8}.devsite-landing-row-item-icon-container[background=google-blue],.devsite-landing-row-item-list-item-icon-container[background=google-blue]{background:#1a73e8}[foreground=google-green] .button{color:#1e8e3e}[foreground=google-green] .button:active,[foreground=google-green] .button:focus,[foreground=google-green] .button:hover{background:#e9f4ec}[foreground=google-green] .button-primary{background:#1e8e3e;color:#fff}[foreground=google-green] .button-primary:active,[foreground=google-green] .button-primary:focus,[foreground=google-green] .button-primary:hover{background:#137333}[background=google-green]{background-color:#1e8e3e}[foreground=google-green] :focus>:not(.material-icons),[foreground=google-green] :link>:not(.material-icons):hover,[foreground=google-green] a:not(.button) h2,[foreground=google-green] a:not(.button) h3{color:#1e8e3e}.devsite-landing-row[background=google-green]+.devsite-landing-row[background=google-green]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=google-green],.devsite-landing-row-item-icon-container[foreground=google-green],.devsite-landing-row-item-list-item-icon-container[background][foreground=google-green],.devsite-landing-row-item-list-item-icon-container[foreground=google-green],.devsite-landing-row-item[foreground=google-green] :link h2,.devsite-landing-row-item[foreground=google-green] :link h3{color:#1e8e3e}.devsite-landing-row-item-icon-container[background=google-green],.devsite-landing-row-item-list-item-icon-container[background=google-green]{background:#1e8e3e}[foreground=google-red] .button{color:#d93025}[foreground=google-red] .button:active,[foreground=google-red] .button:focus,[foreground=google-red] .button:hover{background:#fbeae9}[foreground=google-red] .button-primary{background:#d93025;color:#fff}[foreground=google-red] .button-primary:active,[foreground=google-red] .button-primary:focus,[foreground=google-red] .button-primary:hover{background:#b31412}[background=google-red]{background-color:#d93025}[foreground=google-red] :focus>:not(.material-icons),[foreground=google-red] :link>:not(.material-icons):hover,[foreground=google-red] a:not(.button) h2,[foreground=google-red] a:not(.button) h3{color:#d93025}.devsite-landing-row[background=google-red]+.devsite-landing-row[background=google-red]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=google-red],.devsite-landing-row-item-icon-container[foreground=google-red],.devsite-landing-row-item-list-item-icon-container[background][foreground=google-red],.devsite-landing-row-item-list-item-icon-container[foreground=google-red],.devsite-landing-row-item[foreground=google-red] :link h2,.devsite-landing-row-item[foreground=google-red] :link h3{color:#d93025}.devsite-landing-row-item-icon-container[background=google-red],.devsite-landing-row-item-list-item-icon-container[background=google-red]{background:#d93025}[foreground=indigo] .button{color:#3f51b5}[foreground=indigo] .button:active,[foreground=indigo] .button:focus,[foreground=indigo] .button:hover{background:#eceef8}[foreground=indigo] .button-primary{background:#3f51b5;color:#fff}[foreground=indigo] .button-primary:active,[foreground=indigo] .button-primary:focus,[foreground=indigo] .button-primary:hover{background:#303f9f}[background=indigo]{background-color:#3f51b5}[foreground=indigo] :focus>:not(.material-icons),[foreground=indigo] :link>:not(.material-icons):hover,[foreground=indigo] a:not(.button) h2,[foreground=indigo] a:not(.button) h3{color:#3f51b5}.devsite-landing-row[background=indigo]+.devsite-landing-row[background=indigo]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=indigo],.devsite-landing-row-item-icon-container[foreground=indigo],.devsite-landing-row-item-list-item-icon-container[background][foreground=indigo],.devsite-landing-row-item-list-item-icon-container[foreground=indigo],.devsite-landing-row-item[foreground=indigo] :link h2,.devsite-landing-row-item[foreground=indigo] :link h3{color:#3f51b5}.devsite-landing-row-item-icon-container[background=indigo],.devsite-landing-row-item-list-item-icon-container[background=indigo]{background:#3f51b5}[foreground=light-blue] .button{color:#0288d1}[foreground=light-blue] .button:active,[foreground=light-blue] .button:focus,[foreground=light-blue] .button:hover{background:#e6f3fa}[foreground=light-blue] .button-primary{background:#0288d1;color:#fff}[foreground=light-blue] .button-primary:active,[foreground=light-blue] .button-primary:focus,[foreground=light-blue] .button-primary:hover{background:#01579b}[background=light-blue]{background-color:#0288d1}[foreground=light-blue] :focus>:not(.material-icons),[foreground=light-blue] :link>:not(.material-icons):hover,[foreground=light-blue] a:not(.button) h2,[foreground=light-blue] a:not(.button) h3{color:#0288d1}.devsite-landing-row[background=light-blue]+.devsite-landing-row[background=light-blue]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=light-blue],.devsite-landing-row-item-icon-container[foreground=light-blue],.devsite-landing-row-item-list-item-icon-container[background][foreground=light-blue],.devsite-landing-row-item-list-item-icon-container[foreground=light-blue],.devsite-landing-row-item[foreground=light-blue] :link h2,.devsite-landing-row-item[foreground=light-blue] :link h3{color:#0288d1}.devsite-landing-row-item-icon-container[background=light-blue],.devsite-landing-row-item-list-item-icon-container[background=light-blue]{background:#0288d1}[foreground=nest-theme] .button{color:#00afd8}[foreground=nest-theme] .button:active,[foreground=nest-theme] .button:focus,[foreground=nest-theme] .button:hover{background:#e6f7fb}[foreground=nest-theme] .button-primary{background:#00afd8;color:#fff}[foreground=nest-theme] .button-primary:active,[foreground=nest-theme] .button-primary:focus,[foreground=nest-theme] .button-primary:hover{background:#0096c8}[background=nest-theme]{background-color:#00afd8}[foreground=nest-theme] :focus>:not(.material-icons),[foreground=nest-theme] :link>:not(.material-icons):hover,[foreground=nest-theme] a:not(.button) h2,[foreground=nest-theme] a:not(.button) h3{color:#00afd8}.devsite-landing-row[background=nest-theme]+.devsite-landing-row[background=nest-theme]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=nest-theme],.devsite-landing-row-item-icon-container[foreground=nest-theme],.devsite-landing-row-item-list-item-icon-container[background][foreground=nest-theme],.devsite-landing-row-item-list-item-icon-container[foreground=nest-theme],.devsite-landing-row-item[foreground=nest-theme] :link h2,.devsite-landing-row-item[foreground=nest-theme] :link h3{color:#00afd8}.devsite-landing-row-item-icon-container[background=nest-theme],.devsite-landing-row-item-list-item-icon-container[background=nest-theme]{background:#00afd8}[foreground=pink] .button{color:#e52592}[foreground=pink] .button:active,[foreground=pink] .button:focus,[foreground=pink] .button:hover{background:#fce9f4}[foreground=pink] .button-primary{background:#e52592;color:#fff}[foreground=pink] .button-primary:active,[foreground=pink] .button-primary:focus,[foreground=pink] .button-primary:hover{background:#b80672}[background=pink]{background-color:#e52592}[foreground=pink] :focus>:not(.material-icons),[foreground=pink] :link>:not(.material-icons):hover,[foreground=pink] a:not(.button) h2,[foreground=pink] a:not(.button) h3{color:#e52592}.devsite-landing-row[background=pink]+.devsite-landing-row[background=pink]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=pink],.devsite-landing-row-item-icon-container[foreground=pink],.devsite-landing-row-item-list-item-icon-container[background][foreground=pink],.devsite-landing-row-item-list-item-icon-container[foreground=pink],.devsite-landing-row-item[foreground=pink] :link h2,.devsite-landing-row-item[foreground=pink] :link h3{color:#e52592}.devsite-landing-row-item-icon-container[background=pink],.devsite-landing-row-item-list-item-icon-container[background=pink]{background:#e52592}[foreground=purple] .button{color:#9334e6}[foreground=purple] .button:active,[foreground=purple] .button:focus,[foreground=purple] .button:hover{background:#f4ebfd}[foreground=purple] .button-primary{background:#9334e6;color:#fff}[foreground=purple] .button-primary:active,[foreground=purple] .button-primary:focus,[foreground=purple] .button-primary:hover{background:#7627bb}[background=purple]{background-color:#9334e6}[foreground=purple] :focus>:not(.material-icons),[foreground=purple] :link>:not(.material-icons):hover,[foreground=purple] a:not(.button) h2,[foreground=purple] a:not(.button) h3{color:#9334e6}.devsite-landing-row[background=purple]+.devsite-landing-row[background=purple]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=purple],.devsite-landing-row-item-icon-container[foreground=purple],.devsite-landing-row-item-list-item-icon-container[background][foreground=purple],.devsite-landing-row-item-list-item-icon-container[foreground=purple],.devsite-landing-row-item[foreground=purple] :link h2,.devsite-landing-row-item[foreground=purple] :link h3{color:#9334e6}.devsite-landing-row-item-icon-container[background=purple],.devsite-landing-row-item-list-item-icon-container[background=purple]{background:#9334e6}[foreground=stadia-theme] .button{color:#9b0063}[foreground=stadia-theme] .button:active,[foreground=stadia-theme] .button:focus,[foreground=stadia-theme] .button:hover{background:#f5e6ef}[foreground=stadia-theme] .button-primary{background:#9b0063;color:#fff}[foreground=stadia-theme] .button-primary:active,[foreground=stadia-theme] .button-primary:focus,[foreground=stadia-theme] .button-primary:hover{background:#680039}[background=stadia-theme]{background-color:#9b0063}[foreground=stadia-theme] :focus>:not(.material-icons),[foreground=stadia-theme] :link>:not(.material-icons):hover,[foreground=stadia-theme] a:not(.button) h2,[foreground=stadia-theme] a:not(.button) h3{color:#9b0063}.devsite-landing-row[background=stadia-theme]+.devsite-landing-row[background=stadia-theme]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=stadia-theme],.devsite-landing-row-item-icon-container[foreground=stadia-theme],.devsite-landing-row-item-list-item-icon-container[background][foreground=stadia-theme],.devsite-landing-row-item-list-item-icon-container[foreground=stadia-theme],.devsite-landing-row-item[foreground=stadia-theme] :link h2,.devsite-landing-row-item[foreground=stadia-theme] :link h3{color:#9b0063}.devsite-landing-row-item-icon-container[background=stadia-theme],.devsite-landing-row-item-list-item-icon-container[background=stadia-theme]{background:#9b0063}[foreground=teal] .button{color:#009688}[foreground=teal] .button:active,[foreground=teal] .button:focus,[foreground=teal] .button:hover{background:#e6f5f3}[foreground=teal] .button-primary{background:#009688;color:#fff}[foreground=teal] .button-primary:active,[foreground=teal] .button-primary:focus,[foreground=teal] .button-primary:hover{background:#00796b}[background=teal]{background-color:#009688}[foreground=teal] :focus>:not(.material-icons),[foreground=teal] :link>:not(.material-icons):hover,[foreground=teal] a:not(.button) h2,[foreground=teal] a:not(.button) h3{color:#009688}.devsite-landing-row[background=teal]+.devsite-landing-row[background=teal]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=teal],.devsite-landing-row-item-icon-container[foreground=teal],.devsite-landing-row-item-list-item-icon-container[background][foreground=teal],.devsite-landing-row-item-list-item-icon-container[foreground=teal],.devsite-landing-row-item[foreground=teal] :link h2,.devsite-landing-row-item[foreground=teal] :link h3{color:#009688}.devsite-landing-row-item-icon-container[background=teal],.devsite-landing-row-item-list-item-icon-container[background=teal]{background:#009688}[foreground=youtube-theme] .button{color:red}[foreground=youtube-theme] .button:active,[foreground=youtube-theme] .button:focus,[foreground=youtube-theme] .button:hover{background:#ffe6e6}[foreground=youtube-theme] .button-primary{background:red;color:#fff}[foreground=youtube-theme] .button-primary:active,[foreground=youtube-theme] .button-primary:focus,[foreground=youtube-theme] .button-primary:hover{background:#c20000}[background=youtube-theme]{background-color:red}[foreground=youtube-theme] :focus>:not(.material-icons),[foreground=youtube-theme] :link>:not(.material-icons):hover,[foreground=youtube-theme] a:not(.button) h2,[foreground=youtube-theme] a:not(.button) h3{color:red}.devsite-landing-row[background=youtube-theme]+.devsite-landing-row[background=youtube-theme]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=youtube-theme],.devsite-landing-row-item-icon-container[foreground=youtube-theme],.devsite-landing-row-item-list-item-icon-container[background][foreground=youtube-theme],.devsite-landing-row-item-list-item-icon-container[foreground=youtube-theme],.devsite-landing-row-item[foreground=youtube-theme] :link h2,.devsite-landing-row-item[foreground=youtube-theme] :link h3{color:red}.devsite-landing-row-item-icon-container[background=youtube-theme],.devsite-landing-row-item-list-item-icon-container[background=youtube-theme]{background:red}[foreground=cyan] .button{color:#12b5cb}[foreground=cyan] .button:active,[foreground=cyan] .button:focus,[foreground=cyan] .button:hover{background:#e7f8fa}[foreground=cyan] .button-primary{background:#12b5cb;color:#202124}[foreground=cyan] .button-primary:active,[foreground=cyan] .button-primary:focus,[foreground=cyan] .button-primary:hover{background:#098591}[background=cyan]{background-color:#43cde6}[foreground=cyan] :focus>:not(.material-icons),[foreground=cyan] :link>:not(.material-icons):hover,[foreground=cyan] a:not(.button) h2,[foreground=cyan] a:not(.button) h3{color:#12b5cb}.devsite-landing-row[background=cyan]+.devsite-landing-row[background=cyan]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=cyan],.devsite-landing-row-item-icon-container[foreground=cyan],.devsite-landing-row-item-list-item-icon-container[background][foreground=cyan],.devsite-landing-row-item-list-item-icon-container[foreground=cyan],.devsite-landing-row-item[foreground=cyan] :link h2,.devsite-landing-row-item[foreground=cyan] :link h3{color:#12b5cb}.devsite-landing-row-item-icon-container[background=cyan],.devsite-landing-row-item-list-item-icon-container[background=cyan]{background:#43cde6}[foreground=google-yellow] .button{color:#f9ab00}[foreground=google-yellow] .button:active,[foreground=google-yellow] .button:focus,[foreground=google-yellow] .button:hover{background:#fef7e6}[foreground=google-yellow] .button-primary{background:#f9ab00;color:#202124}[foreground=google-yellow] .button-primary:active,[foreground=google-yellow] .button-primary:focus,[foreground=google-yellow] .button-primary:hover{background:#ea8600}[background=google-yellow]{background-color:#fcc934}[foreground=google-yellow] :focus>:not(.material-icons),[foreground=google-yellow] :link>:not(.material-icons):hover,[foreground=google-yellow] a:not(.button) h2,[foreground=google-yellow] a:not(.button) h3{color:#f9ab00}.devsite-landing-row[background=google-yellow]+.devsite-landing-row[background=google-yellow]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=google-yellow],.devsite-landing-row-item-icon-container[foreground=google-yellow],.devsite-landing-row-item-list-item-icon-container[background][foreground=google-yellow],.devsite-landing-row-item-list-item-icon-container[foreground=google-yellow],.devsite-landing-row-item[foreground=google-yellow] :link h2,.devsite-landing-row-item[foreground=google-yellow] :link h3{color:#f9ab00}.devsite-landing-row-item-icon-container[background=google-yellow],.devsite-landing-row-item-list-item-icon-container[background=google-yellow]{background:#fcc934}[foreground=light-green] .button{color:#8bc34a}[foreground=light-green] .button:active,[foreground=light-green] .button:focus,[foreground=light-green] .button:hover{background:#f3f9ed}[foreground=light-green] .button-primary{background:#8bc34a;color:#202124}[foreground=light-green] .button-primary:active,[foreground=light-green] .button-primary:focus,[foreground=light-green] .button-primary:hover{background:#689f38}[background=light-green]{background-color:#aed581}[foreground=light-green] :focus>:not(.material-icons),[foreground=light-green] :link>:not(.material-icons):hover,[foreground=light-green] a:not(.button) h2,[foreground=light-green] a:not(.button) h3{color:#8bc34a}.devsite-landing-row[background=light-green]+.devsite-landing-row[background=light-green]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=light-green],.devsite-landing-row-item-icon-container[foreground=light-green],.devsite-landing-row-item-list-item-icon-container[background][foreground=light-green],.devsite-landing-row-item-list-item-icon-container[foreground=light-green],.devsite-landing-row-item[foreground=light-green] :link h2,.devsite-landing-row-item[foreground=light-green] :link h3{color:#8bc34a}.devsite-landing-row-item-icon-container[background=light-green],.devsite-landing-row-item-list-item-icon-container[background=light-green]{background:#aed581}[foreground=orange] .button{color:#e8710a}[foreground=orange] .button:active,[foreground=orange] .button:focus,[foreground=orange] .button:hover{background:#fdf1e7}[foreground=orange] .button-primary{background:#e8710a;color:#202124}[foreground=orange] .button-primary:active,[foreground=orange] .button-primary:focus,[foreground=orange] .button-primary:hover{background:#c26401}[background=orange]{background-color:#fcad70}[foreground=orange] :focus>:not(.material-icons),[foreground=orange] :link>:not(.material-icons):hover,[foreground=orange] a:not(.button) h2,[foreground=orange] a:not(.button) h3{color:#e8710a}.devsite-landing-row[background=orange]+.devsite-landing-row[background=orange]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=orange],.devsite-landing-row-item-icon-container[foreground=orange],.devsite-landing-row-item-list-item-icon-container[background][foreground=orange],.devsite-landing-row-item-list-item-icon-container[foreground=orange],.devsite-landing-row-item[foreground=orange] :link h2,.devsite-landing-row-item[foreground=orange] :link h3{color:#e8710a}.devsite-landing-row-item-icon-container[background=orange],.devsite-landing-row-item-list-item-icon-container[background=orange]{background:#fcad70}[foreground=white] .button{color:#fff}[foreground=white] .button:active,[foreground=white] .button:focus,[foreground=white] .button:hover{background:#fff}[foreground=white] .button-primary{background:#fff;color:#202124}[foreground=white] .button-primary:active,[foreground=white] .button-primary:focus,[foreground=white] .button-primary:hover{background:#202124}[background=white]{background-color:#fff}[foreground=white] :focus>:not(.material-icons),[foreground=white] :link>:not(.material-icons):hover,[foreground=white] a:not(.button) h2,[foreground=white] a:not(.button) h3{color:#039be5}.devsite-landing-row[background=white]+.devsite-landing-row[background=white]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=white],.devsite-landing-row-item-icon-container[foreground=white],.devsite-landing-row-item-list-item-icon-container[background][foreground=white],.devsite-landing-row-item-list-item-icon-container[foreground=white],.devsite-landing-row-item[foreground=white] :link h2,.devsite-landing-row-item[foreground=white] :link h3{color:#039be5}.devsite-landing-row-item-icon-container[background=white],.devsite-landing-row-item-list-item-icon-container[background=white]{background:#fff}[background=cyan] .devsite-landing-row-description,[background=cyan] .devsite-landing-row-item-icon-container:not([foreground]),[background=cyan] :link:not(.button),[background=cyan]:not(.devsite-landing-row-cards) h3,[background=cyan]:not([foreground]):not(.devsite-landing-row-cards),[background=cyan] :visited:not(.button),[background=cyan] h2{color:#202124}[background=cyan] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background=cyan] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:rgba(154,160,166,.5)}[background=cyan] .devsite-landing-row-item-list-item-icon-container:not([foreground]),[background=cyan] :focus .devsite-landing-row-item-icon-container[background],[background=cyan] :link .devsite-landing-row-item-icon-container[background]:hover{color:#202124}[background=cyan] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background=cyan] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}[background=cyan] :link .devsite-landing-row-item-list-item-description h4+p,[background=google-yellow] .devsite-landing-row-description,[background=google-yellow] .devsite-landing-row-item-icon-container:not([foreground]),[background=google-yellow] :link:not(.button),[background=google-yellow]:not(.devsite-landing-row-cards) h3,[background=google-yellow]:not([foreground]):not(.devsite-landing-row-cards),[background=google-yellow] :visited:not(.button),[background=google-yellow] h2{color:#202124}[background=google-yellow] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background=google-yellow] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:rgba(154,160,166,.5)}[background=google-yellow] .devsite-landing-row-item-list-item-icon-container:not([foreground]),[background=google-yellow] :focus .devsite-landing-row-item-icon-container[background],[background=google-yellow] :link .devsite-landing-row-item-icon-container[background]:hover{color:#202124}[background=google-yellow] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background=google-yellow] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}[background=google-yellow] :link .devsite-landing-row-item-list-item-description h4+p,[background=light-green] .devsite-landing-row-description,[background=light-green] .devsite-landing-row-item-icon-container:not([foreground]),[background=light-green] :link:not(.button),[background=light-green]:not(.devsite-landing-row-cards) h3,[background=light-green]:not([foreground]):not(.devsite-landing-row-cards),[background=light-green] :visited:not(.button),[background=light-green] h2{color:#202124}[background=light-green] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background=light-green] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:rgba(154,160,166,.5)}[background=light-green] .devsite-landing-row-item-list-item-icon-container:not([foreground]),[background=light-green] :focus .devsite-landing-row-item-icon-container[background],[background=light-green] :link .devsite-landing-row-item-icon-container[background]:hover{color:#202124}[background=light-green] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background=light-green] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}[background=light-green] :link .devsite-landing-row-item-list-item-description h4+p,[background=orange] .devsite-landing-row-description,[background=orange] .devsite-landing-row-item-icon-container:not([foreground]),[background=orange] :link:not(.button),[background=orange]:not(.devsite-landing-row-cards) h3,[background=orange]:not([foreground]):not(.devsite-landing-row-cards),[background=orange] :visited:not(.button),[background=orange] h2{color:#202124}[background=orange] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background=orange] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:rgba(154,160,166,.5)}[background=orange] .devsite-landing-row-item-list-item-icon-container:not([foreground]),[background=orange] :focus .devsite-landing-row-item-icon-container[background],[background=orange] :link .devsite-landing-row-item-icon-container[background]:hover{color:#202124}[background=orange] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background=orange] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}[background=orange] :link .devsite-landing-row-item-list-item-description h4+p,[background=white] .devsite-landing-row-description,[background=white] .devsite-landing-row-item-icon-container:not([foreground]),[background=white] :link:not(.button),[background=white]:not(.devsite-landing-row-cards) h3,[background=white]:not([foreground]):not(.devsite-landing-row-cards),[background=white] :visited:not(.button),[background=white] h2{color:#202124}[background=white] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background=white] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:hsla(0,0%,100%,.7)}[background=white] .devsite-landing-row-item-list-item-icon-container:not([foreground]),[background=white] :focus .devsite-landing-row-item-icon-container[background],[background=white] :link .devsite-landing-row-item-icon-container[background]:hover{color:#202124}[background=white] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background=white] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:hsla(0,0%,100%,.7)}[background=white] :link .devsite-landing-row-item-list-item-description h4+p{color:#202124}body[theme=white] devsite-user div.devsite-user-dialog-signin .devsite-user-dialog-letter,body[theme=white] devsite-user div.devsite-user-dialog .devsite-user-dialog-photo{background-color:#1a73e8;color:#fff}body[theme=cloud-theme] .devsite-feedback-item-icon-container.devsite-feedback-item-icon-color,body[theme=white] .devsite-feedback-item-icon-container.devsite-feedback-item-icon-color{background-color:#1a73e8}devsite-content{display:block;position:relative}body[layout=docs] devsite-content{align-self:start;-ms-grid-column:3;grid-column:2;-ms-grid-row:1;grid-row:1;margin:24px 0;max-width:936px;min-width:0}body[layout=docs] .devsite-article{background-color:#fff;padding:40px}body[layout=docs][type=landing] .devsite-article{padding:0}.devsite-article-meta{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;margin:0 0 20px}.devsite-banner+.devsite-article-meta{margin-top:-16px}body[layout=full] .devsite-article-meta,body[type=landing] .devsite-article-meta{margin:0}devsite-feedback[position=header]{display:block;position:relative;top:-4px}[dir=ltr] devsite-feedback[position=header]{float:right;margin-left:24px}[dir=rtl] devsite-feedback[position=header]{float:left;margin-right:24px}body[layout=full] devsite-feedback[position=header],body[type=landing] devsite-feedback[position=header]{display:none}@media screen and (max-width:840px){body[layout=docs] devsite-content{margin:0}body[layout=docs] .devsite-article{padding:24px}.devsite-banner+.devsite-article-meta{margin-top:0}}@media screen and (max-width:600px){body[layout=docs] .devsite-article{padding:16px}.devsite-article-meta{display:block;margin:0 0 12px}[dir] devsite-feedback[position=header]{float:none;margin:0 0 12px;position:static}}#devsite-support-form{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:0 0 0 -40px}#devsite-support-form>*{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;padding:0 0 0 40px}.devsite-support-form-error{color:#dd2c00}.devsite-support-form-hidden{display:none}.devsite-support-form-field{margin:0 0 8px}.devsite-support-form-field input:not([type=checkbox]):not([type=radio]),.devsite-support-form-field select,.devsite-support-form-field textarea{width:100%}.devsite-support-form-cc{color:#5f6368;display:block;font-size:13px}#devsite-support-form>.devsite-support-form-half{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%}.devsite-support-quota{font:italic 400 12px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-bottom:8px}.devsite-support-quota-help{font-size:18px;margin-left:4px;vertical-align:top}.devsite-support-quota-help:after{content:"help";color:#bdc1c6}@media screen and (max-width:600px){#devsite-support-form{display:block}}.devsite-404-wrapper,.devsite-offline-wrapper{margin:0 auto;max-width:804px;position:relative;text-align:center}.devsite-404-header,.devsite-offline-header{margin:120px 24px 20px;position:relative;z-index:2}.devsite-404-search,.devsite-offline-reload,.devsite-offline-suggestions{margin:0 0 160px;position:relative;z-index:1}.devsite-404-search devsite-search .devsite-popout-result{max-height:304px}.devsite-offline-reload{text-align:center}.devsite-404-header h3{font:400 64px/64px Roboto Mono,monospace}.devsite-offline-header h3{font:400 32px/48px Roboto Mono,monospace}.devsite-404-wrapper devsite-search,.devsite-404-wrapper devsite-search .devsite-searchbox,[dir=rtl] .devsite-404-wrapper devsite-search,[dir=rtl] .devsite-404-wrapper devsite-search .devsite-searchbox{margin:0;width:100%}.devsite-404-wrapper devsite-search .devsite-search-button{display:none}.devsite-offline-wrapper .devsite-offline-suggestions{text-align:left}[dir=rtl] .devsite-offline-wrapper .devsite-offline-suggestions{text-align:right}.devsite-offline-wrapper .devsite-offline-suggestions h3,.devsite-offline-wrapper .devsite-offline-suggestions ul{margin:0}.devsite-404-wrapper .devsite-404-links{border-top:1px solid #dadce0;margin:0 calc(50% - 50vw) 40px;padding:0 calc(50vw - 50%);text-align:left}[dir=rtl] .devsite-404-wrapper .devsite-404-links{text-align:right}.devsite-404-wrapper .devsite-404-links ul{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;list-style:none;padding:0}.devsite-404-wrapper .devsite-404-links li{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 72px)/4);flex:0 0 calc((100% - 72px)/4);margin-left:24px}[dir=rtl] .devsite-404-wrapper .devsite-404-links li{margin-left:0;margin-right:24px}.devsite-404-wrapper .devsite-404-links li:nth-of-type(4n+1){margin-left:0}[dir=rtl] .devsite-404-wrapper .devsite-404-links li:nth-of-type(4n+1){margin-right:0}@media screen and (max-width:840px){.devsite-404-header,.devsite-offline-header{margin-top:40px}.devsite-404-search,.devsite-offline-reload,.devsite-offline-suggestions{margin-bottom:80px}.devsite-404-wrapper .devsite-404-links li{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 24px)/2);flex:0 0 calc((100% - 24px)/2)}.devsite-404-wrapper .devsite-404-links li:nth-of-type(odd){margin-left:0}[dir=rtl] .devsite-404-wrapper .devsite-404-links li:nth-of-type(odd){margin-right:0}}@media screen and (max-width:600px){.devsite-404-search,.devsite-offline-reload,.devsite-offline-suggestions{margin-bottom:40px}}devsite-dynamic-content .devsite-card-image-bg{background-image:url(../images/dynamic-content-card-default.png)}@media print{.caution,.caution a,.devsite-banner,.devsite-banner a,.dogfood,.dogfood a,.key-point,.key-point a,.key-term,.key-term a,.note,.note a,.objective,.objective a,.prettyprint a,.special,.special a,.success,.success a,.warning,.warning a,:link,:visited,a .atn,a .atv,a .com,a .dec,a .kwd,a .lit,a .pln,a .pun,a .str,a .tag,a .typ,a code,aside,aside :link,aside :visited,body,code,h1,h1 code,h2,h2 code,h3,h3 code,h4,h4 code,h5,h5 code,h6,h6 code,html,pre,pre .atn,pre .atv,pre .com,pre .dec,pre .kwd,pre .lit,pre .pln,pre .pun,pre .str,pre .tag,pre .typ,td,td code,th,th :link,th :visited,th code,var{color:#000!important;padding-left:0!important;padding-right:0!important}#gc-wrapper{margin:0!important}devsite-expandable>:not(.showalways):not(.exw-control):not(.exw-expanded-content):not(.expand-control){display:block!important}:link,:visited{text-decoration:underline}.devsite-article-meta,.devsite-banner-confidential .button,.devsite-book-nav-bg,.devsite-code-buttons-container,devsite-book-nav,devsite-feedback,devsite-footer-linkboxes,devsite-footer-promos,devsite-footer-utility,devsite-googler-buttons,devsite-header,devsite-page-rating,devsite-toc{display:none!important}.devsite-article,.devsite-main-content,devsite-content{background:0!important;border:0!important;-webkit-box-shadow:none!important;box-shadow:none!important;display:block!important;margin:0!important;max-width:none!important;padding:0!important;width:auto!important}.devsite-banner{margin-top:0}.attempt-left,.attempt-right,.video-wrapper{float:none;margin:16px 0}img,video{display:block!important;page-break-inside:avoid!important}.devsite-main-content a[href]:after{content:" (" attr(href) ")";display:inline-block;font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;max-width:100%;word-wrap:break-word}}@page{margin:.75in}.devsite-product-platform-row{margin:8px 0;padding:0 24px}.devsite-header-no-lower-tabs .devsite-product-platform-row{margin-top:-12px;padding-bottom:24px}.devsite-platform-container{display:inline-block}.devsite-platform-container+.devsite-platform-container{margin-left:16px}.devsite-platform-icon-container{background:#fff;-webkit-border-radius:50%;border-radius:50%;height:40px;margin:0 auto;width:40px}.devsite-platform-icon{color:#5f6368;font-size:24px;height:24px;margin:8px;width:24px}@media screen and (max-width:1000px){div.devsite-collapsible-section,div.devsite-header-background{background-image:none}}@media screen and (max-width:600px){.devsite-product-platform-row{padding:0 16px}}.devsite-landing-row{padding:40px 0}.devsite-landing-row-group,.devsite-landing-row-html{margin:0 auto;max-width:1520px;padding:0 40px}.devsite-landing-row:not([background]){background-color:#fff}.devsite-landing-row:not([background])+.devsite-landing-row:not([background]),.devsite-landing-row[background=grey]+.devsite-landing-row[background=grey],.devsite-landing-row[background=theme]+.devsite-landing-row[background=theme]{padding-top:0}.devsite-landing-row:not([background])+.devsite-landing-row.devsite-landing-row-cta{padding-top:40px}@media screen and (max-width:840px){.devsite-landing-row{padding:24px 0}.devsite-landing-row-group,.devsite-landing-row-html{padding:0 24px}}@media screen and (max-width:600px){.devsite-landing-row{padding:16px 0}.devsite-landing-row-group,.devsite-landing-row-html{padding:0 16px}}.devsite-landing-row-cta{text-align:center}.devsite-landing-row-cta .devsite-landing-row-item{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.devsite-landing-row-cta h3{font:300 34px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;font-weight:400;letter-spacing:-.01em;margin-bottom:16px}.devsite-landing-row-cta h3+.devsite-landing-row-item-buttons{margin-top:8px}.devsite-landing-row-cta.devsite-landing-row-1-up .devsite-landing-row-item-description{margin:0}.devsite-landing-row-header{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end;margin:0 auto 32px;max-width:1520px;padding:0 40px}.devsite-landing-row-header+.devsite-landing-row-group{margin-top:32px}.devsite-landing-row-header-text{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto}.devsite-landing-row-header-text>h2{margin:0}.devsite-landing-row-description{font:18px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.devsite-landing-row:not([background]):not([foreground]) .devsite-landing-row-description{color:#5f6368}h2+.devsite-landing-row-description{margin-top:16px}.devsite-landing-row-header-buttons{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;margin:-4px}.devsite-landing-row-header-buttons>.button{margin:4px}.devsite-landing-row-header-centered .devsite-landing-row-header{display:block;text-align:center}.devsite-landing-row-header-centered .devsite-landing-row-description{margin-left:auto;margin-right:auto;max-width:856px}.devsite-landing-row-header-centered .devsite-landing-row-header-buttons{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin-top:24px}@media screen and (max-width:840px){.devsite-landing-row-header{padding:0 24px}}@media screen and (max-width:600px){.devsite-landing-row-header{display:block;padding:0 16px}.devsite-landing-row-header-text+.devsite-landing-row-header-buttons{display:block;margin:16px 0 0 -4px}}.devsite-landing-row-group{display:-webkit-box;display:-webkit-flex;display:flex}.devsite-landing-row-column>.devsite-landing-row-item:not(:first-child){margin-top:32px}.devsite-landing-row-column,.devsite-landing-row-item{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;max-width:100%;min-width:0}.devsite-landing-row-item-hidden{visibility:hidden}.devsite-landing-row-1-up .devsite-landing-row-item{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.devsite-landing-row-1-up.devsite-landing-row-100 .devsite-landing-row-item{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-column:not(:first-child),.devsite-landing-row-item:not(:first-child){margin-left:24px}[dir=rtl] .devsite-landing-row-column:not(:first-child),[dir=rtl] .devsite-landing-row-item:not(:first-child){margin-left:0;margin-right:24px}.devsite-landing-row-column>.devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:none;flex:none;margin-left:0}[dir=rtl] .devsite-landing-row-column>.devsite-landing-row-item{margin-right:0}@media screen and (max-width:840px){.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column,.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item{-webkit-flex-basis:-webkit-calc((100% - 24px)/2);flex-basis:calc((100% - 24px)/2)}.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child){margin:24px 0 0}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2),.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column:not(:first-child){margin:0 0 0 24px}[dir=rtl] .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2),[dir=rtl] .devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column:not(:first-child){margin:0 24px 0 0}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column:nth-of-type(3){margin:24px 0 0}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column:nth-of-type(4),.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(4){margin:24px 0 0 24px}[dir=rtl] .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column:nth-of-type(4),[dir=rtl] .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(4){margin:24px 24px 0 0}.devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child){margin:0 0 0 24px}[dir=rtl] .devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child){margin:0 24px 0 0}.devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item{width:100%}.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item:not(:first-child){margin:24px 0 0}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(.devsite-landing-row-item-no-media),.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(.devsite-landing-row-item-no-media){-webkit-flex-basis:100%;flex-basis:100%;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;margin-left:0}.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item.devsite-landing-row-item-no-description{display:block}.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item.devsite-landing-row-item-no-description,.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item.devsite-landing-row-item-no-description .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item,.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item{-webkit-flex-basis:auto;flex-basis:auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column{width:-webkit-calc((100% - 24px)/2);width:calc((100% - 24px)/2)}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2){margin:0 0 0 24px}[dir=rtl] .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2){margin:0 24px 0 0}.devsite-landing-row-3-up.devsite-landing-row-cards .devsite-landing-row-item-hidden:nth-of-type(3),.devsite-landing-row-4-up.devsite-landing-row-cards .devsite-landing-row-item-hidden:nth-of-type(3),.devsite-landing-row-4-up.devsite-landing-row-cards .devsite-landing-row-item-hidden:nth-of-type(3)~.devsite-landing-row-item-hidden,.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item-hidden:nth-of-type(n+3){display:none}}@media screen and (max-width:600px){.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-group{display:block}.devsite-landing-row-1-up .devsite-landing-row-column,.devsite-landing-row-1-up .devsite-landing-row-item,.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(.devsite-landing-row-item-no-media),.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(.devsite-landing-row-item-no-media){-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-2-up .devsite-landing-row-column,.devsite-landing-row-3-up .devsite-landing-row-column,.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column,.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item{-webkit-flex-basis:100%;flex-basis:100%}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column:nth-of-type(2n),.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2n),.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column:not(:first-child),.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child){margin:24px 0 0}.devsite-landing-row .devsite-landing-row-item-no-media{-webkit-flex-basis:100%;flex-basis:100%}.devsite-landing-row-logos .devsite-landing-row-column,.devsite-landing-row-logos .devsite-landing-row-item{-webkit-flex-basis:-webkit-calc((100% - 24px)/2);flex-basis:calc((100% - 24px)/2)}.devsite-landing-row-logos .devsite-landing-row-column:nth-child(n+3),.devsite-landing-row-logos .devsite-landing-row-item:nth-child(n+3){margin:24px 0 0}.devsite-landing-row-3-up.devsite-landing-row-logos .devsite-landing-row-column,.devsite-landing-row-3-up.devsite-landing-row-logos .devsite-landing-row-item{-webkit-flex-basis:-webkit-calc((100% - 32px)/3);flex-basis:calc((100% - 32px)/3)}.devsite-landing-row-3-up.devsite-landing-row-logos .devsite-landing-row-column:not(:first-child),.devsite-landing-row-3-up.devsite-landing-row-logos .devsite-landing-row-item:not(:first-child){margin:0}.devsite-landing-row-logos .devsite-landing-row-item:nth-child(2n){margin-left:24px}[dir=rtl] .devsite-landing-row-logos .devsite-landing-row-item:nth-child(2n){margin-left:0;margin-right:24px}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column{width:100%}.devsite-landing-row-4-up.devsite-landing-row-logos .devsite-landing-row-item-hidden:nth-of-type(3),.devsite-landing-row-4-up.devsite-landing-row-logos .devsite-landing-row-item-hidden:nth-of-type(3)~.devsite-landing-row-item-hidden,.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item-hidden{display:none}}.devsite-landing-row-item-media{margin:0 0 32px;min-width:0}.devsite-landing-row-item-code devsite-code,.devsite-landing-row-item[background] .devsite-landing-row-item-media{margin:0}.devsite-landing-row-item-video{display:block}.devsite-landing-row-item[background] .devsite-landing-row-item-description{padding:16px}.devsite-landing-row-item-body,.devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;min-width:0;width:100%}.devsite-landing-row-item .devsite-landing-row-item-buttons{margin:auto 0 -8px -12px;padding-top:8px}[dir=rtl] .devsite-landing-row-item .devsite-landing-row-item-buttons{margin:auto -12px -8px 0}.devsite-landing-row-1-up .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((200% - 40px)/3);flex:0 0 calc((200% - 40px)/3);margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.devsite-landing-row-1-up .devsite-landing-row-item-description{display:block;-webkit-box-flex:0;-webkit-flex:0 1 -webkit-calc((200% - 40px)/3);flex:0 1 calc((200% - 40px)/3);margin:0 24px 0 0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}[dir=rtl] .devsite-landing-row-1-up .devsite-landing-row-item-description{margin:0 0 0 24px}.devsite-landing-row-75 .devsite-landing-row-item-description,.devsite-landing-row-100 .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}.devsite-landing-row-1-up.devsite-landing-row-100 .devsite-landing-row-item-description{margin:0}.devsite-landing-row-1-up .devsite-landing-row-item-media-left{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.devsite-landing-row-1-up .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0 0 0 40px;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}[dir=rtl] .devsite-landing-row-1-up .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0 24px 0 0}.devsite-landing-row-1-up .devsite-landing-row-item[background] .devsite-landing-row-item-media+.devsite-landing-row-item-description,.devsite-landing-row-1-up .devsite-landing-row-item[background] .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0 40px}.devsite-landing-row-50 .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 24px)/2);flex:0 0 calc((100% - 24px)/2)}.devsite-landing-row-67 .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 48px)/3);flex:0 0 calc((100% - 48px)/3)}.devsite-landing-row-75 .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 72px)/4);flex:0 0 calc((100% - 72px)/4)}.devsite-landing-row-100 .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;margin:32px 0 0}.devsite-landing-row-item-description h2,.devsite-landing-row-large-headings h3,.devsite-landing-row h3:first-child,.devsite-landing-row h4+p,.devsite-landing-row h4:first-child,.devsite-landing-row h5:first-child,.devsite-landing-row h6:first-child,.devsite-landing-row p:first-child{margin-top:0}.devsite-landing-row-item-description-content>:last-child{margin-bottom:0}.devsite-landing-row-item-centered .devsite-landing-row-item-description-content,.devsite-landing-row-item-centered h3{text-align:center}.devsite-landing-row-item-centered .devsite-landing-row-item-buttons{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}@media screen and (max-width:840px){.devsite-landing-row-1-up .devsite-landing-row-item-media,.devsite-landing-row-1-up .devsite-landing-row-item-media-left,.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 24px)/2);flex:0 0 calc((100% - 24px)/2);margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2;overflow:hidden}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item-media{-webkit-box-flex:unset;-webkit-flex:unset;flex:unset;margin:0 0 32px}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item[background] .devsite-landing-row-item-media{margin:0}.devsite-landing-row-1-up .devsite-landing-row-item-description,.devsite-landing-row .devsite-landing-row-item-description{display:block;margin:0 24px 0 0}[dir=rtl] .devsite-landing-row-1-up .devsite-landing-row-item-description,[dir=rtl] .devsite-landing-row .devsite-landing-row-item-description{margin:0 0 0 24px}.devsite-landing-row-4-up .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex;margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.devsite-landing-row .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 24px)/2);flex:0 0 calc((100% - 24px)/2)}.devsite-landing-row-3-up .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 48px)/3);flex:0 0 calc((100% - 48px)/3)}.devsite-landing-row-1-up .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%}.devsite-landing-row-item-no-media .devsite-landing-row-item-description{margin:0}.devsite-landing-row-1-up .devsite-landing-row-item-description{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.devsite-landing-row-1-up .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{-webkit-box-ordinal-group:2;-webkit-order:1;order:1;margin:0 24px 0 0}[dir=rtl] .devsite-landing-row-1-up .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0 0 0 24px}.devsite-landing-row-1-up .devsite-landing-row-item[background] .devsite-landing-row-item-media+.devsite-landing-row-item-description{margin:0;padding:24px}.devsite-landing-row-logos .devsite-landing-row-item-description{margin:0}.devsite-landing-row-100 .devsite-landing-row-item-media,.devsite-landing-row-100:not(.devsite-landing-row-logos) .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;margin:32px 0 0}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item-media,.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;margin:0 0 32px;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item-description,.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item-description{-webkit-box-ordinal-group:3;-webkit-order:2;order:2;width:100%}}@media screen and (max-width:600px){.devsite-landing-row-1-up .devsite-landing-row-item-media,.devsite-landing-row-1-up .devsite-landing-row-item-media-left,.devsite-landing-row-item-media,.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;margin:0 0 32px;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.devsite-landing-row-1-up .devsite-landing-row-item-description,.devsite-landing-row-item-description{margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.devsite-landing-row .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%}.devsite-landing-row-1-up .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0}.devsite-landing-row-1-up .devsite-landing-row-item[background] .devsite-landing-row-item-media+.devsite-landing-row-item-description{padding:16px}.devsite-landing-row-item-no-media:not(:first-child),.devsite-landing-row .devsite-landing-row-item-no-media+.devsite-landing-row-item-no-media:nth-of-type(2n){margin:24px 0 0}.devsite-landing-row-cta .devsite-landing-row-item-description{font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.devsite-landing-row-item-buttons{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin-left:-12px;padding-top:8px}.devsite-landing-row-item-buttons .button{margin:4px 4px 4px 12px}.devsite-landing-row-item-buttons .button>.material-icons{top:-1px}.devsite-landing-row-item-buttons .button-white:not(.button-raised),.devsite-landing-row-item-buttons .button-white:not(.button-raised)+.button-white:not(.button-raised){margin:0 4px}.devsite-landing-row-cta .devsite-landing-row-item-buttons{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin:24px 0 0}p+.devsite-landing-row-item-buttons{margin-top:-8px}.devsite-landing-row-item-custom-image{padding:0 0 56.25%;position:relative}.devsite-landing-row-item-image.devsite-landing-row-item-custom-image:not([background]){background:#455a64}.devsite-landing-row-item-custom-image[background=grey]{background:#f1f3f4}.devsite-landing-row-item-custom-image[background=white]{background:#fff}.devsite-landing-row-no-image-background .devsite-landing-row-item-custom-image:not([background]){background:0}.devsite-landing-row-item-custom-image-icon-wrapper{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;height:100%;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;position:absolute;width:100%}.devsite-landing-row-item-custom-image-icon-container[background]{background:0}.devsite-landing-row-item-custom-image-icon{color:#fff}.devsite-landing-row-item-custom-image[background=grey]:not([foreground]) .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-item-custom-image[background=white]:not([foreground]) .devsite-landing-row-item-custom-image-icon{color:#5f6368}.devsite-landing-row-item-custom-image-icon.material-icons{opacity:.8}.devsite-landing-row-1-up .devsite-landing-row-item-custom-image-icon{font-size:256px;max-height:256px;width:256px}.devsite-landing-row-2-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-50 .devsite-landing-row-item-custom-image-icon{font-size:192px;max-height:192px;width:192px}.devsite-landing-row-3-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-67 .devsite-landing-row-item-custom-image-icon,[layout=docs] .devsite-landing-row-2-up .devsite-landing-row-item-custom-image-icon{font-size:128px;max-height:128px;width:128px}.devsite-landing-row-4-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-75 .devsite-landing-row-item-custom-image-icon,[layout=docs] .devsite-landing-row-2-up .devsite-landing-row-item-custom-image-icon{font-size:96px;max-height:96px;width:96px}@media screen and (max-width:840px){.devsite-landing-row-1-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-2-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-3-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-4-up .devsite-landing-row-item-custom-image-icon{font-size:128px;max-height:128px;width:128px}}.devsite-landing-row-1-up .devsite-landing-row-item-description[icon-position=left],.devsite-landing-row-item-description[icon-position]{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.devsite-landing-row-item-description[icon-position=top]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-item-icon-container{color:#5f6368;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:56px;margin:0 20px 8px 0;-webkit-transition:background .2s,color .2s,-webkit-box-shadow .2s;transition:background .2s,color .2s,-webkit-box-shadow .2s;-o-transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s,-webkit-box-shadow .2s;width:56px}[dir=rtl] .devsite-landing-row-item-icon-container{margin:0 0 8px 20px}.devsite-landing-row-item-icon{font-size:48px;height:48px;margin:0 0 0 4px;width:48px}[dir=rtl] .devsite-landing-row-item-icon{margin:0 4px 0 0}.devsite-landing-row-item-icon-container[background]{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin-bottom:20px}.devsite-landing-row-item-icon-container[background] .devsite-landing-row-item-icon{font-size:36px;height:36px;margin:0;width:36px}[background] .devsite-landing-row-item-icon-container:not([foreground]){color:#fff}[background=grey] .devsite-landing-row-item-icon-container:not([foreground]){color:#5f6368}:focus .devsite-landing-row-item-icon-container,:link .devsite-landing-row-item-icon-container:hover{color:#1a73e8}[foreground] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[foreground] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:rgba(154,160,166,.5)}[background] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:hsla(0,0%,100%,.7)}.devsite-landing-row-item-icon-container[background=grey]:not([foreground]),.devsite-landing-row-item-icon-container[background=white]:not([foreground]),.devsite-landing-row-item-icon-container[foreground=grey]{color:#5f6368}.devsite-landing-row-item-icon-container[background=grey]{background:#f1f3f4}.devsite-landing-row-item-icon-container[background=white]{background:#fff}.devsite-landing-row-item-icon-container[background][foreground=white],.devsite-landing-row-item-icon-container[foreground=white]{color:#fff}:focus .devsite-landing-row-item-icon-container[background][foreground=grey],:link .devsite-landing-row-item-icon-container[background][foreground=grey]:hover{color:#5f6368}:focus .devsite-landing-row-item-icon-container[background][foreground=white],:link .devsite-landing-row-item-icon-container[background][foreground=white]:hover{color:#fff}:focus .devsite-landing-row-item-icon-container[background],:link .devsite-landing-row-item-icon-container[background]:hover{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}[background] :focus .devsite-landing-row-item-icon-container[background],[background] :link .devsite-landing-row-item-icon-container[background]:hover{color:#fff}[background=grey] :focus .devsite-landing-row-item-icon-container[background],[background=grey] :link .devsite-landing-row-item-icon-container[background]:hover{color:#5f6368}.devsite-landing-row-item-image{-webkit-align-self:flex-start;align-self:flex-start}.devsite-landing-row-item-image:not([background]){background:#e8eaed}.devsite-landing-row-item-image img{vertical-align:middle;width:100%}.devsite-landing-row-item-image a{display:block}.devsite-landing-row-no-image-background .devsite-landing-row-item-image:not([background]){background:0}.devsite-landing-row-item-labels>a,.devsite-landing-row-item-labels>span{display:inline-block;font:500 11px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.8px;margin-bottom:8px;text-transform:uppercase}.devsite-landing-row-item-labels>a+a,.devsite-landing-row-item-labels>a+span,.devsite-landing-row-item-labels>span+a,.devsite-landing-row-item-labels>span+span{margin-left:8px}.devsite-landing-row-item-labels>a[background],.devsite-landing-row-item-labels>span[background]{-webkit-border-radius:4px;border-radius:4px;margin-bottom:16px;padding:4px 8px}.devsite-landing-row-item-description-callout{font-weight:700}.devsite-landing-row-item-description-feature{margin-top:16px;position:relative}.devsite-landing-row-item-description-feature+.devsite-landing-row-item-description-feature{margin:0}.devsite-landing-row-item-description-feature-link{border-bottom:1px solid #e8eaed;font-weight:500;padding:12px 0 11px}.devsite-landing-row-item-description-feature-tooltip{background:#455a64;color:hsla(0,0%,100%,.7);-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;min-width:200px;opacity:0;padding:24px;position:absolute;-webkit-transition:opacity .2s,visibility .2s;-o-transition:opacity .2s,visibility .2s;transition:opacity .2s,visibility .2s;visibility:hidden;width:67%;z-index:1020}.no-touch .devsite-landing-row-item-description-feature-link:hover+.devsite-landing-row-item-description-feature-tooltip{opacity:1;visibility:visible}.devsite-landing-row-item-description-feature-tooltip:before{border-bottom:8px solid #455a64;border-left:8px solid transparent;border-right:8px solid transparent;content:"";position:absolute;top:-8px}.devsite-landing-row-item-description-feature-tooltip h3,.devsite-landing-row-large-headings .devsite-landing-row-item-description-feature-tooltip h3{color:#fff;font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-bottom:8px;padding:0}.devsite-landing-row-item-list{padding:0}.devsite-landing-row-item-description-content+.devsite-landing-row-item-list{margin-top:32px}.devsite-landing-row-item-list-item{list-style:none}.devsite-landing-row-item-list-item-content{display:-webkit-box;display:-webkit-flex;display:flex}.devsite-landing-row-item-list-item-content[icon-position=top]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-item-list-item-icon-container{color:#5f6368;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:40px;margin:0 20px 0 0;-webkit-transition:background .2s,color .2s,-webkit-box-shadow .2s;transition:background .2s,color .2s,-webkit-box-shadow .2s;-o-transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s,-webkit-box-shadow .2s;width:40px}[dir=rtl] .devsite-landing-row-item-list-item-icon-container{margin:0 0 0 20px}.devsite-landing-row-item-list-item-icon{font-size:32px;height:32px;width:32px}.devsite-landing-row-item-list-item-icon-container[background]{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.devsite-landing-row-item-list-item-icon-container[background] .devsite-landing-row-item-list-item-icon{font-size:24px;height:24px;width:24px}.devsite-landing-row-item-list h4{font:400 16px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.devsite-landing-row .devsite-landing-row-item-list h4{margin:0 0 4px}.devsite-landing-row-item-list-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;font:400 14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;max-width:-webkit-calc(100% - 40px);max-width:calc(100% - 40px)}@media screen and (max-width:840px){.devsite-landing-row-item-list-item-description{max-width:none}}.devsite-landing-row-item-list-item:not(:last-child) .devsite-landing-row-item-list-item-description-content{margin-bottom:24px}[background] .devsite-landing-row-item-list-item-icon-container:not([foreground]){color:#fff}.devsite-landing-row-item-list-item-icon-container[background=grey]:not([foreground]),.devsite-landing-row-item-list-item-icon-container[background=white]:not([foreground]),.devsite-landing-row-item-list-item-icon-container[background][foreground=grey],.devsite-landing-row-item-list-item-icon-container[foreground=grey]{color:#5f6368}.devsite-landing-row-item-list-item-icon-container[background=grey]{background:#f1f3f4}.devsite-landing-row-item-list-item-icon-container[background=white]{background:#fff}.devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=white]{color:#fff}:focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=grey],:link .devsite-landing-row-item-list-item-icon-container[foreground=grey]:hover{color:rgba(154,160,166,.5)}:focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=white],:link .devsite-landing-row-item-list-item-icon-container[foreground=white]:hover{color:hsla(0,0%,100%,.7)}:focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[background][foreground=grey],:link .devsite-landing-row-item-list-item-icon-container[background][foreground=grey]:hover{color:#5f6368}:focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[background][foreground=white],:link .devsite-landing-row-item-list-item-icon-container[background][foreground=white]:hover{color:#fff}.devsite-landing-row-item-list-item a:focus,:link>.devsite-landing-row-item-list-item-content:not(.material-icons):hover,[background] :link>.devsite-landing-row-item-list-item-content:not(.material-icons):hover{text-decoration:none}[foreground] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[foreground] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}[background] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:hsla(0,0%,100%,.7)}:link .devsite-landing-row-item-list-item-description h4+p{color:#202124;text-decoration:none}[background] :link .devsite-landing-row-item-list-item-description h4+p{color:#fff}:focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[background],:link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container[background]{background:hsla(0,0%,100%,.7);-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}.devsite-landing-row :focus .devsite-landing-row-item-list-item-description>:first-child,[background] :link .devsite-landing-row-item-list-item-description>:first-child{text-decoration:underline}.devsite-landing-row-logos .devsite-landing-row-item-media{margin:0}.devsite-landing-row-logos .devsite-landing-row-item-custom-image{padding-bottom:96px}body[theme] .devsite-landing-row-logos .devsite-landing-row-item-custom-image:not([background]){background:0}.devsite-landing-row-logos .devsite-landing-row-item-custom-image-icon{opacity:1;width:96px}.devsite-landing-row-logos .devsite-landing-row-item-description{text-align:center}.devsite-landing-row-logos .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 72px)/4);flex:0 0 calc((100% - 72px)/4)}@media screen and (max-width:600px){.devsite-landing-row-logos .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 24px)/2);flex:0 0 calc((100% - 24px)/2)}}.devsite-landing-row-cards .devsite-landing-row-item{-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);overflow:hidden;position:relative}.devsite-landing-row-cards .devsite-landing-row-item:not([background]){background-color:#fff}body[theme] .devsite-landing-row-cards[background=theme] :link:not(.button),body[theme] .devsite-landing-row-cards[background=theme] :visited:not(.button){color:#1a73e8}.devsite-landing-row-cards .devsite-landing-row-column{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.devsite-landing-row-cards .devsite-landing-row-column>.devsite-landing-row-item:not(:first-child){margin-top:24px}.devsite-landing-row-cards .devsite-landing-row-column .devsite-landing-row-item{display:block;-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto}.devsite-landing-row-cards .devsite-landing-row-item-media{margin-bottom:0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.devsite-landing-row-cards .devsite-landing-row-item-description{margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2;padding:16px}.devsite-landing-row-cards .devsite-landing-row-item-buttons{padding-top:16px}.devsite-landing-row-cards .devsite-landing-row-item-no-media{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-cards[background=theme] .devsite-landing-row-item-icon{color:#5f6368}.devsite-landing-row-cards .devsite-landing-row-item-no-media h3{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0 0 20px}.devsite-landing-row-cards.devsite-landing-row-1-up .devsite-landing-row-item-media{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;margin:0}@media screen and (max-width:840px){.devsite-landing-row-cards.devsite-landing-row-1-up .devsite-landing-row-item-media,.devsite-landing-row-cards:not(.devsite-landing-row-logos) .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;margin:0}.devsite-landing-row-cards.devsite-landing-row-1-up .devsite-landing-row-item{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item,.devsite-landing-row-cards.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(.devsite-landing-row-item-no-media),.devsite-landing-row-cards.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item{-webkit-flex-basis:-webkit-calc((100% - 24px)/2);flex-basis:calc((100% - 24px)/2);-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-child(2),.devsite-landing-row-cards.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-child(2){margin:0 0 0 24px}[dir=rtl] .devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-child(2),[dir=rtl] .devsite-landing-row-cards.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-child(2){margin:0 24px 0 0}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column>.devsite-landing-row-item:nth-child(2),.devsite-landing-row-cards .devsite-landing-row-column>.devsite-landing-row-item:nth-child(n+2){margin:24px 0 0}.devsite-landing-row-cards .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex}}@media screen and (max-width:600px){.devsite-landing-row-cards{-webkit-flex-basis:100%;flex-basis:100%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;padding-bottom:16px;padding-top:16px}.devsite-landing-row-cards+.devsite-landing-row-cards .devsite-landing-row-header{padding-top:16px}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column>.devsite-landing-row-item:nth-child(2),.devsite-landing-row-cards .devsite-landing-row-column>.devsite-landing-row-item:nth-child(n+2),.devsite-landing-row-cards:not(.devsite-landing-row-logos) .devsite-landing-row-column:not(:first-child){margin:16px 0 0}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item,.devsite-landing-row-cards.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item,.devsite-landing-row-cards.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child),.devsite-landing-row-cards.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child),.devsite-landing-row-cards.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child){margin:16px 0 0}.devsite-landing-row-cards .devsite-landing-row-item-no-media .devsite-landing-row-item-description{padding-top:16px}}.devsite-landing-row-1-up.devsite-landing-row-marquee,.devsite-landing-row-1-up.devsite-landing-row-marquee[background]{padding:0}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-header{margin:0;padding:40px 0 0}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-media{-webkit-align-self:center;align-self:center;margin:0}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-image{background:0}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-description{-webkit-align-self:center;align-self:center;padding:40px 0}@media screen and (max-width:840px){.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-media{margin:0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1;overflow:visible;width:100%}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-description{margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2;width:100%}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0}}@media screen and (max-width:600px){.devsite-landing-row-1-up.devsite-landing-row-marquee[background]{margin:0 -16px;padding:0}.devsite-landing-row-1-up.devsite-landing-row-marquee[background] .devsite-landing-row-item-description{padding:32px 16px}}body[layout=full] devsite-code{overflow:visible}body[layout=full] devsite-code:after{background:#f1f3f4;content:"";display:block;height:100%;left:-webkit-calc(50% - 50vw);left:calc(50% - 50vw);position:absolute;top:0;width:100vw;z-index:-1}body[layout=full] devsite-code[dark-code]:after{background:#283142}[background] :link:not(.button),[background] :visited:not(.button){color:inherit}body[type=landing][layout=docs] h2{border-bottom:0;padding-bottom:0}[background]:not([background=grey]) :link>:not(.material-icons):hover,[background]:not([background=grey]) p>a:not(.button){text-decoration:none}.dac-center{-webkit-box-flex:1;-webkit-flex:1;flex:1;text-align:center}.dac-grow-1{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}.dac-grow-2{-webkit-box-flex:2;-webkit-flex-grow:2;flex-grow:2}.dac-grow-3{-webkit-box-flex:3;-webkit-flex-grow:3;flex-grow:3}.dac-grow-4{-webkit-box-flex:4;-webkit-flex-grow:4;flex-grow:4}.dac-grow-5{-webkit-box-flex:5;-webkit-flex-grow:5;flex-grow:5}.dac-grow-6{-webkit-box-flex:6;-webkit-flex-grow:6;flex-grow:6}.dac-grow-7{-webkit-box-flex:7;-webkit-flex-grow:7;flex-grow:7}.dac-grow-8{-webkit-box-flex:8;-webkit-flex-grow:8;flex-grow:8}.dac-grow-9{-webkit-box-flex:9;-webkit-flex-grow:9;flex-grow:9}.dac-grow-10{-webkit-box-flex:10;-webkit-flex-grow:10;flex-grow:10}.dac-grow-11{-webkit-box-flex:11;-webkit-flex-grow:11;flex-grow:11}.dac-grow-12{-webkit-box-flex:12;-webkit-flex-grow:12;flex-grow:12}.dac-full-width{width:100%}.dac-hidden-sm-up,.dac-hidden-xs-down.dac-hidden-sm-up,.dac-hidden-xs-up{display:none!important}.dac-flex-spread{-webkit-box-align:baseline;-webkit-align-items:baseline;align-items:baseline;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}@media screen and (max-width:720px){.dac-hidden-xs-down{display:none!important}.dac-hidden-sm-up,.dac-hidden-xs-up{display:block!important}}@media screen and (max-width:1000px){.dac-hidden-sm-down{display:none!important}}@media screen and (min-width:720px) and (max-width:1000px){.dac-hidden-xs-down.dac-hidden-sm-up{display:block!important}}.dac-landing-row-item-height-small{min-height:350px}.dac-landing-row-item-height-medium{min-height:500px}.dac-landing-row-item-height-large{min-height:750px}@media screen and (max-width:1000px){.dac-landing-row-item-height-small{min-height:inherit}.dac-landing-row-item-height-large,.dac-landing-row-item-height-medium{min-height:250px}}.dac-heading-hidden h1,.dac-heading-hidden h2,.dac-heading-hidden h3,.dac-heading-hidden h4,.dac-heading-hidden h5{display:none}.dac-banner-card,.dac-banner-card.dac-landing-row-bg-slate,.dac-landing-row-bg-blob-8,.dac-landing-row.dac-landing-row-collapse{background-clip:content-box}.devsite-landing-row:not([background]).dac-landing-row-bg-blob-8,[background=grey]{background-color:#f7f9fa}.dac-landing-row-bg-mint{background-color:#55ffb5}.dac-landing-row-bg-yellow{background-color:#ffd600}.dac-landing-row-bg-mint,.dac-landing-row-bg-yellow .button.dac-outline-button{border-color:#414141;color:#414141}.dac-landing-row-bg-mint:focus,.dac-landing-row-bg-mint:hover,.dac-landing-row-bg-yellow .button.dac-outline-button:focus,.dac-landing-row-bg-yellow .button.dac-outline-button:hover{background:#414141;border-color:#414141;color:#fff}.dac-landing-row-bg-blue{background-color:#2196f3}.dac-landing-row-bg-blue:link:not(.dac-button),[background=grey]:not(.devsite-landing-row-cards) .dac-landing-row-bg-slate h3{color:#fff}.dac-landing-row-bg-slate,.devsite-landing-row:not([background]).dac-landing-row-bg-slate{background-color:#455a64}.dac-landing-row-bg-green,.dac-landing-row-item-description-bg-green .devsite-landing-row-item-description{background-color:#4b7d2f}.dac-landing-row-bg-blue .dac-button,.dac-landing-row-bg-blue .devsite-landing-row-item-body h2,.dac-landing-row-bg-blue .devsite-landing-row-item-body h2 :link,.dac-landing-row-bg-blue .devsite-landing-row-item-body h2 :visited,.dac-landing-row-bg-blue .devsite-landing-row-item-body h3,.dac-landing-row-bg-blue .devsite-landing-row-item-body h3 :link,.dac-landing-row-bg-blue .devsite-landing-row-item-body h3 :visited,.dac-landing-row-bg-blue .devsite-landing-row-item-body h4,.dac-landing-row-bg-blue .devsite-landing-row-item-body h4 :link,.dac-landing-row-bg-blue .devsite-landing-row-item-body h4 :visited,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content :link,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content :visited,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content p,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content p :link,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content p :visited,.dac-landing-row-bg-green .dac-button,.dac-landing-row-bg-green .devsite-landing-row-item-body h2,.dac-landing-row-bg-green .devsite-landing-row-item-body h2 :link,.dac-landing-row-bg-green .devsite-landing-row-item-body h2 :visited,.dac-landing-row-bg-green .devsite-landing-row-item-body h3,.dac-landing-row-bg-green .devsite-landing-row-item-body h3 :link,.dac-landing-row-bg-green .devsite-landing-row-item-body h3 :visited,.dac-landing-row-bg-green .devsite-landing-row-item-description-content,.dac-landing-row-bg-green .devsite-landing-row-item-description-content :link,.dac-landing-row-bg-green .devsite-landing-row-item-description-content :visited,.dac-landing-row-bg-slate .dac-button,.dac-landing-row-bg-slate .devsite-landing-row-item-body h2,.dac-landing-row-bg-slate .devsite-landing-row-item-body h2 :link,.dac-landing-row-bg-slate .devsite-landing-row-item-body h2 :visited,.dac-landing-row-bg-slate .devsite-landing-row-item-body h3,.dac-landing-row-bg-slate .devsite-landing-row-item-body h3 :link,.dac-landing-row-bg-slate .devsite-landing-row-item-body h3 :visited,.dac-landing-row-bg-slate .devsite-landing-row-item-body h4,.dac-landing-row-bg-slate .devsite-landing-row-item-body h4 :link,.dac-landing-row-bg-slate .devsite-landing-row-item-body h4 :visited,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content :link,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content :visited,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content p,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content p :link,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content p :visited{color:#fff}.dac-landing-row-bg-blue .dac-button:focus,.dac-landing-row-bg-blue .dac-button:hover,.dac-landing-row-bg-green .dac-button:focus,.dac-landing-row-bg-green .dac-button:hover,.dac-landing-row-bg-slate .dac-button:focus,.dac-landing-row-bg-slate .dac-button:hover{opacity:.4}.dac-landing-row-bg-blue .dac-outline-button,.dac-landing-row-bg-green .dac-outline-button,.dac-landing-row-bg-slate .dac-outline-button{border-color:#fff}.dac-landing-row-bg-blue .dac-outline-button:focus,.dac-landing-row-bg-blue .dac-outline-button:hover,.dac-landing-row-bg-green .dac-outline-button:focus,.dac-landing-row-bg-green .dac-outline-button:hover,.dac-landing-row-bg-slate .dac-outline-button:focus,.dac-landing-row-bg-slate .dac-outline-button:hover{background:#fff;border-color:#fff;color:#414141;opacity:1}.dac-landing-row-bg-mint .dac-outline-button,.dac-landing-row-bg-yellow .dac-outline-button{border-color:#414141;color:#414141}.dac-landing-row-bg-mint .dac-outline-button:focus,.dac-landing-row-bg-mint .dac-outline-button:hover,.dac-landing-row-bg-yellow .dac-outline-button:focus,.dac-landing-row-bg-yellow .dac-outline-button:hover{background:#414141;border-color:#414141;color:#fff}.devsite-banner-announcement,.devsite-banner-announcement :link,.devsite-banner-announcement :visited{background:#eff7cf}.dac-banner-card.devsite-landing-row .devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0}@media screen and (max-width:720px){.dac-banner-card.devsite-landing-row .devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:none;flex:none}}.dac-banner-card.devsite-landing-row .devsite-landing-row-item-description{padding:32px}@media screen and (max-width:720px){.dac-banner-card.devsite-landing-row .devsite-landing-row-item-description{padding:24px}}.dac-banner-card.devsite-landing-row .devsite-landing-row-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:nowrap;flex-wrap:nowrap}.dac-banner-card.devsite-landing-row img{display:none;margin-top:32px}@media screen and (max-width:720px){.dac-banner-card.devsite-landing-row img{display:block}}.dac-banner-card.devsite-landing-row .dac-banner-card-bg-img-item{background-repeat:no-repeat;-o-background-size:cover;background-size:cover;margin-left:0}.dac-banner{text-align:center}.dac-banner .devsite-landing-row-item{-webkit-box-align:center;-webkit-align-items:center;align-items:center}.dac-banner .devsite-landing-row-item-description{max-width:750px}.dac-banner .devsite-landing-row-item-body h3{font:300 44px/56px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1000px){.dac-banner .devsite-landing-row-item-body h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-banner .devsite-landing-row-item-description-content{font:300 20px/28px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-banner-wide{background-color:transparent;background-repeat:no-repeat}.dac-banner-wide img{display:none}@media screen and (max-width:720px){.dac-banner-wide img{display:block;margin:24px auto;width:70%}}.dac-banner-wide .devsite-landing-row-item-no-media .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-banner-wide .devsite-landing-row-item{max-width:50%;padding:32px 0}@media screen and (max-width:720px){.dac-banner-wide .devsite-landing-row-item{padding:0;max-width:none}}.dac-heading-linked.devsite-landing-row-item .devsite-landing-row-item-buttons,.dac-landing-row-item-buttons-bottom .devsite-landing-row-item-buttons{padding-top:32px}.dac-button,.dac-flat-button-muted{-webkit-box-shadow:none;box-shadow:none;font:500 16px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:48px;letter-spacing:.5px;padding:0 24px;-webkit-transition:background-color .2s,color .2s,-webkit-box-shadow .2s;transition:background-color .2s,color .2s,-webkit-box-shadow .2s;-o-transition:background-color .2s,box-shadow .2s,color .2s;transition:background-color .2s,box-shadow .2s,color .2s;transition:background-color .2s,box-shadow .2s,color .2s,-webkit-box-shadow .2s}.dac-button{background:#4b7d2f;color:#fff}.dac-button:focus,.dac-button:hover{background:#2f4d1f}.dac-flat-button-muted{background:#f7f9fa;color:#000}.dac-flat-button-muted:focus,.dac-flat-button-muted:hover{background:#d8e1e6}.dac-button+.dac-button{margin-left:32px}.dac-outline-button{background:0;border:2px solid #4b7d2f;-webkit-border-radius:4px;border-radius:4px;color:#4b7d2f}.dac-outline-button:focus,.dac-outline-button:hover{background:#4b7d2f;color:#fff}.dac-outline-button:disabled{background:0;border-color:#d7d7d7;color:#d7d7d7}.dac-alt-flat-button,.dac-flat-button{background:0;font:500 16px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0}.dac-alt-flat-button:focus,.dac-alt-flat-button:hover,.dac-flat-button:focus,.dac-flat-button:hover{background:0;opacity:.4}@media screen and (max-width:720px){.dac-alt-flat-button,.dac-flat-button{font:500 14px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-flat-button{color:#414141}.devsite-product-button-row .dac-header-button{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);background:#4b7d2f}.dac-button.dac-flat-button{height:auto;line-height:inherit;white-space:inherit}.dac-alt-flat-button{color:#4b7d2f}.dac-button-group .dac-button{margin-bottom:16px}.dac-button-group .dac-button:not(:last-child){margin-right:16px}.dac-full-width-content .devsite-landing-row-item-buttons{display:block}.dac-full-width-media .devsite-landing-row-item-buttons{display:none}.dac-resource-widget-more{padding:32px 0 0;text-align:center}@media screen and (max-width:600px){.dac-full-width-content .devsite-landing-row-item-buttons{display:none}.dac-full-width-media .devsite-landing-row-item-buttons{display:block}}.dac-jetpack:before,.dac-rss:before{content:"";height:40px;width:40px}.dac-jetpack:before{background:url(../images/custom/jetpack-icon.svg) 50%/40px no-repeat}.dac-rss:before{background:url(../images/custom/rss-icon.png) 50%/24px no-repeat}.dac-jetpack a,.dac-rss a{text-decoration:underline}.dac-jetpack a:focus,.dac-jetpack a:hover,.dac-rss a:focus,.dac-rss a:hover{text-decoration:none}.dac-jetpack a:focus,.dac-rss a:focus{background:rgba(26,115,232,.1);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px}div.dac-jetpack,div.dac-rss,span.dac-jetpack{-webkit-box-align:center;-webkit-align-items:center;align-items:center;color:#414141;display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;font-size:14px;font-weight:700;margin:0;vertical-align:text-bottom}[dir=ltr] div.dac-jetpack:before,[dir=ltr] div.dac-rss:before,[dir=ltr] span.dac-jetpack:before{margin-right:4px}[dir=rtl] div.dac-jetpack:before,[dir=rtl] div.dac-rss:before,[dir=rtl] span.dac-jetpack:before{margin-left:4px}[dir=ltr] div.dac-jetpack,[dir=ltr] div.dac-rss{padding-left:16px}[dir=rtl] div.dac-jetpack,[dir=rtl] div.dac-rss{padding-right:16px}span.dac-jetpack{position:relative}[dir=ltr] span.dac-jetpack{padding-left:12px}[dir=rtl] span.dac-jetpack{padding-right:12px}span.dac-jetpack:after{background:#414141;content:"";display:block;height:70%;margin:0;position:absolute;top:15%;width:2px}[dir=ltr] span.dac-jetpack:after{left:0}[dir=rtl] span.dac-jetpack:after{right:0}h2 span.dac-jetpack{margin-bottom:-6px}h3 span.dac-jetpack,h4 span.dac-jetpack{margin-bottom:-8px}h4 span.dac-jetpack>span{position:relative;top:3px}aside.dac-jetpack,aside.dac-jetpack :link,aside.dac-jetpack :visited,aside.dac-rss,aside.dac-rss :link,aside.dac-rss :visited{background:#e0f2f1;color:#00796b}aside.dac-jetpack :focus,aside.dac-jetpack :hover,aside.dac-rss :focus,aside.dac-rss :hover{background:hsla(0,0%,100%,.7)}aside.dac-jetpack code,aside.dac-rss code{background:0;color:#00796b}aside.dac-jetpack:before,aside.dac-rss:before{margin:-8px 0 0}[dir=ltr] aside.dac-jetpack:before,[dir=ltr] aside.dac-rss:before{margin-left:-44px}[dir=rtl] aside.dac-jetpack:before,[dir=rtl] aside.dac-rss:before{margin-right:-44px}.dac-card-footer{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;margin-top:85px;padding:52px 32px}@media screen and (max-width:1000px){.dac-card-footer{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;padding:32px}}.dac-card-footer img{margin:0 10px;max-width:50%}@media screen and (max-width:1000px){.dac-card-footer img{margin-bottom:32px;max-width:240px}}.devsite-landing-row-item-description-content,.devsite-landing-row:not([background]):not([foreground]) .devsite-landing-row-description{color:#414141;font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:720px){.devsite-landing-row-item-description-content,.devsite-landing-row:not([background]):not([foreground]) .devsite-landing-row-description{font:400 14px/22px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-dynamic-content-no-img .devsite-card-image-bg{display:none}.dac-dynamic-content-stack devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 1 100%;flex:0 1 100%}.dac-dynamic-content-section .devsite-landing-row-item,.dac-resource-widget-section .devsite-landing-row-item{display:block}devsite-dynamic-content .resource-widget[class*=col-]{float:none!important;padding:0!important}devsite-dynamic-content .devsite-card-group{margin:-32px 0 0 -32px}devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(33.33% - 32px);flex:0 0 calc(33.33% - 32px);margin:32px 0 0 32px;padding:0}.dac-full-width-page devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(25% - 32px);flex:0 0 calc(25% - 32px)}devsite-dynamic-content .devsite-card-content{padding:32px}devsite-dynamic-content .devsite-card{background:#f7f9fa;-webkit-box-shadow:none;box-shadow:none}.devsite-card-list-item,devsite-dynamic-content .devsite-card,devsite-dynamic-content .devsite-card-list-item{-webkit-border-radius:0;border-radius:0}devsite-dynamic-content .devsite-card h3{color:#202124}.devsite-card-list-link,devsite-dynamic-content .devsite-card-list-link,devsite-dynamic-content .devsite-card h3{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.devsite-card-list-link,devsite-dynamic-content .devsite-card-list-link{background:#f7f9fa;color:#80868b}.devsite-card-list-link:focus,.devsite-card-list-link:hover,devsite-dynamic-content .devsite-card-list-link:focus,devsite-dynamic-content .devsite-card-list-link:hover{background:#455a64;color:#fff}devsite-dynamic-content .devsite-card-category{font:500 14px/22px Roboto Mono,monospace;letter-spacing:1.5px}devsite-dynamic-content .devsite-pagination-less-button,devsite-dynamic-content .devsite-pagination-more-button{background:#4b7d2f;-webkit-box-shadow:none;box-shadow:none;color:#fff;font:500 16px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:46px;padding:0 24px}devsite-dynamic-content .devsite-pagination-less-button:focus,devsite-dynamic-content .devsite-pagination-less-button:hover,devsite-dynamic-content .devsite-pagination-more-button:focus,devsite-dynamic-content .devsite-pagination-more-button:hover{background:#2f4d1f}devsite-dynamic-content [dynamic-card-style=small] .devsite-card-content{padding:20px}devsite-dynamic-content .devsite-card[dynamic-card-type=medium]{background:rgba(0,0,0,.94)}devsite-dynamic-content .devsite-card[dynamic-card-type=medium],devsite-dynamic-content .devsite-card[dynamic-card-type=medium] h3{color:#fff}devsite-dynamic-content .devsite-card[dynamic-card-type=android\ developers] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=blogger] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=medium] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=video] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=youtube] .devsite-card-author{border:0;padding:12px 32px 32px 80px}devsite-dynamic-content .devsite-card[dynamic-card-type=android\ developers] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=blogger] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=medium] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=video] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=youtube] .devsite-card-author:before{background-repeat:no-repeat;content:"";height:40px;left:32px;position:absolute;width:40px}devsite-dynamic-content .devsite-card[dynamic-card-type=android\ developers] .devsite-card-author:before{background-image:url(../images/custom/android-developers-logo.svg)}devsite-dynamic-content .devsite-card[dynamic-card-type=blogger] .devsite-card-author:before{background-image:url(../images/custom/android-blog-round-icon.svg)}devsite-dynamic-content .devsite-card[dynamic-card-type=medium] .devsite-card-author:before{background-image:url(../images/custom/medium-round-icon.svg)}devsite-dynamic-content .devsite-card[dynamic-card-type=video] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=youtube] .devsite-card-author:before{background-image:url(../images/custom/youtube-round-icon.svg)}@media screen and (max-width:1000px){.dac-full-width-page devsite-dynamic-content .devsite-card-wrapper,body devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(100% - 32px);flex:0 0 calc(100% - 32px)}[has-toc] devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(50% - 32px);flex:0 0 calc(50% - 32px)}}@media screen and (min-width:720px) and (max-width:1000px){[type=landing]:not(.dac-news-page) .devsite-main-content:not([has-toc]) devsite-dynamic-content .devsite-card{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}[type=landing]:not(.dac-news-page) .devsite-main-content:not([has-toc]) devsite-dynamic-content .devsite-card>*{width:50%}[type=landing]:not(.dac-news-page) .devsite-main-content:not([has-toc]) devsite-dynamic-content .devsite-card-image-bg{height:170px;-webkit-box-ordinal-group:3;-webkit-order:2;order:2;padding-bottom:28.125%}}@media screen and (max-width:720px){devsite-dynamic-content .devsite-card[dynamic-card-type=android\ developers] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=blogger] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=medium] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=video] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=youtube] .devsite-card-author{padding-left:64px}devsite-dynamic-content .devsite-card[dynamic-card-type=android\ developers] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=blogger] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=medium] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=video] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=youtube] .devsite-card-author:before{left:24px}[has-toc] devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(100% - 32px);flex:0 0 calc(100% - 32px)}}.dac-label .devsite-landing-row-item-labels a,.dac-label .devsite-landing-row-item-labels span{margin-bottom:0;font:500 14px/22px Roboto Mono,monospace;letter-spacing:1.5px}@media screen and (max-width:720px){.dac-label .devsite-landing-row-item-labels a,.dac-label .devsite-landing-row-item-labels span{font:500 12px/22px Roboto Mono,monospace}}.dac-label.dac-landing-row-bg-blue .devsite-landing-row-item-labels a,.dac-label.dac-landing-row-bg-blue .devsite-landing-row-item-labels span,.dac-label.dac-landing-row-bg-green .devsite-landing-row-item-labels a,.dac-label.dac-landing-row-bg-green .devsite-landing-row-item-labels span,.dac-label.dac-landing-row-bg-slate .devsite-landing-row-item-labels a,.dac-label.dac-landing-row-bg-slate .devsite-landing-row-item-labels span{color:#fff}.dac-label.dac-success-story .devsite-landing-row-item-labels a,.dac-label.dac-success-story .devsite-landing-row-item-labels span{margin-bottom:16px}.dac-label.devsite-landing-row-item h3:first-child,.dac-label.devsite-landing-row h3:first-child,.dac-label h3{margin-top:16px}.dac-featured-cards{margin-bottom:32px}.dac-featured-cards .devsite-landing-row-column{background:#f7f9fa;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;min-width:0}[layout=docs] .dac-featured-cards .devsite-landing-row-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-featured-cards .devsite-landing-row-item:last-child .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-featured-cards a{color:inherit}.dac-featured-cards .devsite-landing-row-column h3{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-featured-cards .devsite-landing-row-column img{display:block;margin:0 auto 16px;max-width:150px}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:first-of-type{margin-top:0;width:60%}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:nth-of-type(2) .devsite-landing-row-item-buttons,[has-book-nav] .dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:first-of-type .devsite-landing-row-item-buttons{display:none}[has-book-nav] .dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:nth-of-type(2) .devsite-landing-row-item-buttons{display:block}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:nth-of-type(2).devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin-top:0}[has-book-nav] .dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:first-of-type{-webkit-box-flex:1;-webkit-flex:1;flex:1;width:100%}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item-description{padding:32px}@media screen and (max-width:720px){.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item-description{padding:24px}}[has-book-nav] .dac-featured-cards .devsite-landing-row-item-buttons .dac-button{white-space:inherit}@media screen and (max-width:1400px){.dac-featured-cards .devsite-landing-row-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:first-of-type{width:100%}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:first-of-type .devsite-landing-row-item-buttons{display:none}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:nth-of-type(2) .devsite-landing-row-item-buttons{display:block}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:nth-of-type(2).devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:none;flex:none}.dac-featured-cards .devsite-landing-row-item:last-child .devsite-landing-row-item-buttons{padding-top:0}}.dac-get-involved.dac-center{width:auto}.dac-footer-promos{font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0 24px}.dac-footer-promos .devsite-footer-promos-list{display:-webkit-box;display:-webkit-flex;display:flex;list-style:none;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:20px 0}.dac-footer-promos .devsite-footer-promo{-webkit-box-flex:0;-webkit-flex:0 1 192px;flex:0 1 192px;margin:20px 0;text-align:center}.dac-footer-promos .devsite-footer-promo:not(:first-child){margin-left:24px}.dac-footer-promos .devsite-footer-promo-icon{display:block;height:48px;margin:0 auto 8px;width:48px}.dac-footer-promos .devsite-footer-promo-title{color:rgba(0,0,0,.87);display:block;font-weight:500}.dac-footer-promos .devsite-footer-promo-title:focus,.dac-footer-promos .devsite-footer-promo-title:hover{color:#1a73e8}@media screen and (max-width:840px){.dac-footer-promos{padding:0}.dac-footer-promos .devsite-footer-promos-list{-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start;padding:12px 0}.dac-footer-promos .devsite-footer-promo{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%;margin:0;padding:8px 8px 8px 0;text-align:left}.dac-footer-promos .devsite-footer-promo:not(:first-child){margin-left:0}.dac-footer-promos .devsite-footer-promo-icon{height:32px;margin:0 8px 0 0;width:32px}.dac-footer-promos .devsite-footer-promo-title{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;font-weight:400}.dac-footer-promos .devsite-footer-promo-description{display:none}}@media screen and (max-width:600px){.dac-footer-promos .devsite-footer-promos-list{display:block}}.dac-landing-row-hero h2,.devsite-landing-row-item h2,.devsite-landing-row-item h3,.devsite-landing-row h1,.devsite-landing-row h2{color:#414141}.devsite-landing-row-item h2,.devsite-landing-row-item h3{font:300 44px/56px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-landing-row-hero h2,.devsite-landing-row-item h2,.devsite-landing-row-item h3{letter-spacing:-.5px}.dac-heading-medium.devsite-landing-row-item h3{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-heading-large.devsite-landing-row-item h3,.dac-landing-row-hero h1,.dac-landing-row-hero h2,.dac-landing-row h1,.dac-sublandings-hero h1,.dac-subtitle h3{font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-subtitle{margin-bottom:16px;margin-top:16px}.dac-subtitle :first-child{margin-top:0}.dac-subtitle :last-child{margin-bottom:0}@media screen and (max-width:720px){.dac-subtitle{margin-bottom:32px;margin-top:32px}}@media screen and (max-width:1200px){.dac-heading-small.devsite-landing-row-item h3,.devsite-landing-row-item h2,.devsite-landing-row-item h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-heading-large.devsite-landing-row-item h3,.dac-landing-row-hero h1,.dac-landing-row-hero h2,.dac-landing-row h1,.dac-sublandings-hero h1,.dac-subtitle h3{font:300 38px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-subtitle{margin-bottom:48px;margin-top:88px}}@media screen and (max-width:1000px){.devsite-landing-row-item .dac-subtitle h3,.devsite-landing-row-item h3{margin-bottom:26px}.devsite-landing-row-item h2,.devsite-landing-row-item h3{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-heading-large.devsite-landing-row-item h3,.dac-landing-row-hero h1,.dac-landing-row-hero h2,.dac-landing-row h1,.dac-sublandings-hero h1,.dac-subtitle h3{font:300 32px/38px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-heading-medium.devsite-landing-row-item h3{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-subtitle{margin-bottom:32px;margin-top:48px}}.dac-heading-logo{padding:24px 0 8px}.dac-security-page [background]:not([background=grey]) :link :not(.material-icons):hover{text-decoration:none}.dac-heading-icon,.dac-heading-icon-small,.dac-heading-icon-tiny{display:block;margin-bottom:24px}.dac-heading-icon{height:80px}.dac-heading-icon.material-icons{font-size:80px}.dac-heading-icon-small{height:62px}.dac-heading-icon-small.material-icons{font-size:62px}.dac-heading-icon-tiny{height:40px}.dac-heading-icon-tiny.material-icons{font-size:40px}.dac-landing-row-hero{color:#414141;margin:84px 0;text-align:center}@media screen and (max-width:1200px){.dac-landing-row-hero{margin:60px 0}}@media screen and (max-width:1000px){.dac-landing-row-hero{margin:48px 0}}@media screen and (max-width:720px){.dac-landing-row-hero{margin:36px 0}}[layout=docs] .dac-landing-row-hero{margin:44px 0}@media screen and (max-width:1000px){[layout=docs] .dac-landing-row-hero{margin:36px 0}}.dac-landing-row-hero-screenshot{margin-top:72px}@media screen and (max-width:720px){.dac-landing-row-hero-screenshot{margin-top:48px}}.dac-landing-row-hero-screenshot img{-webkit-box-shadow:0 2px 3px 0 rgba(60,64,67,.3),0 6px 10px 4px rgba(60,64,67,.15);box-shadow:0 2px 3px 0 rgba(60,64,67,.3),0 6px 10px 4px rgba(60,64,67,.15)}.dac-landing-row-hero-description{color:#263238;font:300 20px/28px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:24px auto;max-width:800px}.dac-landing-hero{text-align:center}.dac-landing-hero .devsite-landing-row-header{display:block}.dac-landing-hero .devsite-landing-row-header-buttons{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin-top:52px}.dac-landing-hero h1{color:inherit}.dac-sub-hero{display:-webkit-box;display:-webkit-flex;display:flex}@media screen and (max-width:1000px){.dac-sub-hero{display:block;margin-top:24px}}@media screen and (max-width:720px){.dac-sub-hero{margin-top:16px}}.dac-sub-hero h1{font:300 44px/56px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-bottom:20px}@media screen and (max-width:1000px){.dac-sub-hero h1{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-sub-hero-image{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:0 0 16px 16px;-webkit-box-ordinal-group:2;-webkit-order:1;order:1;text-align:center}.dac-sub-hero-image img{max-height:310px}@media screen and (max-width:1000px){.dac-sub-hero-image img{max-height:150px}}.dac-sub-hero-copy{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0}@media screen and (max-width:1000px){.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description[icon-position=left]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}}.dac-landing-row-item-icon-container-left .devsite-landing-row-item-body{padding:32px}@media screen and (max-width:720px){.dac-landing-row-item-icon-container-left .devsite-landing-row-item-body{padding:24px}}.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>.devsite-landing-row-item-icon-container,.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>a:first-child{-webkit-box-flex:1;-webkit-flex:1 0 200px;flex:1 0 200px}@media screen and (max-width:1000px){.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>.devsite-landing-row-item-icon-container,.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>a:first-child{height:200px}}.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>.devsite-landing-row-item-icon-container:focus,.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>.devsite-landing-row-item-icon-container:hover,.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>a:first-child:focus,.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>a:first-child:hover{opacity:.8}.dac-landing-row-item-icon-container-left .devsite-landing-row-item-icon{height:auto;margin:0;width:95px}.dac-landing-row-item-icon-container-left .devsite-landing-row-item-icon-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;height:100%;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;width:100%}.dac-landing-row-item-icon-container-bg-yellow .devsite-landing-row-item-icon-container{background-color:#ffd600}.dac-landing-row-item-icon-container-bg-green .devsite-landing-row-item-icon-container{background-color:#00e676}.dac-landing-row-item-icon-container-bg-red .devsite-landing-row-item-icon-container{background-color:#ff5252}.dac-landing-row-item-icon-container-bg-blue .devsite-landing-row-item-icon-container{background-color:#2196f3}.dac-chevron-right:before{content:"arrow_forward";font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;vertical-align:middle}.dac-chevron-right:focus{text-decoration:none}.dac-landing-row-item-buttons-right .devsite-landing-row-item-buttons{-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.dac-landing-row-item-description-bg-white .devsite-landing-row-item-description{background-color:#fff}.dac-landing-row-item-description-bg-transparent .devsite-landing-row-item-description{background-color:none}.dac-home-page .dac-gservices .dac-banner-card-bg-img-item{background:url(../images/custom/home-google-services-for-android.svg) 50%/calc(100% - 32px) no-repeat #f7f9fa}.dac-home-design-card .dac-banner-card-bg-img-item{background-image:url(../images/custom/home-design-illustration.svg)}.dac-about-hero-illustration{background:url(../images/custom/camerax.png) 0/auto 125% no-repeat #f7f9fa}.dac-things-page .dac-banner-card.devsite-landing-row .dac-banner-card-bg-img-item.dac-banner-card-bg-img-contain{background:url(../images/custom/prototype-to-production.svg) no-repeat 0/900px #455a64}.dac-play-page .dac-play-hero-banner:after{background:url(../images/custom/google-play-policies.png) 85%/75% no-repeat}.dac-landing-row-bg-illustration-2:after{background-image:url(../images/custom/keyline-illustration-2.svg)}.dac-landing-row-bg-blob-5{background:url(../images/custom/blob-illustration-5.svg) bottom/cover no-repeat #455a64}.dac-landing-row-bg-blob-7{background:url(../images/custom/blob-illustration-7.svg) -webkit-calc(100% + 280px) -webkit-calc(100% + 150px)/contain no-repeat content-box content-box #f7f9fa;background:url(../images/custom/blob-illustration-7.svg) calc(100% + 280px) calc(100% + 150px)/contain no-repeat content-box content-box #f7f9fa}.dac-landing-row-bg-blob-8{background:url(../images/custom/blob-illustration-8.svg) -webkit-calc(100% + 80px) -webkit-calc(100% + 100px)/contain no-repeat content-box content-box #f7f9fa;background:url(../images/custom/blob-illustration-8.svg) calc(100% + 80px) calc(100% + 100px)/contain no-repeat content-box content-box #f7f9fa}.dac-illustration-block{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:16px 0;text-align:center}.dac-illustration-block img{display:inline-block;max-width:100%}.dac-landing-row-illustration-kotlin .dac-illustration-block img{margin-top:40px}.dac-illustration-block-edge{margin:0 -32px;width:-webkit-calc(100% + 64px);width:calc(100% + 64px)}.dac-illustration-block-edge-left .dac-illustration-block-edge-right{width:-webkit-calc(100% + 32px);width:calc(100% + 32px)}.dac-illustration-block-edge-left{margin-left:-32px}.dac-illustration-block-edge-right{margin-right:-32px}.dac-illustration-block-edge-left img,.dac-illustration-block-edge-right img,.dac-illustration-block-edge img{width:100%}.dac-illustration{position:relative}.dac-illustration:after{background-position:top;-o-background-size:600px auto;background-size:600px auto;content:"";display:block;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;min-height:250px;-webkit-box-ordinal-group:6;-webkit-order:5;order:5;width:100%}.devsite-landing-page-with-side-navs .dac-illustration{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-illustration .devsite-landing-row-item-description{-webkit-box-flex:0;-webkit-flex-grow:0;flex-grow:0}.dac-illustration-bleed-lb:after{margin-bottom:-40px;margin-left:-40px;width:-webkit-calc(100% + 40px);width:calc(100% + 40px)}@media screen and (max-width:1000px){.dac-illustration-bleed-lb:after{margin-bottom:-40px;margin-left:-16px;width:-webkit-calc(100% + 32px);width:calc(100% + 32px)}}.dac-illustration-keyline-1:after{background-image:url(../images/custom/keyline-illustration-1.svg)}.dac-illustration-keyline-3:after{background-image:url(../images/custom/keyline-illustration-3.svg)}.dac-illustration-keyline-4:after{background-image:url(../images/custom/keyline-illustration-4.svg)}.dac-illustration-keyline-5:after{background-image:url(../images/custom/keyline-illustration-5.svg)}.dac-illustration-keyline-3:after,.dac-illustration-keyline-4:after,.dac-illustration-keyline-5:after{-o-background-size:500px auto;background-size:500px auto}.dac-illustration-play-book:after{background:url(../images/custom/playbook-illustration-2.svg) 100% 0 no-repeat content-box}.dac-illustration-auto:after{background:url(../images/custom/auto-illustration.svg) 100% 100%/auto 100% no-repeat}.dac-landing-row-illustration-kotlin{background:url(../images/custom/kotlin-blob.svg) 160px 130%/auto no-repeat content-box #455a64}.dac-illustration-kotlin-bootcamp:after{background:url(../images/custom/kotlin-bootcamp.png) 50%/cover no-repeat}.dac-illustration-guides-policies-families:after{background:url(../images/custom/google-play-guides-families-blob-2.svg) 0 30px/auto 135% no-repeat;width:35%}.dac-illustration-guides-go:after{background:url(../images/custom/google-play-guides-android-go-edition.svg) right 35px/auto 100% no-repeat}.dac-illustration-guides-games:after{background:url(../images/custom/google-play-guides-games.png) center right 15px/auto 65% no-repeat}.dac-illustration-guides-families:after{background:url(../images/custom/google-play-guides-families-simplified.svg) 100%/auto 75% no-repeat}.dac-illustration-guides-indie:after{background:url(../images/custom/google-play-guides-indie-games-simplified.svg) 100%/auto 75% no-repeat}.dac-illustration-guides-start:after{background:url(../images/custom/google-play-guides-start-on-android-simplified.svg) 100%/auto 75% no-repeat}.dac-illustration-guides-subscriptions:after{background:url(../images/custom/google-play-guides-subscriptions-simplified.svg) right 35px/auto 95% no-repeat}.dac-illustration-guides:after{background:url(../images/custom/google-play-guides-android-go-edition.svg) 150% 0/auto 110% no-repeat}.dac-illustration-play-console:after{background:url(../images/custom/floating-playtime-elements-mobile.svg) left 0/cover no-repeat,url(../images/custom/playtime-hero-bg-fluid-mobile.svg) 0 50px/180% no-repeat;min-height:360px}.dac-illustration-newsletter-archive{background:url(../images/custom/newsletter-archive-banner.svg) 75%/30% no-repeat,#f7f9fa}@media screen and (max-width:1200px){.dac-illustration-newsletter-archive{background:url(../images/custom/newsletter-archive-banner.svg) 80%/40% no-repeat,#f7f9fa}}@media screen and (max-width:720px){.dac-illustration-newsletter-archive{background:#f7f9fa}}.dac-landing-row-bg-illustration-3{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.dac-landing-row-bg-illustration-3 .devsite-landing-row-item-description-content p{max-width:100%}.dac-landing-row-bg-illustration-3:after{background:url(../images/custom/success-guide-illustration-1.svg) 0 no-repeat content-box;height:100%;margin-left:-1em;width:100%}.dac-docs-overview-page .dac-landing-row-bg-illustration-2 .devsite-landing-row-item-description,.dac-docs-overview-page .dac-landing-row-bg-illustration-2:after,.dac-illustration-column .devsite-landing-row-item-description,.dac-play-console-page .dac-illustration-play-console .devsite-landing-row-item-description,.dac-play-console-page .dac-illustration-play-console:after,.dac-play-page .dac-illustration-play-console .devsite-landing-row-item-description,.dac-play-page .dac-illustration-play-console:after{width:50%}.dac-landing-row-bg-blob-3:after{background:url(../images/custom/blob-illustration-3-small.svg) bottom/cover content-box content-box no-repeat #fff;min-height:425px;padding-left:0;width:100%}.dac-illustration-column:after{height:100%;width:50%}.dac-illustration-column{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.dac-docs-overview-page .dac-landing-row-bg-illustration-2:after{min-height:500px}.dac-about-hero-illustration .dac-media img{display:none}.dac-play-policies-page .dac-illustration-android-q-ready:after{background:url(../images/custom/play-policies-q-logo.png) 50%/contain no-repeat}.dac-play-policies-page .dac-illustration-play-policies-rating:after{background:url(../images/custom/play-policies-rating.svg) 50%/70% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-icon:after{background:url(../images/custom/play-policies-icon.png) 50%/70% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-target-audience:after{background:url(../images/custom/play-policies-target-audience.png) 50%/45% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-64-bit:after{background:url(../images/custom/play-policies-64-bit.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-target-api:after{background:url(../images/custom/play-policies-target-api.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-developer-api:after{background:url(../images/custom/play-policies-dev-api.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-billing-library:after{background:url(../images/custom/play-policies-billing-library.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-comply:after{background:url(../images/custom/play-policies-comply.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-monetization:after{background:url(../images/custom/play-policies-monetization.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-referrer-api:after{background:url(../images/custom/play-policies-referrer-api.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-workmanager:after{background:url(../images/custom/play-policies-workmanager.png) 50%/50% no-repeat}.devsite-404-header.devsite-header-billboard:before{background:url(../images/custom/404-illustration-question-mark.png) 50%/contain no-repeat}.devsite-404-header.devsite-header-billboard:after{background:url(../images/custom/404-illustration-telescope.png) 50%/contain no-repeat}@media screen and (max-width:1200px){.dac-play-page .dac-play-hero-banner:after{background-position:95%;-o-background-size:90%;background-size:90%}}@media screen and (max-width:1000px){.dac-about-hero-illustration{-o-background-size:auto 94%;background-size:auto 94%}.dac-illustration-column .devsite-landing-row-item-description,.dac-illustration-column:after{width:100%}.dac-landing-row-bg-blob-7{background:url(../images/custom/blob-illustration-7.svg) -webkit-calc(100% + 250px) -webkit-calc(100% + 119px)/contain no-repeat content-box content-box #f7f9fa;background:url(../images/custom/blob-illustration-7.svg) calc(100% + 250px) calc(100% + 119px)/contain no-repeat content-box content-box #f7f9fa}.dac-landing-row-bg-blob-3:after{background-image:url(../images/custom/blob-illustration-3-large.svg);background-position:0 0;padding-left:30%}}@media screen and (max-width:720px){.dac-docs-overview-page .dac-landing-row-bg-illustration-2 .devsite-landing-row-item-description,.dac-docs-overview-page .dac-landing-row-bg-illustration-2:after,.dac-play-console-page .dac-illustration-play-console .devsite-landing-row-item-description,.dac-play-console-page .dac-illustration-play-console:after,.dac-play-page .dac-illustration-play-console .devsite-landing-row-item-description,.dac-play-page .dac-illustration-play-console:after{width:auto}.dac-landing-row-illustration-kotlin{background:url(../images/custom/kotlin-blob-mobile.svg) bottom/100% no-repeat content-box #455a64}.dac-landing-row-bg-blob-3:after{background:url(../images/custom/blob-illustration-3-small.svg) bottom/cover content-box content-box no-repeat #fff;min-height:425px;padding-left:0;width:100%}.dac-landing-row-bg-blob-1{background-position:85% 100%}.dac-landing-row-bg-blob-7{background-position:-webkit-calc(100% + 417px) -webkit-calc(100% + 210px);background-position:calc(100% + 417px) calc(100% + 210px);-o-background-size:750px;background-size:750px}.dac-illustration-block img{max-width:60%}.devsite-landing-page-with-side-navs .dac-illustration{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.devsite-landing-page-with-side-navs .dac-illustration:after{min-height:300px}.dac-illustration-auto:after{-o-background-size:cover;background-size:cover}.dac-illustration-play-console{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.dac-illustration:after{min-height:250px}.dac-illustration-play-console:after{background-clip:content-box;background-image:url(../images/custom/floating-playtime-elements.svg),url(../images/custom/playtime-hero-bg-fluid.svg)}.dac-illustration-block-edge{margin:0;width:auto}}@media screen and (max-width:600px){.dac-play-page .dac-play-hero-banner:after{background-position:63%;-o-background-size:80%;background-size:80%}.dac-about-hero-illustration .dac-media img{display:block}.dac-about-hero-illustration{background-image:none}.dac-about-hero-illustration .dac-full-width-media{margin-top:0!important}.dac-illustration-guides-families:after,.dac-illustration-guides-go:after,.dac-illustration-guides-indie:after,.dac-illustration-guides-start:after,.dac-illustration-guides-subscriptions:after,.dac-illustration-guides:after{background-position:50%}.dac-illustration-guides-policies-families:after{background:url(../images/custom/blob-illustration-3-small.svg) -webkit-calc(100% + 150px) -webkit-calc(100% + 165px)/auto 170% no-repeat;background:url(../images/custom/blob-illustration-3-small.svg) calc(100% + 150px) calc(100% + 165px)/auto 170% no-repeat;width:100%}}ol.callouts{counter-reset:item;list-style-type:none;margin-left:30px;padding-left:0}ol.callouts>li:before{background:url(../images/custom/callout-bg_2x.png) 1px 2px/20px no-repeat;-webkit-box-sizing:content-box;box-sizing:content-box;content:counter(item);counter-increment:item;margin-left:-30px;padding-left:8px;position:absolute;width:16px}.callout{background:url(../images/custom/callout-bg_2x.png) 0 2px/20px no-repeat;display:inline-block;height:22px;text-align:center;width:20px}.callout,ol.callouts>li:before{color:#fff;font-size:12px;font-weight:500}.img-caption,figcaption{font-size:14px;margin-top:-4px}p+.img-caption{margin-top:-20px}li p+.img-caption{margin-top:-12px}.table-caption{clear:both;font-size:14px;margin:0 0 4px}.table-caption+.devsite-table-wrapper,.table-caption+table{margin-top:0}.code-caption{font:14px/1.5 monospace;margin-bottom:4px}a.external-link:after{content:"open_in_new";font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;word-wrap:normal;font-size:18px;margin-left:4px;vertical-align:text-bottom}.sidebox{background:#f1f3f4;padding:12px 24px}.sidebox a.bug{padding-right:25px;background:transparent url(../images/custom/bug.png) no-repeat 100%}.sidebox a.g-plus{padding-right:25px;background:transparent url(../images/custom/g+.ico) no-repeat 100%}.sidebox a.codelab{padding-right:40px;background:transparent url(../images/custom/favicon.png) no-repeat 100%}.sidebox p{margin-top:0}.wrap{margin:0 auto;clear:both}.cols{margin-left:-10px;margin-right:-10px}.cols:after,.cols:before{content:" ";display:table}.cols:after{clear:both}[class*=col-]{-webkit-box-sizing:border-box;box-sizing:border-box;float:left;min-height:1px;padding-left:10px;padding-right:10px;position:relative}.col-1{width:6.25%}.col-2{width:12.5%}.col-3{width:18.75%}.col-4{width:25%}.col-5{width:31.25%}.col-6{width:37.5%}.col-7{width:43.75%}.col-8{width:50%}.col-9{width:56.25%}.col-10{width:62.5%}.col-11{width:68.75%}.col-12{width:75%}.col-13{width:81.25%}.col-14{width:87.5%}.col-15{width:93.75%}.col-16{width:100%}.col-13 .col-1{width:7.69230769%}.col-13 .col-2{width:15.38461538%}.col-13 .col-3{width:23.07692308%}.col-13 .col-4{width:30.76923077%}.col-13 .col-5{width:38.46153846%}.col-13 .col-6{width:46.15384615%}.col-13 .col-7{width:53.84615385%}.col-13 .col-8{width:61.53846154%}.col-13 .col-9{width:69.23076923%}.col-13 .col-10{width:76.92307692%}.col-13 .col-11{width:84.61538462%}.col-13 .col-12{width:92.30769231%}.col-13 .col-13{width:100%}.col-12 .col-1{width:8.33333333%}.col-12 .col-2{width:16.66666667%}.col-12 .col-3{width:25%}.col-12 .col-4{width:33.33333333%}.col-12 .col-5{width:41.66666667%}.col-12 .col-6{width:50%}.col-12 .col-7{width:58.33333333%}.col-12 .col-8{width:66.66666667%}.col-12 .col-9{width:75%}.col-12 .col-10{width:83.33333333%}.col-12 .col-11{width:91.66666667%}.col-1of1,.col-2of2,.col-3of3,.col-4of4,.col-5of5,.col-6of6,.col-8of8,.col-10of10,.col-12 .col-12,.col-12of12,.col-16of16{width:100%}.col-1of2,.col-2of4,.col-3of6,.col-4of8,.col-5of10,.col-6of12,.col-8of16{width:50%}.col-1of3,.col-2of6,.col-4of12{width:33.33333333%}.col-2of3,.col-4of6,.col-8of12{width:66.66666667%}.col-1of4,.col-2of8,.col-3of12,.col-4of16{width:25%}.col-3of4,.col-6of8,.col-9of12,.col-12of16{width:75%}.col-1of5,.col-2of10{width:20%}.col-2of5,.col-4of10{width:40%}.col-3of5,.col-6of10{width:60%}.col-4of5,.col-8of10{width:80%}.col-1of6,.col-2of12{width:16.66666667%}.col-5of6,.col-10of12{width:83.33333333%}.col-1of8,.col-2of16{width:12.5%}.col-3of8,.col-6of16{width:37.5%}.col-5of8,.col-10of16{width:62.5%}.col-7of8,.col-14of16{width:87.5%}.col-1of10{width:10%}.col-3of10{width:30%}.col-7of10{width:70%}.col-9of10{width:90%}.col-1of12{width:8.33333333%}.col-5of12{width:41.66666667%}.col-7of12{width:58.33333333%}.col-11of12{width:91.66666667%}.col-1of16{width:6.25%}.col-3of16{width:18.75%}.col-5of16{width:31.25%}.col-7of16{width:43.75%}.col-9of16{width:56.25%}.col-11of16{width:68.75%}.col-13of16{width:81.25%}.col-15of16{width:93.75%}.col-pull-1of1,.col-pull-2of2,.col-pull-3of3,.col-pull-4of4,.col-pull-5of5,.col-pull-6of6,.col-pull-8of8,.col-pull-10of10,.col-pull-12of12,.col-pull-16of16{left:-100%}.col-pull-1of2,.col-pull-2of4,.col-pull-3of6,.col-pull-4of8,.col-pull-5of10,.col-pull-6of12,.col-pull-8of16{left:-50%}.col-pull-1of3,.col-pull-2of6,.col-pull-4of12{left:-33.33333333%}.col-pull-2of3,.col-pull-4of6,.col-pull-8of12{left:-66.66666667%}.col-pull-1of4,.col-pull-2of8,.col-pull-3of12,.col-pull-4of16{left:-25%}.col-pull-3of4,.col-pull-6of8,.col-pull-9of12,.col-pull-12of16{left:-75%}.col-pull-1of5,.col-pull-2of10{left:-20%}.col-pull-2of5,.col-pull-4of10{left:-40%}.col-pull-3of5,.col-pull-6of10{left:-60%}.col-pull-4of5,.col-pull-8of10{left:-80%}.col-pull-1of6,.col-pull-2of12{left:-16.66666667%}.col-pull-5of6,.col-pull-10of12{left:-83.33333333%}.col-pull-1of8,.col-pull-2of16{left:-12.5%}.col-pull-3of8,.col-pull-6of16{left:-37.5%}.col-pull-5of8,.col-pull-10of16{left:-62.5%}.col-pull-7of8,.col-pull-14of16{left:-87.5%}.col-pull-1of10{left:-10%}.col-pull-3of10{left:-30%}.col-pull-7of10{left:-70%}.col-pull-9of10{left:-90%}.col-pull-1of12{left:-8.33333333%}.col-pull-5of12{left:-41.66666667%}.col-pull-7of12{left:-58.33333333%}.col-pull-11of12{left:-91.66666667%}.col-pull-1of16{left:-6.25%}.col-pull-3of16{left:-18.75%}.col-pull-5of16{left:-31.25%}.col-pull-7of16{left:-43.75%}.col-pull-9of16{left:-56.25%}.col-pull-11of16{left:-68.75%}.col-pull-13of16{left:-81.25%}.col-pull-15of16{left:-93.75%}.col-push-1of1,.col-push-2of2,.col-push-3of3,.col-push-4of4,.col-push-5of5,.col-push-6of6,.col-push-8of8,.col-push-10of10,.col-push-12of12,.col-push-16of16{left:100%}.col-push-1of2,.col-push-2of4,.col-push-3of6,.col-push-4of8,.col-push-5of10,.col-push-6of12,.col-push-8of16{left:50%}.col-push-1of3,.col-push-2of6,.col-push-4of12{left:33.33333333%}.col-push-2of3,.col-push-4of6,.col-push-8of12{left:66.66666667%}.col-push-1of4,.col-push-2of8,.col-push-3of12,.col-push-4of16{left:25%}.col-push-3of4,.col-push-6of8,.col-push-9of12,.col-push-12of16{left:75%}.col-push-1of5,.col-push-2of10{left:20%}.col-push-2of5,.col-push-4of10{left:40%}.col-push-3of5,.col-push-6of10{left:60%}.col-push-4of5,.col-push-8of10{left:80%}.col-push-1of6,.col-push-2of12{left:16.66666667%}.col-push-5of6,.col-push-10of12{left:83.33333333%}.col-push-1of8,.col-push-2of16{left:12.5%}.col-push-3of8,.col-push-6of16{left:37.5%}.col-push-5of8,.col-push-10of16{left:62.5%}.col-push-7of8,.col-push-14of16{left:87.5%}.col-push-1of10{left:10%}.col-push-3of10{left:30%}.col-push-7of10{left:70%}.col-push-9of10{left:90%}.col-push-1of12{left:8.33333333%}.col-push-5of12{left:41.66666667%}.col-push-7of12{left:58.33333333%}.col-push-11of12{left:91.66666667%}.col-push-1of16{left:6.25%}.col-push-3of16{left:18.75%}.col-push-5of16{left:31.25%}.col-push-7of16{left:43.75%}.col-push-9of16{left:56.25%}.col-push-11of16{left:68.75%}.col-push-13of16{left:81.25%}.col-push-15of16{left:93.75%}.dac-row-logos{padding:0}.dac-row-logos .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-row-logos .devsite-landing-row-item{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:40px 5px}.dac-row-logos .devsite-landing-row-item-description{display:none}.dac-row-logos img{width:-webkit-max-content;width:-moz-max-content;width:max-content}.dac-landing-row-media .devsite-landing-row-item-description-content{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-full-width-media .devsite-landing-row-item-body,.dac-media{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-media{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-align-self:flex-end;align-self:flex-end;-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;width:100%}.dac-media,.dac-media-video{display:-webkit-box;display:-webkit-flex;display:flex}.dac-media-video{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start}.dac-media-video-toolbar{height:auto;margin-left:3%;width:15%}.dac-full-width-media[background],.dac-full-width-media[background].dac-grow-7.devsite-landing-row-item-no-media:not(:first-child){margin-left:0}@media screen and (max-width:1000px){.dac-media{margin-top:8px}.devsite-landing-row:not(.devsite-landing-row-logos) .dac-full-width-media .devsite-landing-row-item-description{padding-top:0}}[class*=dac-landing-row-bg] .devsite-landing-row-item-description,[class^=dac] .devsite-landing-row-item[background] .devsite-landing-row-item-description,[class^=dac] .devsite-landing-row[background] .devsite-landing-row-item-description{padding:32px}@media screen and (max-width:720px){[class*=dac-landing-row-bg] .devsite-landing-row-item-description,[class^=dac] .devsite-landing-row-item[background] .devsite-landing-row-item-description,[class^=dac] .devsite-landing-row[background] .devsite-landing-row-item-description{padding:24px}}.dac-landing-row-item-icon-container-left.devsite-landing-row-item .devsite-landing-row-item-description{padding:0}.dac-top-spacing{padding-top:120px}.dac-bottom-spacing{padding-bottom:88px}@media screen and (max-width:720px){.dac-top-spacing{padding-top:64px}.dac-bottom-spacing{padding-bottom:40px}}.dac-success-story{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.dac-success-story:not(.dac-testimonial) .devsite-landing-row-item-description-content{-webkit-box-pack:end;-webkit-justify-content:end;justify-content:end}.dac-success-story:not(.dac-testimonial) .devsite-landing-row-item-description-content p{-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-success-story:not(.dac-testimonial) .devsite-landing-row-item-description-content p strong{padding-left:4px}.dac-success-story h4{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0 0 16px}.dac-success-story .devsite-landing-row-item-description-content{font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.dac-success-story .devsite-landing-row-item-media{-webkit-box-ordinal-group:3;-webkit-order:2;order:2;padding:24px}.dac-success-story:not(.dac-testimonial) .devsite-landing-row-item-media{-webkit-box-flex:1;-webkit-flex:1 1 0;flex:1 1 0}.dac-success-story img{width:128px}.dac-success-story .devsite-landing-row-item-image{-webkit-box-align:center;-webkit-align-items:center;align-items:center;background:transparent;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.dac-success-story .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 1 0;flex:1 1 0;margin:0}@media screen and (max-width:720px){.dac-success-story{display:block}.dac-success-story .devsite-landing-row-item-media{padding:24px 24px 0}.dac-success-story .devsite-landing-row-item-image{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-success-story.devsite-landing-row-item .devsite-landing-row-item-media{margin:0}}.devsite-404-header.devsite-header-billboard:after,.devsite-404-header.devsite-header-billboard:before{content:"";display:block;position:absolute}.devsite-404-header.devsite-header-billboard:before{bottom:-210px;height:428px;left:-35%;width:321px;z-index:1}.devsite-404-header.devsite-header-billboard:after{bottom:-297px;height:508px;left:86%;width:391px;z-index:3}.devsite-404-search{z-index:2}.devsite-404-search devsite-search .devsite-searchbox:before{background:0}.devsite-404-header,.devsite-offline-header{z-index:auto}@media screen and (max-width:1200px){.devsite-404-header.devsite-header-billboard:before{bottom:-180px;left:-21%;width:271px}.devsite-404-header.devsite-header-billboard:after{bottom:-350px;left:74%;width:321px;z-index:2}}@media screen and (max-width:1000px){.devsite-404-header.devsite-header-billboard:after,.devsite-404-header.devsite-header-billboard:before{background:0}}.dac-app-bundle-page h2{color:inherit}.dac-app-bundle-page .dac-logos,.dac-app-bundle-page .devsite-landing-row-header{text-align:center}.dac-app-bundle-page .dac-landing-row-hero .dac-landing-row-hero-description{margin-top:64px}.dac-app-bundle-page .dac-featured-cards .devsite-landing-row-item-description-content>p>a,.dac-app-bundle-page .dac-logos a{color:#1a73e8}.dac-app-bundle-page .dac-smaller-app img{max-width:100%}.dac-app-bundle-page .dac-list-wrapped{padding-bottom:0}.dac-app-bundle-page .dac-list-wrapped h2{font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1200px){.dac-app-bundle-page .dac-list-wrapped h2{font:300 38px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}@media screen and (max-width:720px){.dac-app-bundle-page .dac-list-wrapped h2{font:300 32px/38px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-app-bundle-page .dac-list-wrapped .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;padding-top:40px}.dac-app-bundle-page .dac-app-featured-cards{margin-bottom:0}.dac-app-bundle-page .dac-logos h2{margin-top:120px}.dac-app-bundle-page .dac-logos .devsite-landing-row-item-description{padding:32px 0}.dac-app-bundle-page .dac-logos img{margin:0 auto;max-height:80px;max-width:80px}.dac-app-bundle-page .dac-logos h4,.dac-app-bundle-page .dac-logos p{font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;white-space:nowrap}.dac-app-bundle-page .dac-logos.dac-bottom-logos .devsite-landing-row-group{-webkit-box-pack:space-evenly;-webkit-justify-content:space-evenly;justify-content:space-evenly}.dac-app-bundle-page .dac-logos.dac-bottom-logos .devsite-landing-row-group .devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:0 0 80px;flex:0 0 80px}.dac-app-bundle-page .dac-get-started{background:url(../images/custom/platform-app-bundle-blob.svg) left -webkit-calc(50% + 300px) bottom -250px/500px no-repeat;background:url(../images/custom/platform-app-bundle-blob.svg) left calc(50% + 300px) bottom -250px/500px no-repeat;margin-top:64px}.dac-app-bundle-page .dac-get-started .devsite-landing-row-item{padding:0 32px 32px}.devsite-landing-row-column>.dac-grow-1.devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}.dac-docs-overview-page .dac-grow-column-4-8 .devsite-landing-row-column:first-child,.dac-docs-overview-page .dac-grow-column-4-8.dac-column-switch .devsite-landing-row-column:last-child{-webkit-box-flex:4;-webkit-flex-grow:4;flex-grow:4}.dac-docs-overview-page .dac-grow-column-4-8 .devsite-landing-row-column:last-child,.dac-docs-overview-page .dac-grow-column-4-8.dac-column-switch .devsite-landing-row-column:first-child{-webkit-box-flex:8;-webkit-flex-grow:8;flex-grow:8}.dac-docs-overview-page .dac-column-switch.devsite-landing-row .devsite-landing-row-column:not(:last-child){margin-left:32px;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.dac-docs-overview-page .dac-column-switch.devsite-landing-row .devsite-landing-row-column:not(:first-child){margin-left:0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.dac-docs-overview-page .dac-guides-group{-webkit-column-break-inside:avoid;break-inside:avoid;-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;margin-bottom:16px}.dac-docs-overview-page .dac-guides-flex-columns .dac-guides-group{padding-left:0}.dac-docs-overview-page .dac-guides-group h4{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0}.dac-docs-overview-page .dac-landing-row-bg-blob-5,.dac-docs-overview-page .dac-landing-row-bg-blue{min-width:280px}.dac-docs-overview-page .dac-grow-column-4-8 .dac-guides-flex-columns-xs,.dac-docs-overview-page .dac-illustration-keyline-1,.dac-docs-overview-page .dac-illustration-keyline-3{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-docs-overview-page .dac-heading-small h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-bottom:26px}.dac-guides-flex-columns .dac-guides{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-guides-flex-columns .dac-guides-group{-webkit-box-flex:0;-webkit-flex:0 0 33%;flex:0 0 33%;padding:0 16px}.dac-docs-overview-page .dac-landing-row-bg-blob-5 .devsite-landing-row-item-description,.dac-docs-overview-page .dac-landing-row-bg-blob-5 .devsite-landing-row-item-description-content,.dac-docs-overview-page .dac-landing-row-item .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex}.dac-docs-overview-page .dac-landing-row-bg-blob-5 .devsite-landing-row-item-description-content{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-docs-overview-page .dac-center .dac-samples-form select{margin-left:16px}.dac-docs-overview-page .dac-center .dac-samples-form .dac-button{margin:8px 16px}.dac-docs-overview-page .dac-subtitle{margin-bottom:48px;margin-top:88px}.dac-docs-overview-page .devsite-landing-row[background=grey] .devsite-landing-row-group{-webkit-flex-wrap:nowrap;flex-wrap:nowrap}@media screen and (max-width:1200px){.dac-guides-flex-columns .dac-guides-group{-webkit-flex-basis:50%;flex-basis:50%;max-width:50%}}@media screen and (max-width:1000px){.dac-guides-flex-columns .dac-guides-group{-webkit-flex-basis:100%;flex-basis:100%;max-width:100%}}@media screen and (max-width:720px){.dac-docs-overview-page .dac-subtitle{margin-bottom:16px;margin-top:16px}.dac-docs-overview-page .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-docs-overwiew-page .dac-landing-row-hero{margin-bottom:0}.dac-docs-overview-page .devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column{margin-left:0;margin-top:40px;width:100%}.dac-docs-overview-page .devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child),.dac-docs-overview-page .devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child).dac-landing-row-bg-blob-5{margin:40px 0 0}.dac-docs-overview-page .dac-column-switch.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column:not(:last-child){margin-left:0}}.dac-full-width-page h1{font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1200px){.dac-full-width-page h1{font:300 38px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-full-width-page h2{font:300 44px/56px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1000px){.dac-full-width-page h2{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-full-width-page .dac-sub-hero{margin-top:50px}@media screen and (min-width:1000px) and (max-width:1400px){.dac-full-width-page .dac-sub-hero .dac-sub-hero-image{-webkit-box-flex:2;-webkit-flex-grow:2;flex-grow:2}}@media screen and (max-width:1000px){.dac-full-width-page .dac-sub-hero .dac-sub-hero-image{margin:0}}.dac-full-width-page .dac-sub-hero .dac-sub-hero-image img{margin:0 0 40px 32px;max-width:360px;width:100%}@media screen and (max-width:1000px){.dac-full-width-page .dac-sub-hero .dac-sub-hero-image img{margin:0 0 40px}}@media screen and (min-width:1000px) and (max-width:1400px){.dac-full-width-page .dac-sub-hero .dac-sub-hero-copy{-webkit-box-flex:3;-webkit-flex-grow:3;flex-grow:3}}.dac-full-width-page .dac-sub-hero .dac-sub-hero-buttons{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin-top:64px}@media screen and (max-width:1000px){.dac-full-width-page .dac-sub-hero .dac-sub-hero-buttons{display:block}}.dac-full-width-page .dac-features .devsite-landing-row-item-description{padding:0 0 0 96px}.dac-full-width-page .devsite-landing-row-item{position:relative}.dac-full-width-page .dac-features img{height:auto;left:0;max-width:64px;position:absolute;top:0}.dac-full-width-page .dac-components h4,.dac-full-width-page .dac-features h3{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-full-width-page .dac-components h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-full-width-page .dac-components .devsite-landing-row-item-image{background:0;max-width:320px}.dac-full-width-page .dac-components ul{list-style:none;padding-left:0}.dac-full-width-page .dac-components p{font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-top:0}.dac-full-width-page .dac-components .dac-button{margin:16px auto 16px 0}.dac-full-width-page.devsite-landing-page .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(25% - 32px);flex:0 0 calc(25% - 32px)}.dac-full-width-page .dac-success-story .devsite-landing-row-item-image{-webkit-align-self:flex-start;align-self:flex-start;-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;width:auto}@media screen and (max-width:1000px){.dac-full-width-page .dac-success-story .devsite-landing-row-item-image{-webkit-align-self:center;align-self:center}}.dac-full-width-page .devsite-landing-row-logos .devsite-landing-row-group{-webkit-box-align:center;-webkit-align-items:center;align-items:center;text-align:center}.dac-full-width-page .devsite-landing-row-logos img{max-height:100px;max-width:200px}body[theme=android-theme] devsite-header .devsite-header-billboard h1{color:#414141;font:400 44px/54px Euclid,sans-serif;margin-top:0;text-align:center}body[theme=android-theme] devsite-header .devsite-header-billboard-search devsite-search .devsite-search-field,body[theme=android-theme] devsite-header .devsite-header-billboard-search devsite-search .devsite-search-field:hover{background-color:#fff}.dac-home-page .dac-outline-button{border-color:#414141;color:#414141}.dac-home-page .dac-outline-button:focus,.dac-home-page .dac-outline-button:hover{background:#414141;color:#fff}.dac-home-page .dac-landing-row-bg-slate .dac-outline-button{border-color:#fff;color:#fff}.dac-home-page .dac-landing-row-bg-slate .dac-outline-button:focus,.dac-home-page .dac-landing-row-bg-slate .dac-outline-button:hover{background:#fff;color:#414141}.dac-home-page .dac-alt-flat-button{color:#414141}.dac-home-hero-banner .devsite-landing-row-group{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:64px 40px}.dac-home-hero-banner-text{max-width:630px}.dac-home-hero-banner-text .devsite-landing-row-item-description-content{margin-bottom:16px}.dac-home-hero-banner-image{text-align:center}.dac-home-hero-banner-image img{max-width:450px}.dac-home-hero-banner-image .devsite-landing-row-item-buttons{display:none}.dac-home-page .dac-feature-card-illustration{max-width:200px}.dac-home-page .dac-featured-cards{margin-bottom:112px}.dac-home-build-an-app{padding-top:112px}.dac-home-developer-guides{padding-bottom:112px}.dac-home-developer-guides-heading h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-no-spacing .devsite-landing-row-item-description{padding:0!important}.dac-home-developer-guides img{display:block;margin-bottom:32px;width:80px}.dac-home-design-card h3{margin-bottom:24px}.dac-home-design-card .devsite-landing-row-item-buttons{margin-top:100px}.dac-home-platforms img{display:block;margin:40px auto;max-width:312px}.dac-gservices,.dac-gservices img{margin-top:32px}.dac-gservices .devsite-landing-row-item-buttons{margin-top:200px}.dac-gservices p:last-of-type{margin:0}@media screen and (max-width:1200px){.dac-home-developer-guides .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:-20px}.dac-home-page .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item{-webkit-flex-basis:-webkit-calc((100% - 80px)/2);flex-basis:calc((100% - 80px)/2);margin:20px}.dac-home-developer-guides-heading h3{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}@media screen and (max-width:1000px){body[theme=android-theme] devsite-header .devsite-header-billboard h1{font:400 40px/48px Euclid,sans-serif}.dac-home-build-an-app{padding-top:64px}.dac-home-developer-guides{padding-bottom:64px}.dac-home-developer-guides-heading h3{font:300 20px/28px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}@media screen and (max-width:720px){body[theme=android-theme] devsite-header .devsite-header-billboard h1{font:400 32px/40px Euclid,sans-serif}.dac-home-page .dac-featured-cards{margin-bottom:64px}.dac-home-build-an-app{padding-top:32px}.dac-home-hero-banner .devsite-landing-row-group{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-home-hero-banner-image{margin:0!important;width:100%}.dac-home-hero-banner-image .devsite-landing-row-item-buttons{display:block;text-align:left}.dac-home-hero-banner-text .devsite-landing-row-item-buttons{display:none}.dac-home-developer-guides{padding-bottom:32px}.dac-gservices .devsite-landing-row-item-buttons,.dac-home-design-card .devsite-landing-row-item-buttons{margin-top:24px}}.dac-jetpack-page .dac-cta.dac-center .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin-right:0}.dac-jetpack-page .dac-cta.dac-center .devsite-landing-row-item-buttons{display:block}.dac-jetpack-page .dac-component h3 p{font-weight:400;height:130px}.dac-jetpack-page .dac-component button{display:none}.dac-jetpack-page .devsite-landing-row-logos img{padding:12px}.dac-jetpack-page .dac-testimonial a,.dac-jetpack-page .dac-testimonial a:link,.dac-jetpack-page .dac-testimonial a:visited{color:inherit;font-weight:700}@media screen and (max-width:1200px){.dac-jetpack-page .dac-components .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-jetpack-page .dac-components .dac-component{-webkit-flex-basis:-webkit-calc((100% - 40px)/2);flex-basis:calc((100% - 40px)/2)}.dac-jetpack-page .dac-components.devsite-landing-row .devsite-landing-row-item:not(:first-child){margin:40px 0 0}.dac-jetpack-page .dac-components.devsite-landing-row-4-up .devsite-landing-row-item:nth-of-type(2){margin:0 0 0 40px}.dac-jetpack-page .dac-components.devsite-landing-row-4-up .devsite-landing-row-item:nth-of-type(4){margin:40px 0 0 40px}}@media screen and (max-width:1000px){.dac-jetpack-page .dac-features .devsite-landing-row-group,.dac-jetpack-page .devsite-landing-row-2-up:not(.dac-row-logos) .devsite-landing-row-group{display:block}.dac-jetpack-page .dac-features.devsite-landing-row-3-up .devsite-landing-row-item,.dac-jetpack-page .devsite-landing-row-2-up:not(.dac-row-logos) .devsite-landing-row-group .devsite-landing-row-item{margin:32px 0 0}.dac-jetpack-page .devsite-landing-row-2-up:not(.dac-row-logos) .devsite-landing-row-group .devsite-landing-row-item:first-of-type{margin-top:0}.dac-jetpack-page .devsite-landing-row-2-up:not(.dac-row-logos){padding-bottom:32px}}@media screen and (max-width:600px){.dac-jetpack-page .dac-row-logos .devsite-landing-row-group{display:block}.dac-jetpack-page .dac-components.devsite-landing-row-4-up .devsite-landing-row-item:nth-of-type(2),.dac-jetpack-page .dac-components.devsite-landing-row-4-up .devsite-landing-row-item:nth-of-type(4){margin:40px 0 0}.dac-jetpack-page .dac-component h3 p{height:100px}}.dac-kotlin-page .dac-sub-hero-image img{max-height:300px}.dac-kotlin-page .dac-code-snippet .devsite-landing-row-header{margin:0 auto}.dac-kotlin-page .dac-center .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:0}.dac-kotlin-page .dac-components .devsite-landing-row-item-image img{height:80px;width:auto}.dac-kotlin-page .dac-app-list .devsite-landing-row-item-list{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-flow:row wrap;flex-flow:row wrap;margin:0;text-align:center}.dac-kotlin-page .dac-app-list .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin-right:0}.dac-kotlin-page .dac-app-list .devsite-landing-row-item-description-content{margin-bottom:64px}.dac-kotlin-page .dac-app-list .devsite-landing-row-item-list-item-content{display:block}.dac-kotlin-page .dac-app-list .devsite-landing-row-item-list-item{-webkit-box-flex:1;-webkit-flex:1 0 8.33333%;flex:1 0 8.33333%;margin:8px 0;padding:8px}.dac-kotlin-page .dac-landing-row-bg-blob-8 .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex}.dac-kotlin-page .dac-landing-row-bg-blob-8 .devsite-landing-row-group{-webkit-flex-wrap:nowrap;flex-wrap:nowrap}.dac-kotlin-page .dac-landing-row-item-icon-container-bg-grey .devsite-landing-row-item-icon-container{background-color:#f7f9fa}@media screen and (max-width:1000px){.dac-kotlin-page .dac-features .devsite-landing-row-group{display:block}.dac-kotlin-page .devsite-landing-row-3-up.dac-features .devsite-landing-row-item{margin:32px 0 0}}@media screen and (max-width:840px){.dac-kotlin-page .devsite-landing-row-2-up .dac-landing-row,.dac-kotlin-page .devsite-landing-row-2-up .devsite-landing-row-item-media{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-kotlin-page .devsite-landing-row.dac-label .devsite-landing-row-group{display:block}}@media screen and (max-width:720px){.dac-kotlin-page .dac-app-list .devsite-landing-row-item-list-item{-webkit-box-flex:1;-webkit-flex:1 0 16.66667%;flex:1 0 16.66667%}.dac-kotlin-page .dac-landing-row-bg-blob-8 .devsite-landing-row-item-description-content{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-kotlin-page .dac-landing-row-bg-blob-8 .devsite-landing-row-item-buttons{margin-top:0}.dac-kotlin-page .dac-media{-webkit-align-self:center;align-self:center}}.dac-news-page .dac-resource-widget-more,.dac-news-page [maxResults] .dynamic-content-paging{display:none!important}.dac-news-page h1{font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-news-page .dac-sign-up .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:0}.dac-news-page .dac-sign-up .devsite-landing-row-item-body{-webkit-box-align:center;-webkit-align-items:center;align-items:center;padding:70px;text-align:center}.dac-news-page .dac-sign-up .devsite-landing-row-item-description-content{margin:0 0 24px}.dac-news-page .devsite-card-image-bg{padding:0;height:170px}.dac-news-page .devsite-card-wrapper{background:#f7f9fa;-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(25% - 32px);flex:0 0 calc(25% - 32px);overflow:hidden}.dac-news-page .devsite-card,.dac-news-page .devsite-card-content,.dac-news-page .devsite-card>a{-webkit-transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);-o-transition:-o-transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1),-webkit-transform .2s cubic-bezier(.4,0,.2,1),-o-transform .2s cubic-bezier(.4,0,.2,1)}.dac-news-page .devsite-card>a{background-color:#f7f9fa}.dac-news-page .devsite-card{height:-webkit-calc(100% + 170px);height:calc(100% + 170px);margin-bottom:-170px;-webkit-transform:translateY(-170px);-o-transform:translateY(-170px);transform:translateY(-170px)}.dac-news-page .devsite-card:hover .devsite-card-content,.dac-news-page .devsite-card:hover>a{-webkit-transform:translateY(170px);-o-transform:translateY(170px);transform:translateY(170px)}.dac-news-page .devsite-card-author{background:#f7f9fa}.dac-news-page .devsite-card-author:after{background:-webkit-gradient(linear,left top,left bottom,from(rgba(247,249,250,0)),to(#f7f9fa));background:-webkit-linear-gradient(rgba(247,249,250,0),#f7f9fa);background:-o-linear-gradient(rgba(247,249,250,0),#f7f9fa);background:linear-gradient(rgba(247,249,250,0),#f7f9fa);bottom:100%;content:"";height:25px;left:0;position:absolute;width:100%}.dac-news-page [dynamic-card-type=medium] .devsite-card-author{background:rgba(0,0,0,.94)}.dac-news-page [dynamic-card-type=medium] .devsite-card-author:after{background:-webkit-gradient(linear,left top,left bottom,from(transparent),to(rgba(0,0,0,.94)));background:-webkit-linear-gradient(transparent,rgba(0,0,0,.94));background:-o-linear-gradient(transparent,rgba(0,0,0,.94));background:linear-gradient(transparent,rgba(0,0,0,.94))}@media screen and (max-width:1000px){.dac-news-page .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(50% - 32px);flex:0 0 calc(50% - 32px)}.dac-news-page .devsite-landing-row-group{display:block}.dac-news-page .devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child){margin:0}.dac-news-page .dac-sign-up .devsite-landing-row-item-body{padding:35px}}@media screen and (max-width:720px){.dac-news-page .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(100% - 32px);flex:0 0 calc(100% - 32px)}}.dac-platforms-sublandings .dynamic-content-paging{display:none}.dac-platforms-sublandings .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(50% - 32px);flex:0 0 calc(50% - 32px)}.dac-platforms-sublandings .dac-platforms-sublandings-basics .dac-platforms-img{display:block;margin:16px auto 0;width:480px}.dac-platforms-sublandings .dac-platforms-sublandings-basics .devsite-landing-row-group .devsite-landing-row-column{width:100%}.dac-platforms-sublandings .dac-banner-card.devsite-landing-row .dac-button{margin-top:100px}.dac-platforms-sublandings .devsite-landing-row-logos .devsite-landing-row-item{text-align:center}.dac-platforms-sublandings .devsite-landing-row-logos .devsite-landing-row-item .devsite-landing-row-item-image a{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-platforms-sublandings .dac-featured-cards .devsite-landing-row-column img{max-width:75px}.dac-platforms-sublandings .dac-grow-1 h3,.dac-platforms-sublandings .dac-grow-5 h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1000px){.dac-platforms-sublandings .dac-grow-1 h3,.dac-platforms-sublandings .dac-grow-5 h3{font:300 20px/28px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-platforms-sublandings .dac-grow-1,.dac-platforms-sublandings .dac-sublandings-hero .devsite-landing-row-item-description,.dac-things-page .dac-full-height,.dac-tv-page .dac-platforms-sublandings-basics .dac-full-height{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-platforms-sublandings-basics.devsite-landing-row .devsite-landing-row-group .devsite-landing-row-item:not(:first-child){margin-left:40px;margin-top:0}.dac-platforms-sublandings-basics .devsite-landing-row-column .devsite-landing-row-item:last-child{margin-left:0}.dac-platforms-sublandings-basics .devsite-landing-row-column:not(:first-child) .devsite-landing-row-item:last-child{margin:40px 0 0}.dac-sublanding-title.devsite-landing-row:not([background]):not([foreground]) .devsite-landing-row-description{font-weight:300}.dac-oreo-page .dac-list-wrapped a,.dac-platforms-sublandings .dac-sublanding-title.devsite-landing-row:not([background]):not([foreground]) .devsite-landing-row-description,.dac-platforms-sublandings .dac-sublanding-title h2,.dac-preview-page .dac-list-wrapped a,.dac-security-page .dac-list-wrapped a{color:inherit}.dac-platforms-sublandings .dac-sublanding-title .devsite-landing-row-item-description{padding-left:0;padding-top:0}.dac-chrome-page .dac-featured-cards .devsite-landing-row-item-description,.dac-oreo-page .dac-list-wrapped .devsite-landing-row-item-description,.dac-platforms-sublandings .devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child).dac-grow-lg-5.dac-full-width-media,.dac-preview-page .dac-list-wrapped .devsite-landing-row-item-description,.dac-security-page .dac-list-wrapped .devsite-landing-row-item-description,.dac-things-page .devsite-landing-row-item-description:first-of-type{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:0}.dac-auto-page .dac-landing-row-bg-slate,.dac-platforms-sublandings .dac-landing-row-bg-slate.dac-item-switch{margin-top:0}.dac-oreo-page .devsite-landing-row-1-up .devsite-landing-row-item:not(.dac-stack-items-block-sm) .devsite-landing-row-item-description,.dac-preview-page .devsite-landing-row-1-up .devsite-landing-row-item-description{padding-left:0}.dac-oreo-page .dac-list-wrapped .devsite-landing-row-item-list-item,.dac-preview-page .dac-list-wrapped .devsite-landing-row-item-list-item,.dac-security-page .dac-list-wrapped .devsite-landing-row-item-list-item{-webkit-flex-basis:50%;flex-basis:50%;margin:0 0 24px}.dac-oreo-page .dac-list-wrapped h4,.dac-preview-page .dac-list-wrapped h4,.dac-security-page .dac-list-wrapped h4{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-oreo-page .dac-list-wrapped .devsite-landing-row-item-list,.dac-preview-page .dac-list-wrapped .devsite-landing-row-item-list{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-landing-row-bg-blob-7 .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex}.dac-auto-page .dac-banner-card .dac-banner-card-bg-img-item{background-image:url(../images/custom/auto-design.svg)}.dac-platforms-sublandings.dac-auto-page .devsite-landing-row-logos .devsite-landing-row-item .devsite-landing-row-item-image img{height:100%;max-width:115px;width:100%}.dac-auto-page .devsite-landing-row-logos img{width:-webkit-max-content;width:-moz-max-content;width:max-content}.dac-chrome-page .dac-banner-card-bg-img-item{background-image:url(../images/custom/chrome-design.svg)}.dac-chrome-page .dac-featured-cards .devsite-landing-row-group{display:block}.dac-tv-page .dac-banner-card .dac-banner-card-bg-img-item{background-image:url(../images/custom/tv-design.svg)}.dac-wear-page .dac-banner-card .dac-banner-card-bg-img-item{background-image:url(../images/custom/wear-os-design.svg)}.dac-things-page .dac-banner-card.devsite-landing-row img{display:none;margin-top:32px}.dac-things-page .dac-banner-card-bg-img-item.devsite-landing-row-item-no-media{margin:0}.dac-things-page .dac-banner-card.devsite-landing-row .devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0}.dac-platforms-sublandings .dac-item-switch.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:last-child){margin-left:32px;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.dac-platforms-sublandings .dac-item-switch.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child){margin-left:0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.dac-platforms-sublandings .devsite-landing-row-item-list h4{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1200px){.dac-platforms-sublandings-basics.devsite-landing-row .devsite-landing-row-group .devsite-landing-row-column,.dac-platforms-sublandings-basics.devsite-landing-row .devsite-landing-row-group .devsite-landing-row-item:not(:first-child){margin-left:0;margin-top:40px}.dac-platforms-sublandings .dac-platforms-sublandings-basics .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-chrome-page .dac-featured-cards .devsite-landing-row-group{max-width:100%}.dac-things-page .dac-banner-card.devsite-landing-row img,.dac-tv-page .dac-banner-card.devsite-landing-row img{display:block}.dac-things-page .dac-banner-card.devsite-landing-row .devsite-landing-row-item,.dac-tv-page .dac-banner-card.devsite-landing-row .devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:none;flex:none}}@media screen and (max-width:720px){.dac-platforms-sublandings .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(100% - 32px);flex:0 0 calc(100% - 32px)}.dac-oreo-page .dac-list-wrapped h4,.dac-preview-page .dac-list-wrapped h4,.dac-security-page .dac-list-wrapped h4{font:500 14px/22px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-android-go-page .devsite-landing-row[background=grey] .devsite-landing-row-item-description,.dac-indie-games .dac-landing-row-bg-slate:not(.dac-stack-items-block-sm) .devsite-landing-row-item-description,.dac-indie-games .devsite-landing-row-item[background=grey] .devsite-landing-row-item-description,.dac-play-guides .dac-landing-row-bg-slate .devsite-landing-row-item-description,.dac-play-guides .dac-stack-items-block-sm .devsite-landing-row-item-description:first-of-type,.dac-subscriptions .dac-card-image--bottom .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin-right:0}.dac-play-guides h3{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;margin-bottom:12px}.dac-play-guides h3 img{-webkit-align-self:baseline;align-self:baseline;margin-bottom:24px}.dac-play-guides .devsite-steps{padding:24px 40px 40px}.dac-play-guides .dac-card-image--bottom .devsite-landing-row-item-description-content{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-play-guides .dac-illustration-block{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end}.dac-play-guides .dac-illustration-block img{max-width:100%}.dac-android-go-page .devsite-landing-row-3-up h2,.dac-subscriptions .devsite-landing-row-1-up:not(.dac-heading-large) h2{margin-top:40px}.dac-android-go-page .devsite-landing-row[background=grey],.dac-play-guides .devsite-landing-row-1-up.dac-landing-row-bg-slate{background-clip:content-box}.dac-best-practices h3 img{max-height:90px}.dac-best-practices h3 button{display:none}.dac-play-academy .dac-landing-hero{background:url(../images/custom/play-academy-banner.svg) bottom/35% no-repeat;margin-bottom:0;padding-bottom:180px}.dac-play-academy .dac-cards-row .devsite-landing-row-group{background-color:#fff;margin:0 32px;padding:32px}.dac-play-academy .dac-cards-row .devsite-landing-row-item:last-of-type{-webkit-flex-basis:58%;flex-basis:58%}.dac-play-academy .dac-cards-row img{min-width:120px}.dac-play-academy .dac-play-academy-footer{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-play-academy .dac-play-academy-footer p{text-align:center}.dac-indie-games .dac-landing-row-item-indie-corner .devsite-landing-row-item-description{background:url(../images/custom/indie-games-indie-corner-background.svg) 100% 100%/auto 70% no-repeat;margin-right:0}.dac-indie-games .dac-landing-row-item-device .devsite-landing-row-item-description-content{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-webkit-flex-direction:row-reverse;flex-direction:row-reverse}.dac-indie-games .dac-landing-row-item-device-image{-webkit-box-flex:0;-webkit-flex:0 0 30%;flex:0 0 30%}.dac-indie-games .dac-landing-row-item-device-image img{max-width:100%}.dac-startups-page .dac-sublanding-title-normal{margin-top:64px}.dac-startups-page .dac-illustration-block{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin-top:64px}.dac-android-go-page .devsite-landing-row[background=grey] .devsite-landing-row-item-description{padding:32px}@media screen and (max-width:1400px){.dac-play-guides:not(.dac-play-academy):not(.dac-best-practices) .devsite-landing-row-3-up .devsite-landing-row-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-play-guides .devsite-landing-row-3-up .devsite-landing-row-item.dac-landing-row-bg-slate{margin:32px 0 0}.dac-indie-games .devsite-landing-row-3-up:last-of-type .devsite-landing-row-item:first-of-type,.dac-startups-page .devsite-landing-row-3-up:not(.force-spacing) .devsite-landing-row-group div:first-of-type{margin-top:0}.dac-subscriptions .devsite-landing-row-3-up{padding-bottom:0}}@media screen and (max-width:1200px){.dac-indie-games .dac-landing-row-item-indie-corner .devsite-landing-row-item-description{background-image:none}.dac-indie-games .dac-landing-row-item-device .devsite-landing-row-item-description-content{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}}@media screen and (max-width:1000px){.dac-families-page .devsite-landing-row-2-up .devsite-landing-row-group,.dac-indie-games .devsite-landing-row-2-up .devsite-landing-row-group,.dac-indie-games .devsite-landing-row-3-up .devsite-landing-row-group,.dac-startups-page .devsite-landing-row-2-up .devsite-landing-row-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-families-page .devsite-landing-row-2-up .devsite-landing-row-item.dac-card-image--bottom,.dac-play-guides .devsite-landing-row-2-up .devsite-landing-row-item.dac-stack-items-block-sm{margin:32px 0 0}.dac-play-academy .dac-cards-row img{width:85px}.dac-play-academy .dac-cards-row .devsite-landing-row-item:last-of-type{margin:0}.dac-play-academy .dac-cards-row .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-play-academy .dac-landing-hero{-o-background-size:50%;background-size:50%}.dac-indie-games .dac-landing-row-item-device-image img{max-width:30%}}.dac-gpi-page h2{color:#414141}.dac-gpi-page .dac-landing-row-hero img{margin-bottom:40px;max-width:380px}.dac-gpi-page .video-wrapper{float:none;margin:auto auto 40px;width:-webkit-calc(100% - 40px);width:calc(100% - 40px)}.dac-gpi-page .dac-landing-row-bg-slate{background-clip:content-box}.dac-gpi-page .dac-center .devsite-landing-row-item-description,.dac-gpi-page .dac-landing-row-bg-slate .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-gpi-page .dac-landing-row-bg-slate .devsite-landing-row-item-list{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:55px 0 7px}.dac-gpi-page .dac-landing-row-bg-slate h2{font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding-left:32px}@media screen and (max-width:1200px){.dac-gpi-page .dac-landing-row-bg-slate h2{font:300 38px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}@media screen and (max-width:1000px){.dac-gpi-page .dac-landing-row-bg-slate h2{font:300 32px/38px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-gpi-page .dac-landing-row-bg-slate h4{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-gpi-page .dac-landing-row-bg-slate li{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%;margin:0 0 24px;padding-left:32px}.dac-play-page .dynamic-content-paging{display:none}.dac-play-sublandings .dac-sublandings-hero{padding:86px 0}.dac-play-sublandings .dac-banner-img .devsite-landing-row-item-no-description{height:288px;padding-bottom:0;padding-top:0}.dac-play-sublandings .dac-sublandings-hero-copy img{margin-bottom:16px;width:54px}.dac-play-sublandings .dac-sublandings-hero h1{color:inherit;font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-play-sublandings .dac-sublandings-section-header .dac-sublandings-section-header-img img{display:block;margin:0 auto;max-width:500px}.dac-play-billing-page .dac-sublandings-section-header img,.dac-play-store-page .dac-sublandings-section-header .dac-sublandings-section-header-img .devsite-landing-row-item-description-content{min-height:230px}.dac-play-sublandings .dac-success-story{margin-left:40px}.dac-play-sublandings .dac-cards-1-1-2 .devsite-landing-row-item:nth-of-type(3),.dac-play-sublandings .dac-cards-2-1-1 .devsite-landing-row-item:first-of-type{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(50% - 19px);flex:0 0 calc(50% - 19px)}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-item,.dac-play-console-page .dac-bottom-spacing.dac-bg-img-in-column .devsite-landing-row-item,.dac-play-console-page .dac-cta .devsite-landing-row-item-description,.dac-play-store-page .dac-cards-1-1-2 .devsite-landing-row-item:not(:nth-of-type(3)),.dac-play-sublandings .dac-resources-heading .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-play-billing-page .dac-sublandings-hero-image img,.dac-play-services-page .dac-sublandings-hero-image img{max-width:580px}.dac-play-page .dac-illustration-play-console h3{margin-bottom:26px}.dac-play-page .dac-grow-5 .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex}.dac-play-page .dac-landing-row-bg-illustration-3:after{min-height:300px}.dac-play-page .dac-landing-row-bg-blob-3 .devsite-landing-row-item-buttons,.dac-play-page .dac-landing-row-bg-illustration-3 .devsite-landing-row-item-buttons{bottom:32px;left:32px;position:absolute}.dac-play-page .dac-basis-9.devsite-landing-row-item{-webkit-flex-basis:68.5%;flex-basis:68.5%;margin-bottom:0}.dac-play-console-page .dac-bg-img-in-column .devsite-landing-row-group{background:url(../images/custom/google-play-console-grow-business.svg) 0 100%/700px no-repeat}.dac-play-console-page .dac-sublandings-section-header .devsite-landing-row-item-description{padding:32px}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-group{background:url(../images/custom/google-play-console-release-confidence.svg) 100% 100%/700px no-repeat}.dac-play-console-page .dac-banner-img .devsite-landing-row-item-no-description{background:url(../images/custom/google-play-console-focus-quality.svg) 0/cover no-repeat}.dac-play-console-page .dac-sublandings-hero .devsite-landing-row-item-media{-webkit-align-self:center;align-self:center;-webkit-box-flex:1;-webkit-flex:auto;flex:auto;margin-left:32px;width:-webkit-calc(73% - 16px);width:calc(73% - 16px)}.dac-play-console-page .dac-bottom-spacing:not(.dac-bg-img-in-column) .dac-heading-medium{margin:32px 0 0 20px}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-item:first-of-type{margin:0}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-item:last-of-type{margin:0 0 0 32px}.dac-play-console-page .dac-landing-row-bg-mint:hover{background-color:#55ffb5;color:#414141}.dac-play-console-page .dac-landing-row-bg-yellow:hover{background-color:#ffd600}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-column{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.dac-play-console-page .dac-success-story .devsite-landing-row-item-image img{display:inline-block;float:right;width:120px}.dac-play-console-page .dac-bottom-spacing.dac-bg-img-in-column .devsite-landing-row-item{margin-right:0}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item:not(:first-child){margin-left:20px}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item{max-width:33%}.dac-play-console-page .dac-cta .devsite-landing-row-item-buttons{margin:auto}.dac-play-console-page .dac-chevron-right{color:#fff}.dac-play-store-page .dac-sublandings-hero{padding-bottom:0}.dac-play-store-page .dac-banner-img .devsite-landing-row-item-no-description{background:url(../images/custom/google-play-store-get-discovered.svg) 0/cover no-repeat}.dac-play-store-page .dac-sublandings-hero-image img{display:block;margin:auto}.dac-play-store-page .dac-resources-heading .devsite-landing-row-item{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-play-store-page .dac-cards-2-1-1 .devsite-landing-row-item-description-content a,.dac-play-store-page .dac-cards-2-1-1 [background]:not([background=grey]) p>a:not(.button){color:#1a73e8;text-decoration:none}.dac-play-store-page .dac-cards-1-1-1-1 .devsite-landing-row-group .devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:32px 0 0 20px}.dac-play-services-page .dac-sublandings-hero-copy .devsite-landing-row-item-buttons,.dac-play-services-page .devsite-landing-row-3-up.dac-bottom-spacing .devsite-landing-row-item.dac-heading-medium{margin-top:32px}.dac-play-services-page .dac-sublandings-section-header img{min-height:240px}.dac-resources-page .devsite-landing-row-3-up .devsite-landing-row-item-description{padding:0}.dac-resources-page h3:first-child{color:#1a73e8;font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-top:32px}.dac-resources-page .devsite-landing-row-3-up .devsite-landing-row-item-media{margin-bottom:0}.dac-play-policies-page .dac-code-snippet img{margin:0 auto}@media screen and (max-width:1400px){.dac-play-sublandings .dac-success-story{margin-left:0}.dac-play-sublandings .dac-sublandings-section-header .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-play-console-page .devsite-landing-row-4-up .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}}@media screen and (max-width:1200px){.dac-play-sublandings .dac-sublandings-hero h1{font:300 38px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-play-sublandings .dac-cards-1-1-2 .devsite-landing-row-item:nth-of-type(3),.dac-play-sublandings .dac-cards-2-1-1 .devsite-landing-row-item:first-of-type{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}.dac-play-billing-page .dac-cards-2-1-1 .devsite-landing-row-item,.dac-play-services-page .devsite-landing-row-4-up.dac-bottom-spacing .devsite-landing-row-item,.dac-play-sublandings .dac-cards-2-1-1.devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child),.dac-play-sublandings:not(.dac-play-console-page) .dac-cards-1-1-2.devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child){margin:32px 0 0}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-group,.dac-play-page .devsite-landing-row-4-up .devsite-landing-row-group,.dac-play-store-page .dac-cards-1-1-1-1 .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-play-page .devsite-landing-row-4-up .devsite-landing-row-group .dac-grow-4{-webkit-box-flex:1;-webkit-flex:1 1 40%;flex:1 1 40%}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-item:last-of-type,.dac-play-page .devsite-landing-row-4-up .devsite-landing-row-group .dac-grow-4:nth-of-type(3){margin-left:0;margin-top:32px}.dac-play-console-page .dac-bottom-spacing .dac-heading-medium,.dac-play-page .devsite-landing-row-4-up .devsite-landing-row-group .dac-grow-4:last-of-type{margin-top:32px}.dac-play-console-page .dac-bottom-spacing .dac-heading-medium,.dac-play-store-page .dac-cards-1-1-1-1 .devsite-landing-row-group .devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1 0 -webkit-calc((100% - 40px)/2);flex:1 0 calc((100% - 40px)/2)}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-column,.dac-play-services-page .devsite-landing-row-4-up .devsite-landing-row-group,.dac-play-sublandings .dac-cards-2-1-1 .devsite-landing-row-group,.dac-play-sublandings:not(.dac-play-console-page) .dac-cards-1-1-2 .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}}@media screen and (max-width:1000px){.dac-play-sublandings .dac-cards-1-1-2 .devsite-landing-row-item:nth-of-type(3),.dac-play-sublandings .dac-cards-2-1-1 .devsite-landing-row-item:first-of-type{-webkit-flex-basis:auto;flex-basis:auto}.dac-play-sublandings .dac-sublandings-hero-copy,.dac-play-sublandings .dac-sublandings-hero .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-play-billing-page .dac-sublandings-hero-image img,.dac-play-services-page .dac-sublandings-hero-image img{min-height:400px}.dac-play-billing-page .devsite-landing-row-2-up.dac-bottom-spacing.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:first-of-type,.dac-play-billing-page .devsite-landing-row-2-up.dac-bottom-spacing.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:last-of-type,.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item:last-of-type,.dac-play-console-page .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2),.dac-play-console-page .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(4){margin-top:32px}.dac-play-page .dac-grow-5,.dac-play-page .dac-grow-7{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-play-page .devsite-landing-row:not(.devsite-landing-row-4-up) .dac-grow-4{-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;margin-left:0;margin-top:32px}.dac-play-page .dac-basis-9.devsite-landing-row-item{-webkit-flex-basis:100%;flex-basis:100%}.dac-play-page .devsite-landing-row.dac-landing-row-bg-grey .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-play-console-page .dac-sublandings-hero .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:none;flex:none;margin-left:0;-webkit-box-ordinal-group:4;-webkit-order:3;order:3;padding-top:20px;width:100%}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item:last-of-type,.dac-play-console-page .devsite-landing-row-4-up.dac-bottom-spacing .dac-heading-medium,.dac-play-store-page .dac-cards-1-1-1-1 .devsite-landing-row-group .devsite-landing-row-item{-webkit-flex-basis:100%;flex-basis:100%;margin-left:0}.dac-play-store-page .dac-cards-1-1-1-1 .devsite-landing-row-group .devsite-landing-row-item:first-child{margin-top:0}.dac-play-store-page .devsite-landing-row.dac-cards-1-1-1-1+.dac-cards-1-1-1-1{padding-top:20px}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1;flex:1;max-width:100%}.dac-play-billing-page.dac-play-sublandings .dac-cards-2-1-1 .devsite-landing-row-item:first-of-type{margin-bottom:0}.dac-play-policies-page .dac-sublandings-hero-copy{margin-bottom:32px}}@media screen and (max-width:720px){.dac-play-sublandings .dac-landing-row-item-icon-container-left{margin-top:0}.dac-play-page .dac-landing-row-bg-blob-3 .devsite-landing-row-item-buttons,.dac-play-page .dac-landing-row-bg-illustration-3 .devsite-landing-row-item-buttons{bottom:24px;left:24px}.dac-play-billing-page .devsite-landing-row-2-up.dac-bottom-spacing.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:last-of-type{margin-top:32px}}@media screen and (max-width:600px){.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-column:last-of-type{height:300px}.dac-play-console-page .dac-bg-img-in-column .devsite-landing-row-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:nowrap;flex-wrap:nowrap;height:300px}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item:not(:first-child){margin-left:0}.dac-play-services-page .devsite-landing-row-3-up.dac-bottom-spacing .devsite-landing-row-item.dac-heading-medium:first-of-type{margin-top:0}}.dac-security-page .dac-slate{color:#455a64}.dac-subscribe-page .dac-subscribe-heading{color:#414141;margin:24px 0 40px}.dac-subscribe-page .dac-field,.dac-subscribe-page .dac-field-group{margin:0 4px 32px}.dac-subscribe-page .dac-names-field{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;width:60%}.dac-subscribe-page .dac-names-field .dac-field{-webkit-box-flex:1;-webkit-flex:1;flex:1;min-width:260px}.dac-subscribe-page label,.dac-subscribe-page legend{color:#202124;font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;font-weight:400}.dac-subscribe-page .dac-label-description{color:#5f6368;font:500 12px/18px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;font-weight:400;line-height:24px;margin:0 32px}.dac-subscribe-page .dac-language-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center}[dir=ltr] .dac-subscribe-page .dac-language-container label{margin-right:12px}.dac-subscribe-page .dac-form-info{font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-subscribe-page .dac-form-after-submit h1{color:#414141;font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-top:0}.dac-subscribe-page .dac-form input[type=text]{padding:10px}.dac-subscribe-page .dac-form input[type=email]:focus{border-bottom:2px solid #4285f4}.dac-subscribe-page .dac-form .dac-email{margin-bottom:0;min-width:260px;width:60%}.dac-subscribe-page devsite-framebox{margin-left:20px}.dac-subscribe-page.dac-subscribe-container{background-color:#f7f9fa;margin:0 auto;max-width:1000px;padding:50px;position:relative}.dac-subscribe-page iframe{width:100%}.dac-subscribe-page .dac-form .dac-email{-webkit-border-radius:2px;border-radius:2px;margin-bottom:40px;min-width:225px;padding:10px;width:70%}.dac-subscribe-page .dac-language-container{display:-webkit-box;display:-webkit-flex;display:flex;margin:45px 0 30px}.dac-subscribe-page .dac-language-container h3,.dac-subscribe-page .dac-row{margin:0}.dac-subscribe-page .dac-form-icon{height:230px;position:absolute;right:80px;top:495px}@media screen and (max-width:1024px){.dac-subscribe-page .dac-form-icon .dac-subscribe-page .dac-form-icon{display:none}}.dac-subscribe-page .dac-outline-button{margin-top:45px}@media screen and (max-width:820px){.dac-subscribe-page .dac-form-icon{display:none}body[layout=full][type=landing].dac-subscribe-page .devsite-main-content{padding:0}.dac-subscribe-page .dac-subscribe-container{padding:25px}.dac-subscribe-page .dac-form .dac-email{width:85%}}@media screen and (max-width:470px){.dac-subscribe-page .dac-language-container{display:block}}@font-face{font-family:Euclid;src:url(../fonts/custom/euclid.ttf) format("truetype"),url(../fonts/custom/euclid.otf) format("opentype"),url(../fonts/custom/euclid.woff) format("woff")}.dac-studio-page .download td{vertical-align:middle}.dac-studio-page #studio-downloaded-dialog{height:auto}.dac-studio-page #studio-downloaded-dialog .devsite-dialog-contents{height:-webkit-calc(100% - 36px);height:calc(100% - 36px);margin-bottom:24px}.dac-studio-page .studio-downloaded-links{padding:16px 0}.dac-studio-page .studio-downloaded-links img{background:#d7d7d7;margin:0 0 16px;width:100%}.dac-studio-page .devsite-landing-row-item-description ul{padding-left:16px}.dac-studio-page .dac-info-size{font-size:smaller;margin-top:4px;padding:0 8px}.dac-studio-page .dac-sizes{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-studio-page .dac-feature-last{margin-bottom:40px}.dac-studio-page .dac-btn-block{margin-top:32px;padding:0 32px}.dac-studio-page .dac-landing-row-hero{margin-bottom:32px}.dac-studio-page .dac-download-options{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;width:100%}.dac-studio-page .dac-download-info{border:1px solid #dadce0;-webkit-box-shadow:none;box-shadow:none;height:100%;padding:24px 12px;text-align:center}.dac-studio-page .dac-download-info h2{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;font-weight:500}.dac-studio-page .dac-download-info.show{-webkit-box-flex:6;-webkit-flex:6;flex:6;margin:0 32px 0 0}.dac-studio-page .dac-download-info.solo{-webkit-box-flex:4;-webkit-flex:4;flex:4;max-width:500px}.dac-studio-page .dac-landing-links{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin:24px 0;width:100%}.dac-studio-page .dac-landing-links .button{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:12px 40px;min-width:240px;text-align:center}.dac-studio-page .dac-dropshadow{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}.dac-studio-page .dac-video-card h3{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-studio-page .devsite-landing-row-item-media{background:#455a64;display:-webkit-box;display:-webkit-flex;display:flex}.dac-studio-page .devsite-landing-row-item-image{-webkit-align-self:center;align-self:center;background:#455a64;width:100%}.dac-terms-page .sdk-terms{white-space:pre-wrap;word-wrap:break-word}.dac-archive-page.tos-wall{height:450px;overflow:auto}.dac-archive-page .sdk-terms{color:#757575;border:1px solid #dadce0;-webkit-box-shadow:none;box-shadow:none;font:400 14px/22px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:300px;margin-bottom:16px;overflow:scroll;padding:12px;white-space:pre-wrap;word-wrap:break-word}.dac-archive-page.tos-wall h2,.dac-archive-page.tos-wall h3{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0;padding:0}.dac-releases-page.updates-box{border:1px solid #dadce0;-webkit-box-shadow:none;box-shadow:none;margin:12px 0;padding:12px 24px 0}.dac-test-page .jd-sumtable th{color:inherit}@media screen and (max-width:1000px){.dac-full-width-content .devsite-landing-row-item-no-media+.devsite-landing-row-item-no-media:nth-of-type(2n),.dac-full-width-content .devsite-landing-row-item-no-media:not(:first-child){margin:0}.dac-full-width-content .devsite-landing-row-group .devsite-landing-row-item-no-media,.dac-full-width-content .devsite-landing-row .devsite-landing-row-item-no-media{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-studio-page .dac-btn-block{padding:0}.dac-studio-page .dac-landing-links .button{margin:12px 16px}.dac-studio-page .dac-download-options{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-studio-page .dac-download-info.show,.dac-studio-page .dac-download-info.solo{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-studio-page .dac-download-info.solo{max-width:100%}.dac-studio-page .dac-download-info.show{margin:0 0 16px}}@media screen and (max-width:720px){.dac-full-width-content .devsite-landing-row-item-no-media+.devsite-landing-row-item-no-media:nth-of-type(2n),.dac-full-width-content .devsite-landing-row-item-no-media:not(:first-child){-webkit-box-flex:0;-webkit-flex:0;flex:0}}devsite-book-nav{max-height:100vh;overflow-x:hidden;overflow-y:auto;position:relative;z-index:1004}body[pending] devsite-book-nav{background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);height:100vh}body[ready] devsite-book-nav[fixed]{-webkit-box-shadow:none;box-shadow:none;contain:content;max-height:100%;position:fixed;-webkit-transform:translateZ(0);transform:translateZ(0);will-change:top,max-height,transform}.devsite-book-nav-bg{background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}body[ready] .devsite-book-nav-bg[fixed]{bottom:0;display:block;position:fixed;top:0;z-index:1}.devsite-book-nav-bg:after{bottom:-10px;content:"";display:block;height:10px;left:0;position:fixed;width:278px}[dir=rtl] .devsite-book-nav-bg:after{left:auto;right:0}devsite-book-nav .devsite-nav{-webkit-transform:translateZ(0);transform:translateZ(0)}devsite-book-nav .devsite-nav-list{padding-bottom:36px}devsite-book-nav .devsite-nav-list>.devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider):first-child{border-top:0;margin-top:20px;padding-top:0}devsite-book-nav li .devsite-nav-title{padding-left:24px}[dir=rtl] devsite-book-nav li .devsite-nav-title{padding-left:8px;padding-right:24px}devsite-book-nav devsite-expandable-nav li .devsite-nav-title{padding-left:40px}[dir=rtl] devsite-book-nav devsite-expandable-nav li .devsite-nav-title{padding-left:8px;padding-right:40px}devsite-book-nav devsite-expandable-nav li li .devsite-nav-title{padding-left:56px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li .devsite-nav-title{padding-left:8px;padding-right:56px}devsite-book-nav devsite-expandable-nav li li li .devsite-nav-title{padding-left:72px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li li .devsite-nav-title{padding-left:8px;padding-right:72px}devsite-book-nav devsite-expandable-nav li li li li .devsite-nav-title{padding-left:88px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li li li .devsite-nav-title{padding-left:8px;padding-right:88px}devsite-book-nav devsite-expandable-nav li li li li li .devsite-nav-title{padding-left:104px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li li li li .devsite-nav-title{padding-left:8px;padding-right:104px}devsite-book-nav li.devsite-nav-divider .devsite-nav-title{padding-left:0}[dir=rtl] devsite-book-nav li.devsite-nav-divider .devsite-nav-title{padding-right:0}devsite-book-nav .devsite-nav-title{padding-right:8px}[dir=rtl] devsite-book-nav .devsite-nav-title{padding-left:8px;padding-right:0}devsite-book-nav .devsite-nav-list>.devsite-nav-heading:not(.devsite-nav-divider){border-top:1px solid #dadce0;padding-top:11px}devsite-book-nav .devsite-nav-heading:not(.devsite-nav-divider){margin-top:12px}devsite-book-nav .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:24px}[dir=rtl] devsite-book-nav .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:24px}devsite-book-nav devsite-expandable-nav .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:40px}[dir=rtl] devsite-book-nav devsite-expandable-nav .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:40px}devsite-book-nav devsite-expandable-nav li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:56px}[dir=rtl] devsite-book-nav devsite-expandable-nav li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:56px}devsite-book-nav devsite-expandable-nav li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:72px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:72px}devsite-book-nav devsite-expandable-nav li li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:88px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:88px}devsite-book-nav devsite-expandable-nav li li li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:104px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:104px}devsite-book-nav .devsite-nav-heading.devsite-nav-divider{background:#eceff1;border-bottom:1px solid #dadce0;border-top:1px solid #dadce0;padding:4px 24px 2px}devsite-book-nav .devsite-nav-heading.devsite-nav-divider:first-child{padding-top:4px}devsite-book-nav .devsite-nav-divider>.devsite-nav-title{font:500 11px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.8px;text-transform:uppercase}devsite-book-nav .devsite-nav-accordion+.devsite-nav-accordion,devsite-book-nav .devsite-nav-divider+.devsite-nav-accordion{border-top:0;padding-top:12px}devsite-book-nav .devsite-nav-accordion+.devsite-nav-divider{border-top:0;padding-top:4px}devsite-book-nav .devsite-nav-accordion+.devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider),devsite-book-nav .devsite-nav-divider+.devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider),devsite-book-nav .devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider)+.devsite-nav-accordion,devsite-book-nav .devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider)+.devsite-nav-divider{margin-top:12px}devsite-book-nav .devsite-nav-break{height:24px}#devsite-hamburger-menu,#devsite-hamburger-menu[visually-hidden],devsite-book-nav .devsite-mobile-header,devsite-book-nav .devsite-mobile-nav-top{display:none}#devsite-hamburger-menu:before{content:"menu"}devsite-book-nav #devsite-close-nav:before{content:"arrow_back"}[dir=rtl] devsite-book-nav #devsite-close-nav:before{content:"arrow_forward"}devsite-book-nav[top-level-nav] #devsite-close-nav:before{content:"close"}@media screen and (max-width:840px){devsite-book-nav{display:none;height:100vh;max-height:100vh!important;top:0!important;-webkit-transform:translate3d(-280px,0,0)!important;transform:translate3d(-280px,0,0)!important;-webkit-transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);-o-transition:-o-transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1),-webkit-transform .2s cubic-bezier(.4,0,.2,1),-o-transform .2s cubic-bezier(.4,0,.2,1);z-index:1013}[dir=rtl] devsite-book-nav{-webkit-transform:translate3d(280px,0,0)!important;transform:translate3d(280px,0,0)!important}devsite-book-nav:not([animatable]){-webkit-transition:-webkit-transform 1ms;transition:-webkit-transform 1ms;-o-transition:-o-transform 1ms;transition:transform 1ms;transition:transform 1ms,-webkit-transform 1ms,-o-transform 1ms}body[ready] .devsite-book-nav-bg[fixed]{display:none}body[ready] devsite-book-nav[fixed]{background:#fff;display:block!important;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}devsite-book-nav[visually-hidden]{opacity:1!important;pointer-events:auto!important;visibility:visible!important}#devsite-hamburger-menu{display:inline-block}devsite-book-nav #devsite-close-nav{color:#5f6368;-webkit-flex-shrink:0;flex-shrink:0}#devsite-hamburger-menu,devsite-book-nav #devsite-close-nav{height:auto;padding:8px;position:relative;width:auto;z-index:20}[dir=ltr] #devsite-hamburger-menu,[dir=ltr] devsite-book-nav #devsite-close-nav{margin:0 8px 0 -4px}[dir=rtl] #devsite-hamburger-menu,[dir=rtl] devsite-book-nav #devsite-close-nav{margin:0 -4px 0 8px}devsite-book-nav .devsite-mobile-nav-top{display:block}devsite-book-nav .devsite-book-nav-wrapper{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-transform:translate3d(-268px,0,0)!important;transform:translate3d(-268px,0,0)!important;-webkit-transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);-o-transition:-o-transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1),-webkit-transform .2s cubic-bezier(.4,0,.2,1),-o-transform .2s cubic-bezier(.4,0,.2,1)}[dir=rtl] devsite-book-nav .devsite-book-nav-wrapper{-webkit-transform:translate3d(268px,0,0)!important;transform:translate3d(268px,0,0)!important}devsite-book-nav:not([animatable]) .devsite-book-nav-wrapper{-webkit-transition:-webkit-transform 1ms;transition:-webkit-transform 1ms;-o-transition:-o-transform 1ms;transition:transform 1ms;transition:transform 1ms,-webkit-transform 1ms,-o-transform 1ms}devsite-book-nav .devsite-nav-list{padding-bottom:120px}devsite-book-nav .devsite-nav-list>.devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider):first-child{margin-top:0}devsite-book-nav .devsite-mobile-nav-bottom .devsite-nav-list>.devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider):first-child{margin-top:13px}devsite-book-nav .devsite-mobile-nav-top .devsite-nav-text{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}devsite-book-nav[top-level-nav] .devsite-book-nav-wrapper{-webkit-transform:translateZ(0)!important;transform:translateZ(0)!important}devsite-book-nav:not([top-level-nav]) .devsite-mobile-nav-top,devsite-book-nav[top-level-nav] .devsite-mobile-nav-bottom{height:-webkit-calc(100vh - 64px);height:calc(100vh - 64px);overflow:hidden}devsite-book-nav .devsite-mobile-nav-top>.devsite-nav-list>.devsite-nav-item{border-bottom:1px solid #dadce0}devsite-book-nav .devsite-mobile-nav-top>.devsite-nav-list>.devsite-nav-item>.devsite-nav-title{font-weight:700;padding-bottom:15px;padding-top:16px}devsite-book-nav .devsite-mobile-nav-top>.devsite-nav-list>.devsite-nav-item>.devsite-nav-title:not(.devsite-nav-active){color:#5f6368}devsite-book-nav .devsite-mobile-nav-bottom,devsite-book-nav .devsite-mobile-nav-top{-webkit-flex-shrink:0;flex-shrink:0;width:268px}devsite-book-nav .devsite-mobile-header{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;height:48px;padding:0 16px;position:relative}devsite-book-nav .devsite-mobile-header .devsite-nav-active{font-weight:400}devsite-book-nav .devsite-nav-responsive-tabs{margin-bottom:12px;margin-top:-11px}devsite-book-nav .devsite-lower-tab-item{margin:0}devsite-book-nav .devsite-nav-responsive-tabs>.devsite-nav-item:last-child{margin-bottom:8px}}@media screen and (max-width:600px){#devsite-hamburger-menu,devsite-book-nav #devsite-close-nav{margin:0 4px 0 -12px}}devsite-book-nav .devsite-product-id-row{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;min-height:56px;padding:20px 24px 2px}devsite-book-nav .devsite-header-no-lower-tabs .devsite-product-id-row{min-height:72px;padding:20px 24px}devsite-book-nav .devsite-product-description-row{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}devsite-book-nav .devsite-breadcrumb-list+.devsite-product-description:not(:empty){margin-top:8px}devsite-book-nav .devsite-product-description{font:16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0 180px 0 0}[dir=rtl] devsite-book-nav .devsite-product-description{margin:0 0 0 180px}devsite-book-nav .devsite-product-button-row{display:-webkit-box;display:-webkit-flex;display:flex;margin:0 0 0 24px;z-index:1}[dir=rtl] devsite-book-nav .devsite-product-button-row{margin:0 24px 0 0}@media screen and (max-width:840px){devsite-book-nav .devsite-product-id-row{min-height:72px;padding:20px 24px}[dir=rtl] devsite-book-nav .devsite-product-description,devsite-book-nav .devsite-product-description{margin:0}}@media screen and (max-width:600px){devsite-book-nav .devsite-header-no-lower-tabs .devsite-product-id-row,devsite-book-nav .devsite-product-id-row{-webkit-flex-wrap:wrap;flex-wrap:wrap;padding:20px 16px}devsite-book-nav .devsite-product-button-row{-webkit-flex-basis:100%;flex-basis:100%;margin:16px 0 0}}devsite-book-nav .devsite-product-name-wrapper{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:36px;margin:6px 0}devsite-book-nav .devsite-product-name-link,devsite-book-nav .devsite-site-logo-link{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;opacity:1;-webkit-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}devsite-book-nav .devsite-product-name-link:focus,devsite-book-nav .devsite-product-name-link:hover,devsite-book-nav .devsite-site-logo-link:focus{opacity:.7;text-decoration:none}devsite-book-nav .devsite-site-logo{height:32px}devsite-book-nav .devsite-has-google-wordmark>.devsite-breadcrumb-link,devsite-book-nav .devsite-has-google-wordmark>.devsite-product-name{direction:ltr}devsite-book-nav .devsite-google-wordmark{height:24px;margin:0 4px 0 0;position:relative;top:5px}devsite-book-nav .devsite-google-wordmark-svg-path{-webkit-transition:fill .2s;-o-transition:fill .2s;transition:fill .2s}devsite-book-nav .devsite-site-logo-link canvas{height:auto!important}devsite-book-nav .devsite-product-logo-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;height:36px;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;width:36px}[dir=ltr] devsite-book-nav .devsite-product-logo-container{margin-right:4px}[dir=rtl] devsite-book-nav .devsite-product-logo-container{margin-left:4px}devsite-book-nav .devsite-product-logo{font-size:32px;height:32px;max-width:32px;min-width:32px;overflow:hidden;white-space:nowrap}devsite-book-nav .devsite-product-logo-container[background] .devsite-product-logo{font-size:28px;height:28px;max-width:28px;min-width:28px}devsite-book-nav .devsite-product-name{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:0;margin:0;max-height:32px;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;white-space:nowrap}devsite-book-nav .devsite-site-logo:not([src*=\.svg]){height:auto;max-height:32px}devsite-book-nav .devsite-breadcrumb-link>.devsite-product-name{color:inherit}@media screen and (max-width:840px){devsite-book-nav .devsite-product-name-wrapper{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;min-width:0}devsite-book-nav .devsite-product-name-wrapper .devsite-breadcrumb-item:not(:first-of-type),devsite-book-nav .devsite-product-name-wrapper .devsite-site-logo-link+.devsite-product-name{display:none}devsite-book-nav .devsite-product-name-wrapper .devsite-breadcrumb-item,devsite-book-nav .devsite-product-name-wrapper .devsite-breadcrumb-link,devsite-book-nav .devsite-product-name-wrapper .devsite-breadcrumb-list,devsite-book-nav .devsite-product-name-wrapper .devsite-product-name{width:100%}devsite-book-nav .devsite-product-name-wrapper .devsite-breadcrumb-link{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}}devsite-expandable-nav{cursor:pointer;display:block;position:relative}devsite-expandable-nav>.devsite-nav-section{max-width:100%;overflow-y:hidden;-webkit-transition:height .2s;-o-transition:height .2s;transition:height .2s;width:100%;will-change:height}devsite-expandable-nav:not([animatable])>.devsite-nav-section{-webkit-transition:height 1ms;-o-transition:height 1ms;transition:height 1ms}devsite-expandable-nav[collapsed]:not([animating])>.devsite-nav-section{display:none}devsite-expandable-nav[collapsed]:not([connected])>.devsite-nav-section{height:0}devsite-expandable-nav>.devsite-nav-title-no-path{cursor:pointer;outline:0}devsite-expandable-nav>.devsite-nav-title{padding-left:24px}[dir=rtl] devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:24px}devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:40px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:40px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:56px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:56px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:72px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:72px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:88px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:88px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:104px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:104px}devsite-expandable-nav>.devsite-nav-toggle{color:#bdc1c6;cursor:pointer;font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal}.devsite-nav-item:not(.devsite-nav-accordion)>devsite-expandable-nav>.devsite-nav-toggle{font-size:18px;position:absolute;top:2px;-webkit-transform:rotate(0);-o-transform:rotate(0);transform:rotate(0);-webkit-transition:-webkit-transform .2s ease;transition:-webkit-transform .2s ease;-o-transition:-o-transform .2s ease;transition:transform .2s ease;transition:transform .2s ease,-webkit-transform .2s ease,-o-transform .2s ease;will-change:transform}.devsite-nav-item:not(.devsite-nav-accordion)>devsite-expandable-nav:not([animatable])>.devsite-nav-toggle{-webkit-transition:-webkit-transform 1ms;transition:-webkit-transform 1ms;-o-transition:-o-transform 1ms;transition:transform 1ms;transition:transform 1ms,-webkit-transform 1ms,-o-transform 1ms}devsite-expandable-nav>.devsite-nav-toggle{left:4px}[dir=rtl] devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:4px}devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:20px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:20px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:36px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:36px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:52px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:52px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:68px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:68px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:84px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:84px}.devsite-nav-item:not(.devsite-nav-accordion)>devsite-expandable-nav[collapsed]>.devsite-nav-toggle{-webkit-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}[dir=rtl] .devsite-nav-item:not(.devsite-nav-accordion)>devsite-expandable-nav[collapsed]>.devsite-nav-toggle{-webkit-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}devsite-expandable-nav>.devsite-nav-toggle:before{content:"arrow_drop_down"}.devsite-nav-accordion{border-bottom:1px solid #dadce0;border-top:1px solid #dadce0;padding:11px 0}.devsite-nav-accordion>devsite-expandable-nav{-webkit-flex-wrap:wrap;flex-wrap:wrap}.devsite-nav-accordion>devsite-expandable-nav,.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-title{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex}.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-title{color:rgba(0,0,0,.65);-webkit-box-flex:1;-webkit-flex:1 0 196px;flex:1 0 196px;font-weight:700;overflow:hidden}.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-toggle{font-size:24px;margin:0 8px 0 0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1;-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transition:-webkit-transform .5s;transition:-webkit-transform .5s;-o-transition:-o-transform .5s;transition:transform .5s;transition:transform .5s,-webkit-transform .5s,-o-transform .5s}[dir=rtl] .devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-toggle{margin:0 0 0 8px}.devsite-nav-accordion>devsite-expandable-nav:not([animatable])>.devsite-nav-toggle{-webkit-transition:-webkit-transform 1ms;transition:-webkit-transform 1ms;-o-transition:-o-transform 1ms;transition:transform 1ms;transition:transform 1ms,-webkit-transform 1ms,-o-transform 1ms}.devsite-nav-accordion>devsite-expandable-nav[collapsed]>.devsite-nav-toggle{-webkit-transform:rotateX(180deg);transform:rotateX(180deg)}.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-toggle:before{content:"expand_less"}.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-section{-webkit-box-ordinal-group:3;-webkit-order:2;order:2}devsite-footer-linkboxes{background:#fff;display:block;font:400 14px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0 24px}devsite-footer-linkboxes .devsite-footer-linkboxes-list{border-bottom:1px solid #dadce0;display:-webkit-box;display:-webkit-flex;display:flex;list-style:none;padding:0}devsite-footer-linkboxes .devsite-footer-linkbox{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:24px 0}devsite-footer-linkboxes .devsite-footer-linkbox:not(:first-child){margin-left:24px}devsite-footer-linkboxes .devsite-footer-linkbox-heading{font:500 14px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0 0 8px}devsite-footer-linkboxes .devsite-footer-linkbox-list .devsite-footer-linkbox-heading{margin-top:40px}devsite-footer-linkboxes .devsite-footer-linkbox-list{list-style-type:none;padding:0}devsite-footer-linkboxes .devsite-footer-linkbox-item{margin:0}devsite-footer-linkboxes .devsite-footer-linkbox-link{color:#202124;display:block;padding:8px 0}devsite-footer-linkboxes .devsite-footer-linkbox-link:focus,devsite-footer-linkboxes .devsite-footer-linkbox-link:hover{color:#1a73e8;text-decoration:none}@media screen and (max-width:1252px){.devsite-main-content[has-book-nav]~devsite-footer-linkboxes .devsite-footer-linkbox{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}}@media screen and (max-width:600px){devsite-footer-linkboxes{padding:0 16px}devsite-footer-linkboxes .devsite-footer-linkboxes-list{display:block}devsite-footer-linkboxes .devsite-footer-linkbox{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}devsite-footer-linkboxes .devsite-footer-linkbox:not(:first-child){margin-left:0}}devsite-footer-promos{border-top:1px solid #dadce0;background:#fff;display:block;font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0 24px}devsite-footer-promos .devsite-footer-promos-list{border-bottom:1px solid #dadce0;display:-webkit-box;display:-webkit-flex;display:flex;list-style:none;-webkit-justify-content:space-around;justify-content:space-around;padding:18px 0}devsite-footer-promos .devsite-footer-promo{-webkit-box-flex:0;-webkit-flex:0 1 192px;flex:0 1 192px;margin:20px 0;text-align:center}devsite-footer-promos .devsite-footer-promo:not(:first-child){margin-left:24px}devsite-footer-promos .devsite-footer-promo-icon{color:rgba(0,0,0,.87);display:block;font-size:48px;height:48px;margin:0 auto 8px;width:48px}devsite-footer-promos .devsite-footer-promo-title{color:rgba(0,0,0,.87);display:block;font-weight:500}devsite-footer-promos .devsite-footer-promo-title:focus,devsite-footer-promos .devsite-footer-promo-title:hover{color:#1a73e8;text-decoration:none}@media screen and (max-width:1252px){.devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promos-list{-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}.devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promo{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%;padding:0 20px}.devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promo:not(:first-child){margin-left:0}}@media screen and (max-width:840px){.devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promos-list,devsite-footer-promos .devsite-footer-promos-list{-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start;padding:12px 0}.devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promo,devsite-footer-promos .devsite-footer-promo{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%;margin:0;padding:8px 8px 8px 0;text-align:left}[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promo,[dir=rtl] devsite-footer-promos .devsite-footer-promo{text-align:right}devsite-footer-promos .devsite-footer-promo:not(:first-child){margin-left:0}devsite-footer-promos .devsite-footer-promo-icon{height:32px;margin:0 8px 0 0;width:32px}devsite-footer-promos .devsite-footer-promo-title{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;font-weight:400}devsite-footer-promos .devsite-footer-promo-description{display:none}}@media screen and (max-width:600px){devsite-footer-promos{padding:0 16px}devsite-footer-promos .devsite-footer-promos-list{display:block}}devsite-footer-utility{background:#fff;display:block;font:400 14px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0 24px}devsite-footer-utility nav{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;padding:24px 0}devsite-footer-utility .devsite-footer-sites{-webkit-box-align:center;-webkit-align-items:center;align-items:center;border-bottom:1px solid #dadce0;padding:24px 0 23px}devsite-footer-utility .devsite-footer-sites-list{display:-webkit-box;display:-webkit-flex;display:flex;list-style:none;padding:0}devsite-footer-utility .devsite-footer-sites-item{margin:0 0 0 40px}[dir=rtl] devsite-footer-utility .devsite-footer-sites-item{margin:0 40px 0 0}devsite-footer-utility .devsite-footer-sites-link{color:#202124;display:block;padding:8px 0}devsite-footer-utility .devsite-footer-sites-link:focus,devsite-footer-utility .devsite-footer-sites-link:hover{color:#1a73e8;text-decoration:none}devsite-footer-utility .devsite-footer-sites-logo-link{display:-webkit-box;display:-webkit-flex;display:flex}devsite-footer-utility .devsite-footer-sites-logo{height:32px;margin-top:-4px;width:185px}devsite-footer-utility .devsite-footer-utility-list{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;list-style:none;min-height:36px;padding:0}devsite-footer-utility .devsite-footer-utility-link{color:#202124}devsite-footer-utility .devsite-footer-utility-link:focus,devsite-footer-utility .devsite-footer-utility-link:hover{color:#1a73e8}devsite-footer-utility .devsite-footer-utility-item{display:-webkit-box;display:-webkit-flex;display:flex;margin:0 8px 0 0}[dir=rtl] devsite-footer-utility .devsite-footer-utility-item{margin:0 0 0 8px}devsite-footer-utility .devsite-footer-utility-item:last-child{margin-right:0}[dir=rtl] devsite-footer-utility .devsite-footer-utility-item:last-child{margin-left:0}devsite-footer-utility .devsite-footer-utility-item:not(:first-child):before{content:"|";margin:0 8px 0 0}[dir=rtl] devsite-footer-utility .devsite-footer-utility-item:not(:first-child):before{margin:0 0 0 8px}devsite-footer-utility .devsite-footer-utility-item.devsite-footer-utility-button:before{content:"";margin:0}devsite-footer-utility .devsite-footer-utility-button{-webkit-box-align:center;-webkit-align-items:center;align-items:center;line-height:20px;margin-left:auto;padding-left:16px}devsite-footer-utility .devsite-footer-utility-button>a{-webkit-flex-shrink:0;flex-shrink:0;margin:0 0 0 16px}[dir=rtl] devsite-footer-utility .devsite-footer-utility-button>a{margin:0 16px 0 0}devsite-footer-utility .devsite-footer-utility-button>a:focus{text-decoration:none}devsite-footer-utility devsite-language-selector{margin:0 0 0 16px}[dir=rtl] devsite-footer-utility devsite-language-selector{margin:0 16px 0 0}@media screen and (max-width:1252px){.devsite-main-content[has-book-nav]~devsite-footer-utility .devsite-footer-sites{display:block}.devsite-main-content[has-book-nav]~devsite-footer-utility .devsite-footer-sites-item{margin:0 40px 0 0}[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-utility .devsite-footer-sites-item{margin:0 0 0 40px}.devsite-main-content[has-book-nav]~devsite-footer-utility .devsite-footer-sites-logo{margin-bottom:16px}}@media screen and (max-width:840px){devsite-footer-utility .devsite-footer-sites{display:block}devsite-footer-utility .devsite-footer-sites-item{margin:0 40px 0 0}[dir=rtl] devsite-footer-utility .devsite-footer-sites-item{margin:0 0 0 40px}devsite-footer-utility .devsite-footer-sites-logo{margin-bottom:16px;margin-top:0}}@media screen and (max-width:600px){devsite-footer-utility{padding:0 16px}devsite-footer-utility .devsite-footer-sites,devsite-footer-utility .devsite-footer-sites-list,devsite-footer-utility nav{display:block}.devsite-main-content[has-book-nav]~devsite-footer-utility .devsite-footer-sites-item,devsite-footer-utility .devsite-footer-sites-item{margin:0}devsite-footer-utility devsite-language-selector{display:block;margin:16px 0 0}devsite-footer-utility .devsite-footer-utility-list{-webkit-flex-wrap:wrap;flex-wrap:wrap}devsite-footer-utility .devsite-footer-utility-button{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;margin:16px 0 0;padding:0}devsite-footer-utility .devsite-footer-utility-button>a{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto}}devsite-header{display:block;position:relative;z-index:1006}body[ready] devsite-header[fixed]{contain:layout;pointer-events:none;position:fixed;top:0;width:100%}devsite-header .devsite-top-logo-row-wrapper-wrapper{position:relative;z-index:1}body[ready] devsite-header[fixed] .devsite-top-logo-row-wrapper-wrapper:before{content:"";height:400px;position:absolute;-webkit-transform:translateY(-400px);-o-transform:translateY(-400px);transform:translateY(-400px);width:100%}devsite-header[fixed] .devsite-top-logo-row-wrapper-wrapper{pointer-events:all}devsite-header .devsite-collapsible-section{position:relative}devsite-header .devsite-collapsible-section,devsite-header[no-lower-row][fixed]{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}devsite-header[fixed] .devsite-collapsible-section{contain:style;pointer-events:all;-webkit-transform:translateZ(0);transform:translateZ(0);will-change:transform}devsite-header .devsite-top-logo-row{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;height:48px;padding:0 24px;position:relative}devsite-header .devsite-top-button{background:0;padding:0 8px;-webkit-transition:background .2s,color .2s,-webkit-box-shadow .2s;transition:background .2s,color .2s,-webkit-box-shadow .2s;-o-transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s,-webkit-box-shadow .2s}devsite-header .devsite-top-button,devsite-header .devsite-top-button:active,devsite-header .devsite-top-button:focus,devsite-header .devsite-top-button:hover{border:0}devsite-header .devsite-header-icon-button{display:none;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:24px;min-width:24px;padding:0;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;width:24px}devsite-header .devsite-top-logo-row-middle{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;position:relative}@media screen and (max-width:840px){devsite-header{-webkit-transform:translateZ(0);transform:translateZ(0)}devsite-header .devsite-top-logo-row{padding:0 16px}devsite-header .devsite-header-upper-tabs devsite-tabs{margin:0 0 0 16px}[dir=rtl] devsite-header .devsite-header-upper-tabs devsite-tabs{margin:0 16px 0 0}devsite-header .devsite-header-upper-tabs .devsite-doc-set-nav{display:none}}devsite-header .devsite-header-billboard{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:40px 24px 20px;position:relative;z-index:100}devsite-header .devsite-header-billboard h1{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;line-height:1;margin:14px 0;overflow:visible;padding:0}devsite-header .devsite-header-billboard-logo{max-height:64px}devsite-header .devsite-header-billboard-search{margin:0 auto;max-width:816px;padding-bottom:48px}devsite-header .devsite-header-billboard-search devsite-search{width:100%}devsite-header .devsite-header-billboard-search devsite-search .devsite-popout-result{max-height:50vh}@media screen and (max-width:840px){devsite-header .devsite-header-billboard-search{margin:0 24px}}devsite-header .devsite-doc-set-nav-row{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;min-height:48px;padding:0 24px 0 0}[dir=rtl] devsite-header .devsite-doc-set-nav-row{padding:0 0 0 24px}[dir=ltr] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-list{padding-left:24px}[dir=rtl] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-list{padding-right:24px}@media screen and (max-width:840px){devsite-header .devsite-doc-set-nav-row{display:none}}devsite-header devsite-language-selector{margin:0 0 0 16px}[dir=rtl] devsite-header devsite-language-selector{margin:0 16px 0 0}@media screen and (max-width:840px){devsite-header devsite-language-selector{margin:0 0 0 8px}[dir=rtl] devsite-header devsite-language-selector{margin:0 8px 0 0}}@media screen and (max-width:600px){devsite-header devsite-language-selector{display:none}}devsite-header .devsite-header-link{margin:0 -8px 0 16px;-webkit-transition:background .2s,color .2s,-webkit-box-shadow .2s;transition:background .2s,color .2s,-webkit-box-shadow .2s;-o-transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s,-webkit-box-shadow .2s}[dir=rtl] devsite-header .devsite-header-link{margin:0 16px 0 -8px}@media screen and (max-width:840px){devsite-header .devsite-header-link{display:none}}devsite-header .devsite-product-name-wrapper{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:36px;margin:6px 0}devsite-header .devsite-product-name-link,devsite-header .devsite-site-logo-link{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;opacity:1;-webkit-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}devsite-header .devsite-product-name-link:focus,devsite-header .devsite-product-name-link:hover,devsite-header .devsite-site-logo-link:focus{opacity:.7;text-decoration:none}devsite-header .devsite-site-logo{height:32px}devsite-header .devsite-has-google-wordmark>.devsite-breadcrumb-link,devsite-header .devsite-has-google-wordmark>.devsite-product-name{direction:ltr}devsite-header .devsite-google-wordmark{height:24px;margin:0 4px 0 0;position:relative;top:5px}devsite-header .devsite-google-wordmark-svg-path{-webkit-transition:fill .2s;-o-transition:fill .2s;transition:fill .2s}devsite-header .devsite-site-logo-link canvas{height:auto!important}devsite-header .devsite-product-logo-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;height:36px;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;width:36px}[dir=ltr] devsite-header .devsite-product-logo-container{margin-right:4px}[dir=rtl] devsite-header .devsite-product-logo-container{margin-left:4px}devsite-header .devsite-product-logo{font-size:32px;height:32px;max-width:32px;min-width:32px;overflow:hidden;white-space:nowrap}devsite-header .devsite-product-logo-container[background] .devsite-product-logo{font-size:28px;height:28px;max-width:28px;min-width:28px}devsite-header .devsite-product-name{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:0;margin:0;max-height:32px;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;white-space:nowrap}devsite-header .devsite-site-logo:not([src*=\.svg]){height:auto;max-height:32px}devsite-header .devsite-breadcrumb-link>.devsite-product-name{color:inherit}@media screen and (max-width:840px){devsite-header .devsite-product-name-wrapper{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;min-width:0}devsite-header .devsite-product-name-wrapper .devsite-breadcrumb-item:not(:first-of-type),devsite-header .devsite-product-name-wrapper .devsite-site-logo-link+.devsite-product-name{display:none}devsite-header .devsite-product-name-wrapper .devsite-breadcrumb-item,devsite-header .devsite-product-name-wrapper .devsite-breadcrumb-link,devsite-header .devsite-product-name-wrapper .devsite-breadcrumb-list,devsite-header .devsite-product-name-wrapper .devsite-product-name{width:100%}devsite-header .devsite-product-name-wrapper .devsite-breadcrumb-link{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}}devsite-header .devsite-product-id-row{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;min-height:56px;padding:20px 24px 2px}devsite-header .devsite-header-no-lower-tabs .devsite-product-id-row{min-height:72px;padding:20px 24px}devsite-header .devsite-product-description-row{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}devsite-header .devsite-breadcrumb-list+.devsite-product-description:not(:empty){margin-top:8px}devsite-header .devsite-product-description{font:16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0 180px 0 0}[dir=rtl] devsite-header .devsite-product-description{margin:0 0 0 180px}devsite-header .devsite-product-button-row{display:-webkit-box;display:-webkit-flex;display:flex;margin:0 0 0 24px;z-index:1}[dir=rtl] devsite-header .devsite-product-button-row{margin:0 24px 0 0}@media screen and (max-width:840px){devsite-header .devsite-product-id-row{min-height:72px;padding:20px 24px}[dir=rtl] devsite-header .devsite-product-description,devsite-header .devsite-product-description{margin:0}}@media screen and (max-width:600px){devsite-header .devsite-header-no-lower-tabs .devsite-product-id-row,devsite-header .devsite-product-id-row{-webkit-flex-wrap:wrap;flex-wrap:wrap;padding:20px 16px}devsite-header .devsite-product-button-row{-webkit-flex-basis:100%;flex-basis:100%;margin:16px 0 0}}devsite-header[search-expanded] .devsite-header-upper-tabs{display:none}devsite-header[search-expanded] devsite-search{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0}devsite-header [transition]{-webkit-transition:-webkit-transform .2s;transition:-webkit-transform .2s;-o-transition:-o-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s,-o-transform .2s}@media screen and (max-width:840px){devsite-header[search-active] .devsite-product-name-wrapper,devsite-header[search-active] devsite-language-selector,devsite-header[search-active] devsite-user{display:none}devsite-header[search-active] devsite-search{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;-webkit-transform:none!important;-o-transform:none!important;transform:none!important}devsite-header[search-active] .devsite-header-upper-tabs{-webkit-box-flex:0;-webkit-flex:0 1;flex:0 1;overflow:hidden}devsite-header[search-active] .devsite-top-logo-row devsite-search{margin:6px 0}devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-searchbox{width:100%}devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-searchbox .devsite-search-image{display:-webkit-box;display:-webkit-flex;display:flex}devsite-header .devsite-top-logo-row devsite-search .devsite-searchbox:before,devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-popout{left:-60px;width:-webkit-calc(100vw + 16px);width:calc(100vw + 16px)}[dir=rtl] devsite-header .devsite-top-logo-row devsite-search .devsite-searchbox:before,[dir=rtl] devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-popout{left:auto;right:-60px}devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-search-button{margin:0 0 0 16px}[dir=rtl] devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-search-button{margin:0 16px 0 0}devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-search-button[search-open]{display:none}devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-search-button[search-close]{display:-webkit-box;display:-webkit-flex;display:flex}devsite-header [transition]{-webkit-transition:none;-o-transition:none;transition:none}}devsite-header .devsite-search-background{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-sizing:content-box;box-sizing:content-box;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:stretch;-webkit-justify-content:stretch;justify-content:stretch;margin:0 0 0 24px;padding:6px 0;pointer-events:none;position:absolute;right:0;-webkit-transform-origin:right center;-o-transform-origin:right center;transform-origin:right center;-webkit-transition:-webkit-transform .2s;transition:-webkit-transform .2s;-o-transition:-o-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s,-o-transform .2s;will-change:transition;z-index:9}[dir=rtl] devsite-header .devsite-search-background{left:0;margin:0 24px 0 0;right:auto;-webkit-transform-origin:left center;-o-transform-origin:left center;transform-origin:left center}devsite-header .devsite-search-background:after{background:#f1f3f4;content:"";-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;height:100%;-webkit-transition:background .2s;-o-transition:background .2s;transition:background .2s}devsite-header[billboard] .devsite-search-background{display:none}devsite-header[billboard][bottom-row--hidden] .devsite-search-background{display:-webkit-box;display:-webkit-flex;display:flex}devsite-header[billboard] .devsite-top-logo-row devsite-search .devsite-search-form{opacity:1;-webkit-transition:opacity .2s,-webkit-transform .2s;transition:opacity .2s,-webkit-transform .2s;-o-transition:opacity .2s,-o-transform .2s;transition:opacity .2s,transform .2s;transition:opacity .2s,transform .2s,-webkit-transform .2s,-o-transform .2s}devsite-header[billboard][bottom-row--hidden] .devsite-top-logo-row devsite-search .devsite-search-form{-webkit-transform:translateZ(0);transform:translateZ(0)}body[type=error] devsite-header .devsite-top-logo-row .devsite-search-form,devsite-header[billboard]:not([bottom-row--hidden]) .devsite-top-logo-row devsite-search .devsite-search-form{opacity:0;-webkit-transform:translate3d(200px,0,0);transform:translate3d(200px,0,0)}devsite-header[billboard][bottom-row--hidden] .devsite-header-billboard-search devsite-search{opacity:0}devsite-header[billboard] .devsite-header-billboard-search devsite-search{margin-left:0}[dir=rtl] devsite-header[billboard] .devsite-header-billboard-search devsite-search{margin-right:0}devsite-header[billboard] .devsite-header-billboard-search devsite-search .devsite-popout{max-height:-webkit-calc(100vh - 255px);max-height:calc(100vh - 255px)}@media screen and (max-width:840px){devsite-header .devsite-top-logo-row devsite-search{width:auto}devsite-header .devsite-top-logo-row devsite-search .devsite-searchbox{width:0}devsite-header .devsite-top-logo-row devsite-search .devsite-searchbox .devsite-search-image{display:none}devsite-header .devsite-top-logo-row devsite-search .devsite-search-button{-webkit-box-align:center;-webkit-align-items:center;align-items:center;color:#5f6368;display:-webkit-box;display:-webkit-flex;display:flex;z-index:1}devsite-header .devsite-top-logo-row devsite-search .devsite-search-button[search-open]{display:-webkit-box;display:-webkit-flex;display:flex}devsite-header .devsite-top-logo-row devsite-search .devsite-search-button[search-close]{display:none}devsite-header .devsite-top-logo-row devsite-search .devsite-search-button[search-open]:before{content:"search"}devsite-header .devsite-top-logo-row devsite-search .devsite-search-button[search-close]:before{content:"cancel"}devsite-header .devsite-top-logo-row devsite-search .devsite-result-item a,devsite-header .devsite-top-logo-row devsite-search .devsite-result-label,devsite-header .devsite-top-logo-row devsite-search .devsite-suggest-footer,devsite-header .devsite-top-logo-row devsite-search .devsite-suggest-header{padding-left:60px;padding-right:8px}[dir=rtl] devsite-header .devsite-top-logo-row devsite-search .devsite-result-item a,[dir=rtl] devsite-header .devsite-top-logo-row devsite-search .devsite-result-label,[dir=rtl] devsite-header .devsite-top-logo-row devsite-search .devsite-suggest-footer,[dir=rtl] devsite-header .devsite-top-logo-row devsite-search .devsite-suggest-header{padding-left:8px;padding-right:60px}}devsite-header .devsite-header-upper-tabs{-webkit-box-flex:1;-webkit-flex:1 1 0;flex:1 1 0;margin:0 0 0 48px;position:relative;z-index:8}[dir=rtl] devsite-header .devsite-header-upper-tabs{margin:0 48px 0 0}devsite-header devsite-tabs tab a:focus,devsite-header devsite-tabs tab a:hover{text-decoration:none}@media screen and (max-width:840px){devsite-header .devsite-header-upper-tabs{margin-left:0}[dir=rtl] devsite-header .devsite-header-upper-tabs{margin-right:0}devsite-header devsite-tabs.lower-tabs,devsite-header devsite-tabs.upper-tabs{display:none}}devsite-language-selector>devsite-select .devsite-select-toggle{color:#3c4043;max-width:124px;padding:0 31px 0 15px}devsite-progress{pointer-events:none;-webkit-transform-origin:50% 0;-o-transform-origin:50% 0;transform-origin:50% 0;-webkit-transform:scaleY(0);-o-transform:scaleY(0);transform:scaleY(0);-webkit-transition:-webkit-transform .2s ease;transition:-webkit-transform .2s ease;-o-transition:-o-transform .2s ease;transition:transform .2s ease;transition:transform .2s ease,-webkit-transform .2s ease,-o-transform .2s ease}devsite-progress[type=indeterminate]{-webkit-transform:scaleY(1);-o-transform:scaleY(1);transform:scaleY(1)}devsite-progress .devsite-progress--indeterminate{position:relative;height:2px}devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-1,devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-2,devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-3,devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-4{background:#fff;bottom:0;left:0;position:absolute;right:0;top:0;-webkit-transform-origin:0 0;-o-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0)}devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-1{-webkit-animation:progress-indeterminate-1 2.5s linear infinite;-o-animation:progress-indeterminate-1 2.5s linear infinite;animation:progress-indeterminate-1 2.5s linear infinite;z-index:1}devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-2{-webkit-animation:progress-indeterminate-2 2.5s ease-in infinite;-o-animation:progress-indeterminate-2 2.5s ease-in infinite;animation:progress-indeterminate-2 2.5s ease-in infinite;z-index:2}devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-3{-webkit-animation:progress-indeterminate-3 2.5s ease-out infinite;-o-animation:progress-indeterminate-3 2.5s ease-out infinite;animation:progress-indeterminate-3 2.5s ease-out infinite;z-index:3}devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-4{-webkit-animation:progress-indeterminate-4 2.5s ease-out infinite;-o-animation:progress-indeterminate-4 2.5s ease-out infinite;animation:progress-indeterminate-4 2.5s ease-out infinite;z-index:4}@-webkit-keyframes progress-indeterminate-1{0%{-webkit-transform:scaleX(0);transform:scaleX(0)}50%,to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-o-keyframes progress-indeterminate-1{0%{-o-transform:scaleX(0);transform:scaleX(0)}50%,to{-o-transform:scaleX(1);transform:scaleX(1)}}@keyframes progress-indeterminate-1{0%{-webkit-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0)}50%,to{-webkit-transform:scaleX(1);-o-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes progress-indeterminate-2{0%,20%{-webkit-transform:scaleX(0);transform:scaleX(0)}70%,to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-o-keyframes progress-indeterminate-2{0%,20%{-o-transform:scaleX(0);transform:scaleX(0)}70%,to{-o-transform:scaleX(1);transform:scaleX(1)}}@keyframes progress-indeterminate-2{0%,20%{-webkit-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0)}70%,to{-webkit-transform:scaleX(1);-o-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes progress-indeterminate-3{0%,60%{-webkit-transform:scaleX(0);transform:scaleX(0)}90%,to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-o-keyframes progress-indeterminate-3{0%,60%{-o-transform:scaleX(0);transform:scaleX(0)}90%,to{-o-transform:scaleX(1);transform:scaleX(1)}}@keyframes progress-indeterminate-3{0%,60%{-webkit-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0)}90%,to{-webkit-transform:scaleX(1);-o-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes progress-indeterminate-4{0%,75%{-webkit-transform:scaleX(0);transform:scaleX(0)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-o-keyframes progress-indeterminate-4{0%,75%{-o-transform:scaleX(0);transform:scaleX(0)}to{-o-transform:scaleX(1);transform:scaleX(1)}}@keyframes progress-indeterminate-4{0%,75%{-webkit-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0)}to{-webkit-transform:scaleX(1);-o-transform:scaleX(1);transform:scaleX(1)}}devsite-search{-webkit-border-radius:2px;border-radius:2px;display:inline-block;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:36px;margin:6px 0 6px 24px;overflow:hidden;position:relative;text-align:left;-webkit-transform:translateZ(0);transform:translateZ(0);vertical-align:top;width:200px;will-change:transition;z-index:10}[dir=rtl] devsite-search{margin:6px 24px 6px 0;text-align:right}body[pending] devsite-search{visibility:hidden!important}devsite-search .devsite-search-image{color:#5f6368;left:8px;position:absolute;top:6px;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}[dir=rtl] devsite-search .devsite-search-image{left:auto;right:8px}devsite-search .devsite-search-image:before{content:"search"}devsite-search .devsite-search-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex}devsite-search .devsite-suggest-results-container{border-top:1px solid #dadce0}devsite-search input.devsite-search-field{background:none;border:0;color:#5f6368;height:36px;outline:0;padding:8px 8px 8px 40px;-webkit-transition:background .2s,color .2s;-o-transition:background .2s,color .2s;transition:background .2s,color .2s;width:100%}[dir=rtl] devsite-search input.devsite-search-field{padding:8px 40px 8px 8px}devsite-search input.devsite-search-field::-ms-input-placeholder{color:#5f6368;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-search input.devsite-search-field::placeholder{color:#5f6368;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-search input.devsite-search-field::-webkit-input-placeholder{color:#5f6368;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-search input.devsite-search-field::-moz-placeholder{color:#5f6368;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-search input.devsite-search-field:-ms-input-placeholder{color:#5f6368;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-search input.devsite-search-field:focus{border:0;padding-bottom:8px}devsite-search .devsite-searchbox{width:100%}devsite-search .devsite-searchbox:before{background:#fff;content:"";height:500px;left:-6px;opacity:0;pointer-events:none;position:absolute;top:-458px;-webkit-transition:opacity 1ms .2s;-o-transition:opacity 1ms .2s;transition:opacity 1ms .2s;width:-webkit-calc(100% + 12px);width:calc(100% + 12px);will-change:opacity;z-index:-1}[dir=rtl] devsite-search .devsite-searchbox:before{left:auto;right:-6px}devsite-search[search-active]{overflow:visible}devsite-search[search-active] .devsite-searchbox:before{opacity:1}devsite-search[search-active] .devsite-searchbox:hover{background:#f1f3f4}devsite-search[search-active] .devsite-search-field{color:#202124}devsite-search[search-active] .devsite-search-field::-ms-input-placeholder{color:#5f6368}devsite-search[search-active] .devsite-search-field::placeholder{color:#5f6368}devsite-search[search-active] .devsite-search-field::-webkit-input-placeholder{color:#5f6368}devsite-search[search-active] .devsite-search-field::-moz-placeholder{color:#5f6368}devsite-search[search-active] .devsite-search-field:-ms-input-placeholder{color:#5f6368}devsite-search[search-active] .devsite-search-image{color:#5f6368}devsite-search .devsite-popout{display:block;margin-top:6px;position:absolute;-webkit-transform:translateY(-100vh);-o-transform:translateY(-100vh);transform:translateY(-100vh);-webkit-transition:-webkit-transform .2s;transition:-webkit-transform .2s;-o-transition:-o-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s,-o-transform .2s;visibility:hidden;width:100%;z-index:-2}devsite-search[search-active] .devsite-popout{display:block;-webkit-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0);-webkit-transition-delay:.2s;-o-transition-delay:.2s;transition-delay:.2s;visibility:visible;will-change:transform}devsite-search .devsite-popout-result{max-height:-webkit-calc(100vh - 56px);max-height:calc(100vh - 56px);overflow-y:auto;background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}devsite-search .devsite-popout-result:empty,devsite-search[search-active][no-suggest] .devsite-popout{display:none}devsite-search .devsite-suggest-wrapper{padding:16px 0 0;font-size:14px}devsite-search .devsite-result-item,devsite-search .devsite-result-label{font:13px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0}devsite-search .devsite-result-label{padding-left:40px}[dir=rtl] devsite-search .devsite-result-label{padding-left:0;padding-right:40px}devsite-search .devsite-result-item a{color:#202124;display:block;outline:0;padding:8px;text-decoration:none;-webkit-transition:background .2s;-o-transition:background .2s;transition:background .2s;will-change:transition}[dir=ltr] devsite-search .devsite-result-item a{padding-left:40px}[dir=rtl] devsite-search .devsite-result-item a{padding-right:40px}devsite-search .devsite-result-item.highlight a,devsite-search .devsite-result-item a:focus,devsite-search .devsite-result-item a:hover{background:#f1f3f4}devsite-search .devsite-result-item b{font-weight:500}devsite-search .devsite-suggest-footer{border-top:1px solid #dadce0;margin:8px 0 0;padding:7px 0 8px 40px}[dir=rtl] devsite-search .devsite-suggest-footer{padding:7px 40px 8px 0}devsite-search .devsite-suggest-footer>.button{display:inline-block;margin:6px 0;max-width:-webkit-calc(100% - 16px);max-width:calc(100% - 16px)}[dir=ltr] devsite-search .devsite-suggest-footer>.button{margin-right:16px}[dir=rtl] devsite-search .devsite-suggest-footer>.button{margin-left:16px}devsite-search .devsite-suggest-footer>.button-white{max-width:100%}[dir=ltr] devsite-search .devsite-suggest-footer>.button-white{margin-left:-8px}[dir=rtl] devsite-search .devsite-suggest-footer>.button-white{margin-right:-8px}devsite-search .devsite-suggest-header{font:500 11px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.8px;margin:12px 0;padding-left:40px;text-transform:uppercase}[dir=rtl] devsite-search .devsite-suggest-header{padding-left:0;padding-right:40px}devsite-search hr+.devsite-suggest-header{margin-top:24px}devsite-search .devsite-suggest-header .devsite-suggest-project:before{content:"|";margin:0 8px}devsite-search hr{background:#ddd;margin:8px 0}devsite-search .devsite-suggestion-fragment+.devsite-suggestion-fragment:before{content:"|";margin:0 8px}devsite-search .devsite-search-disabled{padding-bottom:16px}devsite-search[compact]{width:auto}devsite-search[compact] input.devsite-search-field{width:0}devsite-search[compact] .devsite-search-image{left:-webkit-calc(50% - 12px);left:calc(50% - 12px);pointer-events:none}[dir=ltr] devsite-search[compact][search-active] .devsite-search-image{left:8px;right:auto}[dir=rtl] devsite-search[compact][search-active] .devsite-search-image{right:8px;left:auto}devsite-search[compact][search-active] input.devsite-search-field{width:100%}body[theme] devsite-search[compact] .devsite-search-field,body[theme] devsite-search[compact] .devsite-searchbox{background-color:transparent}@media screen and (max-width:840px){devsite-search input.devsite-search-field{padding-left:40px}[dir=rtl] devsite-search input.devsite-search-field{padding-left:0;padding-right:40px}.devsite-search-background,.devsite-search-background:after,[search-active] .devsite-search-background:after,devsite-search .devsite-search-field,devsite-search .devsite-search-field:hover{-webkit-transition:none;-o-transition:none;transition:none}devsite-search .devsite-search-image{left:8px}[dir=rtl] devsite-search .devsite-search-image{left:auto;right:8px}devsite-header devsite-search{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin-left:8px;overflow:visible}[dir=rtl] devsite-header devsite-search{margin-left:0;margin-right:8px}devsite-header devsite-search .devsite-search-form{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}devsite-header .devsite-top-logo-row devsite-search:not([search-active]) input.devsite-search-field{padding:0}}devsite-select{display:inline-block;position:relative}devsite-select+devsite-select{margin:0 0 0 16px}devsite-select select{display:none!important;pointer-events:none!important;position:absolute;z-index:-1}devsite-select .devsite-select{position:relative}devsite-select .devsite-select-toggle{border:1px solid #e8eaed;-webkit-border-radius:2px;border-radius:2px;padding:0 27px 0 7px;-moz-appearance:none;-webkit-appearance:none;background:#fff url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='4' viewBox='0 0 20 4'><path d='M0,0l4,4l4-4H0z' fill='%23212121'/></svg>") no-repeat 100%;-webkit-box-shadow:none;box-shadow:none;color:#202124;cursor:pointer;display:inline-block;font:500 14px/36px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:36px;line-height:34px;max-width:256px;min-width:72px;outline:0;overflow:hidden;text-align:left;text-indent:.01px;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:background-color .2s;-o-transition:background-color .2s;transition:background-color .2s;vertical-align:middle;white-space:nowrap}devsite-select .devsite-select-toggle:focus,devsite-select .devsite-select-toggle:hover{background-color:#f1f3f4}devsite-select .devsite-select-toggle:active{background-color:#e8eaed}devsite-select .devsite-select-toggle:disabled{background:#f1f3f4 url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='4' viewBox='0 0 20 4'><path d='M0,0l4,4l4-4H0z' fill='%23bdbdbd'/></svg>") no-repeat 100%;border-color:transparent;color:#bdc1c6;cursor:default}devsite-select .devsite-select-list{background:#fff;border:1px solid #e8eaed;-webkit-border-radius:2px;border-radius:2px;display:none;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);font:400 14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;max-height:304px;opacity:0;outline:0;overflow-y:auto;padding:8px 0;pointer-events:none;position:absolute;-webkit-transition:opacity .2s,visibility .2s;-o-transition:opacity .2s,visibility .2s;transition:opacity .2s,visibility .2s;z-index:1015}devsite-select[menu--open] .devsite-select-list{display:block;pointer-events:auto}devsite-select[menu--show] .devsite-select-list{opacity:1}devsite-select[menu-position=above] .devsite-select-list{bottom:36px}devsite-select[menu-position=below] .devsite-select-list{top:36px}devsite-select .devsite-select-item{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin:0;min-height:48px;min-width:100%;padding:8px 16px;white-space:nowrap}devsite-select .devsite-select-item.devsite-focused,devsite-select .devsite-select-item:focus,devsite-select .devsite-select-item:hover{background-color:#f1f3f4;cursor:pointer}devsite-select .devsite-select-item[data-selected]{background-color:#f1f3f4;font-weight:500}devsite-select.devsite-select--multiple .devsite-select-item{padding-left:48px;position:relative}devsite-select.devsite-select--multiple .devsite-select-item:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;color:#80868b;content:"check_box_outline_blank";display:block;font-size:24px;left:16px;position:absolute;top:50%;-webkit-transform:translateY(-50%);-o-transform:translateY(-50%);transform:translateY(-50%)}devsite-select.devsite-select--multiple .devsite-select-item[data-selected]:before{color:#1976d2;content:"check_box"}@media screen and (max-width:600px){devsite-select{display:block}devsite-select+devsite-select{margin:16px 0 0}}devsite-sitemask{background:rgba(0,0,0,.4);bottom:-200px;cursor:pointer;left:-200px;opacity:0;pointer-events:none;position:fixed;right:-200px;top:-200px;-webkit-transition:opacity .2s cubic-bezier(.4,0,.2,1),visibility .2s linear;-o-transition:opacity .2s cubic-bezier(.4,0,.2,1),visibility .2s linear;transition:opacity .2s cubic-bezier(.4,0,.2,1),visibility .2s linear;visibility:hidden;z-index:1012;-webkit-tap-highlight-color:transparent}devsite-sitemask[visible]{opacity:1;pointer-events:auto;-webkit-transition:opacity .2s ease;-o-transition:opacity .2s ease;transition:opacity .2s ease;visibility:visible}devsite-tabs{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;height:48px;max-width:-webkit-calc(100% - 208px);max-width:calc(100% - 208px);position:relative}devsite-tabs[connected]{max-width:none}devsite-tabs .devsite-tabs-wrapper{bottom:0;display:-webkit-box;display:-webkit-flex;display:flex;left:0;overflow:hidden;position:absolute;right:0;top:0}devsite-tabs[no-overflow] .devsite-tabs-wrapper{overflow:auto}devsite-tabs[dropdown--open] .devsite-tabs-wrapper,devsite-tabs[overflow-menu--open] .devsite-tabs-wrapper{overflow:visible}devsite-tabs tab{-webkit-flex-shrink:0;flex-shrink:0;position:relative}devsite-tabs tab,devsite-tabs tab>a{display:-webkit-box;display:-webkit-flex;display:flex}devsite-tabs tab>a{-webkit-box-align:center;-webkit-align-items:center;align-items:center;font:500 14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0;padding:0 24px;text-transform:uppercase;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;white-space:nowrap}devsite-tabs tab>a,devsite-tabs tab>a:focus,devsite-tabs tab>a:hover{text-decoration:none}devsite-tabs.upper-tabs tab a{font-weight:400;text-transform:none}devsite-tabs.upper-tabs tab[active]>a{font-weight:500}devsite-tabs tab[active] a:after,devsite-tabs tab a:focus:after,devsite-tabs tab a:hover:after{bottom:0;content:"";display:block;height:2px;left:0;position:absolute;right:0}devsite-tabs tab[dropdown]>a{padding:0 0 0 24px;position:relative;z-index:2}[dir=rtl] devsite-tabs tab[dropdown]>a{padding:0 24px 0 0}devsite-tabs tab[dropdown] .devsite-tabs-dropdown-toggle{-webkit-box-align:center;-webkit-align-items:center;align-items:center;cursor:pointer;display:-webkit-box;display:-webkit-flex;display:flex}[dir=rtl] devsite-tabs tab[dropdown] .devsite-tabs-dropdown-toggle,devsite-tabs tab[dropdown] .devsite-tabs-dropdown-toggle{padding:0}devsite-tabs.upper-tabs .devsite-icon-arrow-drop-down:before,devsite-tabs tab[dropdown] .devsite-tabs-dropdown-toggle:before{-webkit-transform:rotate(0deg);-o-transform:rotate(0deg);transform:rotate(0deg);-webkit-transition:color .2s,-webkit-transform .2s;transition:color .2s,-webkit-transform .2s;-o-transition:color .2s,-o-transform .2s;transition:color .2s,transform .2s;transition:color .2s,transform .2s,-webkit-transform .2s,-o-transform .2s}devsite-tabs.upper-tabs[overflow-menu--open] tab:hover .devsite-icon-arrow-drop-down:before,devsite-tabs tab[dropdown--open] .devsite-tabs-dropdown-toggle:before{-webkit-transform:rotate(-180deg);-o-transform:rotate(-180deg);transform:rotate(-180deg)}devsite-tabs tab[overflow-tab]{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;position:relative}devsite-tabs tab[overflow-tab][collapsed]{-webkit-box-flex:0;-webkit-flex-grow:0;flex-grow:0}devsite-tabs.upper-tabs tab[overflow-tab]:after{content:"";height:48px;position:absolute;z-index:-1}[dir=ltr] devsite-tabs.upper-tabs tab[overflow-tab]:after{left:-6px;right:-100%}[dir=rtl] devsite-tabs.upper-tabs tab[overflow-tab]:after{left:-100%;right:-6px}devsite-tabs tab[overflow-tab] tab>a{padding:0 24px}devsite-tabs tab[overflow-tab] a{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-webkit-flex-direction:row-reverse;flex-direction:row-reverse;position:relative}devsite-tabs tab[overflow-tab] tab .devsite-tabs-dropdown,devsite-tabs tab[overflow-tab] tab .devsite-tabs-dropdown-toggle{display:none}devsite-tabs tab[overflow-tab] .devsite-tabs-overflow-menu{background:#fff;-webkit-border-radius:2px;border-radius:2px;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;max-height:320px;overflow-y:auto;padding:16px 0;position:absolute;top:-16px;z-index:1005}devsite-tabs.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu{-webkit-border-radius:0 0 2px 2px;border-radius:0 0 2px 2px;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15),inset 0 4px 6px -4px rgba(154,160,166,.5);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15),inset 0 4px 6px -4px rgba(154,160,166,.5);top:48px;-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-transition:-webkit-transform .2s;transition:-webkit-transform .2s;-o-transition:-o-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s,-o-transform .2s;z-index:-1}devsite-tabs.upper-tabs .devsite-tabs-overflow-menu .devsite-tabs-dropdown-toggle{display:none!important}devsite-tabs.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu[hidden]{display:block!important;pointer-events:none;-webkit-transform:translate3d(0,-150%,0);transform:translate3d(0,-150%,0)}body devsite-tabs tab[overflow-tab] .devsite-tabs-overflow-menu tab a{background:#fff;color:#5f6368;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;height:48px}body devsite-tabs tab[overflow-tab] .devsite-tabs-overflow-menu tab a:focus,body devsite-tabs tab[overflow-tab] .devsite-tabs-overflow-menu tab a:hover{background:#f1f3f4;color:#202124}devsite-tabs .devsite-tabs-dropdown{display:block;font-size:13px;left:-6px;min-width:-webkit-calc(100% + 12px);min-width:calc(100% + 12px);outline:0;overflow:hidden;padding:0 6px 6px;pointer-events:none;position:absolute;top:100%;z-index:-1}[dir=rtl] devsite-tabs .devsite-tabs-dropdown{right:-6px;left:auto}devsite-tabs [dropdown-full] .devsite-tabs-dropdown{left:0;padding:0 0 6px;width:100vw}devsite-tabs .devsite-tabs-dropdown-content{background-color:#fff;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15),inset 0 4px 6px -4px rgba(154,160,166,.5);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15),inset 0 4px 6px -4px rgba(154,160,166,.5);overflow:auto;max-height:600px;max-width:100vw;padding:0 12px;pointer-events:none;-webkit-transform:translate3d(0,-150%,0);transform:translate3d(0,-150%,0);-webkit-transition:-webkit-transform 0s;transition:-webkit-transform 0s;-o-transition:-o-transform 0s;transition:transform 0s;transition:transform 0s,-webkit-transform 0s,-o-transform 0s;white-space:nowrap}devsite-tabs .devsite-tabs-dropdown[dropdown-transition] .devsite-tabs-dropdown-content{-webkit-transition:-webkit-transform .5s;transition:-webkit-transform .5s;-o-transition:-o-transform .5s;transition:transform .5s;transition:transform .5s,-webkit-transform .5s,-o-transform .5s}devsite-tabs tab[dropdown--open] .devsite-tabs-dropdown-content{pointer-events:all;-webkit-transform:translateZ(0);transform:translateZ(0)}devsite-tabs [dropdown-full] .devsite-tabs-dropdown-column{-webkit-box-flex:1;-webkit-flex:1;flex:1;min-width:0}devsite-tabs .devsite-tabs-dropdown-section{list-style:none;padding:0 12px}devsite-tabs .devsite-tabs-dropdown-section:first-child{margin-top:18px}devsite-tabs .devsite-tabs-dropdown-section:not(:first-child){margin-top:54px}devsite-tabs tab[dropdown] .devsite-nav-item,devsite-tabs tab[dropdown] .devsite-nav-title{line-height:18px;margin:0 0 18px}devsite-tabs tab[dropdown] .devsite-nav-title{color:#5f6368;font-weight:700;padding:0}devsite-tabs [dropdown-full] .devsite-nav-item>a,devsite-tabs [dropdown-full] .devsite-nav-title{display:block;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;white-space:normal}devsite-tabs tab[dropdown] .devsite-nav-item-description{color:#5f6368;white-space:normal}body[theme] devsite-tabs .devsite-tabs-dropdown a,body[theme] devsite-tabs .devsite-tabs-dropdown a:visited{color:#202124;display:block;font-weight:400}body[theme] devsite-tabs .devsite-tabs-dropdown a:focus,body[theme] devsite-tabs .devsite-tabs-dropdown a:hover{color:#1a73e8}devsite-tabs[render-hidden]{width:100%}devsite-tabs[render-hidden] tab[overflow-tab],devsite-tabs tab[overflow-tab][render-hidden]{-webkit-box-flex:0;-webkit-flex:none;flex:none}devsite-tabs tab[dropdown] .devsite-tabs-close-button{color:#202124;cursor:pointer;position:absolute;right:24px;top:24px;visibility:hidden;z-index:1}devsite-tabs tab[dropdown] .devsite-tabs-close-button:focus,devsite-tabs tab[dropdown] .devsite-tabs-close-button:hover{color:#1a73e8}devsite-tabs tab[dropdown--open] .devsite-tabs-close-button{visibility:visible}devsite-toc.devsite-toc{float:right;width:160px}[dir=rtl] devsite-toc.devsite-toc{float:left}devsite-toc>.devsite-nav-list{border-left:4px solid #5f6368;width:160px}[dir=rtl] devsite-toc>.devsite-nav-list{border-left:0;border-right:4px solid #5f6368}devsite-toc[fixed]>.devsite-nav-list{contain:content;overflow-x:hidden;overflow-y:auto;position:fixed;-webkit-transform:translateZ(0);transform:translateZ(0);will-change:max-height,transform}[dir=ltr] devsite-toc[fixed]>.devsite-nav-list{padding-right:8px}[dir=rtl] devsite-toc[fixed]>.devsite-nav-list{padding-left:8px}devsite-toc>.devsite-nav-list>:first-child>.devsite-nav-title{padding-top:0}devsite-toc>.devsite-nav-list>:last-child>.devsite-nav-list>:last-child>.devsite-nav-title:last-child,devsite-toc>.devsite-nav-list>:last-child>.devsite-nav-title:only-child{padding-bottom:0}devsite-toc.devsite-toc-embedded{display:none}devsite-toc.devsite-toc-embedded>.devsite-nav-list{width:auto}devsite-toc .devsite-nav-list{padding:0 0 0 12px}[dir=rtl] devsite-toc .devsite-nav-list{padding:0 12px 0 0}devsite-toc .devsite-nav-more-items,devsite-toc .devsite-nav-show-all{display:none}devsite-toc[expandable] .devsite-nav-more-items,devsite-toc[expandable] .devsite-nav-show-all{color:#5f6368;display:block;height:24px;padding:0}devsite-toc .devsite-nav-show-all{margin:-4px 0 0 4px;min-width:20px}devsite-toc .devsite-nav-show-all:before{content:"expand_more"}devsite-toc .devsite-nav-more-items{margin-bottom:-8px;min-width:0}devsite-toc .devsite-nav-more-items:before{content:"more_horiz"}devsite-toc[expanded] .devsite-nav-more-items:before,devsite-toc[expanded] .devsite-nav-show-all:before{content:"expand_less"}devsite-toc .devsite-toc-toggle{display:-webkit-box;display:-webkit-flex;display:flex;margin:0}devsite-toc .devsite-show-apix{margin-top:12px}@media screen and (max-width:1252px){devsite-toc.devsite-toc,devsite-toc[visible].devsite-toc{display:none}devsite-toc.devsite-toc-embedded:not(:empty){display:block;margin:20px 0 24px}body[type=landing] devsite-toc.devsite-toc-embedded:not(:empty){margin:20px 40px 24px}}@media screen and (max-width:840px){body[type=landing] devsite-toc.devsite-toc-embedded:not(:empty){margin:20px 24px 24px}}@media screen and (max-width:600px){body[type=landing] devsite-toc.devsite-toc-embedded:not(:empty){margin:20px 16px 24px}}@charset "UTF-8";devsite-user{display:block;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto}devsite-user:not(:empty){margin:0 -8px 0 0;min-width:60px}[dir=rtl] devsite-user:not(:empty){margin:0 0 0 -8px}devsite-user #devsite-signin-btn{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;font:500 14px/36px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}devsite-user devsite-spinner{margin:4px 8px 4px 20px}devsite-user devsite-spinner.hide{opacity:0;-webkit-transition:opacity .45s ease;-o-transition:opacity .45s ease;transition:opacity .45s ease;-webkit-animation-delay:.45s;-o-animation-delay:.45s;animation-delay:.45s}devsite-user .ogb-wrapper{display:-webkit-box;display:-webkit-flex;display:flex;opacity:1;-webkit-transition:opacity .2s cubic-bezier(.4,0,.2,1);-o-transition:opacity .2s cubic-bezier(.4,0,.2,1);transition:opacity .2s cubic-bezier(.4,0,.2,1)}devsite-user .ogb-pending{opacity:0}devsite-user .ogb-si{margin:0 0 0 16px}[dir=rtl] devsite-user .ogb-si{margin:0 16px 0 0}devsite-user .ogb-so{margin:0 0 0 12px}[dir=rtl] devsite-user .ogb-so{margin:0 12px 0 0}devsite-user .gb_Sb>.gb_Rb{-webkit-box-sizing:content-box;box-sizing:content-box}devsite-user button.devsite-user-change-account,devsite-user button.devsite-user-signout{height:auto;color:#3c4043}devsite-user button.devsite-user-change-account .material-icons,devsite-user button.devsite-user-signout .material-icons{margin:0;height:auto;width:auto;top:auto}devsite-user button.devsite-user-change-account{border:0}@media (-o-min-device-pixel-ratio:5/4),(-webkit-min-device-pixel-ratio:1.25),(min-resolution:1.25dppx),not all{[dir=rtl] devsite-user .gb_xa:before{-webkit-transform-origin:right 0;-o-transform-origin:right 0;transform-origin:right 0}}devsite-user .devsite-user-dialog{display:none}devsite-user .devsite-user-dialog a:link,devsite-user .devsite-user-dialog a:visited{text-decoration:none}devsite-user[dialog--open] .devsite-user-dialog{background:#fff;border:1px solid rgba(0,0,0,.2);-webkit-border-radius:8px;border-radius:8px;-webkit-box-shadow:0 2px 10px rgba(0,0,0,.2);box-shadow:0 2px 10px rgba(0,0,0,.2);color:#000;display:block;max-height:-webkit-calc(100vh - 86px);max-height:calc(100vh - 86px);outline:none;overflow:auto;position:absolute;right:24px;top:62px;width:354px}[dir=rtl] devsite-user[dialog--open] .devsite-user-dialog{left:24px;right:auto}devsite-user .devsite-user-dialog-photo,devsite-user .devsite-user-dialog-toggle,devsite-user .devsite-user-dialog-toggle .devsite-user-dialog-letter{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin:0;overflow:hidden;padding:0}devsite-user .devsite-user-dialog-toggle{border:4px solid transparent;-webkit-box-sizing:content-box;box-sizing:content-box;height:32px;margin:0 4px 0 0;overflow:hidden;text-decoration:none;width:32px}[dir=rtl] devsite-user .devsite-user-dialog-toggle{margin:0 0 0 4px}devsite-user .devsite-user-dialog-toggle:focus{border-color:rgba(0,0,0,.2)!important}devsite-user .devsite-user-dialog-photo-thumbnail{height:32px;width:32px}devsite-user[js-signin] button{-webkit-box-shadow:none;box-shadow:none}devsite-user[js-signin] .devsite-user-dialog-toggle{opacity:0;-webkit-transition:opacity .45s ease;-o-transition:opacity .45s ease;transition:opacity .45s ease}devsite-user[js-signin] .devsite-user-dialog-toggle.show{opacity:1}devsite-user .devsite-user-dialog-toggle .devsite-user-dialog-letter{-webkit-box-flex:0;-webkit-flex:0 0 32px;flex:0 0 32px;font-size:17px;height:32px}devsite-user .devsite-user-dialog-learn-more{background-color:#e8f0fe;-webkit-border-radius:4px;border-radius:4px;color:#5f6368;font:12px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:4px 4px 0;padding:4px 29px;text-align:center}devsite-user .devsite-user-dialog-learn-more a,devsite-user .devsite-user-dialog-learn-more span{font-weight:500}devsite-user .devsite-user-dialog-learn-more a{color:#1a73e8}devsite-user .devsite-user-dialog-learn-more a:focus,devsite-user .devsite-user-dialog-learn-more a:hover{text-decoration:underline}devsite-user .devsite-user-dialog-user{padding:20px 33px 23px;text-align:center}devsite-user .devsite-user-dialog-photo{margin:0 auto 16px;position:relative;left:-2px}devsite-user .devsite-user-dialog-photo,devsite-user .devsite-user-dialog-photo-portrait{height:80px;width:80px}devsite-user .devsite-user-dialog-letter{text-transform:uppercase}devsite-user .devsite-user-dialog-photo .devsite-user-dialog-letter{font-size:52px}devsite-user .devsite-user-dialog-email,devsite-user .devsite-user-dialog-name{-o-text-overflow:ellipsis;text-overflow:ellipsis;overflow:hidden}devsite-user .devsite-user-dialog-name{color:#202124;font:500 16px/22px Google Sans,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.29px}devsite-user .devsite-user-dialog-email{color:#5f6368;font:400 14px/19px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}devsite-user .devsite-user-manage{-webkit-box-align:center;-webkit-align-items:center;align-items:center;background:0;border:1px solid #dadce0;-webkit-border-radius:17px;border-radius:17px;display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;font:500 14px/20px Google Sans,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:34px;letter-spacing:.25px;margin:16px 0 0;padding:0 16px;white-space:nowrap}devsite-user .devsite-user-manage:link,devsite-user .devsite-user-manage:visited{color:#3c4043}devsite-user .devsite-user-manage:focus,devsite-user .devsite-user-manage:hover{background-color:#f8f9fa}devsite-user .devsite-user-manage:active{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);background-color:#e8eaed;border-color:transparent}devsite-user .devsite-user-dialog-buttons{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}devsite-user .devsite-user-dialog .developer-profile:link,devsite-user .devsite-user-dialog .developer-profile:visited,devsite-user .devsite-user-dialog .devsite-user-developer-profile:link,devsite-user .devsite-user-dialog .devsite-user-developer-profile:visited,devsite-user .devsite-user-dialog .devsite-user-signin:link,devsite-user .devsite-user-dialog .devsite-user-signin:visited,devsite-user .devsite-user-signout:link,devsite-user .devsite-user-signout:visited{color:#3c4043}devsite-user .devsite-user-dialog .developer-profile:focus,devsite-user .devsite-user-dialog .developer-profile:hover,devsite-user .devsite-user-dialog .devsite-user-developer-profile:focus,devsite-user .devsite-user-dialog .devsite-user-developer-profile:hover,devsite-user .devsite-user-dialog .devsite-user-signin:focus,devsite-user .devsite-user-dialog .devsite-user-signin:hover,devsite-user .devsite-user-signout:focus,devsite-user .devsite-user-signout:hover{background-color:#f8f9fa}devsite-user .devsite-user-dialog .developer-profile:active,devsite-user .devsite-user-dialog .devsite-user-developer-profile:active,devsite-user .devsite-user-dialog .devsite-user-signin:active,devsite-user .devsite-user-signout:active{background-color:#e8eaed}devsite-user .devsite-user-dialog .devsite-user-developer-profile,devsite-user .devsite-user-dialog .devsite-user-signin{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;font:500 14px/16px Google Sans,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.25px;padding:15px 39px 16px;width:100%}devsite-user .devsite-user-dialog-buttons>:first-child{border-top:1px solid #e8eaed}devsite-user .devsite-user-dialog .devsite-user-signin{border-bottom:1px solid #e8eaed}devsite-user .devsite-user-dialog .new-notification{background:#1967d2;-webkit-border-radius:10px;border-radius:10px;color:#fff;font-weight:700;font-size:12px;letter-spacing:.3px;padding:2px 8px}[dir=ltr] devsite-user .devsite-user-dialog .new-notification{margin-left:12px}[dir=ltr] devsite-user .devsite-user-signin .devsite-switch-account-icon,[dir=rtl] devsite-user .devsite-user-dialog .new-notification{margin-right:12px}[dir=rtl] devsite-user .devsite-user-signin .devsite-switch-account-icon{margin-left:12px}devsite-user .devsite-user-developer-profile .google-dev-icon{width:28px;position:relative}[dir=ltr] devsite-user .devsite-user-developer-profile .google-dev-icon{margin-left:-4px;margin-right:8px}[dir=rtl] devsite-user .devsite-user-developer-profile .google-dev-icon{margin-left:8px;margin-right:-4px}devsite-user .devsite-user-signout{border:1px solid #dadce0;-webkit-border-radius:4px;border-radius:4px;display:inline-block;font:500 14px/16px Google Sans,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.15px;margin:16px auto;padding:10px 24px}devsite-user .devsite-user-signout:active{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);background-color:#e8eaed;border-color:transparent}devsite-user .devsite-user-dialog-footer{border-top:1px solid #e8eaed;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:14px 20px}devsite-user .devsite-user-dialog-footer-link{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;margin:0}devsite-user .devsite-user-dialog-footer-link:not(:first-child):before{color:#5f6368;content:"•";font-size:13px}devsite-user .devsite-user-dialog-footer-link>a{-webkit-border-radius:4px;border-radius:4px;display:inline-block;font:400 12px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:4px 8px}devsite-user .devsite-user-dialog-footer-link>a:link,devsite-user .devsite-user-dialog-footer-link>a:visited{color:#5f6368}devsite-user .devsite-user-dialog-footer-link>a:focus,devsite-user .devsite-user-dialog-footer-link>a:hover{background-color:#f8f9fa}devsite-user .devsite-user-dialog-footer-link>a:active{background-color:#e8eaed}@media screen and (max-width:840px){devsite-user .ogb-si{margin:0 0 0 4px}[dir=rtl] devsite-user .ogb-si{margin:0 4px 0 0}devsite-user[dialog--open] .devsite-user-dialog{right:16px}[dir=rtl] devsite-user[dialog--open] .devsite-user-dialog{left:16px;right:auto}}devsite-content-footer{clear:both;color:rgba(0,0,0,.65);display:block;font:13px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}google-codelab-step{line-height:24px;display:block}google-codelab-step:focus{outline:none}google-codelab-step code,google-codelab-step pre{font-family:Source Code Pro,Helvetica,Arial;font-size:inherit;-webkit-border-radius:4px;border-radius:4px;overflow-x:auto;overflow-y:visible}google-codelab-step code{background-color:#e8eaed;padding:.1em .3em}google-codelab-step pre{display:block;color:#fff;background-color:#28323f;padding:14px;-webkit-text-size-adjust:none;line-height:1.4}google-codelab-step pre>code{padding:0;background-color:transparent}google-codelab-step code em{color:#97c8f2}google-codelab-step code .str,google-codelab-step pre .str{color:#34a853}google-codelab-step code .kwd,google-codelab-step pre .kwd{color:#f538a0}google-codelab-step code .com,google-codelab-step pre .com{color:#bdc1c6;font-style:italic}google-codelab-step code .typ,google-codelab-step pre .typ{color:#24c1e0}google-codelab-step code .lit,google-codelab-step pre .lit{color:#4285f4}google-codelab-step code .pln,google-codelab-step code .pun,google-codelab-step pre .pln,google-codelab-step pre .pun{color:#f8f9fa}google-codelab-step code .tag,google-codelab-step pre .tag{color:#24c1e0}google-codelab-step code .atn,google-codelab-step pre .atn{color:#eda912}google-codelab-step code .atv,google-codelab-step pre .atv{color:#34a853}google-codelab-step code .dec,google-codelab-step pre .dec{color:#5195ea}google-codelab-step paper-button{display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;min-width:5.14em;margin:0 .29em;background:transparent;-webkit-tap-highlight-color:transparent;font:inherit;text-transform:uppercase;outline-width:0;-webkit-border-radius:3px;border-radius:3px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:pointer;z-index:0;padding:.7em .57em;font-family:Roboto,Noto,sans-serif;-webkit-font-smoothing:antialiased;-webkit-box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2)}google-codelab-step h2.step-title{font-family:Google Sans,Arial,sans-serif!important;font-size:28px!important;font-weight:400!important;line-height:1em!important;margin:0 0 30px!important}google-codelab:not([theme=minimal]) google-codelab-step .instructions{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);background:#fff;max-width:800px;font-size:14px;margin:0 auto 90px;-webkit-border-radius:4px;border-radius:4px}google-codelab-step .instructions .inner{padding:24px}google-codelab[theme=minimal] google-codelab-step .instructions .inner{padding:0 24px}@media (max-width:800px){google-codelab .instructions{margin:0 0 16px}}google-codelab:not([theme=minimal]) google-codelab-step .instructions a,google-codelab:not([theme=minimal]) google-codelab-step .instructions a:visited{color:#1a73e8}google-codelab:not([theme=minimal]) google-codelab-step .instructions h2,google-codelab:not([theme=minimal]) google-codelab-step .instructions h3,google-codelab:not([theme=minimal]) google-codelab-step .instructions h4{font-weight:400;margin:0}google-codelab:not([theme=minimal]) google-codelab-step .instructions h2{font-weight:300;line-height:1em;font-size:22px}google-codelab:not([theme=minimal]) google-codelab-step .instructions{line-height:24px}google-codelab:not([theme=minimal]) google-codelab-step .instructions li{margin:.5em 0}google-codelab:not([theme=minimal]) google-codelab-step .instructions h2{font-weight:500;margin:20px 0 0;font-size:20px}google-codelab:not([theme=minimal]) google-codelab-step .instructions h3{font-weight:500;margin:20px 0 0}google-codelab:not([theme=minimal]) google-codelab-step .instructions aside{padding:.5em 1em;margin:2em 0;border-left:4px solid;-webkit-border-radius:4px;border-radius:4px}google-codelab:not([theme=minimal]) google-codelab-step .instructions aside p{margin:.5em 0}google-codelab:not([theme=minimal]) google-codelab-step .instructions aside.note,google-codelab:not([theme=minimal]) google-codelab-step .instructions aside.notice{border-color:#ea8600;background:#fef7e0;color:#212124}google-codelab:not([theme=minimal]) google-codelab-step .instructions aside.special,google-codelab:not([theme=minimal]) google-codelab-step .instructions aside.tip{border-color:#137333;background:#e6f4ea;color:#212124}google-codelab:not([theme=minimal]) google-codelab-step .instructions aside.warning{border-color:#ea8600;background:#fef7e0;color:#212124}google-codelab-step .instructions aside.callout{background-color:#e8f0fe;margin:20px 0;padding:15px;border-left:3px solid #185abc;-webkit-border-radius:4px;border-radius:4px;color:#212124;font-size:14px;line-height:1.5}google-codelab-step aside.callout b{color:#185abc}google-codelab-step .instructions ul.checklist{list-style:none;padding:0 0 0 1em}google-codelab-step .instructions ::content ul.checklist li,google-codelab-step .instructions ul.checklist li{padding-left:24px;-o-background-size:20px;background-size:20px;background-repeat:no-repeat;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAWlBMVEUAAAAxokwwoks1pFAxokwxokwxokwxokwxokwnnkQnnkQnnkRou3y84cTS69cxokwonkQxokwnnkRqvH1VsmtluXlVsmsnnkRdtnLw+PIxokwqn0YinEAfmj3goh/UAAAAGnRSTlMA2CcEo+6AQT7+2IOBJxPl27alhoBnX15SCCe258UAAAB+SURBVEjH7dA5EoAgEERR3BcQ923Q+1/T0SqKlNbMouP3gxkRFvZpyQb64VSQT4mOcYc8mU5DnqIG8zXoozj4d34tML+YrET8XBFx4e2F4oAL4N7J3EUB/EfSUwD/zG3hvFdROu9XtL31vgXguQA9F6DnAvM8WbOHpkXYD3cBBCcPjtASYjwAAAAASUVORK5CYII=")}google-codelab-step .instructions h2 code,google-codelab-step .instructions table code{background:#fff}google-codelab-step .instructions .indented{margin-left:40px}google-codelab-step .instructions strong{font-weight:600}google-codelab-step .instructions :link paper-button{text-decoration:none!important}google-codelab-step .instructions paper-button{display:inline-block;-webkit-border-radius:4px;border-radius:4px;color:#fff;font-family:Google Sans,Arial,sans-serif;font-size:14px;font-weight:600;letter-spacing:.6px;padding:6px 16px 6px 12px;text-transform:none}google-codelab-step .instructions paper-button a{text-decoration:none;color:inherit!important}google-codelab-step a paper-button{display:inline-block}google-codelab-step .instructions paper-button.colored{background-color:#1e8e3e}google-codelab-step .instructions paper-button.red{background-color:#d93025}google-codelab-step .instructions iron-icon{vertical-align:sub;margin-right:7px;margin-left:3px;font-size:16px;top:-1px;position:relative}google-codelab-step .instructions img{max-width:100%;vertical-align:bottom}google-codelab-step .instructions .image-container{text-align:center}google-codelab-step .instructions table{border-spacing:0}google-codelab-step .instructions td{vertical-align:top;border-bottom:1px solid #ccc;padding:8px}google-codelab-step .instructions table p{margin:0}google-codelab:not([theme=minimal]) .instructions h3.faq{border-bottom:1px solid #ddd}google-codelab:not([theme=minimal]) .instructions ul.faq{list-style:none;padding-left:1em}google-codelab:not([theme=minimal]) .instructions .faq li{font-size:1.1em;margin-bottom:.8em}google-codelab:not([theme=minimal]) .instructions .faq a{color:inherit;text-decoration:none}google-codelab:not([theme=minimal]) .instructions .faq a:hover{text-decoration:underline}google-codelab-step .instructions .faq a[href*=cloud\.google\.com]{padding-left:22px;-o-background-size:20px;background-size:20px;background-repeat:no-repeat;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAC9FBMVEX////u7u7v7+/ZRDf/zEH/zkPbRTlJifT/z0XYRDhRj/XWRDdPjvVOjfX/zkH/z0fXQzdGiPXUQzdMi/VKi/Xr6+3t7e1SkPXbRDdLjPW70fPaQjVOjPXx8PD9/f1nnff19fXRQzdHifVJhOxEh/Tu7/JLifH9y0DUQjVtofhOi/NKh+9HgelGf+b+7saaWXX/zDz+yTrZRzrRQjT7+/vxx8PeqkTsuj32uzrOQzdqn/j39/fr6Oj9yT/3vjvaQDPUPTBJivVKifPq7O/z0Mz968T4wj7zwD3NPTHGPDD0+P9Bifrv8/Xx8vJJhu5EfOPo3Nv/0Ez8y0rxvT3ZPjBDi/tBhPT55ePkysj+3Hr/0VLHVErcpkPcTUH7xD35wTzztznpsTjKRTjo8P3U4/z6+vr//PmQt/mBrff//fayy/ZUkfZjmfSXufP78/L18+//+Oj66Of+9eBCdt3t59f/89TvxcPsurXasKztqKL+4Y7jjIT0033ShH3hc2ngaV7/1F3/0ljjv1fbWU37xkDaoEDCSj/xujvptjvutjrxtjn8xDflrDb5vjXJQjX2tzP4+v+bwf3I3Pyiw/t0pvhel/VZlPW90fP/+/DV3/Cdu+/e4uv33Nk+btP+8Mz+8Mniwb7+7L3u371Vdrv+6bDusq3+56j+5aTx2Z+Gjpj+4pbolI3RjYehnIT+2XPedW370mbHaGDMZl3bY1jQVUvbU0f/00PYTkLKSj/tsjjwpjfeXjfnfjbEQjbXPjTSPS/w9vu50vs7hfr5+flypPf89vb89fTF1fHM2vBsnfB3ou/67+6swOpekert6uNdi+M5deLq4OA/dt83bt80a9xmjdby19VKc8jx5cf12cP/7r/gwr+pnr/v4b7xwLtffbf+6rHx3KnorKjYqKTXpqHXo57aop3ZkovTwIboh3+jX3f30XDgenCWUm3Kc2zIsGnMcGj4y1zgYFXgX1TfrEXWlz7mhjfsrTbkljbabDbcVTbsmTBDSJ02AAAFFklEQVRYw+2WV1jTUABGkyZpi6W0tKUWC0hBsAporbMWrCgqKA6WCooK7r333nvvvffee++99957b33x3uTetIEGnnzx83y89Tunf25SWuI/f4Oqk6cU4ijLkr9sfp5Ckyfk6h8o5JkH4umpVPr5+fnqdLro6GiZTBYkk3l71z5SNbdA8zyt8wLYhKefUunr66vV6WQsMDExF3/StLytka/0VPopfbUgoEWBoCBv76lNcj6AFnnQ+6MB0BcsKNA8x2M4iPbjAThQgVsAA545TWg8RQlc5MMBflrg8wHgg3NsIX6OAa10Lr6SDQgHgAXetSeJD5iqRTrytfAKdHwgiAu0EZ3QvEBhYLLAu6+LDoIOtCApKSnVUqoBDondyiahoTggazt9ej4hd94Wxbya4z5wskZoYVgA0/NXlGSlVr3iJpOpOMsbt36r4BphsACQHSezBfSP4zQQs8Zs3n7P3QmeDQ8OAwVwdkEt0ADhhI3jFCxms7lfu+yBthHhwcFhoaCgnXZC4obqPYbZPABqgHVpNj/yak1QABdRQatriwcIiXy312alrBRFqff175jFb9lrZecIVGgz09+NTurjbw/LNFmtUqnVw8PRM0AY6JowaHFN7iIKz2rvtCIjfbBPkvF992aapABKakzvKByQymxbO5e7iDb8BfjoA5uWsPv7kJxPSi4Oy7RpgA/+kl8KArdolUq1uDMshLXCA8jD6NXqpEQPAmDCszibTSFlccS4+DMGMCrVtvdzO0eEh59CPulfgsDYoQ/Rb4q1mbiCsX8zZ2AVrQLIV16uGRGMTxD6PIEkh2TBOPA4og09nSeYJoe+fND1SxdmSRCBgg+6PypE9o01mTRq6Bs+1yEQqcCHJK69cn62xDnA3YS6W4qD51kBA9LV6MX5aXKuIB90/wyJnztCQFMJKuif2zQANfANn9CnchlYzyWYF7OFV8ATgAPV624BAzQKCgTSSxIsDxIYOQR00m7kFgjpsQP6CgUlNWxFd7LDAEaOoPvoc74ESa1NsUBXK9QelLHfafTqclZmKwXvsjpJihyi/mkc9j3Su/P7BngxeMJHPXoru9vbCP4vYZ9aT/DMpxkAW7CsCCFZ3D5I5BowAPnp1wgnqRaGQ84kduEKemchwC5BJ1jX6Tt6Ey50SvBiMH1AgCvYA1i9BP8UkRtjsW8c3E5wjcstfCBxUQgW/APt9sBAfxIR/2ScBvnS5O6EgA4baL7w+hzpRELyhNSqZ1Jgf/0xQsgSuXPCQzxBSPwaG/YpawyRhRnrvBh0DvSGLkITn2A9M/Kljt5HiawsTGAwxVbohS7+JGPfOBgNEJDK3wg60d2EBTvM2KdWE27opPLCWPqEZP9W+BCLfIoa3JFwxyoLX4haVNFHSMUecQrsGx8RbmmZxgfoIV9KIeoDhg4d+jXTzPubCREWWmhOB0SV5hgxYkRGxpgxY36i+2cEgRixQLN1FpqnYJWkKklJScNLjxy5MyPjhwf0Kegn9yZEmZdAuxQqVwEkDR8+auTO77uhD3XKsbWOeKBZL4ugULlyuXJlyjQaNeqbFfhG6BuSwdeqOF0H0q6FBmyhUaNdu6FPQd/RHw4Qpxfj4heMatCwIZywh1JTLAYD+mkhSoeBLoGoqCENYGDXL943oB834iyhLdjHgfF79mHfsb0bkRvLBhZDjB49euzYsePH/96/vzyHeXN3Inc6zSuCqcRys1tJjm4xdYj//JP8AXE5S/JuAn7MAAAAAElFTkSuQmCC")}google-codelab-step .instructions .faq a[href*=stackoverflow\.com]{padding-left:22px;-o-background-size:24px;background-size:24px;background-repeat:no-repeat;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAATwAAAE8CAMAAABq2/00AAAA4VBMVEUAAACCg4aCg4aCg4bIj0P1fx/2hh+ojHCojHCojHD2ih/UjCn0eiD0eiD2hB/2hR/RjjDUjCnKkkPUjCnUjCn2ih/UjCnCllPCllP2ih/CllPCllPCllP2ih/CllPCllPCllP2ih/2ih/CllPCllP2ih/2ih/CllPUjCnCllPUjCn0eiD0eiD2ih+ojHD0eiDUjCn0eiD0eiCojHDUjCn0eiDUjCn0eiDUjCnUjCn0eiCojHD0eiCojHD2ih+ojHD2ih+ojHCojHCdiXaViHuCg4aojHD0eiDCllPUjCn2ih8XYwy7AAAARXRSTlMAv0CAEEAQv4BA7++/gCBQQN8wIIDPv2Dfv5+PUDDPryCAcO+/r49wn4Bg79/frJ9QcGDPr6+Pj3DPz2AwIJ9wYOaPVDAbIL/gAAAGhElEQVR42uzbsYrCQBDG8WmyxaJJlcIixQmxMSIRgiBB7Bbm/R/oPAnH3e0SxwSvyf/3DLt8zDeMAAAAAAAAAAAAAAAAAAAAAAAAAAAA4F9k7Xm1Xwlek53WqyY87AVWfbHumvCTE5hkIZYJTFyIrQU2+xA5CmxWIULcWq1DTGBzCrFcMDluC4ENcTtDEyKdwKYLkYuAuH27ggFtuj7ETgLiNo0B7Z3c1YnNMUQaWbTC6/W1uKUPHbha7wr60GnP7ot3YpGHWCsL5Wod1JP70LMsU+H12424nfDsBj4XgzMD2kN20F82YtDSh965nf61JW7Nzy7iKwY067OLlfJcw/qx1KQPQ9zSh1aaVtGHGnxoUummrB97WZhSk3byRM/6UST3mpQRtwZbTTo4GXdhQLvbaNJVxnX0oSMftyBuDQpN8o4BzaDWpFrG5KwfH5zXpJuMCfShox83pw812GnSxrx+XPI5hjvowF7ttawfB5km+Yq4nf5xS84x5lR7nGPMqfaI2znVHgPajGqPc4w51R7nGJ/sm29P2zAQh49h1kJgJHGWlrbQItAIebNO67owadpgwOzv/4Wm8S8NLsXxGSTu/HyEk/O78+McRu0FH4pQe2EdA6P2wjoGRu2FdotQe2EdA6P2wjoGRu2FdotQe+GChlF7DNcxKulN7bFbx4h1lPlSe+x8aKK1Tqae1B6zdYxM35BLP2qP1TqGjPQdmfCi9ji125l+ICp9qD1G6xixXiSPPag9PusYiW5SCLza49JuK/2YKEOrPSYXNBFpkyTGqj0e6xiFXkoukWqPwzpGrJ8iEyi1x8GHnuknSUqU2qO/jlHpVeQpwhB8oL6OISK9mkK4qz3q7bbQzxFV7mqP9jpGqi1IYme1R3sdI060BTPpaAiot9ss0hZkwk3tUb+gyUJbkEzd1B55HxqfaQvy1Ent0V/HKCNtwVy0NwSfGPhQMdcWRJWD2uOwjiFzbcFZ3F7tsVjHmDqMLTZqj3S7bTm2RJloZwh2maxjyJnd2NJS7XFZx7AcW2QrtUe93dZUDmOLjdrbaTTa7e9fNgj1i7ZjS9lK7dU+dPvzzgaRZ8elpPZji73a2/j749vOLuWytRtbCmmv9jghLMcWuw936zfwwnZssTAEexw+VidTmstn1N4Wr2/2gcrBlO6HY3eHKNqPLYfh2FmPLeZ/aXvh2D1Q2o0tYkHt8ZxQliIybTm2NNXex30IWI8tcePDPYSAywPv+61w7NwfeHfJHrvJS44tSQmUGan+oCfAgTi3fOCly1D952A4eqkH3gTocqDuOT5NX2JsmQJZhFrkaHwy8fzAmwNdeuoRRgQiTakEugyUSR2B+LFlDoTpK4M6AvH/pUUC6JIqAyMCMQ+8pMe8E2XgGIFlxKxbAIyVgUUEWj/wUh6QAZQBIgJlzqlbQKwMUBE4Tdh0C4ChMsBFoMgiJt2ivpvVoCNQznh0CxDKAB2B9dgSA2l6ygAbgfUDbwG0GSgDPxEo5tS7xYq7GT4CU8Im6pbRsNkxMBFIex5ezqQ3qM8fPgL5kZ6Mj1QNLgKpR90y4tNjpbxFID+ExwjsAUN8ReAYmOIjAlmePF8RyLHv+orAPgScI3AIb531tQZ/XjECR3DL5VoTeDOsXTVYf8UIFHDLu6sm8GZAFA8ZgccQimcgLCPwNBRvOROLCExD8Z4mXR2BRxCK5+wCx6F47hHYC8Vzj8BJKJ5zBPYhFM85Agf0i3dx3ulcdLubm5e+I3BEv3id65r7Ov70EoGCV/EW+NrpdLu/cHWkX7zz65pn6hiK9689O8hJKIaiMCxK0nYzTBgSYxDs/pekiTohEq/v0eRKv28J/+zkXMbrF2IdxQvEC3ScOF7r6+3LrtZDa1vxVinfHaeId+qjTBCv9kGO4i1XJoi365/EWxCv9EF24i1XJ4jXv4iXKd7p/uNt+yjt/uO1PspZvOVmmGcPrZ1q3ZUi3qoDaNva4WYdj5PFu2nHMm+8nzqKF4y3vuOreL923PcrqngRz6291FrKXrxbdWzifXB6iydehHh5iDco3maQpxnivQ2yEU888cQLEi8R8cSLES8R8cSLES8R8cSLES8R8cSLES8R8QbFexzkPEO86xxA4on3F+LlIZ54QeLlIZ54QeLlIZ54QeLlId6qeOk8/BviiRciXibiiRciXibiiRciXibiiRciXibiiQcAAAAAAAAAAAAAAAAAwHTeAe21evvWi2VXAAAAAElFTkSuQmCC")}google-codelab-step .instructions .faq a[href*=support\.google\.com\/webmasters\/]{padding-left:24px;-o-background-size:24px;background-size:24px;background-repeat:no-repeat;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAolBMVEUAAADW1tbW1tZ6enrQ0dJ6enrMzMzq6+zq6+x6enru7+/m5+ju7+/m5+j////S09REi/XW1tbm5+ju7+9PT0/Q0dK8vLxGjfVZWVlNTU16enrMzMzIyMj09PS/0uuSkpJpaWmbm5tim/e0tLSEhIStx+3e3+GsrKxwcHBZk/dPk/R1p/KPtu/U3eqmpqZlZWVfmO5woeqHreWvwN6nu9qLi4vZSE73AAAADnRSTlMAEcxmzO7MzDMz7u6IiHn/rpYAAALMSURBVHja7dXpjtowFIZhUkqZlSV1ncQdEsK+zD7T+7+1xjTiwxVOAmeORyP5+2UspPchEtDy8/Pz8/Pz8/M7cZeduHKdyxbL0FfVAMUraHdUHUB12i22tZWqBRSDgKFfC4CApV8PgIClXw/gE6hyTd/lAR7gAV8fEHwbWtc/ul/WfQ8IfQKAIECfCoCA3sdq+nQB+nQABIQ+CQABtY9Z+lQB+gwACBj6AFAE6DM9AL0fAalPB/yGgNAfntwHAAJKnwCAwNLneQAAVAvM/ttzVLlB4z2/A6AFVoDRf43IfewVgGKNAG/Uvrl39BsCXqh9cy8nA/51tmk63+jDZp6m9wTA4DzANiyW6lOqTxAMHAD22XASReuwpKDvELCNookBGJwx9E8B3OvsQp8e9GmOviNANA/Dh4k+TApBukHfFSBa7/I7whqf3xWA/gOAoU8DDJwBCHk+wIAw9CsAPw/W++CFB/MAD/hCgL59PXPS8toRYKHEndkTauEQIIUQ0hToG2eAXOicKdBXmSPAo9CTpkDo5U4Ad6IEGILy0gFgFJctCACIh/yATABwIChvVyE3IBR7AAQAiOmIGZADAMEBQMkRL+AJAAgOALGUI07AQgBgCPb3uRbwAaYAGAIAZlIL2AA5AIYAgJXUAjaAAsAQADCWO4EjAAQGQAvYAUsDsDQAeuwAMZvu89PZ/jZhBmQCi2dl60+MPiMAf8UgZEUpK/LoJ4oX0C9LIIyF0U8yXkBPVCzRWzIDZjX9sWQG9Kv7ScYN6KnKfizZAf2qfpLzA3p5RV9JboDeytp/kk4Ao5WtP+UFQKCO9sdTSQQ03+OR/kxKdgAWZv/11VI6AWBmP5HSNQD5zwQknwxIqIBu468hBgDqNYCuFXBDASSNAbdWQHB1PiBpDLgOWtZdXPEDri/QswjYAOjbF9x0OQHd26Dl5+fn5+fn5+dn7i/3LEaKJNV/0wAAAABJRU5ErkJggg==")}google-codelab-step .instructions .faq a[href*=android-developer],google-codelab-step .instructions .faq a[href*=developer\.android\.com]{padding-left:20px;background-repeat:no-repeat;-o-background-size:20px;background-size:20px;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAq1BMVEX///+lyjmfxyGhyCqkyjakyTP+/vuiyS7z+OWlyjKgxyTX57ChyCfa6bTx9uLy9+Sz0l6szk/D24Tt9NrN4ZnK4JSiyCyexh34+/D6/PTp8dPh7cjf7L7k78vc6rnW5qzU5ajS5KPP4p+21GWnzD3F3Yi813Kx0ViqzUWexhr8/vjv9d7Z6LKy0lyZxADh7cLA2n2rzkr1+erJ35C61m6uz1KcxRHn8M6/2XgEePWtAAACr0lEQVRo3u3Z2XKiQBiG4Y9ebNO2aCCyzOC+7zH7/V/ZRJZQZaJpWnKS4TlJUItXbX4pFJXK71DTusvccI6z5oMSAutdhDP2uzpK4CxwxsRBGYJdA19qLAOU4s4LkyUN7G4Ude2glmzuRihHKDqAPZhNuJRKSckms37rvctrKMnG728l5cxKMU7JuCF7KEvf87h1gnu0iXK4B2p9Sc3dUgZNMusMLvu42r20LiBXT8JMWRepN1xlGu+fK8/6xFPxwsvH697/eP/j7uDTOvB+dxwX/AaM2SQ+6icrYHDyGmgfWE3irNeCqTaPAwcArydroboAbuMAb8NQRNJn20SYtHK8HaKZzoffhZl5tlNxsD5PsnUQ2b9jwwnO35WvRo3lN/o2TDwKS5M3gokF0w2wZxgIlpY2FpgsgdQPkLXJQar0A/LVIDB6Idp2GxQQrJI/rbq2Voijlc5S9LZs8uYCqBVyXLTZhLW/fSn3hDMmZAQ05I020gT2PmeM+w4uGpLksFs+oEEtbaqJVnpUkz4uyebf6xQK0CY6XjoSC1xQ9/PPr4KB52zuvTrOa5EssC0auM0C0tYKtI0DqgpUgSpQBapAFfifAj90Tn5YZoGnooH8eutB56KDDosGhlTrYiTyk0epVdFAKJLn9rLHRSNfcE4tF0UDcAXlXPhDfGPtjGejEMUDCEdPY8eFHoOADvPAxiDQLBBQkUHAJfoB34UBS/9CfAETe6lZYKZft/QIFe94vicRy7tcvKOkB0PhYOo4zlNWYNvjpjOdZwU+O24OQlynK62EGCF2J9IbpIsy/P0I3CHW+Qj8qQJVoAr8VOCx3EB+YvDSwIDmXzaW4oZZseU6Lfr5aaAUPT8uyPuT33VeIpRkI5RHyRQfHEKFUj2UJuh1GjaQsxudKECl8jv8A6GtQkKSkMLrAAAAAElFTkSuQmCC")}google-codelab-step .instructions h3>a[href*=github],google-codelab-step .instructions h3>a[href*=github]:visited{color:#000;text-decoration:none;padding-left:24px;background-repeat:no-repeat;-o-background-size:18px;background-size:18px;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAMAAAAOusbgAAAAflBMVEUAAACXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZbf2s+YAAAAKXRSTlMA+SAC7QrIBPXROWMX17rCSmusXEaAD9zNeXFWJqOLMx3mtJJAK5xP4Jw4LyAAAAT6SURBVGje1NbdcqJAEAXg0zJAQEFA0QhojJHE8/4vuBfrJlvQGHSGbO13CzVT09M/g//HIssPRZAYIUkxSVAc8myBSfnrMjVUmbRc+5jEsUqFN0lazeDYogo4SlC5jPq6EI4mxRpOhKuId4pWof228y0fsJ1bbp0lfFCS4XFNSgtpg8eEO6EV2YV4QBvRWtTiXl4udEByD3fxCzpS+LhDk9CZpMFosaFDJsZItdApqTHKnM7NMULOCeQW5534zDUnUuOmWDgRiXFDYzgZ02CQn3BCiY8BXsFJFd7YQjLPkeGDTPRsRhZVK+zIABxX+yfe6Wm/OgL4YIe0UIQRO8x1jm/yiHeI8s11RdP7FKJvx649PmUBRwoyfNqza6dUktzsNt7HliNsa+9m95UGXSl7XvQn2OV0qOpzO1tsNotZe66rw+lyXbb08bcX9qToyNgjYTcoS3PaxT4UflydzLJ7nFDYk3V+SdhzQZfnQTP89VW5jPDbmRTAWvDdnAq1zFnC2pLfHHnFn9uYK3yJ+HOhZoRPa2peYe2VmjX+KKjawNKGqgJXC6HqDEtnqmSB3yrqSlgqqau+UkAVwVLEKz1tZxxQw1LNATObSNvHOqUqCGEtDKhKAcAXqlo40Ag14g92D77BiTcO9pByqNbsDfeIcvCK3+HI++AlG2piOBJTY4AjNVs4s6XmiMwitSzSK0Nu0bQs2leOAzUzODOj5qDPYvHgTCj6TA703HLoog+oxGIgWgzHBMbigWnx1DSQyTcOqBDw34Sav9q1sx1HYSAKoNcYE4cl0AlhIB22rF3//4PTI43ULbrMYjzKy5zHROiGyNhlyqAXDC5z8Acc6mcFu3+OpeCD+Y87ONMRR1isik6qet+wah3gzMEwfIPXLIsB9sQp4UxJnL2pDFRwRBHrauoEZHDkaeoV5PSKKpNydMTSCk4oTawO8O07Nva9JN+8Z0skHNgk5l3bkf7hLUcjW+CceH2B1YqeeDmzTXU5sB/EE4rZmDucsA9ksBtvJuoQq4SaDE5sqc8n2+eaNyqBOTmHtcycG8xo3B4lrMgjmZ0GrwtYaQsLbUBmImZeojKuHRbyHjRmz5ZFQRUdByfGxCPEfPJW07gbVyVoiU9xIwYz97GVs1LD5oMmbPknvcKntLmXNNDvT2GBEV1WXXyadjA0Q64bIMygLsTo00aCsbmWfCbfDOEnt6QF8EzjlBi6BasVdscU5HYwbdz9LO65y9ae4tjKwTzDTJUZfxlPJjRLhoELfdPHgNrl9ZIFK6I5LhjyxOB75QdFsqAEVIKmCW+qcZ0B9xvOPft7eReaVnFPRPmj1lPvbcJdx6toUrkB46zpmyeAnFLViMGDZvSkKfo8Y3ikAORjB8RVqonIT4+hxJjW/qRRzezNvd1VoegKoG1uGOPRhBomaktfAok/PJ9a3PykQEk1xsQ0bqtg5Pk/+04qBtryotDlG4wpaJTvzS7Q3sCwDdbhgu1sw9yi7V/9XFSEb3MwbAbXYWn5H7zF3+5J2gZHNhuPX5frsaqaOvWpsAw+rC3FY6tg/cRMYU88zya4DzGbV7oL3npYQNWugmuFZSLtIlhHWOwcrA8OzrAgT3pdsD5J2PH2a4L3HuzdStvgMscq8j2hL/e5wcm7BLA2OuBbFebVKWBirYR/D6OWc16JizqEO0WUCrHzMMrbCZFGBf5j/QYa/td1VlNoIAAAAABJRU5ErkJggg==")}google-codelab-step .embedded-iframe,google-codelab-step .youtube-video{display:-webkit-box;display:-webkit-flex;display:flex;margin:auto;width:560px;height:315px;border:none;max-width:100%;max-height:51vw}google-codelab google-codelab-step .note:before,google-codelab google-codelab-step .special:before,google-codelab google-codelab-step aside:before{content:unset!important}google-codelab google-codelab-step .instructions aside.special,google-codelab google-codelab-step .instructions aside.warning{margin:10px 0!important;padding:15px 20px!important}iron-icon{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;font-size:inherit}iron-icon[icon=file-download]:after{content:"cloud_download"}google-codelab .warning,google-codelab .warning :link,google-codelab .warning :visited,google-codelab .warning code{background:#fef7e0}google-codelab .special :link,google-codelab .special :visited,google-codelab .special code,google-codelab aside :link,google-codelab aside :visited,google-codelab aside code{background:#e6f4ea}google-codelab-step td{background:transparent}google-codelab-step .instructions h3>a[href*=github],google-codelab-step .instructions h3>a[href*=github]:visited{background-position:0 3px}google-codelab-step code .pln,google-codelab-step code .pun,google-codelab-step pre .pln,google-codelab-step pre .pun{color:inherit}google-codelab-step code{font:500 90%/1 Roboto Mono,monospace}google-codelab-step pre{background:#f1f3f4;-webkit-border-radius:0;border-radius:0;color:inherit;margin:16px 0;overflow-x:auto;padding:8px 80px 8px 8px;position:relative}google-codelab-step pre,google-codelab-step pre code{font:14px/20px Roboto Mono,monospace}.button-blue,.button-green,.button-primary,.button-red,body devsite-footer-utility .devsite-footer-utility-button>a{color:#fff!important}google-codelab{width:100%;height:100%;padding-top:64px}google-codelab,google-codelab #main{display:-webkit-box;display:-webkit-flex;display:flex}google-codelab #main{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;position:relative;background:#f8f9fa}google-codelab #codelab-title{position:fixed;top:0;left:0;width:100%;background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);color:#3c4043;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;height:64px;padding:0 36px 0 16px;-webkit-font-smoothing:antialiased;z-index:1000}google-codelab #codelab-title h1{font-size:20px;font-weight:400;margin:0 8px;font-family:Roboto,Noto,sans-serif;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;-webkit-flex-shrink:1;flex-shrink:1;white-space:nowrap;-o-text-overflow:ellipsis;text-overflow:ellipsis;overflow:hidden;width:0;display:inline-block}google-codelab #codelab-title .time-remaining{-webkit-flex-shrink:0;flex-shrink:0;-webkit-box-flex:0;-webkit-flex-grow:0;flex-grow:0;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;font-size:16px;font-weight:400;white-space:nowrap}google-codelab #codelab-title .time-remaining i{margin-right:3px}google-codelab #codelab-nav-buttons{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-flex:0;-webkit-flex-grow:0;flex-grow:0;-webkit-flex-shrink:0;flex-shrink:0}google-codelab #codelab-nav-buttons #arrow-back,google-codelab #codelab-nav-buttons #menu{text-decoration:none;color:#3c4043;width:40px;height:40px;-webkit-box-align:center;-webkit-align-items:center;align-items:center}google-codelab #codelab-nav-buttons #arrow-back,google-codelab #codelab-nav-buttons #menu,google-codelab #controls{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}google-codelab #controls{position:absolute;bottom:32px;left:0;right:0;padding:0 32px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;z-index:1001}google-codelab #fabs{display:-webkit-box;display:-webkit-flex;display:flex;max-width:1025px;width:100%;margin:0 auto}google-codelab #fabs,google-codelab #fabs .spacer{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}#done,#next-step,#previous-step{-webkit-border-radius:4px;border-radius:4px;font-family:Google Sans,Arial,sans-serif;font-size:14px;font-weight:600;letter-spacing:.6px;line-height:24px;padding:6px 24px;pointer-events:auto;text-transform:none;background:#fff;color:#1a73e8;-webkit-transform:scale(1);-o-transform:scale(1);transform:scale(1);-webkit-transition:-webkit-transform .3s ease-in-out;transition:-webkit-transform .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out;transition:transform .3s ease-in-out;transition:transform .3s ease-in-out,-webkit-transform .3s ease-in-out,-o-transform .3s ease-in-out;-webkit-box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);text-decoration:none;-webkit-font-smoothing:antialiased}#next-step{color:#fff;background:#1a73e8}#done{background:#1e8e3e;color:#fff}google-codelab #fabs a[disappear]{-webkit-transform:scale(0);-o-transform:scale(0);transform:scale(0)}#done{background:#0f9d58}google-codelab #drawer .codelab-time-container{display:none}@media (max-width:768px){google-codelab #codelab-title .codelab-time-container{display:none}google-codelab #drawer .codelab-time-container{display:block;padding:20px 10px 10px 23px}google-codelab #drawer .time-remaining i{margin-right:9px}}google-codelab #drawer{background:#fff;width:256px;-webkit-flex-shrink:0;flex-shrink:0;position:relative;z-index:100;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;background:#f8f9fa}google-codelab #drawer,google-codelab #drawer .steps{display:-webkit-box;display:-webkit-flex;display:flex}google-codelab #drawer .steps{-webkit-flex-shrink:1;flex-shrink:1;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;overflow-x:visible;max-height:-webkit-calc(100% - 54px);max-height:calc(100% - 54px)}google-codelab #drawer .steps:only-child{max-height:100%}google-codelab #drawer .metadata .material-icons{top:6px;position:relative}google-codelab #drawer ol{margin:0;padding:16px 12px;counter-reset:li-count;list-style:none;overflow-x:visible;overflow-y:auto;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}google-codelab #drawer ol li{display:block;counter-increment:li-count}google-codelab #drawer ol li a{text-decoration:none;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;font-size:14px;color:#80868b;padding:3px 10px;min-height:48px;font-weight:400;line-height:20px;-webkit-box-sizing:content-box;box-sizing:content-box;position:relative;font-family:Roboto,Noto,sans-serif;-webkit-font-smoothing:antialiased;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out;border:1px solid #dadce0;-webkit-border-radius:5px;border-radius:5px;margin:6px 0;background:#fff}google-codelab #drawer ol li a:active,google-codelab #drawer ol li a:focus{background:#c6c6c6;-webkit-tap-highlight-color:transparent;outline:0;border-color:#c6c6c6!important}google-codelab #drawer ol li a .step{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center}google-codelab #drawer ol li .step:before{content:counter(li-count);display:inline-block;font-style:normal;width:26px;min-width:26px;color:#fff;background:#80868b;-webkit-border-radius:50%;border-radius:50%;text-align:center;height:26px;vertical-align:middle;line-height:26px;margin-right:8px;font-weight:400;position:relative;z-index:2;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}google-codelab #drawer ol li[selected] a,google-codelab #drawer ol li a:focus{color:#212121;font-weight:600;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}google-codelab #drawer ol li[selected] a{border-color:#fff}google-codelab #drawer ol li[selected] .step:before{font-weight:600}google-codelab #drawer ol li[completed] a{color:#212121}google-codelab #drawer ol li[completed] .step:before{background-color:#1a73e8;color:#fff}google-codelab #drawer .metadata{color:#777;font-size:14px;padding:16px;-webkit-flex-shrink:0;flex-shrink:0}google-codelab #drawer .metadata a{color:currentcolor;margin-left:4px}google-codelab #codelab-nav-buttons #menu{display:none}google-codelab #drawer ol ::-webkit-scrollbar{-webkit-appearance:none;width:7px}google-codelab #drawer ol ::-webkit-scrollbar-thumb{-webkit-border-radius:4px;border-radius:4px;background-color:rgba(0,0,0,.5);-webkit-box-shadow:0 0 1px hsla(0,0%,100%,.5)}@media (max-width:768px){google-codelab{display:block;position:relative}google-codelab #main{height:100%}google-codelab #codelab-nav-buttons #arrow-back{display:none}google-codelab #codelab-nav-buttons #menu{display:-webkit-box;display:-webkit-flex;display:flex}google-codelab #drawer{width:256px;position:absolute;left:0;top:0;bottom:0;z-index:10000;will-change:transform;-webkit-box-shadow:2px 2px 4px transparent;box-shadow:2px 2px 4px transparent;pointer-events:none;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);-webkit-transition:-webkit-transform .3s ease-in-out,-webkit-box-shadow .3s;transition:-webkit-transform .3s ease-in-out,-webkit-box-shadow .3s;-o-transition:box-shadow .3s,-o-transform ease-in-out .3s;transition:transform .3s ease-in-out,box-shadow .3s;transition:transform .3s ease-in-out,box-shadow .3s,-webkit-transform .3s ease-in-out,-o-transform .3s ease-in-out,-webkit-box-shadow .3s}google-codelab[drawer--open] #drawer{-webkit-box-shadow:2px 2px 4px rgba(0,0,0,.15);box-shadow:2px 2px 4px rgba(0,0,0,.15);-webkit-transform:translateZ(0);transform:translateZ(0);pointer-events:all}google-codelab #main:before{content:"";top:0;left:0;right:0;bottom:0;position:absolute;-webkit-transition:opacity .38s ease-in-out;-o-transition:opacity ease-in-out .38s;transition:opacity .38s ease-in-out;background-color:rgba(0,0,0,.3);z-index:10;pointer-events:none;opacity:0}google-codelab[drawer--open] #main:before{opacity:1;pointer-events:all}}google-codelab #steps{overflow:hidden;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;position:relative;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}google-codelab google-codelab-step{display:none;width:100%;-webkit-transform:translateZ(0);transform:translateZ(0);position:absolute;top:0;left:0;right:0;bottom:0;padding-top:32px;overflow-y:auto;overflow-x:hidden}google-codelab google-codelab-step[animating],google-codelab google-codelab-step[selected]{display:block;-webkit-transform-origin:0 50% 0;-o-transform-origin:0 50% 0;transform-origin:0 50% 0;-webkit-animation-fill-mode:both;-o-animation-fill-mode:both;animation-fill-mode:both}google-codelab google-codelab-step[animating]{pointer-events:none;position:absolute;overflow:hidden}google-codelab #drawer ol li{padding:0;margin:0}google-codelab{height:100vh;left:0;position:fixed;top:0}google-codelab #codelab-title h1{width:auto;color:#3c4043;top:0}google-codelab .title{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;margin-left:10px;overflow-x:hidden}google-codelab #drawer .metadata .material-icons,google-codelab-about .about-card .material-icons{top:0!important}body[type=codelab]{color:#5c5c5c;font-family:Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;overflow:hidden}body[type=codelab] google-codelab{opacity:0;display:-webkit-box;display:-webkit-flex;display:flex}body[type=codelab] devsite-googler-buttons{bottom:95px}body[type=codelab][ready] google-codelab{opacity:1}body[type=codelab] .devsite-main-content{max-width:100%!important;padding:0!important}body[type=codelab] a:focus{text-decoration:none}body[type=codelab] .devsite-badger-award{left:256px}body[type=codelab] .devsite-back-to-top-link,body[type=codelab] .devsite-banner,body[type=codelab] .devsite-footer,body[type=codelab] .devsite-heading-link,body[type=codelab] devsite-code:after,body[type=codelab] devsite-header{display:none}@media screen and (max-width:840px){body[type=codelab] .devsite-badger-award{left:0}}body.google-samples{background:#f1f3f4}body.google-samples .devsite-article{margin:0;width:auto}android-samples{display:block;--mdc-theme-primary:$WHITE}android-samples .mdc-button:not(:disabled){color:#fff}android-samples .sample-grid{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:-24px 0 16px -24px}android-samples .resource-card{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 33.3333%;flex:0 0 33.3333%;min-width:0;padding:24px 0 0 24px}android-samples .resource-card:active,android-samples .resource-card:focus{text-decoration:none}android-samples .sample-card{background:#fff;-webkit-box-shadow:0 1px 3px 0 rgba(0,0,0,.21);box-shadow:0 1px 3px 0 rgba(0,0,0,.21);-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;max-height:165px;min-width:0;overflow:hidden;padding:10px 20px 20px;position:relative}android-samples .card-featured .card-info:before{color:#1a73e8;content:"FEATURED";font-size:10px;position:absolute;right:10px;text-align:right;top:8px}android-samples .card-info .section{color:#9aa0a6;font:700 11px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.3px;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;text-transform:uppercase;white-space:nowrap}android-samples .card-info .title{color:#3c4043;font:500 18px/23px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:7px 0;max-height:46px;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;white-space:normal}android-samples .sample-description-area .text{color:#5f6368;font-size:14px;line-height:20px;margin:0 0 100px;max-height:60px;overflow:hidden}android-samples .no-display,android-samples .sample-description-area :not(.text){display:none}android-samples .block-display{display:-webkit-box;display:-webkit-flex;display:flex}@media screen and (min-width:1440px){android-samples .resource-card{-webkit-box-flex:0;-webkit-flex:0 0 25%;flex:0 0 25%}}@media screen and (max-width:840px){android-samples .resource-card{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%}}@media screen and (max-width:600px){android-samples .sample-grid{margin:-16px 0 8px -16px}android-samples .resource-card{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;padding:16px 0 0 16px}}android-samples .navrow{-webkit-box-align:center;-webkit-align-items:center;align-items:center;background:#546e7a;display:-webkit-box;display:-webkit-flex;display:flex;max-height:68px;padding:16px;visibility:hidden}@media screen and (max-width:840px){android-samples .navrow{max-height:112px}}@media screen and (max-width:600px){android-samples .navrow{max-height:184px}}body[mdc--ready] android-samples .navrow{max-height:none;visibility:visible}android-samples .mdc-menu-surface[open]{display:block;opacity:1}android-samples .navrow .buttongroup{display:-webkit-box;display:-webkit-flex;display:flex}android-samples .sample-menu{padding:0 4px 0 16px}android-samples .sample-button,android-samples .sample-menu{-webkit-box-align:center;-webkit-align-items:center;align-items:center;background:0;-webkit-box-shadow:none;box-shadow:none;color:#fff;display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;opacity:.87}android-samples .sample-button:hover,android-samples .sample-menu:hover{background-color:hsla(0,0%,62%,.2)}android-samples .sample-button:active,android-samples .sample-button:focus,android-samples .sample-menu:active,android-samples .sample-menu:focus{background-color:hsla(0,0%,62%,.4);-webkit-box-shadow:none;box-shadow:none}android-samples .mdc-button{letter-spacing:0}android-samples .mdc-list-item__text{font-size:14px}android-samples .googlesamples-filter-menu{padding-left:0}android-samples .menu-indicator{font-size:20px;visibility:hidden}android-samples .menu-indicator.yes-visible{visibility:visible}android-samples .navrow .searchfield{font-size:16px;margin:0 0 0 auto;width:300px}android-samples .navrow input#search{background:#fff;border:0;-webkit-border-radius:2px;border-radius:2px;margin:0;padding:4px 0 4px 8px;width:100%}android-samples .navrow .searchfield label{display:none}@media screen and (max-width:840px){android-samples .navrow{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;padding:16px 16px 24px}android-samples .navrow .searchfield{margin:8px 0 0}}@media screen and (max-width:600px){android-samples .navrow{margin:0 -16px}android-samples .navrow .buttongroup{-webkit-align-self:flex-start;align-self:flex-start;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}android-samples .navrow .searchfield{margin:8px 16px 0;width:100%}}android-samples .controls-grid{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;padding:16px 24px}android-samples .matching-samples-count,android-samples .per-page-selector{-webkit-box-flex:1;-webkit-flex:1 1 20%;flex:1 1 20%;white-space:nowrap}android-samples .page-number-selector{-webkit-box-flex:1;-webkit-flex:1 1 60%;flex:1 1 60%;text-align:center}android-samples .matching-samples-count{text-align:right}android-samples .page-number-selector-button,android-samples .per-page-item{background:0;border:0;-webkit-border-radius:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none;color:#1a73e8;cursor:pointer;font-size:16px;font-weight:400;height:auto;margin:0;min-width:auto;padding:0 8px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}android-samples .page-number-selector-button:active,android-samples .page-number-selector-button:focus,android-samples .page-number-selector-button:hover,android-samples .per-page-item:active,android-samples .per-page-item:focus,android-samples .per-page-item:hover{background:0}android-samples .page-number-selector-button:hover,android-samples .per-page-item:hover{color:#000}android-samples .page-number-selector-button:active,android-samples .page-number-selector-button:focus,android-samples .per-page-item:active,android-samples .per-page-item:focus{-webkit-box-shadow:none;box-shadow:none}android-samples .page-number-selector-button .material-icons{margin-bottom:4px;vertical-align:middle}android-samples .page-number-selector-inactive,android-samples .per-page-selected-item{color:#000;cursor:default}@media screen and (max-width:840px){android-samples .controls-grid{display:block;padding:16px 0;text-align:center}android-samples .matching-samples-count,android-samples .per-page-selector{margin:8px auto;text-align:center}}@font-face{font-family:Euclid;src:url(../fonts/custom/euclid.ttf) format("truetype"),url(../fonts/custom/euclid.otf) format("opentype"),url(../fonts/custom/euclid.woff) format("woff")}android-sticky-toc ul,android-sticky-toc ul ul{list-style:none;padding-left:0}android-sticky-toc a:focus,android-sticky-toc a:hover,android-sticky-toc a:link,android-sticky-toc a:visited{color:#414141}android-sticky-toc[sticky]{padding-left:32px;position:-webkit-sticky;position:sticky;top:164px;width:240px}android-sticky-toc[embedded]{display:none;margin-top:120px}android-sticky-toc .dac-nav-item.dac-nav-heading{line-height:16px;margin-bottom:16px;max-width:160px}android-sticky-toc .dac-nav-subtitle{font:500 12px/22px Roboto Mono,monospace;color:#414141;text-transform:uppercase}android-sticky-toc .dac-nav-title{font-size:13px}android-sticky-toc .dac-nav-item{margin:0;padding:5px 0}android-sticky-toc div>ul{border-left:4px solid #f5f5f5;padding-left:24px}android-sticky-toc div>ul .dac-nav-title{border-left:4px solid transparent;display:inline-block;margin-left:-28px}android-sticky-toc ul ul .dac-nav-title{padding-left:24px}android-sticky-toc div>ul>.dac-nav-item:first-child{padding-top:0}android-sticky-toc div>ul>.dac-nav-item:last-child,android-sticky-toc div>ul>.dac-nav-item:last-child .dac-nav-item:last-child{padding-bottom:0}android-sticky-toc .dac-nav-title.dac-nav-active{border-left:4px solid #3ddc84;font-weight:500}android-sticky-toc .dac-nav-title .dac-nav-arrow:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;content:"arrow_right";vertical-align:middle}android-sticky-toc .dac-nav-title .dac-nav-arrow.open:before{content:"arrow_drop_down"}android-sticky-toc .dac-nav-list.nested{height:0;visibility:hidden}android-sticky-toc .dac-nav-list.nested.collapse{height:auto;padding-top:5px;visibility:visible}@media screen and (max-width:1200px){android-sticky-toc[sticky]{display:none}android-sticky-toc[embedded]{display:block}}@media screen and (max-width:1000px){android-sticky-toc[embedded]{display:none}}.dac-tutorial-page[ready] .devsite-wrapper{overflow:inherit}.dac-tutorial-page .dac-preview-wrapper{height:-webkit-calc(100vh - 96px);height:calc(100vh - 96px);position:-webkit-sticky;position:sticky;top:64px;width:100%}.dac-tutorial-page .dac-preview-wrapper .dac-step-preview{opacity:0;position:absolute;visibility:hidden;width:100%}.dac-tutorial-page .dac-preview-wrapper .dac-step-preview.active{opacity:1;visibility:visible}.dac-tutorial-page .dac-preview-wrapper .dac-step-preview.screenshot{border:0;-webkit-transition:visibility 0s .3s,opacity .3s linear;-o-transition:visibility 0s .3s,opacity .3s linear;transition:visibility 0s .3s,opacity .3s linear}.dac-tutorial-page .dac-step-wrapper section.active:before{content:"";border-left:4px solid #3ddc84;-webkit-border-radius:4px;border-radius:4px;margin-left:-20px;padding-left:16px}.dac-tutorial-page .dac-preview-toggle-wrapper{color:#fff;position:absolute;right:0;top:27px;z-index:1}.dac-tutorial-page devsite-code .devsite-icon-copy:before{z-index:2}.dac-tutorial-page .dac-preview-toggle-wrapper .dac-preview-phone,.dac-tutorial-page .dac-preview-toggle-wrapper .hide,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .show{display:block}.dac-tutorial-page .dac-preview-toggle-wrapper .show,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .dac-preview-phone,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .hide{display:none}@media screen and (max-width:1000px){.dac-tutorial-page .dac-step-wrapper section.active:before{content:none}.dac-tutorial-page .dac-preview-toggle-wrapper,.dac-tutorial-page .dac-preview-toggle-wrapper .dac-preview-toggle{color:#414141;left:0;position:relative;right:0;top:0}.dac-tutorial-page .dac-preview-toggle-wrapper .dac-preview-toggle+.dac-preview-phone{display:none;padding-top:40px;margin:auto;width:200px}.dac-tutorial-page .dac-preview-toggle-wrapper .dac-preview-toggle.toggle+.dac-preview-phone{display:block}.dac-tutorial-page .dac-preview-toggle-wrapper .dac-preview-phone,.dac-tutorial-page .dac-preview-toggle-wrapper .hide,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .show{display:none!important}.dac-tutorial-page .dac-preview-toggle-wrapper .show,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .dac-preview-phone,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .hide{display:block!important}.dac-tutorial-page .dac-preview-toggle-wrapper .show:after,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .hide:after{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;height:23px;position:absolute;width:50px}.dac-tutorial-page .dac-preview-toggle-wrapper .show:after{content:"expand_more"}.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .hide:after{content:"expand_less"}}
\ No newline at end of file
diff --git a/development/referenceDocs/stageReferenceDocsWithDackka.sh b/development/referenceDocs/stageReferenceDocsWithDackka.sh
index 7d488c5..cae0421 100755
--- a/development/referenceDocs/stageReferenceDocsWithDackka.sh
+++ b/development/referenceDocs/stageReferenceDocsWithDackka.sh
@@ -79,28 +79,24 @@
if (( FLAGS_useToT )); then
printf "Downloading docs-tip-of-tree zip files \n"
- androidxDoclavaZip="doclava-tip-of-tree-docs-${FLAGS_buildId}.zip"
androidxDokkaZip="dokka-tip-of-tree-docs-${FLAGS_buildId}.zip"
androidxDackkaZip="dackka-tip-of-tree-docs-${FLAGS_buildId}.zip"
else
printf "Downloading docs-public zip files \n"
- androidxDoclavaZip="doclava-public-docs-${FLAGS_buildId}.zip"
androidxDokkaZip="dokka-public-docs-${FLAGS_buildId}.zip"
androidxDackkaZip="dackka-public-docs-${FLAGS_buildId}.zip"
fi
if (( "${FLAGS_buildId::1}" == "P" )); then
- /google/data/ro/projects/android/fetch_artifact --bid $FLAGS_buildId --target androidx_incremental incremental/$androidxDoclavaZip
/google/data/ro/projects/android/fetch_artifact --bid $FLAGS_buildId --target androidx_incremental incremental/$androidxDokkaZip
/google/data/ro/projects/android/fetch_artifact --bid $FLAGS_buildId --target androidx_incremental incremental/$androidxDackkaZip
else
- /google/data/ro/projects/android/fetch_artifact --bid $FLAGS_buildId --target androidx $androidxDoclavaZip
/google/data/ro/projects/android/fetch_artifact --bid $FLAGS_buildId --target androidx $androidxDokkaZip
/google/data/ro/projects/android/fetch_artifact --bid $FLAGS_buildId --target androidx $androidxDackkaZip
fi
# Let's double check we succeeded before continuing
- if [[ -f "$androidxDoclavaZip" ]] && [[ -f "$androidxDokkaZip" ]] && [[ -f "$androidxDackkaZip" ]]; then
+ if [[ -f "$androidxDokkaZip" ]] && [[ -f "$androidxDackkaZip" ]]; then
echo "Download completed"
else
printf "\n"
@@ -114,7 +110,6 @@
printf "== Unzip the doc zip files \n"
printf "=================================================================== \n"
- unzip $androidxDoclavaZip -d $doclavaNewDir
unzip $androidxDokkaZip -d $dokkaNewDir
unzip $androidxDackkaZip -d $newDir
else
@@ -122,7 +117,6 @@
printf "== Copying doc sources from local directory $FLAGS_sourceDir \n"
printf "=================================================================== \n"
- cp -r "$FLAGS_sourceDir/javadoc/." $doclavaNewDir
cp -r "$FLAGS_sourceDir/dokkaKotlinDocs/." $dokkaNewDir
cp -r "$FLAGS_sourceDir/dackkaDocs/." $newDir
diff --git a/development/validateRefactor.sh b/development/validateRefactor.sh
index 2fd06a3..b73021b 100755
--- a/development/validateRefactor.sh
+++ b/development/validateRefactor.sh
@@ -157,5 +157,5 @@
# Don't care about maven-metadata files because they have timestamps in them
# We might care to know whether .sha1 or .md5 files have changed, but changes in those files will always be accompanied by more meaningful changes in other files, so we don't need to show changes in .sha1 or .md5 files
# We also don't care about several specific files, either
-echoAndDo diff -r -x "*.md5*" -x "*.sha*" -x "*maven-metadata.xml" -x buildSrc.jar -x jetpad-integration.jar -x "top-of-tree-m2repository-all-0.zip" -x noto-emoji-compat-java.jar -x versionedparcelable-annotation.jar -x dokkaTipOfTreeDocs-0.zip -x fakeannotations.jar -x "doclava*.jar" "$oldOutPath/dist" "$newOutPath/dist"
+echoAndDo diff -r -x "*.md5*" -x "*.sha*" -x "*maven-metadata.xml" -x buildSrc.jar -x jetpad-integration.jar -x "top-of-tree-m2repository-all-0.zip" -x noto-emoji-compat-java.jar -x versionedparcelable-annotation.jar -x dokkaTipOfTreeDocs-0.zip -x fakeannotations.jar "$oldOutPath/dist" "$newOutPath/dist"
echo end of difference
diff --git a/emoji2/emoji2-emojipicker/build.gradle b/emoji2/emoji2-emojipicker/build.gradle
index e28cb79..008a65a 100644
--- a/emoji2/emoji2-emojipicker/build.gradle
+++ b/emoji2/emoji2-emojipicker/build.gradle
@@ -24,7 +24,10 @@
dependencies {
api(libs.kotlinStdlib)
- // Add dependencies here
+ implementation("androidx.core:core-ktx:1.8.0")
+ implementation project(path: ':emoji2:emoji2')
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testCore)
}
android {
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
new file mode 100644
index 0000000..00f13b4
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@SmallTest
+class BundledEmojiListLoaderTest {
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private val emojiCompatMetadata = EmojiPickerView.EmojiCompatMetadata(null, false)
+
+ @Test
+ fun testGetCategorizedEmojiData_loaded() {
+ BundledEmojiListLoader.load(context, emojiCompatMetadata)
+ assertTrue(BundledEmojiListLoader.categorizedEmojiData.isNotEmpty())
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 21)
+ fun testGetEmojiVariantsLookup_loaded() {
+ BundledEmojiListLoader.load(context, emojiCompatMetadata)
+ // 👃 has variants (👃,👃,👃🏻,👃🏼,👃🏽,👃🏾,👃🏿)
+ assertTrue(
+ BundledEmojiListLoader
+ .emojiVariantsLookup["\uD83D\uDC43"]
+ !!.contains("\uD83D\uDC43\uD83C\uDFFD")
+ )
+ // 😀 has no variant
+ assertFalse(
+ BundledEmojiListLoader.emojiVariantsLookup.containsKey("\uD83D\uDE00")
+ )
+ }
+}
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/utils/UnicodeRenderableManagerTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/utils/UnicodeRenderableManagerTest.kt
new file mode 100644
index 0000000..df69ce8
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/utils/UnicodeRenderableManagerTest.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker.utils
+
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@SmallTest
+class UnicodeRenderableManagerTest {
+ @Test
+ @SdkSuppress(minSdkVersion = 21, maxSdkVersion = 22)
+ fun testGetClosestRenderable_lowerVersionTrimmed() {
+ // #️⃣
+ assertEquals(
+ UnicodeRenderableManager.getClosestRenderable("\u0023\uFE0F\u20E3"),
+ "\u0023\u20E3"
+ )
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 23)
+ fun testGetClosestRenderable_higherVersionNoTrim() {
+ // #️⃣
+ assertEquals(
+ UnicodeRenderableManager.getClosestRenderable("\u0023\uFE0F\u20E3"),
+ "\u0023\uFE0F\u20E3"
+ )
+ }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
new file mode 100644
index 0000000..9d1b492
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker
+
+import android.content.Context
+import androidx.core.content.res.use
+import androidx.emoji2.emojipicker.utils.UnicodeRenderableManager
+
+/**
+ * A data loader that loads the following objects either from file based caches or from resources.
+ *
+ * @property categorizedEmojiData: a list that holds bundled emoji separated by category, filtered
+ * by renderability check. This is the data source for EmojiPickerView.
+ *
+ * @property emojiVariantsLookup: a map of emoji variants in bundled emoji, keyed by the primary
+ * emoji. This allows faster variants lookup.
+ */
+internal object BundledEmojiListLoader {
+
+ private var _categorizedEmojiData: List<EmojiDataCategory>? = null
+ private var _emojiVariantsLookup: Map<String, List<String>>? = null
+
+ internal fun load(context: Context, emojiCompatMetadata: EmojiPickerView.EmojiCompatMetadata) {
+ // TODO(chelseahao): load from cache.
+ val categoryNames = context.resources.getStringArray(R.array.category_names)
+
+ _categorizedEmojiData = context.resources
+ .obtainTypedArray(R.array.emoji_by_category_raw_resources)
+ .use { ta ->
+ (0 until ta.length()).map {
+ EmojiDataCategory(
+ categoryNames[it],
+ loadSingleCategory(
+ context,
+ emojiCompatMetadata,
+ ta.getResourceId(it, 0)
+ )
+ )
+ }.toList()
+ }
+
+ _emojiVariantsLookup =
+ _categorizedEmojiData!!
+ .map { it.emojiDataList }
+ .flatten()
+ .filter { it.variants.isNotEmpty() }
+ .associate { it.primary to it.variants }
+ }
+
+ internal val categorizedEmojiData: List<EmojiDataCategory>
+ get() = _categorizedEmojiData
+ ?: throw IllegalStateException("BundledEmojiListLoader.load is not called")
+
+ internal val emojiVariantsLookup: Map<String, List<String>>
+ get() = _emojiVariantsLookup
+ ?: throw IllegalStateException("BundledEmojiListLoader.load is not called")
+
+ private fun loadSingleCategory(
+ context: Context,
+ emojiCompatMetadata: EmojiPickerView.EmojiCompatMetadata,
+ resId: Int,
+ ): List<EmojiData> =
+ context.resources
+ .openRawResource(resId)
+ .bufferedReader()
+ .useLines { it.toList() }
+ .map { filterRenderableEmojis(it.split(","), emojiCompatMetadata) }
+ .filter { it.isNotEmpty() }
+ .map { EmojiData(it.first(), it.drop(1)) }
+
+ /**
+ * To eliminate 'Tofu' (the fallback glyph when an emoji is not renderable), check the
+ * renderability of emojis and keep only when they are renderable on the current device.
+ */
+ private fun filterRenderableEmojis(
+ emojiList: List<String>,
+ emojiCompatMetadata: EmojiPickerView.EmojiCompatMetadata,
+ ) = emojiList.filter {
+ UnicodeRenderableManager.isEmojiRenderable(it, emojiCompatMetadata)
+ }.toList()
+
+ internal data class EmojiData(val primary: String, val variants: List<String>)
+
+ internal data class EmojiDataCategory(
+ val categoryName: String,
+ val emojiDataList: List<EmojiData>
+ )
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
new file mode 100644
index 0000000..70efdef
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker
+
+/**
+ * The emoji picker view that provides up-to-date emojis in a vertical scrollable view with a
+ * clickable horizontal header.
+ */
+// TODO(scduan): API surface and implementation
+internal class EmojiPickerView {
+
+ /**
+ * MetaVersion will be null if EmojiCompat is not enabled.
+ */
+ internal data class EmojiCompatMetadata(val metaVersion: Int?, val replaceAll: Boolean)
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/UnicodeRenderableManager.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/UnicodeRenderableManager.kt
new file mode 100644
index 0000000..5f6386a
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/UnicodeRenderableManager.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker.utils
+
+import android.os.Build
+import android.text.TextPaint
+import androidx.annotation.VisibleForTesting
+import androidx.core.graphics.PaintCompat
+import androidx.emoji2.emojipicker.EmojiPickerView
+import androidx.emoji2.text.EmojiCompat
+
+/**
+ * Checks renderability of unicode characters.
+ */
+internal object UnicodeRenderableManager {
+
+ private const val VARIATION_SELECTOR = "\uFE0F"
+
+ private val paint = TextPaint()
+
+ /**
+ * Some emojis were usual (non-emoji) characters.
+ * Old devices cannot render them with variation selector (U+FE0F)
+ * so it's worth trying to check renderability again without variation selector.
+ */
+ private val CATEGORY_MOVED_EMOJIS =
+ listOf( // These three characters have been emoji since Unicode emoji version 4.
+ // version 3: https://unicode.org/Public/emoji/3.0/emoji-data.txt
+ // version 4: https://unicode.org/Public/emoji/4.0/emoji-data.txt
+ "\u2695\uFE0F", // STAFF OF AESCULAPIUS
+ "\u2640\uFE0F", // FEMALE SIGN
+ "\u2642\uFE0F", // MALE SIGN
+ // These three characters have been emoji since Unicode emoji version 11.
+ // version 5: https://unicode.org/Public/emoji/5.0/emoji-data.txt
+ // version 11: https://unicode.org/Public/emoji/11.0/emoji-data.txt
+ "\u265F\uFE0F", // BLACK_CHESS_PAWN
+ "\u267E\uFE0F" // PERMANENT_PAPER_SIGN
+ )
+
+ /**
+ * For a given emoji, check it's renderability with EmojiCompat if enabled. Otherwise, use
+ * [PaintCompat#hasGlyph].
+ *
+ * Note: For older API version, codepoints {@code U+0xFE0F} are removed.
+ */
+ internal fun isEmojiRenderable(
+ emoji: String,
+ emojiCompatMetaData: EmojiPickerView.EmojiCompatMetadata
+ ) = emojiCompatMetaData.metaVersion?.run {
+ EmojiCompat.get().getEmojiMatch(emoji, this) > 0
+ } ?: (getClosestRenderable(emoji) != null)
+
+ @VisibleForTesting
+ fun getClosestRenderable(emoji: String): String? {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ return emoji.replace(VARIATION_SELECTOR, "").takeIfHasGlyph()
+ }
+ return emoji.takeIfHasGlyph() ?: run {
+ if (CATEGORY_MOVED_EMOJIS.contains(emoji))
+ emoji.replace(VARIATION_SELECTOR, "").takeIfHasGlyph()
+ else null
+ }
+ }
+
+ private fun String.takeIfHasGlyph() = takeIf { PaintCompat.hasGlyph(paint, this) }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_activity.csv b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_activity.csv
new file mode 100644
index 0000000..2df9810
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_activity.csv
@@ -0,0 +1,113 @@
+🎉
+🎊
+🎈
+🎂
+🎀
+🎁
+🎇
+🎆
+🧨
+🧧
+🪔
+🪅
+🪩
+🎐
+🎏
+🎎
+🎑
+🎍
+🎋
+🎄
+🎃
+🎗️
+🥇
+🥈
+🥉
+🏅
+🎖️
+🏆
+📢
+⚽
+⚾
+🥎
+🏀
+🏐
+🏈
+🏉
+🎾
+🥅
+🏸
+🥍
+🏏
+🏑
+🏒
+🥌
+🛷
+🎿
+⛸️
+🛼
+🩰
+🛹
+⛳
+🎯
+🏹
+🥏
+🪃
+🪁
+🎣
+🤿
+🩱
+🎽
+🥋
+🥊
+🎱
+🏓
+🎳
+♟️
+🪀
+🧩
+🎮
+🕹️
+👾
+🔫
+🎲
+🎰
+🎴
+🀄
+🃏
+🪄
+🎩
+📷
+📸
+🖼️
+🎨
+🖌️
+🖍️
+🪡
+🧵
+🧶
+🎹
+🎷
+🎺
+🎸
+🪕
+🎻
+🪘
+🥁
+🪗
+🎤
+🎧
+🎚️
+🎛️
+🎙️
+📻
+📺
+📼
+📹
+📽️
+🎥
+🎞️
+🎬
+🎭
+🎫
+🎟️
diff --git a/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_animals_nature.csv b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_animals_nature.csv
new file mode 100644
index 0000000..d66c14b
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_animals_nature.csv
@@ -0,0 +1,210 @@
+💐
+🌹
+🥀
+🌺
+🌷
+🪷
+🌸
+💮
+🏵️
+🌻
+🌼
+🍂
+🍁
+🍄
+🌾
+🌱
+🌿
+🍃
+☘️
+🍀
+🪴
+🌵
+🌴
+🌳
+🌲
+🪹
+🪺
+🪵
+🪨
+⛰️
+🏔️
+❄️
+☃️
+⛄
+🌊
+🫧
+🌬️
+🌀
+🌪️
+🌁
+🌫️
+🌡️
+🔥
+🌋
+🏜️
+🏞️
+🏖️
+⛱️
+🌅
+🌄
+☀️
+🌤️
+⛅
+🌥️
+🌦️
+☁️
+🌨️
+⛈️
+🌩️
+🌧️
+💧
+☔
+⚡
+🌈
+⭐
+🌟
+💫
+✨
+🌙
+☄️
+🌠
+🌌
+🪐
+🌑
+🌒
+🌓
+🌔
+🌕
+🌖
+🌗
+🌘
+🌍
+🌎
+🌏
+🙈
+🙉
+🙊
+🐵
+🦁
+🐯
+🐱
+🐶
+🐺
+🐻
+🐻❄️
+🐨
+🐼
+🐹
+🐭
+🐰
+🦊
+🦝
+🐮
+🐷
+🐽
+🐗
+🦓
+🦄
+🐴
+🐸
+🐲
+🦎
+🐉
+🦖
+🦕
+🐢
+🐊
+🐍
+🐁
+🐀
+🐇
+🐈
+🐈⬛
+🐩
+🐕
+🦮
+🐕🦺
+🐅
+🐆
+🐎
+🐖
+🐄
+🐂
+🐃
+🦬
+🐏
+🐑
+🐐
+🦌
+🦙
+🦥
+🦘
+🐘
+🦣
+🦏
+🦛
+🦒
+🐒
+🦍
+🦧
+🐪
+🐫
+🐿️
+🦫
+🦨
+🦡
+🦔
+🦦
+🦇
+🪶
+🦅
+🦉
+🐓
+🐔
+🐣
+🐤
+🐥
+🐦
+🦜
+🕊️
+🦤
+🦢
+🦩
+🦚
+🦃
+🦆
+🐧
+🦭
+🦈
+🐬
+🐋
+🐳
+🐟
+🐠
+🐡
+🦐
+🦞
+🦀
+🦑
+🐙
+🦪
+🪸
+🦂
+🕷️
+🕸️
+🐚
+🐌
+🐜
+🦗
+🪲
+🦟
+🪳
+🪰
+🐝
+🐞
+🦋
+🐛
+🪱
+🦠
+🐾
diff --git a/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_emotions.csv b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_emotions.csv
new file mode 100644
index 0000000..0a1fcd4
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_emotions.csv
@@ -0,0 +1,236 @@
+😀
+😃
+😄
+😁
+😆
+😅
+😂
+🤣
+😭
+😉
+😗
+😙
+😚
+😘
+🥰
+😍
+🤩
+🥳
+🙃
+🙂
+🥲
+🥹
+😋
+😛
+😝
+😜
+🤪
+😇
+😊
+☺️
+😏
+😌
+😔
+😑
+😐
+😶
+🫡
+🤔
+🤫
+🫢
+🤭
+🥱
+🤗
+🫣
+😱
+🤨
+🧐
+😒
+🙄
+😮💨
+😤
+😠
+😡
+🤬
+🥺
+😟
+😥
+😢
+☹️
+🙁
+🫤
+😕
+🤐
+😰
+😨
+😧
+😦
+😮
+😯
+😲
+😳
+🤯
+😬
+😓
+😞
+😖
+😣
+😩
+😫
+😵
+😵💫
+🫥
+😴
+😪
+🤤
+🌛
+🌜
+🌚
+🌝
+🌞
+🫠
+😶🌫️
+🥴
+🥵
+🥶
+🤢
+🤮
+🤧
+🤒
+🤕
+😷
+🤠
+🤑
+😎
+🤓
+🥸
+🤥
+🤡
+👻
+💩
+👽
+🤖
+🎃
+😈
+👿
+👹
+👺
+🔥
+💫
+⭐
+🌟
+✨
+💥
+💯
+💢
+💨
+💦
+🫧
+💤
+🕳️
+🎉
+🎊
+🙈
+🙉
+🙊
+😺
+😸
+😹
+😻
+😼
+😽
+🙀
+😿
+😾
+❤️
+🧡
+💛
+💚
+💙
+💜
+🤎
+🖤
+🤍
+♥️
+💘
+💝
+💖
+💗
+💓
+💞
+💕
+💌
+💟
+❣️
+❤️🩹
+💔
+❤️🔥
+💋
+🫂
+👥
+👤
+🗣️
+👣
+🧠
+🫀
+🫁
+🩸
+🦠
+🦷
+🦴
+☠️
+💀
+👀
+👁️
+👄
+🫦
+👅
+👃,👃,👃🏻,👃🏼,👃🏽,👃🏾,👃🏿
+👂,👂,👂🏻,👂🏼,👂🏽,👂🏾,👂🏿
+🦻,🦻,🦻🏻,🦻🏼,🦻🏽,🦻🏾,🦻🏿
+🦶,🦶,🦶🏻,🦶🏼,🦶🏽,🦶🏾,🦶🏿
+🦵,🦵,🦵🏻,🦵🏼,🦵🏽,🦵🏾,🦵🏿
+🦿
+🦾
+💪,💪,💪🏻,💪🏼,💪🏽,💪🏾,💪🏿
+👍,👍,👍🏻,👍🏼,👍🏽,👍🏾,👍🏿
+👎,👎,👎🏻,👎🏼,👎🏽,👎🏾,👎🏿
+👏,👏,👏🏻,👏🏼,👏🏽,👏🏾,👏🏿
+🫶,🫶,🫶🏻,🫶🏼,🫶🏽,🫶🏾,🫶🏿
+🙌,🙌,🙌🏻,🙌🏼,🙌🏽,🙌🏾,🙌🏿
+👐,👐,👐🏻,👐🏼,👐🏽,👐🏾,👐🏿
+🤲,🤲,🤲🏻,🤲🏼,🤲🏽,🤲🏾,🤲🏿
+🤝,🤝,🤝🏻,🫱🏻🫲🏼,🫱🏻🫲🏽,🫱🏻🫲🏾,🫱🏻🫲🏿,🫱🏼🫲🏻,🤝🏼,🫱🏼🫲🏽,🫱🏼🫲🏾,🫱🏼🫲🏿,🫱🏽🫲🏻,🫱🏽🫲🏼,🤝🏽,🫱🏽🫲🏾,🫱🏽🫲🏿,🫱🏾🫲🏻,🫱🏾🫲🏼,🫱🏾🫲🏽,🤝🏾,🫱🏾🫲🏿,🫱🏿🫲🏻,🫱🏿🫲🏼,🫱🏿🫲🏽,🫱🏿🫲🏾,🤝🏿
+🤜,🤜,🤜🏻,🤜🏼,🤜🏽,🤜🏾,🤜🏿
+🤛,🤛,🤛🏻,🤛🏼,🤛🏽,🤛🏾,🤛🏿
+✊,✊,✊🏻,✊🏼,✊🏽,✊🏾,✊🏿
+👊,👊,👊🏻,👊🏼,👊🏽,👊🏾,👊🏿
+🫳,🫳,🫳🏻,🫳🏼,🫳🏽,🫳🏾,🫳🏿
+🫴,🫴,🫴🏻,🫴🏼,🫴🏽,🫴🏾,🫴🏿
+🫱,🫱,🫱🏻,🫱🏼,🫱🏽,🫱🏾,🫱🏿
+🫲,🫲,🫲🏻,🫲🏼,🫲🏽,🫲🏾,🫲🏿
+🤚,🤚,🤚🏻,🤚🏼,🤚🏽,🤚🏾,🤚🏿
+👋,👋,👋🏻,👋🏼,👋🏽,👋🏾,👋🏿
+🖐️,🖐️,🖐🏻,🖐🏼,🖐🏽,🖐🏾,🖐🏿
+✋,✋,✋🏻,✋🏼,✋🏽,✋🏾,✋🏿
+🖖,🖖,🖖🏻,🖖🏼,🖖🏽,🖖🏾,🖖🏿
+🤟,🤟,🤟🏻,🤟🏼,🤟🏽,🤟🏾,🤟🏿
+🤘,🤘,🤘🏻,🤘🏼,🤘🏽,🤘🏾,🤘🏿
+✌️,✌️,✌🏻,✌🏼,✌🏽,✌🏾,✌🏿
+🤞,🤞,🤞🏻,🤞🏼,🤞🏽,🤞🏾,🤞🏿
+🫰,🫰,🫰🏻,🫰🏼,🫰🏽,🫰🏾,🫰🏿
+🤙,🤙,🤙🏻,🤙🏼,🤙🏽,🤙🏾,🤙🏿
+🤌,🤌,🤌🏻,🤌🏼,🤌🏽,🤌🏾,🤌🏿
+🤏,🤏,🤏🏻,🤏🏼,🤏🏽,🤏🏾,🤏🏿
+👌,👌,👌🏻,👌🏼,👌🏽,👌🏾,👌🏿
+🖕,🖕,🖕🏻,🖕🏼,🖕🏽,🖕🏾,🖕🏿
+☝️,☝️,☝🏻,☝🏼,☝🏽,☝🏾,☝🏿
+👆,👆,👆🏻,👆🏼,👆🏽,👆🏾,👆🏿
+👇,👇,👇🏻,👇🏼,👇🏽,👇🏾,👇🏿
+👉,👉,👉🏻,👉🏼,👉🏽,👉🏾,👉🏿
+👈,👈,👈🏻,👈🏼,👈🏽,👈🏾,👈🏿
+🫵,🫵,🫵🏻,🫵🏼,🫵🏽,🫵🏾,🫵🏿
+✍️,✍️,✍🏻,✍🏼,✍🏽,✍🏾,✍🏿
+🤳,🤳,🤳🏻,🤳🏼,🤳🏽,🤳🏾,🤳🏿
+🙏,🙏,🙏🏻,🙏🏼,🙏🏽,🙏🏾,🙏🏿
+💅,💅,💅🏻,💅🏼,💅🏽,💅🏾,💅🏿
diff --git a/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_flags.csv b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_flags.csv
new file mode 100644
index 0000000..c8fd0c0
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_flags.csv
@@ -0,0 +1,269 @@
+🏁
+🚩
+🎌
+🏴
+🏳️
+🏳️🌈
+🏳️⚧️
+🏴☠️
+🇦🇨
+🇦🇩
+🇦🇪
+🇦🇫
+🇦🇬
+🇦🇮
+🇦🇱
+🇦🇲
+🇦🇴
+🇦🇶
+🇦🇷
+🇦🇸
+🇦🇹
+🇦🇺
+🇦🇼
+🇦🇽
+🇦🇿
+🇧🇦
+🇧🇧
+🇧🇩
+🇧🇪
+🇧🇫
+🇧🇬
+🇧🇭
+🇧🇮
+🇧🇯
+🇧🇱
+🇧🇲
+🇧🇳
+🇧🇴
+🇧🇶
+🇧🇷
+🇧🇸
+🇧🇹
+🇧🇻
+🇧🇼
+🇧🇾
+🇧🇿
+🇨🇦
+🇨🇨
+🇨🇩
+🇨🇫
+🇨🇬
+🇨🇭
+🇨🇮
+🇨🇰
+🇨🇱
+🇨🇲
+🇨🇳
+🇨🇴
+🇨🇵
+🇨🇷
+🇨🇺
+🇨🇻
+🇨🇼
+🇨🇽
+🇨🇾
+🇨🇿
+🇩🇪
+🇩🇬
+🇩🇯
+🇩🇰
+🇩🇲
+🇩🇴
+🇩🇿
+🇪🇦
+🇪🇨
+🇪🇪
+🇪🇬
+🇪🇭
+🇪🇷
+🇪🇸
+🇪🇹
+🇪🇺
+🇫🇮
+🇫🇯
+🇫🇰
+🇫🇲
+🇫🇴
+🇫🇷
+🇬🇦
+🇬🇧
+🇬🇩
+🇬🇪
+🇬🇫
+🇬🇬
+🇬🇭
+🇬🇮
+🇬🇱
+🇬🇲
+🇬🇳
+🇬🇵
+🇬🇶
+🇬🇷
+🇬🇸
+🇬🇹
+🇬🇺
+🇬🇼
+🇬🇾
+🇭🇰
+🇭🇲
+🇭🇳
+🇭🇷
+🇭🇹
+🇭🇺
+🇮🇨
+🇮🇩
+🇮🇪
+🇮🇱
+🇮🇲
+🇮🇳
+🇮🇴
+🇮🇶
+🇮🇷
+🇮🇸
+🇮🇹
+🇯🇪
+🇯🇲
+🇯🇴
+🇯🇵
+🇰🇪
+🇰🇬
+🇰🇭
+🇰🇮
+🇰🇲
+🇰🇳
+🇰🇵
+🇰🇷
+🇰🇼
+🇰🇾
+🇰🇿
+🇱🇦
+🇱🇧
+🇱🇨
+🇱🇮
+🇱🇰
+🇱🇷
+🇱🇸
+🇱🇹
+🇱🇺
+🇱🇻
+🇱🇾
+🇲🇦
+🇲🇨
+🇲🇩
+🇲🇪
+🇲🇫
+🇲🇬
+🇲🇭
+🇲🇰
+🇲🇱
+🇲🇲
+🇲🇳
+🇲🇴
+🇲🇵
+🇲🇶
+🇲🇷
+🇲🇸
+🇲🇹
+🇲🇺
+🇲🇻
+🇲🇼
+🇲🇽
+🇲🇾
+🇲🇿
+🇳🇦
+🇳🇨
+🇳🇪
+🇳🇫
+🇳🇬
+🇳🇮
+🇳🇱
+🇳🇴
+🇳🇵
+🇳🇷
+🇳🇺
+🇳🇿
+🇴🇲
+🇵🇦
+🇵🇪
+🇵🇫
+🇵🇬
+🇵🇭
+🇵🇰
+🇵🇱
+🇵🇲
+🇵🇳
+🇵🇷
+🇵🇸
+🇵🇹
+🇵🇼
+🇵🇾
+🇶🇦
+🇷🇪
+🇷🇴
+🇷🇸
+🇷🇺
+🇷🇼
+🇸🇦
+🇸🇧
+🇸🇨
+🇸🇩
+🇸🇪
+🇸🇬
+🇸🇭
+🇸🇮
+🇸🇯
+🇸🇰
+🇸🇱
+🇸🇲
+🇸🇳
+🇸🇴
+🇸🇷
+🇸🇸
+🇸🇹
+🇸🇻
+🇸🇽
+🇸🇾
+🇸🇿
+🇹🇦
+🇹🇨
+🇹🇩
+🇹🇫
+🇹🇬
+🇹🇭
+🇹🇯
+🇹🇰
+🇹🇱
+🇹🇲
+🇹🇳
+🇹🇴
+🇹🇷
+🇹🇹
+🇹🇻
+🇹🇼
+🇹🇿
+🇺🇦
+🇺🇬
+🇺🇲
+🇺🇳
+🇺🇸
+🇺🇾
+🇺🇿
+🇻🇦
+🇻🇨
+🇻🇪
+🇻🇬
+🇻🇮
+🇻🇳
+🇻🇺
+🇼🇫
+🇼🇸
+🇽🇰
+🇾🇪
+🇾🇹
+🇿🇦
+🇿🇲
+🇿🇼
+🏴
+🏴
+🏴
diff --git a/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_food_drink.csv b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_food_drink.csv
new file mode 100644
index 0000000..0afc9c8
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_food_drink.csv
@@ -0,0 +1,127 @@
+🍓
+🍒
+🍎
+🍉
+🍑
+🍊
+🥭
+🍍
+🍌
+🍋
+🍈
+🍏
+🍐
+🥝
+🫒
+🫐
+🍇
+🥥
+🍅
+🌶️
+🥕
+🍠
+🧅
+🌽
+🥦
+🥒
+🥬
+🫑
+🥑
+🍆
+🧄
+🥔
+🫘
+🌰
+🥜
+🍞
+🫓
+🥐
+🥖
+🥯
+🧇
+🥞
+🍳
+🥚
+🧀
+🥓
+🥩
+🍗
+🍖
+🍔
+🌭
+🥪
+🥨
+🍟
+🍕
+🫔
+🌮
+🌯
+🥙
+🧆
+🥘
+🍝
+🥫
+🫕
+🥣
+🥗
+🍲
+🍛
+🍜
+🦪
+🦞
+🍣
+🍤
+🥡
+🍚
+🍱
+🥟
+🍢
+🍙
+🍘
+🍥
+🍡
+🥠
+🥮
+🍧
+🍨
+🍦
+🥧
+🍰
+🍮
+🎂
+🧁
+🍭
+🍬
+🍫
+🍩
+🍪
+🍯
+🧂
+🧈
+🍿
+🧊
+🫙
+🥤
+🧋
+🧃
+🥛
+🍼
+🍵
+☕
+🫖
+🧉
+🍺
+🍻
+🥂
+🍾
+🍷
+🥃
+🫗
+🍸
+🍹
+🍶
+🥢
+🍴
+🥄
+🔪
+🍽️
diff --git a/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_objects.csv b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_objects.csv
new file mode 100644
index 0000000..80437da
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_objects.csv
@@ -0,0 +1,259 @@
+📱
+☎️
+📞
+📟
+📠
+🔌
+🔋
+🪫
+🖲️
+💽
+💾
+💿
+📀
+🖥️
+💻
+⌨️
+🖨️
+🖱️
+🪙
+💸
+💵
+💴
+💶
+💷
+💳
+💰
+🧾
+🧮
+⚖️
+🛒
+🛍️
+🕯️
+💡
+🔦
+🏮
+🧱
+🪟
+🪞
+🚪
+🪑
+🛏️
+🛋️
+🚿
+🛁
+🚽
+🧻
+🪠
+🧸
+🪆
+🧷
+🪢
+🧹
+🧴
+🧽
+🧼
+🪥
+🪒
+🧺
+🧦
+🧤
+🧣
+👖
+👕
+🎽
+👚
+👔
+👗
+👘
+🥻
+🩱
+👙
+🩳
+🩲
+🧥
+🥼
+🦺
+⛑️
+🪖
+🎓
+🎩
+👒
+🧢
+👑
+🎒
+👝
+👛
+👜
+💼
+🧳
+☂️
+🌂
+💍
+💎
+💄
+👠
+👟
+👞
+🥿
+🩴
+👡
+👢
+🥾
+👓
+🕶️
+🦯
+🥽
+⚗️
+🧫
+🧪
+🌡️
+🧬
+💉
+💊
+🩹
+🩺
+🩻
+🔭
+🔬
+📡
+🛰️
+🧯
+🪓
+🪜
+🪣
+🪝
+🧲
+🧰
+🗜️
+🔩
+🪛
+🪚
+🔧
+🔨
+⚒️
+🛠️
+⛏️
+⚙️
+🔗
+⛓️
+📎
+🖇️
+📏
+📐
+✂️
+📌
+📍
+🗑️
+🖌️
+🖍️
+🖊️
+🖋️
+✒️
+✏️
+📝
+📒
+📔
+📕
+📓
+📗
+📘
+📙
+📚
+📖
+🔖
+🗒️
+📄
+📃
+📋
+📇
+📑
+🗃️
+🗄️
+🗂️
+📂
+📁
+📰
+🗞️
+📊
+📈
+📉
+🪧
+🪪
+🏷️
+📦
+📫
+📪
+📬
+📭
+📮
+✉️
+📧
+📩
+📨
+💌
+📤
+📥
+🗳️
+⏱️
+🕛
+🕧
+🕐
+🕜
+🕑
+🕝
+🕒
+🕞
+🕓
+🕟
+🕔
+🕠
+🕕
+🕡
+🕖
+🕢
+🕗
+🕣
+🕘
+🕤
+🕙
+🕥
+🕚
+🕦
+⌛
+⏳
+🕰️
+⌚
+⏲️
+⏰
+🗓️
+📅
+📆
+🛎️
+🔔
+📯
+📢
+📣
+🔍
+🔎
+🔮
+🧿
+🪬
+📿
+🏺
+⚱️
+⚰️
+🪦
+🚬
+💣
+🪤
+📜
+⚔️
+🗡️
+🛡️
+🗝️
+🔑
+🔐
+🔏
+🔒
+🔓
diff --git a/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_people.csv b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_people.csv
new file mode 100644
index 0000000..f6e06f6
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_people.csv
@@ -0,0 +1,151 @@
+🙇,🙇♀️,🙇🏻♀️,🙇🏼♀️,🙇🏽♀️,🙇🏾♀️,🙇🏿♀️,🙇♂️,🙇🏻♂️,🙇🏼♂️,🙇🏽♂️,🙇🏾♂️,🙇🏿♂️
+🙋,🙋♀️,🙋🏻♀️,🙋🏼♀️,🙋🏽♀️,🙋🏾♀️,🙋🏿♀️,🙋♂️,🙋🏻♂️,🙋🏼♂️,🙋🏽♂️,🙋🏾♂️,🙋🏿♂️
+💁,💁♀️,💁🏻♀️,💁🏼♀️,💁🏽♀️,💁🏾♀️,💁🏿♀️,💁♂️,💁🏻♂️,💁🏼♂️,💁🏽♂️,💁🏾♂️,💁🏿♂️
+🙆,🙆♀️,🙆🏻♀️,🙆🏼♀️,🙆🏽♀️,🙆🏾♀️,🙆🏿♀️,🙆♂️,🙆🏻♂️,🙆🏼♂️,🙆🏽♂️,🙆🏾♂️,🙆🏿♂️
+🙅,🙅♀️,🙅🏻♀️,🙅🏼♀️,🙅🏽♀️,🙅🏾♀️,🙅🏿♀️,🙅♂️,🙅🏻♂️,🙅🏼♂️,🙅🏽♂️,🙅🏾♂️,🙅🏿♂️
+🤷,🤷♀️,🤷🏻♀️,🤷🏼♀️,🤷🏽♀️,🤷🏾♀️,🤷🏿♀️,🤷♂️,🤷🏻♂️,🤷🏼♂️,🤷🏽♂️,🤷🏾♂️,🤷🏿♂️
+🤦,🤦♀️,🤦🏻♀️,🤦🏼♀️,🤦🏽♀️,🤦🏾♀️,🤦🏿♀️,🤦♂️,🤦🏻♂️,🤦🏼♂️,🤦🏽♂️,🤦🏾♂️,🤦🏿♂️
+🙍,🙍♀️,🙍🏻♀️,🙍🏼♀️,🙍🏽♀️,🙍🏾♀️,🙍🏿♀️,🙍♂️,🙍🏻♂️,🙍🏼♂️,🙍🏽♂️,🙍🏾♂️,🙍🏿♂️
+🙎,🙎♀️,🙎🏻♀️,🙎🏼♀️,🙎🏽♀️,🙎🏾♀️,🙎🏿♀️,🙎♂️,🙎🏻♂️,🙎🏼♂️,🙎🏽♂️,🙎🏾♂️,🙎🏿♂️
+🧏,🧏♀️,🧏🏻♀️,🧏🏼♀️,🧏🏽♀️,🧏🏾♀️,🧏🏿♀️,🧏♂️,🧏🏻♂️,🧏🏼♂️,🧏🏽♂️,🧏🏾♂️,🧏🏿♂️
+💆,💆♀️,💆🏻♀️,💆🏼♀️,💆🏽♀️,💆🏾♀️,💆🏿♀️,💆♂️,💆🏻♂️,💆🏼♂️,💆🏽♂️,💆🏾♂️,💆🏿♂️
+💇,💇♀️,💇🏻♀️,💇🏼♀️,💇🏽♀️,💇🏾♀️,💇🏿♀️,💇♂️,💇🏻♂️,💇🏼♂️,💇🏽♂️,💇🏾♂️,💇🏿♂️
+🧖,🧖♀️,🧖🏻♀️,🧖🏼♀️,🧖🏽♀️,🧖🏾♀️,🧖🏿♀️,🧖♂️,🧖🏻♂️,🧖🏼♂️,🧖🏽♂️,🧖🏾♂️,🧖🏿♂️
+🛀,🛀,🛀🏻,🛀🏼,🛀🏽,🛀🏾,🛀🏿
+🛌,🛌,🛌🏻,🛌🏼,🛌🏽,🛌🏾,🛌🏿
+🧘,🧘♀️,🧘🏻♀️,🧘🏼♀️,🧘🏽♀️,🧘🏾♀️,🧘🏿♀️,🧘♂️,🧘🏻♂️,🧘🏼♂️,🧘🏽♂️,🧘🏾♂️,🧘🏿♂️
+👨🦯,👨🦯,👨🏻🦯,👨🏼🦯,👨🏽🦯,👨🏾🦯,👨🏿🦯
+👩🦯,👩🦯,👩🏻🦯,👩🏼🦯,👩🏽🦯,👩🏾🦯,👩🏿🦯
+👨🦼,👨🦼,👨🏻🦼,👨🏼🦼,👨🏽🦼,👨🏾🦼,👨🏿🦼
+👩🦼,👩🦼,👩🏻🦼,👩🏼🦼,👩🏽🦼,👩🏾🦼,👩🏿🦼
+👨🦽,👨🦽,👨🏻🦽,👨🏼🦽,👨🏽🦽,👨🏾🦽,👨🏿🦽
+👩🦽,👩🦽,👩🏻🦽,👩🏼🦽,👩🏽🦽,👩🏾🦽,👩🏿🦽
+🧎,🧎♀️,🧎🏻♀️,🧎🏼♀️,🧎🏽♀️,🧎🏾♀️,🧎🏿♀️,🧎♂️,🧎🏻♂️,🧎🏼♂️,🧎🏽♂️,🧎🏾♂️,🧎🏿♂️
+🧍,🧍♀️,🧍🏻♀️,🧍🏼♀️,🧍🏽♀️,🧍🏾♀️,🧍🏿♀️,🧍♂️,🧍🏻♂️,🧍🏼♂️,🧍🏽♂️,🧍🏾♂️,🧍🏿♂️
+🚶,🚶♀️,🚶🏻♀️,🚶🏼♀️,🚶🏽♀️,🚶🏾♀️,🚶🏿♀️,🚶♂️,🚶🏻♂️,🚶🏼♂️,🚶🏽♂️,🚶🏾♂️,🚶🏿♂️
+🏃,🏃♀️,🏃🏻♀️,🏃🏼♀️,🏃🏽♀️,🏃🏾♀️,🏃🏿♀️,🏃♂️,🏃🏻♂️,🏃🏼♂️,🏃🏽♂️,🏃🏾♂️,🏃🏿♂️
+🤸,🤸♀️,🤸🏻♀️,🤸🏼♀️,🤸🏽♀️,🤸🏾♀️,🤸🏿♀️,🤸♂️,🤸🏻♂️,🤸🏼♂️,🤸🏽♂️,🤸🏾♂️,🤸🏿♂️
+🏋️,🏋️♀️,🏋🏻♀️,🏋🏼♀️,🏋🏽♀️,🏋🏾♀️,🏋🏿♀️,🏋️♂️,🏋🏻♂️,🏋🏼♂️,🏋🏽♂️,🏋🏾♂️,🏋🏿♂️
+⛹️,⛹️♀️,⛹🏻♀️,⛹🏼♀️,⛹🏽♀️,⛹🏾♀️,⛹🏿♀️,⛹️♂️,⛹🏻♂️,⛹🏼♂️,⛹🏽♂️,⛹🏾♂️,⛹🏿♂️
+🤾,🤾♀️,🤾🏻♀️,🤾🏼♀️,🤾🏽♀️,🤾🏾♀️,🤾🏿♀️,🤾♂️,🤾🏻♂️,🤾🏼♂️,🤾🏽♂️,🤾🏾♂️,🤾🏿♂️
+🚴,🚴♀️,🚴🏻♀️,🚴🏼♀️,🚴🏽♀️,🚴🏾♀️,🚴🏿♀️,🚴♂️,🚴🏻♂️,🚴🏼♂️,🚴🏽♂️,🚴🏾♂️,🚴🏿♂️
+🚵,🚵♀️,🚵🏻♀️,🚵🏼♀️,🚵🏽♀️,🚵🏾♀️,🚵🏿♀️,🚵♂️,🚵🏻♂️,🚵🏼♂️,🚵🏽♂️,🚵🏾♂️,🚵🏿♂️
+🧗,🧗♀️,🧗🏻♀️,🧗🏼♀️,🧗🏽♀️,🧗🏾♀️,🧗🏿♀️,🧗♂️,🧗🏻♂️,🧗🏼♂️,🧗🏽♂️,🧗🏾♂️,🧗🏿♂️
+🤼,🤼♀️,🤼♂️
+🤹,🤹♀️,🤹🏻♀️,🤹🏼♀️,🤹🏽♀️,🤹🏾♀️,🤹🏿♀️,🤹♂️,🤹🏻♂️,🤹🏼♂️,🤹🏽♂️,🤹🏾♂️,🤹🏿♂️
+🏌️,🏌️♀️,🏌🏻♀️,🏌🏼♀️,🏌🏽♀️,🏌🏾♀️,🏌🏿♀️,🏌️♂️,🏌🏻♂️,🏌🏼♂️,🏌🏽♂️,🏌🏾♂️,🏌🏿♂️
+🏇,🏇,🏇🏻,🏇🏼,🏇🏽,🏇🏾,🏇🏿
+🤺
+⛷️
+🏂,🏂,🏂🏻,🏂🏼,🏂🏽,🏂🏾,🏂🏿
+🪂
+🏄,🏄♀️,🏄🏻♀️,🏄🏼♀️,🏄🏽♀️,🏄🏾♀️,🏄🏿♀️,🏄♂️,🏄🏻♂️,🏄🏼♂️,🏄🏽♂️,🏄🏾♂️,🏄🏿♂️
+🚣,🚣♀️,🚣🏻♀️,🚣🏼♀️,🚣🏽♀️,🚣🏾♀️,🚣🏿♀️,🚣♂️,🚣🏻♂️,🚣🏼♂️,🚣🏽♂️,🚣🏾♂️,🚣🏿♂️
+🏊,🏊♀️,🏊🏻♀️,🏊🏼♀️,🏊🏽♀️,🏊🏾♀️,🏊🏿♀️,🏊♂️,🏊🏻♂️,🏊🏼♂️,🏊🏽♂️,🏊🏾♂️,🏊🏿♂️
+🤽,🤽♀️,🤽🏻♀️,🤽🏼♀️,🤽🏽♀️,🤽🏾♀️,🤽🏿♀️,🤽♂️,🤽🏻♂️,🤽🏼♂️,🤽🏽♂️,🤽🏾♂️,🤽🏿♂️
+🧜,🧜♀️,🧜🏻♀️,🧜🏼♀️,🧜🏽♀️,🧜🏾♀️,🧜🏿♀️,🧜♂️,🧜🏻♂️,🧜🏼♂️,🧜🏽♂️,🧜🏾♂️,🧜🏿♂️
+🧚,🧚♀️,🧚🏻♀️,🧚🏼♀️,🧚🏽♀️,🧚🏾♀️,🧚🏿♀️,🧚♂️,🧚🏻♂️,🧚🏼♂️,🧚🏽♂️,🧚🏾♂️,🧚🏿♂️
+🧞,🧞♀️,🧞♂️
+🦸,🦸♀️,🦸🏻♀️,🦸🏼♀️,🦸🏽♀️,🦸🏾♀️,🦸🏿♀️,🦸♂️,🦸🏻♂️,🦸🏼♂️,🦸🏽♂️,🦸🏾♂️,🦸🏿♂️
+🦹,🦹♀️,🦹🏻♀️,🦹🏼♀️,🦹🏽♀️,🦹🏾♀️,🦹🏿♀️,🦹♂️,🦹🏻♂️,🦹🏼♂️,🦹🏽♂️,🦹🏾♂️,🦹🏿♂️
+🧝,🧝♀️,🧝🏻♀️,🧝🏼♀️,🧝🏽♀️,🧝🏾♀️,🧝🏿♀️,🧝♂️,🧝🏻♂️,🧝🏼♂️,🧝🏽♂️,🧝🏾♂️,🧝🏿♂️
+🧙,🧙♀️,🧙🏻♀️,🧙🏼♀️,🧙🏽♀️,🧙🏾♀️,🧙🏿♀️,🧙♂️,🧙🏻♂️,🧙🏼♂️,🧙🏽♂️,🧙🏾♂️,🧙🏿♂️
+🧟,🧟♀️,🧟♂️
+🧛,🧛♀️,🧛🏻♀️,🧛🏼♀️,🧛🏽♀️,🧛🏾♀️,🧛🏿♀️,🧛♂️,🧛🏻♂️,🧛🏼♂️,🧛🏽♂️,🧛🏾♂️,🧛🏿♂️
+👼,👼,👼🏻,👼🏼,👼🏽,👼🏾,👼🏿
+🎅,🎅,🎅🏻,🎅🏼,🎅🏽,🎅🏾,🎅🏿
+🤶,🤶,🤶🏻,🤶🏼,🤶🏽,🤶🏾,🤶🏿
+💂,💂♀️,💂🏻♀️,💂🏼♀️,💂🏽♀️,💂🏾♀️,💂🏿♀️,💂♂️,💂🏻♂️,💂🏼♂️,💂🏽♂️,💂🏾♂️,💂🏿♂️
+🤴,🤴,🤴🏻,🤴🏼,🤴🏽,🤴🏾,🤴🏿
+👸,👸,👸🏻,👸🏼,👸🏽,👸🏾,👸🏿
+🤵,🤵,🤵🏻,🤵🏼,🤵🏽,🤵🏾,🤵🏿
+👰,👰,👰🏻,👰🏼,👰🏽,👰🏾,👰🏿
+👩🚀,👨🚀,👨🏻🚀,👨🏼🚀,👨🏽🚀,👨🏾🚀,👨🏿🚀,👩🚀,👩🏻🚀,👩🏼🚀,👩🏽🚀,👩🏾🚀,👩🏿🚀
+👷,👷♀️,👷🏻♀️,👷🏼♀️,👷🏽♀️,👷🏾♀️,👷🏿♀️,👷♂️,👷🏻♂️,👷🏼♂️,👷🏽♂️,👷🏾♂️,👷🏿♂️
+👮,👮♀️,👮🏻♀️,👮🏼♀️,👮🏽♀️,👮🏾♀️,👮🏿♀️,👮♂️,👮🏻♂️,👮🏼♂️,👮🏽♂️,👮🏾♂️,👮🏿♂️
+🕵️,🕵️♀️,🕵🏻♀️,🕵🏼♀️,🕵🏽♀️,🕵🏾♀️,🕵🏿♀️,🕵️♂️,🕵🏻♂️,🕵🏼♂️,🕵🏽♂️,🕵🏾♂️,🕵🏿♂️
+👩✈️,👨✈️,👨🏻✈️,👨🏼✈️,👨🏽✈️,👨🏾✈️,👨🏿✈️,👩✈️,👩🏻✈️,👩🏼✈️,👩🏽✈️,👩🏾✈️,👩🏿✈️
+👩🔬,👨🔬,👨🏻🔬,👨🏼🔬,👨🏽🔬,👨🏾🔬,👨🏿🔬,👩🔬,👩🏻🔬,👩🏼🔬,👩🏽🔬,👩🏾🔬,👩🏿🔬
+👩⚕️,👨⚕️,👨🏻⚕️,👨🏼⚕️,👨🏽⚕️,👨🏾⚕️,👨🏿⚕️,👩⚕️,👩🏻⚕️,👩🏼⚕️,👩🏽⚕️,👩🏾⚕️,👩🏿⚕️
+👩🔧,👨🔧,👨🏻🔧,👨🏼🔧,👨🏽🔧,👨🏾🔧,👨🏿🔧,👩🔧,👩🏻🔧,👩🏼🔧,👩🏽🔧,👩🏾🔧,👩🏿🔧
+👩🏭,👨🏭,👨🏻🏭,👨🏼🏭,👨🏽🏭,👨🏾🏭,👨🏿🏭,👩🏭,👩🏻🏭,👩🏼🏭,👩🏽🏭,👩🏾🏭,👩🏿🏭
+👩🚒,👨🚒,👨🏻🚒,👨🏼🚒,👨🏽🚒,👨🏾🚒,👨🏿🚒,👩🚒,👩🏻🚒,👩🏼🚒,👩🏽🚒,👩🏾🚒,👩🏿🚒
+👩🌾,👨🌾,👨🏻🌾,👨🏼🌾,👨🏽🌾,👨🏾🌾,👨🏿🌾,👩🌾,👩🏻🌾,👩🏼🌾,👩🏽🌾,👩🏾🌾,👩🏿🌾
+👩🏫,👨🏫,👨🏻🏫,👨🏼🏫,👨🏽🏫,👨🏾🏫,👨🏿🏫,👩🏫,👩🏻🏫,👩🏼🏫,👩🏽🏫,👩🏾🏫,👩🏿🏫
+👩🎓,👨🎓,👨🏻🎓,👨🏼🎓,👨🏽🎓,👨🏾🎓,👨🏿🎓,👩🎓,👩🏻🎓,👩🏼🎓,👩🏽🎓,👩🏾🎓,👩🏿🎓
+👩💼,👨💼,👨🏻💼,👨🏼💼,👨🏽💼,👨🏾💼,👨🏿💼,👩💼,👩🏻💼,👩🏼💼,👩🏽💼,👩🏾💼,👩🏿💼
+👩⚖️,👨⚖️,👨🏻⚖️,👨🏼⚖️,👨🏽⚖️,👨🏾⚖️,👨🏿⚖️,👩⚖️,👩🏻⚖️,👩🏼⚖️,👩🏽⚖️,👩🏾⚖️,👩🏿⚖️
+👩💻,👨💻,👨🏻💻,👨🏼💻,👨🏽💻,👨🏾💻,👨🏿💻,👩💻,👩🏻💻,👩🏼💻,👩🏽💻,👩🏾💻,👩🏿💻
+👩🎤,👨🎤,👨🏻🎤,👨🏼🎤,👨🏽🎤,👨🏾🎤,👨🏿🎤,👩🎤,👩🏻🎤,👩🏼🎤,👩🏽🎤,👩🏾🎤,👩🏿🎤
+👩🎨,👨🎨,👨🏻🎨,👨🏼🎨,👨🏽🎨,👨🏾🎨,👨🏿🎨,👩🎨,👩🏻🎨,👩🏼🎨,👩🏽🎨,👩🏾🎨,👩🏿🎨
+👩🍳,👨🍳,👨🏻🍳,👨🏼🍳,👨🏽🍳,👨🏾🍳,👨🏿🍳,👩🍳,👩🏻🍳,👩🏼🍳,👩🏽🍳,👩🏾🍳,👩🏿🍳
+👳,👳♀️,👳🏻♀️,👳🏼♀️,👳🏽♀️,👳🏾♀️,👳🏿♀️,👳♂️,👳🏻♂️,👳🏼♂️,👳🏽♂️,👳🏾♂️,👳🏿♂️
+🧕,🧕,🧕🏻,🧕🏼,🧕🏽,🧕🏾,🧕🏿
+👲,👲,👲🏻,👲🏼,👲🏽,👲🏾,👲🏿
+👶,👶,👶🏻,👶🏼,👶🏽,👶🏾,👶🏿
+🧒,🧒,🧒🏻,🧒🏼,🧒🏽,🧒🏾,🧒🏿
+👦,👦,👦🏻,👦🏼,👦🏽,👦🏾,👦🏿
+👧,👧,👧🏻,👧🏼,👧🏽,👧🏾,👧🏿
+🧑,🧑,🧑🏻,🧑🏼,🧑🏽,🧑🏾,🧑🏿
+👨,👨,👨🏻,👨🏼,👨🏽,👨🏾,👨🏿
+👩,👩,👩🏻,👩🏼,👩🏽,👩🏾,👩🏿
+🧓,🧓,🧓🏻,🧓🏼,🧓🏽,🧓🏾,🧓🏿
+👴,👴,👴🏻,👴🏼,👴🏽,👴🏾,👴🏿
+👵,👵,👵🏻,👵🏼,👵🏽,👵🏾,👵🏿
+👨🦳,👨🦳,👨🏻🦳,👨🏼🦳,👨🏽🦳,👨🏾🦳,👨🏿🦳
+👩🦳,👩🦳,👩🏻🦳,👩🏼🦳,👩🏽🦳,👩🏾🦳,👩🏿🦳
+👨🦰,👨🦰,👨🏻🦰,👨🏼🦰,👨🏽🦰,👨🏾🦰,👨🏿🦰
+👩🦰,👩🦰,👩🏻🦰,👩🏼🦰,👩🏽🦰,👩🏾🦰,👩🏿🦰
+👱,👱♀️,👱🏻♀️,👱🏼♀️,👱🏽♀️,👱🏾♀️,👱🏿♀️,👱♂️,👱🏻♂️,👱🏼♂️,👱🏽♂️,👱🏾♂️,👱🏿♂️
+👨🦱,👨🦱,👨🏻🦱,👨🏼🦱,👨🏽🦱,👨🏾🦱,👨🏿🦱
+👩🦱,👩🦱,👩🏻🦱,👩🏼🦱,👩🏽🦱,👩🏾🦱,👩🏿🦱
+👨🦲,👨🦲,👨🏻🦲,👨🏼🦲,👨🏽🦲,👨🏾🦲,👨🏿🦲
+👩🦲,👩🦲,👩🏻🦲,👩🏼🦲,👩🏽🦲,👩🏾🦲,👩🏿🦲
+🧔,🧔,🧔🏻,🧔🏼,🧔🏽,🧔🏾,🧔🏿
+🕴️,🕴️,🕴🏻,🕴🏼,🕴🏽,🕴🏾,🕴🏿
+💃,💃,💃🏻,💃🏼,💃🏽,💃🏾,💃🏿
+🕺,🕺,🕺🏻,🕺🏼,🕺🏽,🕺🏾,🕺🏿
+👯,👯♀️,👯♂️
+👭
+👫
+👬
+💏
+👩❤️💋👨
+👨❤️💋👨
+👩❤️💋👩
+💑
+👩❤️👨
+👨❤️👨
+👩❤️👩
+👪
+👨👩👦
+👨👩👧
+👨👩👧👦
+👨👩👦👦
+👨👩👧👧
+👨👨👦
+👨👨👧
+👨👨👧👦
+👨👨👦👦
+👨👨👧👧
+👩👩👦
+👩👩👧
+👩👩👧👦
+👩👩👦👦
+👩👩👧👧
+👨👦
+👨👦👦
+👨👧
+👨👧👦
+👨👧👧
+👩👦
+👩👦👦
+👩👧
+👩👧👦
+👩👧👧
+🤰,🤰,🤰🏻,🤰🏼,🤰🏽,🤰🏾,🤰🏿
+🤱,🤱,🤱🏻,🤱🏼,🤱🏽,🤱🏾,🤱🏿
+🗣️
+👤
+👥
+👣
diff --git a/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_people_gender_inclusive.csv b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_people_gender_inclusive.csv
new file mode 100644
index 0000000..6927525
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_people_gender_inclusive.csv
@@ -0,0 +1,111 @@
+🙇,🙇,🙇🏻,🙇🏼,🙇🏽,🙇🏾,🙇🏿,🙇♀️,🙇🏻♀️,🙇🏼♀️,🙇🏽♀️,🙇🏾♀️,🙇🏿♀️,🙇♂️,🙇🏻♂️,🙇🏼♂️,🙇🏽♂️,🙇🏾♂️,🙇🏿♂️
+🙋,🙋,🙋🏻,🙋🏼,🙋🏽,🙋🏾,🙋🏿,🙋♀️,🙋🏻♀️,🙋🏼♀️,🙋🏽♀️,🙋🏾♀️,🙋🏿♀️,🙋♂️,🙋🏻♂️,🙋🏼♂️,🙋🏽♂️,🙋🏾♂️,🙋🏿♂️
+💁,💁,💁🏻,💁🏼,💁🏽,💁🏾,💁🏿,💁♀️,💁🏻♀️,💁🏼♀️,💁🏽♀️,💁🏾♀️,💁🏿♀️,💁♂️,💁🏻♂️,💁🏼♂️,💁🏽♂️,💁🏾♂️,💁🏿♂️
+🙆,🙆,🙆🏻,🙆🏼,🙆🏽,🙆🏾,🙆🏿,🙆♀️,🙆🏻♀️,🙆🏼♀️,🙆🏽♀️,🙆🏾♀️,🙆🏿♀️,🙆♂️,🙆🏻♂️,🙆🏼♂️,🙆🏽♂️,🙆🏾♂️,🙆🏿♂️
+🙅,🙅,🙅🏻,🙅🏼,🙅🏽,🙅🏾,🙅🏿,🙅♀️,🙅🏻♀️,🙅🏼♀️,🙅🏽♀️,🙅🏾♀️,🙅🏿♀️,🙅♂️,🙅🏻♂️,🙅🏼♂️,🙅🏽♂️,🙅🏾♂️,🙅🏿♂️
+🤷,🤷,🤷🏻,🤷🏼,🤷🏽,🤷🏾,🤷🏿,🤷♀️,🤷🏻♀️,🤷🏼♀️,🤷🏽♀️,🤷🏾♀️,🤷🏿♀️,🤷♂️,🤷🏻♂️,🤷🏼♂️,🤷🏽♂️,🤷🏾♂️,🤷🏿♂️
+🤦,🤦,🤦🏻,🤦🏼,🤦🏽,🤦🏾,🤦🏿,🤦♀️,🤦🏻♀️,🤦🏼♀️,🤦🏽♀️,🤦🏾♀️,🤦🏿♀️,🤦♂️,🤦🏻♂️,🤦🏼♂️,🤦🏽♂️,🤦🏾♂️,🤦🏿♂️
+🙍,🙍,🙍🏻,🙍🏼,🙍🏽,🙍🏾,🙍🏿,🙍♀️,🙍🏻♀️,🙍🏼♀️,🙍🏽♀️,🙍🏾♀️,🙍🏿♀️,🙍♂️,🙍🏻♂️,🙍🏼♂️,🙍🏽♂️,🙍🏾♂️,🙍🏿♂️
+🙎,🙎,🙎🏻,🙎🏼,🙎🏽,🙎🏾,🙎🏿,🙎♀️,🙎🏻♀️,🙎🏼♀️,🙎🏽♀️,🙎🏾♀️,🙎🏿♀️,🙎♂️,🙎🏻♂️,🙎🏼♂️,🙎🏽♂️,🙎🏾♂️,🙎🏿♂️
+🧏,🧏,🧏🏻,🧏🏼,🧏🏽,🧏🏾,🧏🏿,🧏♀️,🧏🏻♀️,🧏🏼♀️,🧏🏽♀️,🧏🏾♀️,🧏🏿♀️,🧏♂️,🧏🏻♂️,🧏🏼♂️,🧏🏽♂️,🧏🏾♂️,🧏🏿♂️
+💆,💆,💆🏻,💆🏼,💆🏽,💆🏾,💆🏿,💆♀️,💆🏻♀️,💆🏼♀️,💆🏽♀️,💆🏾♀️,💆🏿♀️,💆♂️,💆🏻♂️,💆🏼♂️,💆🏽♂️,💆🏾♂️,💆🏿♂️
+💇,💇,💇🏻,💇🏼,💇🏽,💇🏾,💇🏿,💇♀️,💇🏻♀️,💇🏼♀️,💇🏽♀️,💇🏾♀️,💇🏿♀️,💇♂️,💇🏻♂️,💇🏼♂️,💇🏽♂️,💇🏾♂️,💇🏿♂️
+🧖,🧖,🧖🏻,🧖🏼,🧖🏽,🧖🏾,🧖🏿,🧖♀️,🧖🏻♀️,🧖🏼♀️,🧖🏽♀️,🧖🏾♀️,🧖🏿♀️,🧖♂️,🧖🏻♂️,🧖🏼♂️,🧖🏽♂️,🧖🏾♂️,🧖🏿♂️
+🛀,🛀,🛀🏻,🛀🏼,🛀🏽,🛀🏾,🛀🏿
+🛌,🛌,🛌🏻,🛌🏼,🛌🏽,🛌🏾,🛌🏿
+🧘,🧘,🧘🏻,🧘🏼,🧘🏽,🧘🏾,🧘🏿,🧘♀️,🧘🏻♀️,🧘🏼♀️,🧘🏽♀️,🧘🏾♀️,🧘🏿♀️,🧘♂️,🧘🏻♂️,🧘🏼♂️,🧘🏽♂️,🧘🏾♂️,🧘🏿♂️
+🧑🦯,🧑🦯,🧑🏻🦯,🧑🏼🦯,🧑🏽🦯,🧑🏾🦯,🧑🏿🦯,👩🦯,👩🏻🦯,👩🏼🦯,👩🏽🦯,👩🏾🦯,👩🏿🦯,👨🦯,👨🏻🦯,👨🏼🦯,👨🏽🦯,👨🏾🦯,👨🏿🦯
+🧑🦼,🧑🦼,🧑🏻🦼,🧑🏼🦼,🧑🏽🦼,🧑🏾🦼,🧑🏿🦼,👩🦼,👩🏻🦼,👩🏼🦼,👩🏽🦼,👩🏾🦼,👩🏿🦼,👨🦼,👨🏻🦼,👨🏼🦼,👨🏽🦼,👨🏾🦼,👨🏿🦼
+🧑🦽,🧑🦽,🧑🏻🦽,🧑🏼🦽,🧑🏽🦽,🧑🏾🦽,🧑🏿🦽,👩🦽,👩🏻🦽,👩🏼🦽,👩🏽🦽,👩🏾🦽,👩🏿🦽,👨🦽,👨🏻🦽,👨🏼🦽,👨🏽🦽,👨🏾🦽,👨🏿🦽
+🧎,🧎,🧎🏻,🧎🏼,🧎🏽,🧎🏾,🧎🏿,🧎♀️,🧎🏻♀️,🧎🏼♀️,🧎🏽♀️,🧎🏾♀️,🧎🏿♀️,🧎♂️,🧎🏻♂️,🧎🏼♂️,🧎🏽♂️,🧎🏾♂️,🧎🏿♂️
+🧍,🧍,🧍🏻,🧍🏼,🧍🏽,🧍🏾,🧍🏿,🧍♀️,🧍🏻♀️,🧍🏼♀️,🧍🏽♀️,🧍🏾♀️,🧍🏿♀️,🧍♂️,🧍🏻♂️,🧍🏼♂️,🧍🏽♂️,🧍🏾♂️,🧍🏿♂️
+🚶,🚶,🚶🏻,🚶🏼,🚶🏽,🚶🏾,🚶🏿,🚶♀️,🚶🏻♀️,🚶🏼♀️,🚶🏽♀️,🚶🏾♀️,🚶🏿♀️,🚶♂️,🚶🏻♂️,🚶🏼♂️,🚶🏽♂️,🚶🏾♂️,🚶🏿♂️
+🏃,🏃,🏃🏻,🏃🏼,🏃🏽,🏃🏾,🏃🏿,🏃♀️,🏃🏻♀️,🏃🏼♀️,🏃🏽♀️,🏃🏾♀️,🏃🏿♀️,🏃♂️,🏃🏻♂️,🏃🏼♂️,🏃🏽♂️,🏃🏾♂️,🏃🏿♂️
+🤸,🤸,🤸🏻,🤸🏼,🤸🏽,🤸🏾,🤸🏿,🤸♀️,🤸🏻♀️,🤸🏼♀️,🤸🏽♀️,🤸🏾♀️,🤸🏿♀️,🤸♂️,🤸🏻♂️,🤸🏼♂️,🤸🏽♂️,🤸🏾♂️,🤸🏿♂️
+🏋️,🏋️,🏋🏻,🏋🏼,🏋🏽,🏋🏾,🏋🏿,🏋️♀️,🏋🏻♀️,🏋🏼♀️,🏋🏽♀️,🏋🏾♀️,🏋🏿♀️,🏋️♂️,🏋🏻♂️,🏋🏼♂️,🏋🏽♂️,🏋🏾♂️,🏋🏿♂️
+⛹️,⛹️,⛹🏻,⛹🏼,⛹🏽,⛹🏾,⛹🏿,⛹️♀️,⛹🏻♀️,⛹🏼♀️,⛹🏽♀️,⛹🏾♀️,⛹🏿♀️,⛹️♂️,⛹🏻♂️,⛹🏼♂️,⛹🏽♂️,⛹🏾♂️,⛹🏿♂️
+🤾,🤾,🤾🏻,🤾🏼,🤾🏽,🤾🏾,🤾🏿,🤾♀️,🤾🏻♀️,🤾🏼♀️,🤾🏽♀️,🤾🏾♀️,🤾🏿♀️,🤾♂️,🤾🏻♂️,🤾🏼♂️,🤾🏽♂️,🤾🏾♂️,🤾🏿♂️
+🚴,🚴,🚴🏻,🚴🏼,🚴🏽,🚴🏾,🚴🏿,🚴♀️,🚴🏻♀️,🚴🏼♀️,🚴🏽♀️,🚴🏾♀️,🚴🏿♀️,🚴♂️,🚴🏻♂️,🚴🏼♂️,🚴🏽♂️,🚴🏾♂️,🚴🏿♂️
+🚵,🚵,🚵🏻,🚵🏼,🚵🏽,🚵🏾,🚵🏿,🚵♀️,🚵🏻♀️,🚵🏼♀️,🚵🏽♀️,🚵🏾♀️,🚵🏿♀️,🚵♂️,🚵🏻♂️,🚵🏼♂️,🚵🏽♂️,🚵🏾♂️,🚵🏿♂️
+🧗,🧗,🧗🏻,🧗🏼,🧗🏽,🧗🏾,🧗🏿,🧗♀️,🧗🏻♀️,🧗🏼♀️,🧗🏽♀️,🧗🏾♀️,🧗🏿♀️,🧗♂️,🧗🏻♂️,🧗🏼♂️,🧗🏽♂️,🧗🏾♂️,🧗🏿♂️
+🤼,🤼,🤼♀️,🤼♂️
+🤹,🤹,🤹🏻,🤹🏼,🤹🏽,🤹🏾,🤹🏿,🤹♀️,🤹🏻♀️,🤹🏼♀️,🤹🏽♀️,🤹🏾♀️,🤹🏿♀️,🤹♂️,🤹🏻♂️,🤹🏼♂️,🤹🏽♂️,🤹🏾♂️,🤹🏿♂️
+🏌️,🏌️,🏌🏻,🏌🏼,🏌🏽,🏌🏾,🏌🏿,🏌️♀️,🏌🏻♀️,🏌🏼♀️,🏌🏽♀️,🏌🏾♀️,🏌🏿♀️,🏌️♂️,🏌🏻♂️,🏌🏼♂️,🏌🏽♂️,🏌🏾♂️,🏌🏿♂️
+🏇,🏇,🏇🏻,🏇🏼,🏇🏽,🏇🏾,🏇🏿
+🤺
+⛷️
+🏂,🏂,🏂🏻,🏂🏼,🏂🏽,🏂🏾,🏂🏿
+🪂
+🏄,🏄,🏄🏻,🏄🏼,🏄🏽,🏄🏾,🏄🏿,🏄♀️,🏄🏻♀️,🏄🏼♀️,🏄🏽♀️,🏄🏾♀️,🏄🏿♀️,🏄♂️,🏄🏻♂️,🏄🏼♂️,🏄🏽♂️,🏄🏾♂️,🏄🏿♂️
+🚣,🚣,🚣🏻,🚣🏼,🚣🏽,🚣🏾,🚣🏿,🚣♀️,🚣🏻♀️,🚣🏼♀️,🚣🏽♀️,🚣🏾♀️,🚣🏿♀️,🚣♂️,🚣🏻♂️,🚣🏼♂️,🚣🏽♂️,🚣🏾♂️,🚣🏿♂️
+🏊,🏊,🏊🏻,🏊🏼,🏊🏽,🏊🏾,🏊🏿,🏊♀️,🏊🏻♀️,🏊🏼♀️,🏊🏽♀️,🏊🏾♀️,🏊🏿♀️,🏊♂️,🏊🏻♂️,🏊🏼♂️,🏊🏽♂️,🏊🏾♂️,🏊🏿♂️
+🤽,🤽,🤽🏻,🤽🏼,🤽🏽,🤽🏾,🤽🏿,🤽♀️,🤽🏻♀️,🤽🏼♀️,🤽🏽♀️,🤽🏾♀️,🤽🏿♀️,🤽♂️,🤽🏻♂️,🤽🏼♂️,🤽🏽♂️,🤽🏾♂️,🤽🏿♂️
+🧜,🧜,🧜🏻,🧜🏼,🧜🏽,🧜🏾,🧜🏿,🧜♀️,🧜🏻♀️,🧜🏼♀️,🧜🏽♀️,🧜🏾♀️,🧜🏿♀️,🧜♂️,🧜🏻♂️,🧜🏼♂️,🧜🏽♂️,🧜🏾♂️,🧜🏿♂️
+🧚,🧚,🧚🏻,🧚🏼,🧚🏽,🧚🏾,🧚🏿,🧚♀️,🧚🏻♀️,🧚🏼♀️,🧚🏽♀️,🧚🏾♀️,🧚🏿♀️,🧚♂️,🧚🏻♂️,🧚🏼♂️,🧚🏽♂️,🧚🏾♂️,🧚🏿♂️
+🧞,🧞,🧞♀️,🧞♂️
+🧝,🧝,🧝🏻,🧝🏼,🧝🏽,🧝🏾,🧝🏿,🧝♀️,🧝🏻♀️,🧝🏼♀️,🧝🏽♀️,🧝🏾♀️,🧝🏿♀️,🧝♂️,🧝🏻♂️,🧝🏼♂️,🧝🏽♂️,🧝🏾♂️,🧝🏿♂️
+🧙,🧙,🧙🏻,🧙🏼,🧙🏽,🧙🏾,🧙🏿,🧙♀️,🧙🏻♀️,🧙🏼♀️,🧙🏽♀️,🧙🏾♀️,🧙🏿♀️,🧙♂️,🧙🏻♂️,🧙🏼♂️,🧙🏽♂️,🧙🏾♂️,🧙🏿♂️
+🧛,🧛,🧛🏻,🧛🏼,🧛🏽,🧛🏾,🧛🏿,🧛♀️,🧛🏻♀️,🧛🏼♀️,🧛🏽♀️,🧛🏾♀️,🧛🏿♀️,🧛♂️,🧛🏻♂️,🧛🏼♂️,🧛🏽♂️,🧛🏾♂️,🧛🏿♂️
+🧟,🧟,🧟♀️,🧟♂️
+🧌
+🦸,🦸,🦸🏻,🦸🏼,🦸🏽,🦸🏾,🦸🏿,🦸♀️,🦸🏻♀️,🦸🏼♀️,🦸🏽♀️,🦸🏾♀️,🦸🏿♀️,🦸♂️,🦸🏻♂️,🦸🏼♂️,🦸🏽♂️,🦸🏾♂️,🦸🏿♂️
+🦹,🦹,🦹🏻,🦹🏼,🦹🏽,🦹🏾,🦹🏿,🦹♀️,🦹🏻♀️,🦹🏼♀️,🦹🏽♀️,🦹🏾♀️,🦹🏿♀️,🦹♂️,🦹🏻♂️,🦹🏼♂️,🦹🏽♂️,🦹🏾♂️,🦹🏿♂️
+🥷,🥷,🥷🏻,🥷🏼,🥷🏽,🥷🏾,🥷🏿
+🧑🎄,🧑🎄,🧑🏻🎄,🧑🏼🎄,🧑🏽🎄,🧑🏾🎄,🧑🏿🎄,🤶,🤶🏻,🤶🏼,🤶🏽,🤶🏾,🤶🏿,🎅,🎅🏻,🎅🏼,🎅🏽,🎅🏾,🎅🏿
+👼,👼,👼🏻,👼🏼,👼🏽,👼🏾,👼🏿
+💂,💂,💂🏻,💂🏼,💂🏽,💂🏾,💂🏿,💂♀️,💂🏻♀️,💂🏼♀️,💂🏽♀️,💂🏾♀️,💂🏿♀️,💂♂️,💂🏻♂️,💂🏼♂️,💂🏽♂️,💂🏾♂️,💂🏿♂️
+🫅,🫅,🫅🏻,🫅🏼,🫅🏽,🫅🏾,🫅🏿,👸,👸🏻,👸🏼,👸🏽,👸🏾,👸🏿,🤴,🤴🏻,🤴🏼,🤴🏽,🤴🏾,🤴🏿
+🤵,🤵,🤵🏻,🤵🏼,🤵🏽,🤵🏾,🤵🏿,🤵♀️,🤵🏻♀️,🤵🏼♀️,🤵🏽♀️,🤵🏾♀️,🤵🏿♀️,🤵♂️,🤵🏻♂️,🤵🏼♂️,🤵🏽♂️,🤵🏾♂️,🤵🏿♂️
+👰,👰,👰🏻,👰🏼,👰🏽,👰🏾,👰🏿,👰♀️,👰🏻♀️,👰🏼♀️,👰🏽♀️,👰🏾♀️,👰🏿♀️,👰♂️,👰🏻♂️,👰🏼♂️,👰🏽♂️,👰🏾♂️,👰🏿♂️
+🧑🚀,🧑🚀,🧑🏻🚀,🧑🏼🚀,🧑🏽🚀,🧑🏾🚀,🧑🏿🚀,👩🚀,👩🏻🚀,👩🏼🚀,👩🏽🚀,👩🏾🚀,👩🏿🚀,👨🚀,👨🏻🚀,👨🏼🚀,👨🏽🚀,👨🏾🚀,👨🏿🚀
+👷,👷,👷🏻,👷🏼,👷🏽,👷🏾,👷🏿,👷♀️,👷🏻♀️,👷🏼♀️,👷🏽♀️,👷🏾♀️,👷🏿♀️,👷♂️,👷🏻♂️,👷🏼♂️,👷🏽♂️,👷🏾♂️,👷🏿♂️
+👮,👮,👮🏻,👮🏼,👮🏽,👮🏾,👮🏿,👮♀️,👮🏻♀️,👮🏼♀️,👮🏽♀️,👮🏾♀️,👮🏿♀️,👮♂️,👮🏻♂️,👮🏼♂️,👮🏽♂️,👮🏾♂️,👮🏿♂️
+🕵️,🕵️,🕵🏻,🕵🏼,🕵🏽,🕵🏾,🕵🏿,🕵️♀️,🕵🏻♀️,🕵🏼♀️,🕵🏽♀️,🕵🏾♀️,🕵🏿♀️,🕵️♂️,🕵🏻♂️,🕵🏼♂️,🕵🏽♂️,🕵🏾♂️,🕵🏿♂️
+🧑✈️,🧑✈️,🧑🏻✈️,🧑🏼✈️,🧑🏽✈️,🧑🏾✈️,🧑🏿✈️,👩✈️,👩🏻✈️,👩🏼✈️,👩🏽✈️,👩🏾✈️,👩🏿✈️,👨✈️,👨🏻✈️,👨🏼✈️,👨🏽✈️,👨🏾✈️,👨🏿✈️
+🧑🔬,🧑🔬,🧑🏻🔬,🧑🏼🔬,🧑🏽🔬,🧑🏾🔬,🧑🏿🔬,👩🔬,👩🏻🔬,👩🏼🔬,👩🏽🔬,👩🏾🔬,👩🏿🔬,👨🔬,👨🏻🔬,👨🏼🔬,👨🏽🔬,👨🏾🔬,👨🏿🔬
+🧑⚕️,🧑⚕️,🧑🏻⚕️,🧑🏼⚕️,🧑🏽⚕️,🧑🏾⚕️,🧑🏿⚕️,👩⚕️,👩🏻⚕️,👩🏼⚕️,👩🏽⚕️,👩🏾⚕️,👩🏿⚕️,👨⚕️,👨🏻⚕️,👨🏼⚕️,👨🏽⚕️,👨🏾⚕️,👨🏿⚕️
+🧑🔧,🧑🔧,🧑🏻🔧,🧑🏼🔧,🧑🏽🔧,🧑🏾🔧,🧑🏿🔧,👩🔧,👩🏻🔧,👩🏼🔧,👩🏽🔧,👩🏾🔧,👩🏿🔧,👨🔧,👨🏻🔧,👨🏼🔧,👨🏽🔧,👨🏾🔧,👨🏿🔧
+🧑🏭,🧑🏭,🧑🏻🏭,🧑🏼🏭,🧑🏽🏭,🧑🏾🏭,🧑🏿🏭,👩🏭,👩🏻🏭,👩🏼🏭,👩🏽🏭,👩🏾🏭,👩🏿🏭,👨🏭,👨🏻🏭,👨🏼🏭,👨🏽🏭,👨🏾🏭,👨🏿🏭
+🧑🚒,🧑🚒,🧑🏻🚒,🧑🏼🚒,🧑🏽🚒,🧑🏾🚒,🧑🏿🚒,👩🚒,👩🏻🚒,👩🏼🚒,👩🏽🚒,👩🏾🚒,👩🏿🚒,👨🚒,👨🏻🚒,👨🏼🚒,👨🏽🚒,👨🏾🚒,👨🏿🚒
+🧑🌾,🧑🌾,🧑🏻🌾,🧑🏼🌾,🧑🏽🌾,🧑🏾🌾,🧑🏿🌾,👩🌾,👩🏻🌾,👩🏼🌾,👩🏽🌾,👩🏾🌾,👩🏿🌾,👨🌾,👨🏻🌾,👨🏼🌾,👨🏽🌾,👨🏾🌾,👨🏿🌾
+🧑🏫,🧑🏫,🧑🏻🏫,🧑🏼🏫,🧑🏽🏫,🧑🏾🏫,🧑🏿🏫,👩🏫,👩🏻🏫,👩🏼🏫,👩🏽🏫,👩🏾🏫,👩🏿🏫,👨🏫,👨🏻🏫,👨🏼🏫,👨🏽🏫,👨🏾🏫,👨🏿🏫
+🧑🎓,🧑🎓,🧑🏻🎓,🧑🏼🎓,🧑🏽🎓,🧑🏾🎓,🧑🏿🎓,👩🎓,👩🏻🎓,👩🏼🎓,👩🏽🎓,👩🏾🎓,👩🏿🎓,👨🎓,👨🏻🎓,👨🏼🎓,👨🏽🎓,👨🏾🎓,👨🏿🎓
+🧑💼,🧑💼,🧑🏻💼,🧑🏼💼,🧑🏽💼,🧑🏾💼,🧑🏿💼,👩💼,👩🏻💼,👩🏼💼,👩🏽💼,👩🏾💼,👩🏿💼,👨💼,👨🏻💼,👨🏼💼,👨🏽💼,👨🏾💼,👨🏿💼
+🧑⚖️,🧑⚖️,🧑🏻⚖️,🧑🏼⚖️,🧑🏽⚖️,🧑🏾⚖️,🧑🏿⚖️,👩⚖️,👩🏻⚖️,👩🏼⚖️,👩🏽⚖️,👩🏾⚖️,👩🏿⚖️,👨⚖️,👨🏻⚖️,👨🏼⚖️,👨🏽⚖️,👨🏾⚖️,👨🏿⚖️
+🧑💻,🧑💻,🧑🏻💻,🧑🏼💻,🧑🏽💻,🧑🏾💻,🧑🏿💻,👩💻,👩🏻💻,👩🏼💻,👩🏽💻,👩🏾💻,👩🏿💻,👨💻,👨🏻💻,👨🏼💻,👨🏽💻,👨🏾💻,👨🏿💻
+🧑🎤,🧑🎤,🧑🏻🎤,🧑🏼🎤,🧑🏽🎤,🧑🏾🎤,🧑🏿🎤,👩🎤,👩🏻🎤,👩🏼🎤,👩🏽🎤,👩🏾🎤,👩🏿🎤,👨🎤,👨🏻🎤,👨🏼🎤,👨🏽🎤,👨🏾🎤,👨🏿🎤
+🧑🎨,🧑🎨,🧑🏻🎨,🧑🏼🎨,🧑🏽🎨,🧑🏾🎨,🧑🏿🎨,👩🎨,👩🏻🎨,👩🏼🎨,👩🏽🎨,👩🏾🎨,👩🏿🎨,👨🎨,👨🏻🎨,👨🏼🎨,👨🏽🎨,👨🏾🎨,👨🏿🎨
+🧑🍳,🧑🍳,🧑🏻🍳,🧑🏼🍳,🧑🏽🍳,🧑🏾🍳,🧑🏿🍳,👩🍳,👩🏻🍳,👩🏼🍳,👩🏽🍳,👩🏾🍳,👩🏿🍳,👨🍳,👨🏻🍳,👨🏼🍳,👨🏽🍳,👨🏾🍳,👨🏿🍳
+👳,👳,👳🏻,👳🏼,👳🏽,👳🏾,👳🏿,👳♀️,👳🏻♀️,👳🏼♀️,👳🏽♀️,👳🏾♀️,👳🏿♀️,👳♂️,👳🏻♂️,👳🏼♂️,👳🏽♂️,👳🏾♂️,👳🏿♂️
+🧕,🧕,🧕🏻,🧕🏼,🧕🏽,🧕🏾,🧕🏿
+👲,👲,👲🏻,👲🏼,👲🏽,👲🏾,👲🏿
+👶,👶,👶🏻,👶🏼,👶🏽,👶🏾,👶🏿
+🧒,🧒,🧒🏻,🧒🏼,🧒🏽,🧒🏾,🧒🏿,👧,👧🏻,👧🏼,👧🏽,👧🏾,👧🏿,👦,👦🏻,👦🏼,👦🏽,👦🏾,👦🏿
+🧑,🧑,🧑🏻,🧑🏼,🧑🏽,🧑🏾,🧑🏿,👩,👩🏻,👩🏼,👩🏽,👩🏾,👩🏿,👨,👨🏻,👨🏼,👨🏽,👨🏾,👨🏿
+🧓,🧓,🧓🏻,🧓🏼,🧓🏽,🧓🏾,🧓🏿,👵,👵🏻,👵🏼,👵🏽,👵🏾,👵🏿,👴,👴🏻,👴🏼,👴🏽,👴🏾,👴🏿
+🧑🦳,🧑🦳,🧑🏻🦳,🧑🏼🦳,🧑🏽🦳,🧑🏾🦳,🧑🏿🦳,👩🦳,👩🏻🦳,👩🏼🦳,👩🏽🦳,👩🏾🦳,👩🏿🦳,👨🦳,👨🏻🦳,👨🏼🦳,👨🏽🦳,👨🏾🦳,👨🏿🦳
+🧑🦰,🧑🦰,🧑🏻🦰,🧑🏼🦰,🧑🏽🦰,🧑🏾🦰,🧑🏿🦰,👩🦰,👩🏻🦰,👩🏼🦰,👩🏽🦰,👩🏾🦰,👩🏿🦰,👨🦰,👨🏻🦰,👨🏼🦰,👨🏽🦰,👨🏾🦰,👨🏿🦰
+👱,👱,👱🏻,👱🏼,👱🏽,👱🏾,👱🏿,👱♀️,👱🏻♀️,👱🏼♀️,👱🏽♀️,👱🏾♀️,👱🏿♀️,👱♂️,👱🏻♂️,👱🏼♂️,👱🏽♂️,👱🏾♂️,👱🏿♂️
+🧑🦱,🧑🦱,🧑🏻🦱,🧑🏼🦱,🧑🏽🦱,🧑🏾🦱,🧑🏿🦱,👩🦱,👩🏻🦱,👩🏼🦱,👩🏽🦱,👩🏾🦱,👩🏿🦱,👨🦱,👨🏻🦱,👨🏼🦱,👨🏽🦱,👨🏾🦱,👨🏿🦱
+🧑🦲,🧑🦲,🧑🏻🦲,🧑🏼🦲,🧑🏽🦲,🧑🏾🦲,🧑🏿🦲,👩🦲,👩🏻🦲,👩🏼🦲,👩🏽🦲,👩🏾🦲,👩🏿🦲,👨🦲,👨🏻🦲,👨🏼🦲,👨🏽🦲,👨🏾🦲,👨🏿🦲
+🧔,🧔,🧔🏻,🧔🏼,🧔🏽,🧔🏾,🧔🏿,🧔♀️,🧔🏻♀️,🧔🏼♀️,🧔🏽♀️,🧔🏾♀️,🧔🏿♀️,🧔♂️,🧔🏻♂️,🧔🏼♂️,🧔🏽♂️,🧔🏾♂️,🧔🏿♂️
+🕴️,🕴️,🕴🏻,🕴🏼,🕴🏽,🕴🏾,🕴🏿
+💃,💃,💃🏻,💃🏼,💃🏽,💃🏾,💃🏿
+🕺,🕺,🕺🏻,🕺🏼,🕺🏽,🕺🏾,🕺🏿
+👯,👯,👯♂️,👯♀️
+🧑🤝🧑,🧑🤝🧑,🧑🏻🤝🧑🏻,🧑🏻🤝🧑🏼,🧑🏻🤝🧑🏽,🧑🏻🤝🧑🏾,🧑🏻🤝🧑🏿,🧑🏼🤝🧑🏻,🧑🏼🤝🧑🏼,🧑🏼🤝🧑🏽,🧑🏼🤝🧑🏾,🧑🏼🤝🧑🏿,🧑🏽🤝🧑🏻,🧑🏽🤝🧑🏼,🧑🏽🤝🧑🏽,🧑🏽🤝🧑🏾,🧑🏽🤝🧑🏿,🧑🏾🤝🧑🏻,🧑🏾🤝🧑🏼,🧑🏾🤝🧑🏽,🧑🏾🤝🧑🏾,🧑🏾🤝🧑🏿,🧑🏿🤝🧑🏻,🧑🏿🤝🧑🏼,🧑🏿🤝🧑🏽,🧑🏿🤝🧑🏾,🧑🏿🤝🧑🏿
+👭,👭,👭🏻,👩🏻🤝👩🏼,👩🏻🤝👩🏽,👩🏻🤝👩🏾,👩🏻🤝👩🏿,👩🏼🤝👩🏻,👭🏼,👩🏼🤝👩🏽,👩🏼🤝👩🏾,👩🏼🤝👩🏿,👩🏽🤝👩🏻,👩🏽🤝👩🏼,👭🏽,👩🏽🤝👩🏾,👩🏽🤝👩🏿,👩🏾🤝👩🏻,👩🏾🤝👩🏼,👩🏾🤝👩🏽,👭🏾,👩🏾🤝👩🏿,👩🏿🤝👩🏻,👩🏿🤝👩🏼,👩🏿🤝👩🏽,👩🏿🤝👩🏾,👭🏿
+👬,👬,👬🏻,👨🏻🤝👨🏼,👨🏻🤝👨🏽,👨🏻🤝👨🏾,👨🏻🤝👨🏿,👨🏼🤝👨🏻,👬🏼,👨🏼🤝👨🏽,👨🏼🤝👨🏾,👨🏼🤝👨🏿,👨🏽🤝👨🏻,👨🏽🤝👨🏼,👬🏽,👨🏽🤝👨🏾,👨🏽🤝👨🏿,👨🏾🤝👨🏻,👨🏾🤝👨🏼,👨🏾🤝👨🏽,👬🏾,👨🏾🤝👨🏿,👨🏿🤝👨🏻,👨🏿🤝👨🏼,👨🏿🤝👨🏽,👨🏿🤝👨🏾,👬🏿
+👫,👫,👫🏻,👩🏻🤝👨🏼,👩🏻🤝👨🏽,👩🏻🤝👨🏾,👩🏻🤝👨🏿,👩🏼🤝👨🏻,👫🏼,👩🏼🤝👨🏽,👩🏼🤝👨🏾,👩🏼🤝👨🏿,👩🏽🤝👨🏻,👩🏽🤝👨🏼,👫🏽,👩🏽🤝👨🏾,👩🏽🤝👨🏿,👩🏾🤝👨🏻,👩🏾🤝👨🏼,👩🏾🤝👨🏽,👫🏾,👩🏾🤝👨🏿,👩🏿🤝👨🏻,👩🏿🤝👨🏼,👩🏿🤝👨🏽,👩🏿🤝👨🏾,👫🏿
+💏,💏,💏🏻,🧑🏻❤️💋🧑🏼,🧑🏻❤️💋🧑🏽,🧑🏻❤️💋🧑🏾,🧑🏻❤️💋🧑🏿,🧑🏼❤️💋🧑🏻,💏🏼,🧑🏼❤️💋🧑🏽,🧑🏼❤️💋🧑🏾,🧑🏼❤️💋🧑🏿,🧑🏽❤️💋🧑🏻,🧑🏽❤️💋🧑🏼,💏🏽,🧑🏽❤️💋🧑🏾,🧑🏽❤️💋🧑🏿,🧑🏾❤️💋🧑🏻,🧑🏾❤️💋🧑🏼,🧑🏾❤️💋🧑🏽,💏🏾,🧑🏾❤️💋🧑🏿,🧑🏿❤️💋🧑🏻,🧑🏿❤️💋🧑🏼,🧑🏿❤️💋🧑🏽,🧑🏿❤️💋🧑🏾,💏🏿
+👩❤️💋👨,👩❤️💋👨,👩🏻❤️💋👨🏻,👩🏻❤️💋👨🏼,👩🏻❤️💋👨🏽,👩🏻❤️💋👨🏾,👩🏻❤️💋👨🏿,👩🏼❤️💋👨🏻,👩🏼❤️💋👨🏼,👩🏼❤️💋👨🏽,👩🏼❤️💋👨🏾,👩🏼❤️💋👨🏿,👩🏽❤️💋👨🏻,👩🏽❤️💋👨🏼,👩🏽❤️💋👨🏽,👩🏽❤️💋👨🏾,👩🏽❤️💋👨🏿,👩🏾❤️💋👨🏻,👩🏾❤️💋👨🏼,👩🏾❤️💋👨🏽,👩🏾❤️💋👨🏾,👩🏾❤️💋👨🏿,👩🏿❤️💋👨🏻,👩🏿❤️💋👨🏼,👩🏿❤️💋👨🏽,👩🏿❤️💋👨🏾,👩🏿❤️💋👨🏿
+👨❤️💋👨,👨❤️💋👨,👨🏻❤️💋👨🏻,👨🏻❤️💋👨🏼,👨🏻❤️💋👨🏽,👨🏻❤️💋👨🏾,👨🏻❤️💋👨🏿,👨🏼❤️💋👨🏻,👨🏼❤️💋👨🏼,👨🏼❤️💋👨🏽,👨🏼❤️💋👨🏾,👨🏼❤️💋👨🏿,👨🏽❤️💋👨🏻,👨🏽❤️💋👨🏼,👨🏽❤️💋👨🏽,👨🏽❤️💋👨🏾,👨🏽❤️💋👨🏿,👨🏾❤️💋👨🏻,👨🏾❤️💋👨🏼,👨🏾❤️💋👨🏽,👨🏾❤️💋👨🏾,👨🏾❤️💋👨🏿,👨🏿❤️💋👨🏻,👨🏿❤️💋👨🏼,👨🏿❤️💋👨🏽,👨🏿❤️💋👨🏾,👨🏿❤️💋👨🏿
+👩❤️💋👩,👩❤️💋👩,👩🏻❤️💋👩🏻,👩🏻❤️💋👩🏼,👩🏻❤️💋👩🏽,👩🏻❤️💋👩🏾,👩🏻❤️💋👩🏿,👩🏼❤️💋👩🏻,👩🏼❤️💋👩🏼,👩🏼❤️💋👩🏽,👩🏼❤️💋👩🏾,👩🏼❤️💋👩🏿,👩🏽❤️💋👩🏻,👩🏽❤️💋👩🏼,👩🏽❤️💋👩🏽,👩🏽❤️💋👩🏾,👩🏽❤️💋👩🏿,👩🏾❤️💋👩🏻,👩🏾❤️💋👩🏼,👩🏾❤️💋👩🏽,👩🏾❤️💋👩🏾,👩🏾❤️💋👩🏿,👩🏿❤️💋👩🏻,👩🏿❤️💋👩🏼,👩🏿❤️💋👩🏽,👩🏿❤️💋👩🏾,👩🏿❤️💋👩🏿
+💑,💑,💑🏻,🧑🏻❤️🧑🏼,🧑🏻❤️🧑🏽,🧑🏻❤️🧑🏾,🧑🏻❤️🧑🏿,🧑🏼❤️🧑🏻,💑🏼,🧑🏼❤️🧑🏽,🧑🏼❤️🧑🏾,🧑🏼❤️🧑🏿,🧑🏽❤️🧑🏻,🧑🏽❤️🧑🏼,💑🏽,🧑🏽❤️🧑🏾,🧑🏽❤️🧑🏿,🧑🏾❤️🧑🏻,🧑🏾❤️🧑🏼,🧑🏾❤️🧑🏽,💑🏾,🧑🏾❤️🧑🏿,🧑🏿❤️🧑🏻,🧑🏿❤️🧑🏼,🧑🏿❤️🧑🏽,🧑🏿❤️🧑🏾,💑🏿
+👩❤️👨,👩❤️👨,👩🏻❤️👨🏻,👩🏻❤️👨🏼,👩🏻❤️👨🏽,👩🏻❤️👨🏾,👩🏻❤️👨🏿,👩🏼❤️👨🏻,👩🏼❤️👨🏼,👩🏼❤️👨🏽,👩🏼❤️👨🏾,👩🏼❤️👨🏿,👩🏽❤️👨🏻,👩🏽❤️👨🏼,👩🏽❤️👨🏽,👩🏽❤️👨🏾,👩🏽❤️👨🏿,👩🏾❤️👨🏻,👩🏾❤️👨🏼,👩🏾❤️👨🏽,👩🏾❤️👨🏾,👩🏾❤️👨🏿,👩🏿❤️👨🏻,👩🏿❤️👨🏼,👩🏿❤️👨🏽,👩🏿❤️👨🏾,👩🏿❤️👨🏿
+👨❤️👨,👨❤️👨,👨🏻❤️👨🏻,👨🏻❤️👨🏼,👨🏻❤️👨🏽,👨🏻❤️👨🏾,👨🏻❤️👨🏿,👨🏼❤️👨🏻,👨🏼❤️👨🏼,👨🏼❤️👨🏽,👨🏼❤️👨🏾,👨🏼❤️👨🏿,👨🏽❤️👨🏻,👨🏽❤️👨🏼,👨🏽❤️👨🏽,👨🏽❤️👨🏾,👨🏽❤️👨🏿,👨🏾❤️👨🏻,👨🏾❤️👨🏼,👨🏾❤️👨🏽,👨🏾❤️👨🏾,👨🏾❤️👨🏿,👨🏿❤️👨🏻,👨🏿❤️👨🏼,👨🏿❤️👨🏽,👨🏿❤️👨🏾,👨🏿❤️👨🏿
+👩❤️👩,👩❤️👩,👩🏻❤️👩🏻,👩🏻❤️👩🏼,👩🏻❤️👩🏽,👩🏻❤️👩🏾,👩🏻❤️👩🏿,👩🏼❤️👩🏻,👩🏼❤️👩🏼,👩🏼❤️👩🏽,👩🏼❤️👩🏾,👩🏼❤️👩🏿,👩🏽❤️👩🏻,👩🏽❤️👩🏼,👩🏽❤️👩🏽,👩🏽❤️👩🏾,👩🏽❤️👩🏿,👩🏾❤️👩🏻,👩🏾❤️👩🏼,👩🏾❤️👩🏽,👩🏾❤️👩🏾,👩🏾❤️👩🏿,👩🏿❤️👩🏻,👩🏿❤️👩🏼,👩🏿❤️👩🏽,👩🏿❤️👩🏾,👩🏿❤️👩🏿
+🫄,🫄,🫄🏻,🫄🏼,🫄🏽,🫄🏾,🫄🏿,🤰,🤰🏻,🤰🏼,🤰🏽,🤰🏾,🤰🏿,🫃,🫃🏻,🫃🏼,🫃🏽,🫃🏾,🫃🏿
+🤱,🤱,🤱🏻,🤱🏼,🤱🏽,🤱🏾,🤱🏿
+🧑🍼,🧑🍼,🧑🏻🍼,🧑🏼🍼,🧑🏽🍼,🧑🏾🍼,🧑🏿🍼,👩🍼,👩🏻🍼,👩🏼🍼,👩🏽🍼,👩🏾🍼,👩🏿🍼,👨🍼,👨🏻🍼,👨🏼🍼,👨🏽🍼,👨🏾🍼,👨🏿🍼
+👪,👪,👨👩👦,👨👩👧,👨👩👧👦,👨👩👦👦,👨👩👧👧,👨👨👦,👨👨👧,👨👨👧👦,👨👨👦👦,👨👨👧👧,👨👦,👨👧,👨👧👦,👨👦👦,👨👧👧,👩👩👦,👩👩👧,👩👩👧👦,👩👩👦👦,👩👩👧👧,👩👦,👩👧,👩👧👦,👩👦👦,👩👧👧
diff --git a/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_symbols.csv b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_symbols.csv
new file mode 100644
index 0000000..26d7459
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_symbols.csv
@@ -0,0 +1,254 @@
+❤️
+🧡
+💛
+💚
+💙
+💜
+🤎
+🖤
+🤍
+🔴
+🟠
+🟡
+🟢
+🔵
+🟣
+🟤
+⚫
+⚪
+🟥
+🟧
+🟨
+🟩
+🟦
+🟪
+🟫
+⬛
+⬜
+♈
+♉
+♊
+♋
+♌
+♍
+♎
+♏
+♐
+♑
+♒
+♓
+⛎
+♀️
+♂️
+⚧️
+🔻
+🔺
+❕
+❗
+❔
+❓
+⁉️
+‼️
+⭕
+❌
+🚫
+🚳
+🚭
+🚯
+🚱
+🚷
+📵
+🔞
+🔕
+🔇
+🅰️
+🆎
+🅱️
+🅾️
+🆑
+🆘
+🛑
+⛔
+📛
+♨️
+🉐
+㊙️
+㊗️
+🈴
+🈵
+🈹
+🈲
+🉑
+🈶
+🈚
+🈸
+🈺
+🈷️
+🔶
+🔸
+🔆
+🔅
+✴️
+🆚
+🎦
+📶
+🔁
+🔂
+🔀
+▶️
+⏩
+⏭️
+⏯️
+◀️
+⏪
+⏮️
+🔼
+⏫
+🔽
+⏬
+⏸️
+⏹️
+⏺️
+⏏️
+📴
+📳
+📲
+🔈
+🔉
+🔊
+🎼
+🎵
+🎶
+☢️
+☣️
+⚠️
+🚸
+⚜️
+🔱
+〽️
+🔰
+✳️
+❇️
+♻️
+💱
+💲
+💹
+🈯
+❎
+✅
+✔️
+☑️
+⬆️
+↗️
+➡️
+↘️
+⬇️
+↙️
+⬅️
+↖️
+↕️
+↔️
+↩️
+↪️
+⤴️
+⤵️
+🔃
+🔄
+🔙
+🔛
+🔝
+🔚
+🔜
+🆕
+🆓
+🆙
+🆗
+🆒
+🆖
+ℹ️
+🅿️
+🈁
+🈂️
+🈳
+🔣
+🔤
+🔠
+🔡
+🔢
+#️⃣
+*️⃣
+0️⃣
+1️⃣
+2️⃣
+3️⃣
+4️⃣
+5️⃣
+6️⃣
+7️⃣
+8️⃣
+9️⃣
+🔟
+💠
+🔷
+🔹
+🌐
+🏧
+Ⓜ️
+🚾
+🚻
+🚹
+🚺
+♿
+🚼
+🛗
+🚮
+🚰
+🛂
+🛃
+🛄
+🛅
+💟
+⚛️
+🛐
+🕉️
+☸️
+☮️
+☯️
+☪️
+✝️
+☦️
+✡️
+🔯
+🕎
+♾️
+🆔
+⚕️
+✖️
+➕
+➖
+➗
+🟰
+➰
+➿
+〰️
+©️
+®️
+™️
+♥️
+♦️
+♣️
+♠️
+🔘
+🔳
+◼️
+◾
+▪️
+🔲
+◻️
+◽
+▫️
+💭
+🗯️
+💬
+🗨️
+👁️🗨️
diff --git a/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_travel_places.csv b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_travel_places.csv
new file mode 100644
index 0000000..5e95477
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/raw/emoji_category_travel_places.csv
@@ -0,0 +1,122 @@
+🛑
+🚧
+🚨
+⛽
+🛢️
+🧭
+🛞
+🛟
+⚓
+🚏
+🚇
+🚥
+🚦
+🛴
+🦽
+🦼
+🩼
+🚲
+🛵
+🏍️
+🚙
+🚗
+🛻
+🚐
+🚚
+🚛
+🚜
+🏎️
+🚒
+🚑
+🚓
+🚕
+🛺
+🚌
+🚈
+🚝
+🚅
+🚄
+🚂
+🚃
+🚋
+🚎
+🚞
+🚊
+🚉
+🚍
+🚔
+🚘
+🚖
+🚆
+🚢
+🛳️
+🛥️
+🚤
+⛴️
+⛵
+🛶
+🚟
+🚠
+🚡
+🚁
+🛸
+🚀
+✈️
+🛫
+🛬
+🛩️
+🛝
+🎢
+🎡
+🎠
+🎪
+🗼
+🗽
+🗿
+🗻
+🏛️
+💈
+⛲
+⛩️
+🕍
+🕌
+🕋
+🛕
+⛪
+💒
+🏩
+🏯
+🏰
+🏗️
+🏢
+🏭
+🏬
+🏪
+🏟️
+🏦
+🏫
+🏨
+🏣
+🏤
+🏥
+🏚️
+🏠
+🏡
+🏘️
+🛖
+⛺
+🏕️
+🏙️
+🌆
+🌇
+🌃
+🌉
+🌁
+🛤️
+🛣️
+🏝️
+🗾
+🗺️
+🌐
+💺
+🧳
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/arrays.xml b/emoji2/emoji2-emojipicker/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..c2d25d3
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/arrays.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ Copyright 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+
+ <array name="emoji_by_category_raw_resources">
+ <item>@raw/emoji_category_emotions</item>
+ <item>@raw/emoji_category_people</item>
+ <item>@raw/emoji_category_animals_nature</item>
+ <item>@raw/emoji_category_food_drink</item>
+ <item>@raw/emoji_category_travel_places</item>
+ <item>@raw/emoji_category_activity</item>
+ <item>@raw/emoji_category_objects</item>
+ <item>@raw/emoji_category_symbols</item>
+ <item>@raw/emoji_category_flags</item>
+ </array>
+
+ <string-array name="category_names">
+ <item>@string/emoji_category_emotions</item>
+ <item>@string/emoji_category_people</item>
+ <item>@string/emoji_category_animals_nature</item>
+ <item>@string/emoji_category_food_drink</item>
+ <item>@string/emoji_category_travel_places</item>
+ <item>@string/emoji_category_activity</item>
+ <item>@string/emoji_category_objects</item>
+ <item>@string/emoji_category_symbols</item>
+ <item>@string/emoji_category_flags</item>
+ </string-array>
+</resources>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
new file mode 100644
index 0000000..7c57c2d
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <!-- Emoji keyboard's smileys and emotions sub category. -->
+ <string name="emoji_category_emotions">SMILEYS AND EMOTIONS</string>
+ <!-- Emoji keyboard's people sub category. -->
+ <string name="emoji_category_people">PEOPLE</string>
+ <!-- Emoji keyboard's animals and nature sub category. -->
+ <string name="emoji_category_animals_nature">ANIMALS AND NATURE</string>
+ <!-- Emoji keyboard's food and drink sub category. -->
+ <string name="emoji_category_food_drink">FOOD AND DRINK</string>
+ <!-- Emoji keyboard's travel and places sub category. -->
+ <string name="emoji_category_travel_places">TRAVEL AND PLACES</string>
+ <!-- Emoji keyboard's activity sub category. -->
+ <string name="emoji_category_activity">ACTIVITIES AND EVENTS</string>
+ <!-- Emoji keyboard's objects sub category. -->
+ <string name="emoji_category_objects">OBJECTS</string>
+ <!-- Emoji keyboard's symbols sub category. [CHAR_LIMIT=40] -->
+ <string name="emoji_category_symbols">SYMBOLS</string>
+ <!-- Emoji keyboard's flags sub category. Flags sub category contains emojis that represent country and regional flags. -->
+ <string name="emoji_category_flags">FLAGS</string>
+</resources>
\ No newline at end of file
diff --git a/fakeannotations/src/main/java/androidx/annotation/NonNull.java b/fakeannotations/src/main/java/androidx/annotation/NonNull.java
deleted file mode 100644
index fd3692c..0000000
--- a/fakeannotations/src/main/java/androidx/annotation/NonNull.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.annotation;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-import static java.lang.annotation.ElementType.FIELD;
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.PARAMETER;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-/**
- * Denotes that a parameter, field or method return value can never be null.
- * <p>
- * This is a marker annotation and it has no specific attributes.
- *
- * @paramDoc This value must never be {@code null}.
- * @returnDoc This value will never be {@code null}.
- * @hide
- */
-@Retention(SOURCE)
-@Target({METHOD, PARAMETER, FIELD})
-public @interface NonNull {
-}
diff --git a/fakeannotations/src/main/java/androidx/annotation/Nullable.java b/fakeannotations/src/main/java/androidx/annotation/Nullable.java
deleted file mode 100644
index 776aea7..0000000
--- a/fakeannotations/src/main/java/androidx/annotation/Nullable.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.annotation;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-import static java.lang.annotation.ElementType.FIELD;
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.PARAMETER;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-/**
- * Denotes that a parameter, field or method return value can be null.
- * <p>
- * When decorating a method call parameter, this denotes that the parameter can
- * legitimately be null and the method will gracefully deal with it. Typically
- * used on optional parameters.
- * <p>
- * When decorating a method, this denotes the method might legitimately return
- * null.
- * <p>
- * This is a marker annotation and it has no specific attributes.
- *
- * @paramDoc This value may be {@code null}.
- * @returnDoc This value may be {@code null}.
- * @hide
- */
-@Retention(SOURCE)
-@Target({METHOD, PARAMETER, FIELD})
-public @interface Nullable {
-}
diff --git a/fakeannotations/src/main/java/androidx/annotation/RecentlyNonNull.java b/fakeannotations/src/main/java/androidx/annotation/RecentlyNonNull.java
deleted file mode 100644
index 194aab1..0000000
--- a/fakeannotations/src/main/java/androidx/annotation/RecentlyNonNull.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.annotation;
-
-import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
-import static java.lang.annotation.ElementType.FIELD;
-import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.PACKAGE;
-import static java.lang.annotation.ElementType.PARAMETER;
-import static java.lang.annotation.RetentionPolicy.CLASS;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-@Retention(CLASS)
-@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE})
-public @interface RecentlyNonNull {
-}
diff --git a/fakeannotations/src/main/java/androidx/annotation/RecentlyNullable.java b/fakeannotations/src/main/java/androidx/annotation/RecentlyNullable.java
deleted file mode 100644
index 4ccdf93..0000000
--- a/fakeannotations/src/main/java/androidx/annotation/RecentlyNullable.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.annotation;
-
-import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
-import static java.lang.annotation.ElementType.FIELD;
-import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.PACKAGE;
-import static java.lang.annotation.ElementType.PARAMETER;
-import static java.lang.annotation.RetentionPolicy.CLASS;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-@Retention(CLASS)
-@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE})
-public @interface RecentlyNullable {
-}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index cbb7376..12edbcd 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -89,7 +89,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.0.2" }
+dackka = { module = "com.google.devsite:dackka", version = "1.0.3" }
dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
daggerCompiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
dexmakerMockito = { module = "com.linkedin.dexmaker:dexmaker-mockito", version.ref = "dexmaker" }
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/SdkConfig.java b/health/connect/connect-client/src/main/java/androidx/health/platform/client/SdkConfig.java
index 2697168..b955846 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/platform/client/SdkConfig.java
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/SdkConfig.java
@@ -25,7 +25,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class SdkConfig {
// should be increased everytime a new SDK is released
- public static final int SDK_VERSION = 5;
+ public static final int SDK_VERSION = 6;
private SdkConfig() {}
}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ipc/internal/ConnectionManager.java b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ipc/internal/ConnectionManager.java
index 716c654..25cb64c 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ipc/internal/ConnectionManager.java
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ipc/internal/ConnectionManager.java
@@ -22,12 +22,15 @@
import android.os.Message;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.VisibleForTesting;
import java.util.HashMap;
import java.util.Map;
+
/**
* Manages connections to a service in a different process.
*
@@ -37,12 +40,17 @@
public final class ConnectionManager implements Handler.Callback, ServiceConnection.Callback {
private static final String TAG = "ConnectionManager";
+ @VisibleForTesting
+ static final int UNBIND_IDLE_DELAY_MILLISECONDS = 15_000;
+
private static final int MSG_CONNECTED = 1;
private static final int MSG_DISCONNECTED = 2;
private static final int MSG_EXECUTE = 3;
private static final int MSG_REGISTER_LISTENER = 4;
private static final int MSG_UNREGISTER_LISTENER = 5;
+ private static final int MSG_UNBIND = 6;
+
private final Context mContext;
private final Handler mHandler;
private final Map<String, ServiceConnection> mServiceConnectionMap = new HashMap<>();
@@ -66,9 +74,10 @@
/**
* Registers a listener by executing an operation represented by the {@link QueueOperation}.
*
- * @param listenerKey Key based on which listeners will be distinguished.
+ * @param listenerKey Key based on which listeners will be distinguished.
* @param registerOperation Queue operation executed against the corresponding connection to
- * register the listener. Will be used to re-register when connection is lost.
+ * register the listener. Will be used to re-register when
+ * connection is lost.
*/
public void registerListener(ListenerKey listenerKey, QueueOperation registerOperation) {
mHandler.sendMessage(
@@ -79,9 +88,9 @@
/**
* Unregisters a listener by executing an operation represented by the {@link QueueOperation}.
*
- * @param listenerKey Key based on which listeners will be distinguished.
+ * @param listenerKey Key based on which listeners will be distinguished.
* @param unregisterOperation Queue operation executed against the corresponding connection to
- * unregister the listener.
+ * unregister the listener.
*/
public void unregisterListener(ListenerKey listenerKey, QueueOperation unregisterOperation) {
mHandler.sendMessage(
@@ -90,6 +99,19 @@
new ListenerHolder(listenerKey, unregisterOperation)));
}
+ /**
+ * Delays any existing {@code MSG_UNBIND} message to {@code UNBIND_IDLE_DELAY_MILLISECONDS}
+ * later.
+ *
+ * @param serviceConnection Service connection that we will auto-unbind.
+ */
+ void delayIdleServiceUnbindCheck(@NonNull ServiceConnection serviceConnection) {
+ mHandler.removeMessages(MSG_UNBIND, serviceConnection);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_UNBIND, serviceConnection),
+ UNBIND_IDLE_DELAY_MILLISECONDS);
+ }
+
@Override
public void onConnected(ServiceConnection connection) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_CONNECTED, connection));
@@ -107,40 +129,62 @@
}
@Override
- public boolean handleMessage(Message msg) {
+ public boolean handleMessage(@NonNull Message msg) {
switch (msg.what) {
case MSG_CONNECTED:
ServiceConnection serviceConnection = ((ServiceConnection) msg.obj);
serviceConnection.reRegisterAllListeners();
serviceConnection.refreshServiceVersion();
serviceConnection.flushQueue();
+ delayIdleServiceUnbindCheck(serviceConnection);
return true;
case MSG_DISCONNECTED:
((ServiceConnection) msg.obj).maybeReconnect();
return true;
case MSG_EXECUTE:
QueueOperation queueOperation = (QueueOperation) msg.obj;
- getConnection(queueOperation.getConnectionConfiguration()).enqueue(queueOperation);
+ ServiceConnection serviceConnectionForExecute =
+ getConnection(queueOperation.getConnectionConfiguration());
+ serviceConnectionForExecute.enqueue(queueOperation);
+ delayIdleServiceUnbindCheck(serviceConnectionForExecute);
return true;
case MSG_REGISTER_LISTENER:
ListenerHolder registerListenerHolder = (ListenerHolder) msg.obj;
- getConnection(
+ ServiceConnection serviceConnectionForRegister =
+ getConnection(
registerListenerHolder
.getListenerOperation()
- .getConnectionConfiguration())
+ .getConnectionConfiguration());
+ serviceConnectionForRegister
.registerListener(
registerListenerHolder.getListenerKey(),
registerListenerHolder.getListenerOperation());
+ delayIdleServiceUnbindCheck(serviceConnectionForRegister);
return true;
case MSG_UNREGISTER_LISTENER:
ListenerHolder unregisterListenerHolder = (ListenerHolder) msg.obj;
- getConnection(
+ ServiceConnection serviceConnectionForUnregister =
+ getConnection(
unregisterListenerHolder
.getListenerOperation()
- .getConnectionConfiguration())
+ .getConnectionConfiguration());
+ serviceConnectionForUnregister
.unregisterListener(
unregisterListenerHolder.getListenerKey(),
unregisterListenerHolder.getListenerOperation());
+ delayIdleServiceUnbindCheck(serviceConnectionForUnregister);
+ return true;
+ case MSG_UNBIND:
+ ServiceConnection serviceConnectionToClear = ((ServiceConnection) msg.obj);
+ if (mHandler.hasMessages(MSG_EXECUTE)
+ || mHandler.hasMessages(MSG_REGISTER_LISTENER)
+ || mHandler.hasMessages(MSG_UNREGISTER_LISTENER)) {
+ return true;
+ }
+ boolean isIdle = serviceConnectionToClear.clearConnectionIfIdle();
+ if (!isIdle) {
+ delayIdleServiceUnbindCheck(serviceConnectionToClear);
+ }
return true;
default:
Log.e(TAG, "Received unknown message: " + msg.what);
@@ -152,7 +196,8 @@
this.mBindToSelfEnabled = bindToSelfEnabled;
}
- private ServiceConnection getConnection(ConnectionConfiguration connectionConfiguration) {
+ @VisibleForTesting
+ ServiceConnection getConnection(ConnectionConfiguration connectionConfiguration) {
String connectionKey = connectionConfiguration.getKey();
ServiceConnection serviceConnection = mServiceConnectionMap.get(connectionKey);
if (serviceConnection == null) {
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ipc/internal/ServiceConnection.java b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ipc/internal/ServiceConnection.java
index e770bfa..3372711 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ipc/internal/ServiceConnection.java
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ipc/internal/ServiceConnection.java
@@ -34,6 +34,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
+import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -86,9 +87,12 @@
@VisibleForTesting
@Nullable
IBinder mBinder;
- private volatile boolean mIsServiceBound;
+
+ @VisibleForTesting
+ volatile boolean mIsServiceBound;
/** Denotes how many times connection to the service failed and we retried. */
private int mServiceConnectionRetry;
+ private final IBinder.DeathRecipient mDeathRecipient;
ServiceConnection(
Context context,
@@ -99,6 +103,13 @@
this.mConnectionConfiguration = checkNotNull(connectionConfiguration);
this.mExecutionTracker = checkNotNull(executionTracker);
this.mCallback = checkNotNull(callback);
+ this.mDeathRecipient = () -> {
+ Logger.warning(
+ TAG,
+ "Binder died for client:"
+ + mConnectionConfiguration.getClientName());
+ handleRetriableDisconnection(new RemoteException("Binder died"));
+ };
}
private String getBindPackageName() {
@@ -182,20 +193,49 @@
return (200 << retryNumber);
}
+ /**
+ * Unbinds the service if there is no pending operation queued.
+ *
+ * @return true if the service is idle and unbind service is attempted.
+ */
+ boolean clearConnectionIfIdle() {
+ if (mOperationQueue.isEmpty() && mRegisteredListeners.isEmpty()) {
+ tryClearConnection();
+ return true;
+ }
+ return false;
+ }
+
@VisibleForTesting
void clearConnection(Throwable throwable) {
+ tryClearConnection();
+ mExecutionTracker.cancelPendingFutures(throwable);
+ cancelAllOperationsInQueue(throwable);
+ }
+
+ private void tryClearConnection() {
+ // See Android Service unbind code sample.
+ // https://developer.android.com/reference/android/app/Service#local-service-sample
if (mIsServiceBound) {
try {
mContext.unbindService(this);
- mIsServiceBound = false;
} catch (IllegalArgumentException e) {
+ // In the unlikely scenario that we couldn't unbind the service, we will continue
+ // assuming the service is invalid. Future operations will try connect to service
+ // again.
Logger.error(TAG, "Failed to unbind the service. Ignoring and continuing", e);
}
+ mIsServiceBound = false;
}
-
- mBinder = null;
- mExecutionTracker.cancelPendingFutures(throwable);
- cancelAllOperationsInQueue(throwable);
+ if (mBinder != null) {
+ try {
+ mBinder.unlinkToDeath(mDeathRecipient, 0);
+ } catch (NoSuchElementException e) {
+ Logger.error(TAG, "mDeathRecipient not linked", e);
+ }
+ mBinder = null;
+ }
+ Logger.debug(TAG, "unbindService called");
}
void enqueue(QueueOperation operation) {
@@ -300,15 +340,7 @@
private void cleanOnDeath(IBinder binder) {
try {
- binder.linkToDeath(
- () -> {
- Logger.warning(
- TAG,
- "Binder died for client:"
- + mConnectionConfiguration.getClientName());
- handleRetriableDisconnection(new RemoteException("Binder died"));
- },
- /* flags= */ 0);
+ binder.linkToDeath(mDeathRecipient, /* flags= */ 0);
} catch (RemoteException exception) {
Logger.warning(
TAG,
diff --git a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ipc/internal/ConnectionManagerTest.java b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ipc/internal/ConnectionManagerTest.java
new file mode 100644
index 0000000..c273bb2
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ipc/internal/ConnectionManagerTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.platform.client.impl.ipc.internal;
+
+import static android.os.Looper.getMainLooper;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Application;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.concurrent.TimeUnit;
+
+@RunWith(RobolectricTestRunner.class)
+public class ConnectionManagerTest {
+
+ @Rule
+ public final MockitoRule mocks = MockitoJUnit.rule();
+ @Mock
+ private IBinder mBinder;
+
+ private ConnectionManager mConnectionManager;
+ private final VersionQueueOperation mVersionOperation =
+ new VersionQueueOperation();
+ private final ConnectionConfiguration mClientConfiguration =
+ new ConnectionConfiguration("", "client_name", "bind_action", mVersionOperation);
+
+ @Before
+ public void setUp() {
+ mConnectionManager = new ConnectionManager(
+ ApplicationProvider.getApplicationContext(),
+ getMainLooper());
+ Intent bindIntent =
+ new Intent()
+ .setPackage(mClientConfiguration.getPackageName())
+ .setAction(mClientConfiguration.getBindAction());
+ shadowOf((Application) getApplicationContext())
+ .setComponentNameAndServiceForBindServiceForIntent(
+ bindIntent,
+ new ComponentName(
+ mClientConfiguration.getPackageName(),
+ mClientConfiguration.getClientName()),
+ mBinder);
+ when(mBinder.isBinderAlive()).thenReturn(true);
+ }
+
+ @Test
+ public void scheduleExecution_connect() {
+ QueueOperation queueOperation = new FakeQueueOperation(mClientConfiguration);
+
+ mConnectionManager.scheduleForExecution(queueOperation);
+ shadowOf(getMainLooper()).idle();
+
+ assertThat(mConnectionManager.getConnection(mClientConfiguration).mIsServiceBound).isTrue();
+
+ shadowOf(getMainLooper()).runToEndOfTasks();
+ assertThat(
+ mConnectionManager.getConnection(mClientConfiguration).mIsServiceBound).isFalse();
+ }
+
+ @Test
+ public void scheduleExecution_delayDisconnection() {
+ QueueOperation queueOperation = new FakeQueueOperation(mClientConfiguration);
+
+ mConnectionManager.scheduleForExecution(queueOperation);
+ shadowOf(getMainLooper()).idleFor(2000, TimeUnit.MILLISECONDS);
+ assertThat(mConnectionManager.getConnection(mClientConfiguration).mIsServiceBound).isTrue();
+ mConnectionManager.scheduleForExecution(queueOperation);
+
+ shadowOf(getMainLooper()).idleFor(ConnectionManager.UNBIND_IDLE_DELAY_MILLISECONDS - 2000,
+ TimeUnit.MILLISECONDS);
+ assertThat(mConnectionManager.getConnection(mClientConfiguration).mIsServiceBound).isTrue();
+
+ shadowOf(getMainLooper()).runToEndOfTasks();
+ assertThat(
+ mConnectionManager.getConnection(mClientConfiguration).mIsServiceBound).isFalse();
+ }
+
+ private static class FakeQueueOperation extends BaseQueueOperation {
+ boolean mExecuted = false;
+ Throwable mThrowable = null;
+
+ FakeQueueOperation(ConnectionConfiguration connectionConfiguration) {
+ super(connectionConfiguration);
+ }
+
+ @Override
+ public void execute(@NonNull IBinder binder) {
+ mExecuted = true;
+ }
+
+ @Override
+ public void setException(@NonNull Throwable exception) {
+ mThrowable = exception;
+ }
+
+ public boolean isExecuted() {
+ return mExecuted;
+ }
+ }
+
+ private static class VersionQueueOperation implements QueueOperation {
+ public boolean mWasExecuted = false;
+
+ @Override
+ public void execute(IBinder binder) throws RemoteException {
+ mWasExecuted = true;
+ }
+
+ @Override
+ public void setException(Throwable exception) {
+ }
+
+ @Override
+ public QueueOperation trackExecution(ExecutionTracker tracker) {
+ return this;
+ }
+
+ @Override
+ public ConnectionConfiguration getConnectionConfiguration() {
+ return new ConnectionConfiguration("", "client_name", "bind_action", this);
+ }
+ }
+}
diff --git a/include-composite-deps.gradle b/include-composite-deps.gradle
deleted file mode 100644
index b368673..0000000
--- a/include-composite-deps.gradle
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-// This file is part of a a workaround for https://github.com/gradle/gradle/issues/1909 to enable
-// including this Support Library build as an included build. See include-support-library.gradle
-// for usage instructions.
-
-boolean currentBuildIsRootBuild = (gradle.parent == null)
-
-File getExternalProjectPath() {
- def scriptDir = file(buildscript.sourceFile.parent)
- if (System.getenv("COMPOSE_DESKTOP_GITHUB_BUILD") != null) {
- def path = new File(System.env.OUT_DIR)
- if (!(new File(path, "doclava").isDirectory())) {
- throw new GradleException("Please checkout doclava to $path")
- }
- return path.getCanonicalFile()
- } else {
- return new File(scriptDir, "../../external").getCanonicalFile()
- }
-}
-
-// Add included builds. This only works if this is currently the root build, so this script should
-// be applied to several builds and will only enable itself when part of the root build.
-if (currentBuildIsRootBuild) {
- includeBuild(new File(getExternalProjectPath(), "doclava"))
-}
diff --git a/libraryversions.toml b/libraryversions.toml
index 658cc95..2130a4c 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -13,14 +13,14 @@
BLUETOOTH = "1.0.0-alpha01"
BROWSER = "1.5.0-alpha01"
BUILDSRC_TESTS = "1.0.0-alpha01"
-CAMERA = "1.2.0-beta03"
+CAMERA = "1.2.0-rc01"
CAMERA_PIPE = "1.0.0-alpha01"
CARDVIEW = "1.1.0-alpha01"
CAR_APP = "1.3.0-beta02"
COLLECTION = "1.3.0-dev01"
-COMPOSE = "1.3.0-beta04"
+COMPOSE = "1.3.0-rc01"
COMPOSE_COMPILER = "1.3.2"
-COMPOSE_MATERIAL3 = "1.0.0-beta04"
+COMPOSE_MATERIAL3 = "1.0.0-rc01"
COMPOSE_RUNTIME_TRACING = "1.0.0-alpha01"
CONSTRAINTLAYOUT_COMPOSE = "1.1.0-alpha01"
CONSTRAINTLAYOUT = "2.2.0-alpha01"
@@ -31,7 +31,7 @@
CORE_ANIMATION = "1.0.0-beta02"
CORE_ANIMATION_TESTING = "1.0.0-beta01"
CORE_APPDIGEST = "1.0.0-alpha01"
-CORE_GOOGLE_SHORTCUTS = "1.1.0-beta02"
+CORE_GOOGLE_SHORTCUTS = "1.1.0-rc01"
CORE_I18N = "1.0.0-alpha01"
CORE_PERFORMANCE = "1.0.0-alpha03"
CORE_REMOTEVIEWS = "1.0.0-beta03"
@@ -58,7 +58,7 @@
GLANCE_TEMPLATE = "1.0.0-alpha01"
GRAPHICS = "1.0.0-alpha01"
GRIDLAYOUT = "1.1.0-alpha01"
-HEALTH_CONNECT = "1.0.0-alpha05"
+HEALTH_CONNECT = "1.0.0-alpha06"
HEALTH_SERVICES_CLIENT = "1.0.0-beta01"
HEIFWRITER = "1.1.0-alpha02"
HILT = "1.1.0-alpha01"
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouterJellybean.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouterJellybean.java
index 44daca7..f1e7a65 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouterJellybean.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouterJellybean.java
@@ -337,7 +337,10 @@
}
}
- @SuppressLint("BanUncheckedReflection") // TODO: b/232075564
+ // Suppress BanUncheckedReflection as the lint raises false-positive exception around this
+ // code: the reflection is used for a specific Android version and the real Android API
+ // check is happening in the class' constructor and in SystemMediaRouteProvider#obtain
+ @SuppressLint("BanUncheckedReflection")
public void selectRoute(@NonNull Object routerObj, int types, @NonNull Object routeObj) {
android.media.MediaRouter router = (android.media.MediaRouter) routerObj;
android.media.MediaRouter.RouteInfo route =
@@ -391,7 +394,10 @@
}
}
- @SuppressLint("BanUncheckedReflection") // TODO: b/232075564
+ // Suppress BanUncheckedReflection as the lint raises false-positive exception around this
+ // code: the reflection is used for a specific Android version and the real Android API
+ // check is happening in the class' constructor and in SystemMediaRouteProvider#obtain
+ @SuppressLint("BanUncheckedReflection")
@NonNull
public Object getDefaultRoute(@NonNull Object routerObj) {
android.media.MediaRouter router = (android.media.MediaRouter) routerObj;
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouterJellybeanMr1.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouterJellybeanMr1.java
index de46e8e..6dc606d 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouterJellybeanMr1.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouterJellybeanMr1.java
@@ -127,7 +127,10 @@
}
}
- @SuppressLint("BanUncheckedReflection") // TODO: b/232075564
+ // Suppress BanUncheckedReflection as the lint raises false-positive exception around this
+ // code: the reflection is used for a specific Android version and the real Android API
+ // check is happening in the class' constructor and in SystemMediaRouteProvider#obtain
+ @SuppressLint("BanUncheckedReflection")
@Override
public void run() {
if (mActivelyScanningWifiDisplays) {
@@ -168,7 +171,10 @@
}
}
- @SuppressLint("BanUncheckedReflection") // TODO: b/232075564
+ // Suppress BanUncheckedReflection as the lint raises false-positive exception around this
+ // code: the reflection is used for a specific Android version and the real Android API
+ // check is happening in the class' constructor and in SystemMediaRouteProvider#obtain
+ @SuppressLint("BanUncheckedReflection")
public boolean isConnecting(@NonNull Object routeObj) {
android.media.MediaRouter.RouteInfo route =
(android.media.MediaRouter.RouteInfo) routeObj;
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/TestPager.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/TestPager.kt
index 4ca869f..8e4fa49 100644
--- a/paging/paging-testing/src/main/java/androidx/paging/testing/TestPager.kt
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/TestPager.kt
@@ -115,7 +115,6 @@
*/
private suspend fun doInitialLoad(initialKey: Key?): LoadResult<Key, Value> {
return lock.withLock {
- ensureValidPagingSource()
pagingSource.load(
LoadParams.Refresh(initialKey, config.initialLoadSize, config.enablePlaceholders)
).also { result ->
@@ -131,7 +130,6 @@
*/
private suspend fun doLoad(loadType: LoadType): LoadResult<Key, Value>? {
return lock.withLock {
- ensureValidPagingSource()
if (!hasRefreshed.get()) {
throw IllegalStateException("TestPager's first load operation must be a refresh. " +
"Please call refresh() once before calling ${loadType.name.lowercase()}().")
@@ -269,12 +267,4 @@
0
}
}
-
- private fun ensureValidPagingSource() {
- check(!pagingSource.invalid) {
- "This TestPager cannot perform further loads as PagingSource $pagingSource has " +
- "been invalidated. If the PagingSource is expected to be invalid, you can " +
- "continue to load by creating a new TestPager with a new PagingSource."
- }
- }
}
\ No newline at end of file
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/TestPagerTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/TestPagerTest.kt
index af95c90..7dad115 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/TestPagerTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/TestPagerTest.kt
@@ -110,11 +110,10 @@
runTest {
source.invalidate()
assertTrue(source.invalid)
- assertFailsWith<IllegalStateException> {
- pager.run {
- refresh()
- }
- }
+ // simulate a PagingSource that returns LoadResult.Invalid when it's invalidated
+ source.nextLoadResult = LoadResult.Invalid()
+
+ assertThat(pager.refresh()).isInstanceOf(LoadResult.Invalid::class.java)
}
}
@@ -374,13 +373,15 @@
val source = TestPagingSource()
val pager = TestPager(source, CONFIG)
- assertFailsWith<IllegalStateException> {
- pager.run {
- refresh()
- source.invalidate()
- append()
- }
+ val result = pager.run {
+ refresh()
+ source.invalidate()
+ assertThat(source.invalid).isTrue()
+ // simulate a PagingSource which returns LoadResult.Invalid when it's invalidated
+ source.nextLoadResult = LoadResult.Invalid()
+ append()
}
+ assertThat(result).isInstanceOf(LoadResult.Invalid::class.java)
}
@Test
@@ -388,13 +389,15 @@
val source = TestPagingSource()
val pager = TestPager(source, CONFIG)
- assertFailsWith<IllegalStateException> {
- pager.run {
- refresh()
- source.invalidate()
- prepend()
- }
+ val result = pager.run {
+ refresh(initialKey = 20)
+ source.invalidate()
+ assertThat(source.invalid).isTrue()
+ // simulate a PagingSource which returns LoadResult.Invalid when it's invalidated
+ source.nextLoadResult = LoadResult.Invalid()
+ prepend()
}
+ assertThat(result).isInstanceOf(LoadResult.Invalid::class.java)
}
@Test
diff --git a/placeholder/build.gradle b/placeholder/build.gradle
new file mode 100644
index 0000000..3965b29
--- /dev/null
+++ b/placeholder/build.gradle
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+apply from: "../buildSrc/out.gradle"
+init.chooseOutDir()
\ No newline at end of file
diff --git a/placeholder/settings.gradle b/placeholder/settings.gradle
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/placeholder/settings.gradle
diff --git a/privacysandbox/tools/tools-apicompiler/build.gradle b/privacysandbox/tools/tools-apicompiler/build.gradle
index f5db6da..d45bcdf 100644
--- a/privacysandbox/tools/tools-apicompiler/build.gradle
+++ b/privacysandbox/tools/tools-apicompiler/build.gradle
@@ -30,6 +30,7 @@
implementation project(path: ':privacysandbox:tools:tools')
implementation project(path: ':privacysandbox:tools:tools-core')
+ testImplementation(project(":privacysandbox:tools:tools-testing"))
testImplementation(project(":room:room-compiler-processing-testing"))
testImplementation(libs.junit)
testImplementation(libs.truth)
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
index 8b5b6f1..89b3b44 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
@@ -16,6 +16,7 @@
package androidx.privacysandbox.tools.apicompiler.generator
+import androidx.privacysandbox.tools.core.generator.build
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.ParsedApi
import androidx.privacysandbox.tools.core.model.getOnlyService
@@ -65,30 +66,30 @@
}
private fun generateOnLoadSdkFunction(): FunSpec {
- return FunSpec.builder("onLoadSdk")
- .addModifiers(KModifier.OVERRIDE)
- .addParameter("params", BUNDLE_CLASS)
- .returns(SANDBOXED_SDK_CLASS)
- .addStatement(
+ return FunSpec.builder("onLoadSdk").build {
+ addModifiers(KModifier.OVERRIDE)
+ addParameter("params", BUNDLE_CLASS)
+ returns(SANDBOXED_SDK_CLASS)
+ addStatement(
"val sdk = ${getCreateServiceFunctionName(api.getOnlyService())}(context!!)"
)
- .addStatement(
+ addStatement(
"return ${SANDBOXED_SDK_CLASS.simpleName}" +
"(${api.getOnlyService().stubDelegateName()}(sdk))"
)
- .build()
+ }
}
private fun generateGetViewFunction(): FunSpec {
- return FunSpec.builder("getView")
- .addModifiers(KModifier.OVERRIDE)
- .addParameter("windowContext", CONTEXT_CLASS)
- .addParameter("params", BUNDLE_CLASS)
- .addParameter("width", Int::class)
- .addParameter("height", Int::class)
- .returns(VIEW_CLASS)
- .addStatement("TODO(\"Implement\")")
- .build()
+ return FunSpec.builder("getView").build {
+ addModifiers(KModifier.OVERRIDE)
+ addParameter("windowContext", CONTEXT_CLASS)
+ addParameter("params", BUNDLE_CLASS)
+ addParameter("width", Int::class)
+ addParameter("height", Int::class)
+ returns(VIEW_CLASS)
+ addStatement("TODO(\"Implement\")")
+ }
}
private fun generateCreateServiceFunction(service: AnnotatedInterface): FunSpec {
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/StubDelegatesGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/StubDelegatesGenerator.kt
index 20dbd91..643225d 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/StubDelegatesGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/StubDelegatesGenerator.kt
@@ -16,6 +16,9 @@
package androidx.privacysandbox.tools.apicompiler.generator
+import androidx.privacysandbox.tools.core.generator.addCode
+import androidx.privacysandbox.tools.core.generator.build
+import androidx.privacysandbox.tools.core.generator.addControlFlow
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.Method
import androidx.privacysandbox.tools.core.model.ParsedApi
@@ -91,22 +94,25 @@
private fun toSuspendFunSpec(method: Method): FunSpec {
val resultStatement =
"delegate.${method.name}(${method.parameters.joinToString(", ") { it.name }})"
- return FunSpec.builder(method.name)
- .addModifiers(KModifier.OVERRIDE)
- .addParameters(getParameters(method))
- .addStatement("val job = GlobalScope.launch(Dispatchers.Main) {")
- .addStatement(" try {")
- .addStatement(" val result = $resultStatement")
- .addStatement(" transactionCallback.onSuccess(result)")
- .addStatement(" } catch (t: Throwable) {")
- .addStatement(" transactionCallback.onFailure(404, t.message)")
- .addStatement(" }")
- .addStatement("}")
- .addStatement(
- "val cancellationSignal = TransportCancellationCallback(){ job.cancel() }"
- )
- .addStatement("transactionCallback.onCancellable(cancellationSignal)")
- .build()
+ return FunSpec.builder(method.name).build {
+ addModifiers(KModifier.OVERRIDE)
+ addParameters(getParameters(method))
+ addCode {
+ addControlFlow("val job = GlobalScope.launch(Dispatchers.Main)") {
+ addControlFlow("try") {
+ addStatement("val result = $resultStatement")
+ addStatement("transactionCallback.onSuccess(result)")
+ }
+ addControlFlow("catch (t: Throwable)") {
+ addStatement("transactionCallback.onFailure(404, t.message)")
+ }
+ }
+ addStatement(
+ "val cancellationSignal = TransportCancellationCallback() { job.cancel() }"
+ )
+ addStatement("transactionCallback.onCancellable(cancellationSignal)")
+ }
+ }
}
private fun toNonSuspendFunSpec(method: Method): FunSpec {
@@ -157,15 +163,17 @@
"hasCancelled",
ATOMIC_BOOLEAN_CLASS,
KModifier.PRIVATE
- ).initializer("${ATOMIC_BOOLEAN_CLASS.simpleName}(false)").build()
+ ).initializer("%T(false)", ATOMIC_BOOLEAN_CLASS).build()
)
.addFunction(
- FunSpec.builder("cancel")
- .addModifiers(KModifier.OVERRIDE)
- .addStatement("if (hasCancelled.compareAndSet(false, true)) {")
- .addStatement(" onCancel()")
- .addStatement("}")
- .build()
+ FunSpec.builder("cancel").build {
+ addModifiers(KModifier.OVERRIDE)
+ addCode {
+ addControlFlow("if (hasCancelled.compareAndSet(false, true))") {
+ addStatement("onCancel()")
+ }
+ }
+ }
)
val fileSpec = FileSpec.builder(packageName, className)
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
index 39b0d62..535f9b4 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
@@ -16,10 +16,13 @@
package androidx.privacysandbox.tools.apicompiler
-import androidx.privacysandbox.tools.apicompiler.util.CompilationResultSubject.Companion.assertThat
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.compileAll
+import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
import androidx.room.compiler.processing.util.compiler.compile
import androidx.room.compiler.processing.util.Source
+import java.io.File
import java.nio.file.Files
import org.junit.Test
import org.junit.runner.RunWith
@@ -29,29 +32,17 @@
class PrivacySandboxKspCompilerTest {
@Test
fun compileServiceInterface_ok() {
- val source =
- Source.kotlin(
- "com/mysdk/MySdk.kt",
- """
- package com.mysdk
- import androidx.privacysandbox.tools.PrivacySandboxService
- @PrivacySandboxService
- interface MySdk {
- suspend fun doStuff(x: Int, y: Int): String
- fun doMoreStuff()
- }
- """
- )
+ val inputTestDataDir = File("src/test/test-data/testinterface/input")
+ val outputTestDataDir = File("src/test/test-data/testinterface/output")
+ val sources = loadSourcesFromDirectory(inputTestDataDir)
+ val expectedOutput = loadSourcesFromDirectory(outputTestDataDir)
val provider = PrivacySandboxKspCompiler.Provider()
// Check that compilation is successful
assertThat(
- compile(
- Files.createTempDirectory("test").toFile(),
- TestCompilationArguments(
- sources = listOf(source) + getSyntheticAndroidClasses(),
- symbolProcessorProviders = listOf(provider),
- processorOptions = getProcessorOptions(),
- )
+ compileAll(
+ sources,
+ symbolProcessorProviders = listOf(provider),
+ processorOptions = getProcessorOptions(),
)
).also {
it.generatesExactlySources(
@@ -63,95 +54,7 @@
"com/mysdk/TransportCancellationCallback.kt",
)
}.also {
- it.generatesSourcesWithContents(
- "com/mysdk/AbstractSandboxedSdkProvider.kt" to """
- |package com.mysdk
- |
- |import android.app.sdksandbox.SandboxedSdk
- |import android.app.sdksandbox.SandboxedSdkProvider
- |import android.content.Context
- |import android.os.Bundle
- |import android.view.View
- |import kotlin.Int
- |import kotlin.Unit
- |
- |public abstract class AbstractSandboxedSdkProvider : SandboxedSdkProvider() {
- | public override fun onLoadSdk(params: Bundle): SandboxedSdk {
- | val sdk = createMySdk(context!!)
- | return SandboxedSdk(MySdkStubDelegate(sdk))
- | }
- |
- | public override fun getView(
- | windowContext: Context,
- | params: Bundle,
- | width: Int,
- | height: Int,
- | ): View {
- | TODO("Implement")
- | }
- |
- | public override fun onDataReceived(`data`: Bundle,
- | callback: SandboxedSdkProvider.DataReceivedCallback): Unit {
- | }
- |
- | protected abstract fun createMySdk(context: Context): MySdk
- |}
- |
- """.trimMargin(),
- "com/mysdk/MySdkStubDelegate.kt" to """
- |package com.mysdk
- |
- |import kotlin.Int
- |import kotlin.Unit
- |import kotlinx.coroutines.CoroutineScope
- |import kotlinx.coroutines.Dispatchers
- |import kotlinx.coroutines.GlobalScope
- |import kotlinx.coroutines.launch
- |
- |public class MySdkStubDelegate internal constructor(
- | private val `delegate`: MySdk,
- |) : IMySdk.Stub() {
- | public override fun doStuff(
- | x: Int,
- | y: Int,
- | transactionCallback: IStringTransactionCallback,
- | ): Unit {
- | val job = GlobalScope.launch(Dispatchers.Main) {
- | try {
- | val result = delegate.doStuff(x, y)
- | transactionCallback.onSuccess(result)
- | } catch (t: Throwable) {
- | transactionCallback.onFailure(404, t.message)
- | }
- | }
- | val cancellationSignal = TransportCancellationCallback(){ job.cancel() }
- | transactionCallback.onCancellable(cancellationSignal)
- | }
- |
- | public override fun doMoreStuff(): Unit = delegate.doMoreStuff()
- |}
- |
- """.trimMargin(),
- "com/mysdk/TransportCancellationCallback.kt" to """
- |package com.mysdk
- |
- |import java.util.concurrent.atomic.AtomicBoolean
- |import kotlin.Unit
- |
- |internal class TransportCancellationCallback internal constructor(
- | private val onCancel: () -> Unit,
- |) : ICancellationSignal.Stub() {
- | private val hasCancelled: AtomicBoolean = AtomicBoolean(false)
- |
- | public override fun cancel(): Unit {
- | if (hasCancelled.compareAndSet(false, true)) {
- | onCancel()
- | }
- | }
- |}
- |
- """.trimMargin(),
- )
+ it.generatesSourcesWithContents(expectedOutput)
}
}
@@ -189,13 +92,10 @@
val provider = PrivacySandboxKspCompiler.Provider()
// Check that compilation fails
assertThat(
- compile(
- Files.createTempDirectory("test").toFile(),
- TestCompilationArguments(
- sources = listOf(source) + getSyntheticAndroidClasses(),
- symbolProcessorProviders = listOf(provider),
- processorOptions = getProcessorOptions(),
- )
+ compileAll(
+ listOf(source),
+ symbolProcessorProviders = listOf(provider),
+ processorOptions = getProcessorOptions(),
)
).fails()
}
@@ -205,65 +105,4 @@
"aidl_compiler_path" to (System.getProperty("aidl_compiler_path")
?: throw IllegalArgumentException("aidl_compiler_path flag not set."))
)
-
- private fun getSyntheticAndroidClasses() =
- listOf(
- Source.java(
- "android.app.sdksandbox.SandboxedSdk",
- """
- package android.app.sdksandbox;
- import android.os.IBinder;
- public class SandboxedSdk {
- public SandboxedSdk(IBinder sdkInterface) {}
- }
- """.trimIndent()
- ),
- Source.java(
- "android.app.sdksandbox.SandboxedSdkProvider",
- """
- package android.app.sdksandbox;
- import android.content.Context;
- import android.os.Bundle;
- import android.view.View;
- public abstract class SandboxedSdkProvider {
- public abstract SandboxedSdk onLoadSdk(Bundle params) throws LoadSdkException;
- public abstract View getView(
- Context windowContext, Bundle params, int width, int height);
- public final Context getContext() {
- return null;
- }
- public abstract void onDataReceived(
- Bundle data, DataReceivedCallback callback);
- public interface DataReceivedCallback {
- void onDataReceivedSuccess(Bundle params);
- void onDataReceivedError(String errorMessage);
- }
- }
- """.trimIndent()
- ),
- Source.java(
- "android.app.sdksandbox.LoadSdkException",
- """
- package android.app.sdksandbox;
- import android.os.Parcel;
- import android.os.Parcelable;
- public final class LoadSdkException extends Exception implements Parcelable {
- @Override
- public int describeContents() {
- return 0;
- }
- @Override
- public void writeToParcel(Parcel destination, int flags) {
- }
- }
- """.trimIndent()
- ),
- Source.java(
- "android.app.sdksandbox.SandboxedSdkContext",
- """
- package android.app.sdksandbox;
- public final class SandboxedSdkContext {}
- """.trimIndent()
- ),
- )
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/util/KspTestRunner.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/util/KspTestRunner.kt
index 619f506..a042edf 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/util/KspTestRunner.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/util/KspTestRunner.kt
@@ -18,13 +18,11 @@
import androidx.privacysandbox.tools.core.model.ParsedApi
import androidx.privacysandbox.tools.apicompiler.parser.ApiParser
-import androidx.room.compiler.processing.util.DiagnosticMessage
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
+import androidx.privacysandbox.tools.testing.CompilationResultSubject
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
-import androidx.room.compiler.processing.util.compiler.TestCompilationResult
import androidx.room.compiler.processing.util.compiler.compile
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
@@ -32,20 +30,21 @@
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.google.devtools.ksp.symbol.KSAnnotated
import java.nio.file.Files
-import javax.tools.Diagnostic
/**
* Helper to run KSP processing functionality in tests.
*/
fun parseSource(source: Source): ParsedApi {
val provider = CapturingSymbolProcessor.Provider()
- compile(
- Files.createTempDirectory("test").toFile(),
- TestCompilationArguments(
- sources = listOf(source),
- symbolProcessorProviders = listOf(provider),
+ assertThat(
+ compile(
+ Files.createTempDirectory("test").toFile(),
+ TestCompilationArguments(
+ sources = listOf(source),
+ symbolProcessorProviders = listOf(provider),
+ )
)
- )
+ ).succeeds()
assert(provider.processor.capture != null) { "KSP run didn't produce any output." }
return provider.processor.capture!!
}
@@ -59,64 +58,7 @@
symbolProcessorProviders = listOf(provider)
)
)
- assertThat(result.success).isFalse()
- return CompilationResultSubject(result)
-}
-
-class CompilationResultSubject(private val result: TestCompilationResult) {
- companion object {
- fun assertThat(result: TestCompilationResult) = CompilationResultSubject(result)
- }
-
- fun succeeds() {
- assertWithMessage(
- "UnexpectedErrors:\n${getFullErrorMessages()?.joinToString("\n")}"
- ).that(
- result.success
- ).isTrue()
- }
-
- fun generatesExactlySources(vararg sourcePaths: String) {
- succeeds()
- assertThat(result.generatedSources.map(Source::relativePath))
- .containsExactlyElementsIn(sourcePaths)
- }
-
- fun generatesSourcesWithContents(vararg sources: Pair<String, String>) {
- succeeds()
- val contentsByFile = result.generatedSources.associate { it.relativePath to it.contents }
- for ((file, content) in sources) {
- assertWithMessage("File $file was not generated").that(contentsByFile).containsKey(file)
- assertThat(contentsByFile[file]).isEqualTo(content)
- }
- }
-
- fun fails() {
- assertThat(result.success).isFalse()
- }
-
- fun containsError(error: String) {
- assertThat(getErrorMessages())
- .contains(error)
- }
-
- fun containsExactlyErrors(vararg errors: String) {
- assertThat(getErrorMessages())
- .containsExactly(*errors)
- }
-
- private fun getErrorMessages() =
- result.diagnostics[Diagnostic.Kind.ERROR]?.map(DiagnosticMessage::msg)
-
- private fun getFullErrorMessages() =
- result.diagnostics[Diagnostic.Kind.ERROR]?.map(::toReadableMessage)
-
- private fun toReadableMessage(message: DiagnosticMessage) = """
- |Error: ${message.msg}
- |Location: ${message.location?.source?.relativePath}:${message.location?.line}
- |File:
- |${message.location?.source?.contents}
- """.trimMargin()
+ return assertThat(result).also { it.fails() }
}
private class CapturingSymbolProcessor(private val logger: KSPLogger) : SymbolProcessor {
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
new file mode 100644
index 0000000..353655c
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
@@ -0,0 +1,10 @@
+package com.mysdk
+
+import androidx.privacysandbox.tools.PrivacySandboxService
+
+@PrivacySandboxService
+interface MySdk {
+ suspend fun doStuff(x: Int, y: Int): String
+
+ fun doMoreStuff()
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/AbstractSandboxedSdkProvider.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/AbstractSandboxedSdkProvider.kt
new file mode 100644
index 0000000..97ab29e
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/AbstractSandboxedSdkProvider.kt
@@ -0,0 +1,31 @@
+package com.mysdk
+
+import android.app.sdksandbox.SandboxedSdk
+import android.app.sdksandbox.SandboxedSdkProvider
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import kotlin.Int
+import kotlin.Unit
+
+public abstract class AbstractSandboxedSdkProvider : SandboxedSdkProvider() {
+ public override fun onLoadSdk(params: Bundle): SandboxedSdk {
+ val sdk = createMySdk(context!!)
+ return SandboxedSdk(MySdkStubDelegate(sdk))
+ }
+
+ public override fun getView(
+ windowContext: Context,
+ params: Bundle,
+ width: Int,
+ height: Int,
+ ): View {
+ TODO("Implement")
+ }
+
+ public override fun onDataReceived(`data`: Bundle,
+ callback: SandboxedSdkProvider.DataReceivedCallback): Unit {
+ }
+
+ protected abstract fun createMySdk(context: Context): MySdk
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
new file mode 100644
index 0000000..a6d8695
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
@@ -0,0 +1,32 @@
+package com.mysdk
+
+import kotlin.Int
+import kotlin.Unit
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+public class MySdkStubDelegate internal constructor(
+ private val `delegate`: MySdk,
+) : IMySdk.Stub() {
+ public override fun doStuff(
+ x: Int,
+ y: Int,
+ transactionCallback: IStringTransactionCallback,
+ ): Unit {
+ val job = GlobalScope.launch(Dispatchers.Main) {
+ try {
+ val result = delegate.doStuff(x, y)
+ transactionCallback.onSuccess(result)
+ }
+ catch (t: Throwable) {
+ transactionCallback.onFailure(404, t.message)
+ }
+ }
+ val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+ transactionCallback.onCancellable(cancellationSignal)
+ }
+
+ public override fun doMoreStuff(): Unit = delegate.doMoreStuff()
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/TransportCancellationCallback.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/TransportCancellationCallback.kt
new file mode 100644
index 0000000..287dc52
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/TransportCancellationCallback.kt
@@ -0,0 +1,16 @@
+package com.mysdk
+
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.Unit
+
+internal class TransportCancellationCallback internal constructor(
+ private val onCancel: () -> Unit,
+) : ICancellationSignal.Stub() {
+ private val hasCancelled: AtomicBoolean = AtomicBoolean(false)
+
+ public override fun cancel(): Unit {
+ if (hasCancelled.compareAndSet(false, true)) {
+ onCancel()
+ }
+ }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/build.gradle b/privacysandbox/tools/tools-apigenerator/build.gradle
index 2b70c56..7c22295 100644
--- a/privacysandbox/tools/tools-apigenerator/build.gradle
+++ b/privacysandbox/tools/tools-apigenerator/build.gradle
@@ -33,8 +33,9 @@
implementation project(path: ':privacysandbox:tools:tools')
implementation project(path: ':privacysandbox:tools:tools-core')
- testImplementation(project(":room:room-compiler-processing-testing"))
testImplementation(project(":internal-testutils-truth"))
+ testImplementation(project(":privacysandbox:tools:tools-testing"))
+ testImplementation(project(":room:room-compiler-processing-testing"))
testImplementation(libs.kotlinCoroutinesCore)
testImplementation(libs.junit)
testImplementation(libs.truth)
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ClientProxyTypeGenerator.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ClientProxyTypeGenerator.kt
index 7e9ca6b..894b253 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ClientProxyTypeGenerator.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ClientProxyTypeGenerator.kt
@@ -16,15 +16,17 @@
package androidx.privacysandbox.tools.apigenerator
-import androidx.privacysandbox.tools.core.model.AnnotatedInterface
-import androidx.privacysandbox.tools.core.model.Method
-import androidx.privacysandbox.tools.core.model.Parameter
import androidx.privacysandbox.tools.core.generator.build
import androidx.privacysandbox.tools.core.generator.poetSpec
import androidx.privacysandbox.tools.core.generator.primaryConstructor
+import androidx.privacysandbox.tools.core.model.AnnotatedInterface
+import androidx.privacysandbox.tools.core.model.Method
+import androidx.privacysandbox.tools.core.model.Parameter
import com.squareup.kotlinpoet.ClassName
+import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
@@ -33,6 +35,8 @@
ClassName(service.packageName, "${service.name}ClientProxy")
internal val remoteBinderClassName =
ClassName(service.packageName, "I${service.name}")
+ private val cancellationSignalClassName =
+ ClassName(service.packageName, "ICancellationSignal")
fun generate(): TypeSpec = TypeSpec.classBuilder(className).build {
addModifiers(KModifier.PRIVATE)
@@ -43,29 +47,104 @@
.addModifiers(KModifier.PRIVATE).build()
)
)
- addFunctions(service.methods.map(::generateProxyMethodImplementation))
+ addFunctions(service.methods.map { method ->
+ if (method.isSuspend) {
+ generateSuspendProxyMethodImplementation(method)
+ } else {
+ generateProxyMethodImplementation(method)
+ }
+ })
}
private fun generateProxyMethodImplementation(method: Method) =
FunSpec.builder(method.name).build {
- addParameters(method.parameters.map { it.poetSpec() })
addModifiers(KModifier.OVERRIDE)
+ addParameters(method.parameters.map { it.poetSpec() })
- val parameterList = buildList {
- addAll(method.parameters.map(Parameter::name))
- if (method.isSuspend)
- add("null")
- }
- val returnsUnit =
- method.returnType.name == Unit::class.qualifiedName || method.isSuspend
- if (returnsUnit) {
+ val parameterList = method.parameters.map(Parameter::name)
+ if (method.returnsUnit) {
addStatement(
"remote.${method.name}(${parameterList.joinToString()})"
)
} else {
addStatement(
- "return remote.${method.name}(${parameterList.joinToString()})!!"
+ "return remote.${method.name}(${parameterList.joinToString()})"
)
}
}
+
+ private fun generateSuspendProxyMethodImplementation(method: Method) =
+ FunSpec.builder(method.name).build {
+ addModifiers(KModifier.OVERRIDE)
+ addModifiers(KModifier.SUSPEND)
+ addParameters(method.parameters.map { it.poetSpec() })
+ returns(method.returnType.poetSpec())
+
+ beginControlFlow("return suspendCancellableCoroutine")
+
+ addStatement("var mCancellationSignal: %T? = null", cancellationSignalClassName)
+
+ addCode(generateTransactionCallbackObject(method))
+
+ val parameterList = buildList {
+ addAll(method.parameters.map(Parameter::name))
+ add("transactionCallback")
+ }
+ addStatement("remote.${method.name}(${parameterList.joinToString()})")
+
+ beginControlFlow("it.invokeOnCancellation")
+ addStatement("mCancellationSignal?.cancel()")
+ endControlFlow()
+
+ endControlFlow()
+ }
+
+ private fun generateTransactionCallbackObject(method: Method) = CodeBlock.builder().build {
+ val transactionCallbackClassName = ClassName(
+ service.packageName,
+ "I${method.returnType.poetSpec().simpleName}TransactionCallback",
+ "Stub"
+ )
+ beginControlFlow(
+ "val transactionCallback = object: %T()",
+ transactionCallbackClassName
+ )
+
+ beginControlFlow(
+ "override fun onCancellable(cancellationSignal: %T)",
+ cancellationSignalClassName
+ )
+ beginControlFlow("if (it.isCancelled)")
+ addStatement("cancellationSignal.cancel()")
+ endControlFlow()
+ addStatement("mCancellationSignal = cancellationSignal")
+ endControlFlow()
+
+ if (method.returnsUnit) {
+ beginControlFlow("override fun onSuccess()")
+ addStatement("it.resumeWith(Result.success(Unit))")
+ endControlFlow()
+ } else {
+ beginControlFlow(
+ "override fun onSuccess(result: %T)",
+ method.returnType.poetSpec()
+ )
+ addStatement("it.resumeWith(Result.success(result))")
+ endControlFlow()
+ }
+
+ beginControlFlow("override fun onFailure(errorCode: Int, errorMessage: String)")
+ addStatement(
+ "it.%M(RuntimeException(errorMessage))",
+ MemberName("kotlin.coroutines", "resumeWithException")
+ )
+ endControlFlow()
+
+ endControlFlow()
+ }
+
+ private val Method.returnsUnit: Boolean
+ get() {
+ return returnType.name == Unit::class.qualifiedName
+ }
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceInterfaceFileGenerator.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceInterfaceFileGenerator.kt
index c91dc06..daf3a2e 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceInterfaceFileGenerator.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceInterfaceFileGenerator.kt
@@ -42,6 +42,9 @@
private fun generateInterfaceMethod(method: Method) =
FunSpec.builder(method.name).build {
addModifiers(KModifier.ABSTRACT)
+ if (method.isSuspend) {
+ addModifiers(KModifier.SUSPEND)
+ }
addParameters(method.parameters.map { it.poetSpec() })
returns(method.returnType.poetSpec())
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGeneratorTest.kt
index 6796a56..9bb5810 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGeneratorTest.kt
@@ -17,6 +17,8 @@
package androidx.privacysandbox.tools.apigenerator
import androidx.room.compiler.processing.util.Source
+import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
import com.google.common.truth.Truth.assertThat
import java.io.File
import java.nio.file.Files
@@ -49,7 +51,9 @@
.containsExactlyElementsIn(
expectedSources.map(Source::relativePath) + listOf(
"com/mysdk/ITestSandboxSdk.java",
+ "com/mysdk/IBooleanTransactionCallback.java",
"com/mysdk/ICancellationSignal.java",
+ "com/mysdk/IUnitTransactionCallback.java",
)
)
@@ -60,14 +64,6 @@
}
}
- private fun loadSourcesFromDirectory(directory: File): List<Source> {
- return directory.walk().filter { it.isFile }.map {
- val relativePath = directory.toPath().relativize(it.toPath()).toString()
- val qualifiedName = relativePath.removeSuffix(".${it.extension}").replace('/', '.')
- Source.load(file = it, qName = qualifiedName, relativePath = relativePath)
- }.toList()
- }
-
companion object {
val inputTestDataDir = File("src/test/test-data/input")
val outputTestDataDir = File("src/test/test-data/output")
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
index c05ced6..941a432 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
@@ -16,40 +16,16 @@
package androidx.privacysandbox.tools.apigenerator
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
import androidx.room.compiler.processing.util.Source
-import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
-import androidx.room.compiler.processing.util.compiler.TestCompilationResult
-import androidx.room.compiler.processing.util.compiler.compile
import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
import java.io.File
import java.nio.file.Files.createTempDirectory
import java.nio.file.Path
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
-import javax.tools.Diagnostic
import kotlin.io.path.name
-fun assertCompiles(sources: List<Source>): TestCompilationResult {
- val result = compileAll(sources)
- assertWithMessage(
- "Compilation of java files generated from AIDL failed with errors: " +
- "${result.diagnostics[Diagnostic.Kind.ERROR]?.joinToString("\n") { it.msg }}"
- ).that(
- result.success
- ).isTrue()
- return result
-}
-
-fun compileAll(sources: List<Source>): TestCompilationResult {
- val tempDir = createTempDirectory("compile").toFile().also { it.deleteOnExit() }
- return compile(
- tempDir, TestCompilationArguments(
- sources = sources + syntheticPrivacySandboxSources,
- )
- )
-}
-
fun compileIntoInterfaceDescriptorsJar(vararg sources: Source): Path {
val tempDir = createTempDirectory("compile").toFile().also { it.deleteOnExit() }
@@ -70,43 +46,3 @@
return sdkInterfaceDescriptors.toPath()
}
-
-// PrivacySandbox platform APIs are not available in AndroidX prebuilts nor are they stable, so
-// while that's the case we use fake stubs to run our compilation tests.
-private val syntheticPrivacySandboxSources = listOf(
- Source.java(
- "android.app.sdksandbox.SdkSandboxManager", """
- |package android.app.sdksandbox;
- |
- |import android.os.Bundle;
- |import android.os.OutcomeReceiver;
- |import java.util.concurrent.Executor;
- |
- |public final class SdkSandboxManager {
- | public void loadSdk(
- | String sdkName,
- | Bundle params,
- | Executor executor,
- | OutcomeReceiver<SandboxedSdk, LoadSdkException> receiver) {}
- |}
- |""".trimMargin()
- ),
- Source.java(
- "android.app.sdksandbox.SandboxedSdk", """
- |package android.app.sdksandbox;
- |
- |import android.os.IBinder;
- |
- |public final class SandboxedSdk {
- | public IBinder getInterface() { return null; }
- |}
- |""".trimMargin()
- ),
- Source.java(
- "android.app.sdksandbox.LoadSdkException", """
- |package android.app.sdksandbox;
- |
- |public final class LoadSdkException extends Exception {}
- |""".trimMargin()
- ),
-)
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/input/com/mysdk/TestSandboxSdk.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/input/com/mysdk/TestSandboxSdk.kt
index f5429e1..976194c 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/input/com/mysdk/TestSandboxSdk.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/input/com/mysdk/TestSandboxSdk.kt
@@ -21,4 +21,8 @@
fun receiveMultipleArguments(first: Int, second: String, third: Long)
fun receiveAndReturnNothing()
+
+ suspend fun doSomethingAsync(first: Int, second: String, third: Long): Boolean
+
+ suspend fun receiveAndReturnNothingAsync()
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/output/com/mysdk/TestSandboxSdk.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/output/com/mysdk/TestSandboxSdk.kt
index bb06d30..c8fddcf 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/output/com/mysdk/TestSandboxSdk.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/output/com/mysdk/TestSandboxSdk.kt
@@ -1,6 +1,12 @@
package com.mysdk
public interface TestSandboxSdk {
+ public suspend fun doSomethingAsync(
+ first: Int,
+ second: String,
+ third: Long,
+ ): Boolean
+
public fun echoBoolean(input: Boolean): Boolean
public fun echoChar(input: Char): Char
@@ -17,6 +23,8 @@
public fun receiveAndReturnNothing(): Unit
+ public suspend fun receiveAndReturnNothingAsync(): Unit
+
public fun receiveMultipleArguments(
first: Int,
second: String,
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/output/com/mysdk/TestSandboxSdkFactory.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/output/com/mysdk/TestSandboxSdkFactory.kt
index ece4122..0f436ea 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/output/com/mysdk/TestSandboxSdkFactory.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/output/com/mysdk/TestSandboxSdkFactory.kt
@@ -31,24 +31,72 @@
private class TestSandboxSdkClientProxy(
private val remote: ITestSandboxSdk,
) : TestSandboxSdk {
- public override fun echoBoolean(input: Boolean) = remote.echoBoolean(input)!!
+ public override suspend fun doSomethingAsync(
+ first: Int,
+ second: String,
+ third: Long,
+ ): Boolean = suspendCancellableCoroutine {
+ var mCancellationSignal: ICancellationSignal? = null
+ val transactionCallback = object: IBooleanTransactionCallback.Stub() {
+ override fun onCancellable(cancellationSignal: ICancellationSignal) {
+ if (it.isCancelled) {
+ cancellationSignal.cancel()
+ }
+ mCancellationSignal = cancellationSignal
+ }
+ override fun onSuccess(result: Boolean) {
+ it.resumeWith(Result.success(result))
+ }
+ override fun onFailure(errorCode: Int, errorMessage: String) {
+ it.resumeWithException(RuntimeException(errorMessage))
+ }
+ }
+ remote.doSomethingAsync(first, second, third, transactionCallback)
+ it.invokeOnCancellation {
+ mCancellationSignal?.cancel()
+ }
+ }
- public override fun echoChar(input: Char) = remote.echoChar(input)!!
+ public override fun echoBoolean(input: Boolean) = remote.echoBoolean(input)
- public override fun echoDouble(input: Double) = remote.echoDouble(input)!!
+ public override fun echoChar(input: Char) = remote.echoChar(input)
- public override fun echoFloat(input: Float) = remote.echoFloat(input)!!
+ public override fun echoDouble(input: Double) = remote.echoDouble(input)
- public override fun echoInt(input: Int) = remote.echoInt(input)!!
+ public override fun echoFloat(input: Float) = remote.echoFloat(input)
- public override fun echoLong(input: Long) = remote.echoLong(input)!!
+ public override fun echoInt(input: Int) = remote.echoInt(input)
- public override fun echoString(input: String) = remote.echoString(input)!!
+ public override fun echoLong(input: Long) = remote.echoLong(input)
+
+ public override fun echoString(input: String) = remote.echoString(input)
public override fun receiveAndReturnNothing(): Unit {
remote.receiveAndReturnNothing()
}
+ public override suspend fun receiveAndReturnNothingAsync(): Unit = suspendCancellableCoroutine {
+ var mCancellationSignal: ICancellationSignal? = null
+ val transactionCallback = object: IUnitTransactionCallback.Stub() {
+ override fun onCancellable(cancellationSignal: ICancellationSignal) {
+ if (it.isCancelled) {
+ cancellationSignal.cancel()
+ }
+ mCancellationSignal = cancellationSignal
+ }
+ override fun onSuccess() {
+ it.resumeWith(Result.success(Unit))
+ }
+ override fun onFailure(errorCode: Int, errorMessage: String) {
+ it.resumeWithException(RuntimeException(errorMessage))
+ }
+ }
+ remote.receiveAndReturnNothingAsync(transactionCallback)
+ it.invokeOnCancellation {
+ mCancellationSignal?.cancel()
+ }
+ }
+
public override fun receiveMultipleArguments(
first: Int,
second: String,
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
index c09c5c4..426ca45 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
@@ -34,7 +34,7 @@
}
/** [TypeName] equivalent to this parameter. */
-public fun Type.poetSpec(): TypeName {
+public fun Type.poetSpec(): ClassName {
val splits = name.split('.')
return ClassName(splits.dropLast(1).joinToString("."), splits.last())
}
@@ -84,3 +84,17 @@
block()
return build()
}
+
+public fun FunSpec.Builder.addCode(block: CodeBlock.Builder.() -> Unit) {
+ addCode(CodeBlock.builder().build { block() })
+}
+
+/** Auto-closing control flow construct and its code. */
+public fun CodeBlock.Builder.addControlFlow(
+ controlFlow: String,
+ block: CodeBlock.Builder.() -> Unit
+) {
+ beginControlFlow(controlFlow)
+ block()
+ endControlFlow()
+}
diff --git a/privacysandbox/tools/tools-testing/build.gradle b/privacysandbox/tools/tools-testing/build.gradle
index 3b54de9..8132d84 100644
--- a/privacysandbox/tools/tools-testing/build.gradle
+++ b/privacysandbox/tools/tools-testing/build.gradle
@@ -23,6 +23,10 @@
dependencies {
api(libs.kotlinStdlib)
+ api(libs.kspApi)
+ api(libs.junit)
+ api(libs.truth)
+ implementation project(":room:room-compiler-processing-testing")
}
androidx {
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
new file mode 100644
index 0000000..b34a80b
--- /dev/null
+++ b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.testing
+
+import androidx.room.compiler.processing.util.DiagnosticLocation
+import androidx.room.compiler.processing.util.compiler.TestCompilationResult
+import androidx.room.compiler.processing.util.DiagnosticMessage
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
+import androidx.room.compiler.processing.util.compiler.compile
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import com.google.devtools.ksp.processing.SymbolProcessorProvider
+import java.nio.file.Files
+import javax.tools.Diagnostic
+
+object CompilationTestHelper {
+ fun assertCompiles(sources: List<Source>): TestCompilationResult {
+ val result = compileAll(sources)
+ assertThat(result).succeeds()
+ return result
+ }
+
+ fun compileAll(
+ sources: List<Source>,
+ symbolProcessorProviders: List<SymbolProcessorProvider> = emptyList(),
+ processorOptions: Map<String, String> = emptyMap()
+ ): TestCompilationResult {
+ val tempDir = Files.createTempDirectory("compile").toFile().also { it.deleteOnExit() }
+ return compile(
+ tempDir,
+ TestCompilationArguments(
+ sources = sources + syntheticPrivacySandboxSources,
+ symbolProcessorProviders = symbolProcessorProviders,
+ processorOptions = processorOptions,
+ )
+ )
+ }
+
+ fun assertThat(result: TestCompilationResult) = CompilationResultSubject(result)
+}
+
+class CompilationResultSubject(private val result: TestCompilationResult) {
+ fun succeeds() {
+ assertWithMessage(
+ "UnexpectedErrors:\n${getFullErrorMessages()?.joinToString("\n")}"
+ ).that(
+ result.success
+ ).isTrue()
+ }
+
+ fun generatesExactlySources(vararg sourcePaths: String) {
+ succeeds()
+ assertThat(result.generatedSources.map(Source::relativePath))
+ .containsExactlyElementsIn(sourcePaths)
+ }
+
+ fun generatesSourcesWithContents(sources: List<Source>) {
+ succeeds()
+ val contentsByFile = result.generatedSources.associate { it.relativePath to it.contents }
+ for (source in sources) {
+ assertWithMessage("File ${source.relativePath} was not generated")
+ .that(contentsByFile).containsKey(source.relativePath)
+ assertWithMessage("Contents of file ${source.relativePath} don't match.")
+ .that(contentsByFile[source.relativePath]).isEqualTo(source.contents)
+ }
+ }
+
+ fun fails() {
+ assertThat(result.success).isFalse()
+ }
+
+ fun containsError(error: String) {
+ assertThat(getShortErrorMessages()).contains(error)
+ }
+
+ fun containsExactlyErrors(vararg errors: String) {
+ assertThat(getShortErrorMessages()).containsExactly(*errors)
+ }
+
+ private fun getShortErrorMessages() =
+ result.diagnostics[Diagnostic.Kind.ERROR]?.map(DiagnosticMessage::msg)
+
+ private fun getFullErrorMessages() =
+ result.diagnostics[Diagnostic.Kind.ERROR]?.map { it.toFormattedMessage() }
+
+ private fun DiagnosticMessage.toFormattedMessage() = """
+ |Error: $msg
+ |${location?.toFormattedLocation()}$
+ """.trimMargin()
+
+ private fun DiagnosticLocation.toFormattedLocation(): String {
+ if (source == null) return "Location information missing"
+ return """
+ |Location: ${source!!.relativePath}:$line
+ |File:
+ |${contentsHighlightingLine(source!!.contents, line)}
+ """.trimMargin()
+ }
+
+ private fun contentsHighlightingLine(contents: String, line: Int): String {
+ var lineCountdown = line
+ // Insert a "->" in the beginning of the highlighted line.
+ return contents.split("\n").map {
+ val lineHeader = if (--lineCountdown == 0) "->" else " "
+ "$lineHeader$it"
+ }.joinToString("\n")
+ }
+}
+
+// PrivacySandbox platform APIs are not available in AndroidX prebuilts nor are they stable, so
+// while that's the case we use fake stubs to run our compilation tests.
+val syntheticPrivacySandboxSources = listOf(
+ Source.java(
+ "android.app.sdksandbox.SdkSandboxManager", """
+ |package android.app.sdksandbox;
+ |
+ |import android.os.Bundle;
+ |import android.os.OutcomeReceiver;
+ |import java.util.concurrent.Executor;
+ |
+ |public final class SdkSandboxManager {
+ | public void loadSdk(
+ | String sdkName,
+ | Bundle params,
+ | Executor executor,
+ | OutcomeReceiver<SandboxedSdk, LoadSdkException> receiver) {}
+ |}
+ |""".trimMargin()
+ ),
+ Source.java(
+ "android.app.sdksandbox.SandboxedSdk", """
+ |package android.app.sdksandbox;
+ |
+ |import android.os.IBinder;
+ |
+ |public final class SandboxedSdk {
+ | public SandboxedSdk(IBinder sdkInterface) {}
+ | public IBinder getInterface() { return null; }
+ |}
+ |""".trimMargin()
+ ),
+ Source.java(
+ "android.app.sdksandbox.SandboxedSdkProvider", """
+ |package android.app.sdksandbox;
+ |
+ |import android.content.Context;
+ |import android.os.Bundle;
+ |import android.view.View;
+ |
+ |public abstract class SandboxedSdkProvider {
+ | public abstract SandboxedSdk onLoadSdk(Bundle params) throws LoadSdkException;
+ | public abstract View getView(
+ | Context windowContext, Bundle params, int width, int height);
+ | public final Context getContext() {
+ | return null;
+ | }
+ | public abstract void onDataReceived(
+ | Bundle data, DataReceivedCallback callback);
+ | public interface DataReceivedCallback {
+ | void onDataReceivedSuccess(Bundle params);
+ | void onDataReceivedError(String errorMessage);
+ | }
+ |}
+ |""".trimMargin()
+ ),
+ Source.java(
+ "android.app.sdksandbox.LoadSdkException", """
+ |package android.app.sdksandbox;
+ |
+ |public final class LoadSdkException extends Exception {}
+ |""".trimMargin()
+ ),
+ Source.java(
+ "android.app.sdksandbox.SandboxedSdkContext", """
+ |package android.app.sdksandbox;
+ |
+ |public final class SandboxedSdkContext {}
+ |""".trimMargin()
+ ),
+)
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/TestDataHelper.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/TestDataHelper.kt
new file mode 100644
index 0000000..da5bd3c
--- /dev/null
+++ b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/TestDataHelper.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.testing
+
+import androidx.room.compiler.processing.util.Source
+import java.io.File
+
+fun loadSourcesFromDirectory(directory: File): List<Source> {
+ check(directory.exists()) { "${directory.path} doesn't exist." }
+ check(directory.isDirectory) { "${directory.path} is not a directory." }
+ return directory.walk().filter { it.isFile }.map {
+ val relativePath = directory.toPath().relativize(it.toPath()).toString()
+ val qualifiedName = relativePath.removeSuffix(".${it.extension}").replace('/', '.')
+ Source.load(file = it, qName = qualifiedName, relativePath = relativePath)
+ }.toList()
+}
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewAccessibilityLifecycleTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewAccessibilityLifecycleTest.java
index 0843578..4b71b5b 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewAccessibilityLifecycleTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewAccessibilityLifecycleTest.java
@@ -20,6 +20,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.never;
@@ -228,6 +229,9 @@
AccessibilityNodeInfo info = recyclerView.getChildAt(i)
.createAccessibilityNodeInfo();
assertTrue("custom delegate sets isChecked", info.isChecked());
+ if (Build.VERSION.SDK_INT >= 19) {
+ assertNotNull(info.getCollectionItemInfo());
+ }
children.add(view);
}
}
@@ -250,6 +254,9 @@
assertTrue(children.contains(view));
AccessibilityNodeInfo info = view.createAccessibilityNodeInfo();
assertTrue("custom delegate sets isChecked", info.isChecked());
+ if (Build.VERSION.SDK_INT >= 19) {
+ assertNotNull(info.getCollectionItemInfo());
+ }
}
}
});
@@ -299,6 +306,10 @@
View view = recyclerView.getChildAt(i);
assertEquals(i, recyclerView.getChildAdapterPosition(view));
assertTrue(accessibiltyDelegateIsItemDelegate(recyclerView, view));
+ AccessibilityNodeInfo info = view.createAccessibilityNodeInfo();
+ if (Build.VERSION.SDK_INT >= 19) {
+ assertNotNull(info.getCollectionItemInfo());
+ }
}
}
});
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewAccessibilityTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewAccessibilityTest.java
index 8551053..6eeb355 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewAccessibilityTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewAccessibilityTest.java
@@ -19,7 +19,6 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.os.Build;
@@ -32,7 +31,6 @@
import androidx.annotation.Px;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
-import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import org.junit.Test;
@@ -159,7 +157,17 @@
(info.getActions() & AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD) != 0);
assertEquals(mHorizontalScrollAfter || mVerticalScrollAfter,
(info.getActions() & AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD) != 0);
- assertEmptyCollectionInfo(info);
+ if (SUPPORTS_COLLECTION_INFO) {
+ final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo = info
+ .getCollectionInfo();
+ assertNotNull(collectionInfo);
+ if (recyclerView.getLayoutManager().canScrollVertically()) {
+ assertEquals(adapter.getItemCount(), collectionInfo.getRowCount());
+ }
+ if (recyclerView.getLayoutManager().canScrollHorizontally()) {
+ assertEquals(adapter.getItemCount(), collectionInfo.getColumnCount());
+ }
+ }
final AccessibilityEvent event = AccessibilityEvent.obtain();
mActivityRule.runOnUiThread(new Runnable() {
@@ -184,7 +192,20 @@
onInitializeAccessibilityNodeInfo(view, childInfo);
}
});
- assertNull(childInfo.getCollectionItemInfo());
+ final AccessibilityNodeInfoCompat.CollectionItemInfoCompat collectionItemInfo =
+ childInfo.getCollectionItemInfo();
+ assertNotNull(collectionItemInfo);
+ if (recyclerView.getLayoutManager().canScrollHorizontally()) {
+ assertEquals(i, collectionItemInfo.getColumnIndex());
+ } else {
+ assertEquals(0, collectionItemInfo.getColumnIndex());
+ }
+
+ if (recyclerView.getLayoutManager().canScrollVertically()) {
+ assertEquals(i, collectionItemInfo.getRowIndex());
+ } else {
+ assertEquals(0, collectionItemInfo.getRowIndex());
+ }
}
}
@@ -217,89 +238,6 @@
assertEquals(mVerticalScrollAfter, vScrolledFwd.get());
}
- private void assertEmptyCollectionInfo(AccessibilityNodeInfoCompat info) {
- if (SUPPORTS_COLLECTION_INFO) {
- final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo = info
- .getCollectionInfo();
- assertNotNull(collectionInfo);
- assertEquals(-1, collectionInfo.getRowCount());
- assertEquals(-1, collectionInfo.getColumnCount());
- }
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 19)
- public void onInitializeAccessibilityNodeInfoTestWithCollectionInfo() throws Throwable {
- final RecyclerView recyclerView = new RecyclerView(getActivity());
- final TestAdapter adapter = new TestAdapter(10);
- recyclerView.setAdapter(adapter);
- recyclerView.setLayoutManager(new TestLayoutManager() {
-
- @Override
- public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
- layoutRange(recycler, 0, 5);
- }
-
- @Override
- public RecyclerView.LayoutParams generateDefaultLayoutParams() {
- return new RecyclerView.LayoutParams(-1, -1);
- }
-
- @Override
- public int getRowCountForAccessibility(
- RecyclerView.Recycler recycler, RecyclerView.State state) {
- return 10;
- }
-
- @Override
- public int getColumnCountForAccessibility(
- RecyclerView.Recycler recycler, RecyclerView.State state) {
- return 1;
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
- RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
- RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) host.getLayoutParams();
- int position = lp.getViewAdapterPosition();
- info.setCollectionItemInfo(
- AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
- position, 0, 0, 0, false, false));
-
- }
- });
- setRecyclerView(recyclerView);
- final RecyclerViewAccessibilityDelegate delegateCompat = recyclerView
- .getCompatAccessibilityDelegate();
- final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- delegateCompat.onInitializeAccessibilityNodeInfo(recyclerView, info);
- }
- });
- final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo = info
- .getCollectionInfo();
- assertNotNull(collectionInfo);
- assertEquals(adapter.getItemCount(), collectionInfo.getRowCount());
- for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
- final View view = mRecyclerView.getChildAt(i);
- final AccessibilityNodeInfoCompat childInfo = AccessibilityNodeInfoCompat.obtain();
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- delegateCompat.getItemDelegate().onInitializeAccessibilityNodeInfo(
- view, childInfo);
- }
- });
- final AccessibilityNodeInfoCompat.CollectionItemInfoCompat collectionItemInfo =
- childInfo.getCollectionItemInfo();
- assertNotNull(collectionItemInfo);
- assertEquals(0, collectionItemInfo.getColumnIndex());
- assertEquals(i, collectionItemInfo.getRowIndex());
- }
- }
-
@Test
public void ignoreAccessibilityIfAdapterHasChanged() throws Throwable {
final RecyclerView recyclerView = new RecyclerView(getActivity()) {
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
index 866b4c4..10bbb35 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
@@ -11183,6 +11183,12 @@
public void onInitializeAccessibilityNodeInfoForItem(@NonNull Recycler recycler,
@NonNull State state, @NonNull View host,
@NonNull AccessibilityNodeInfoCompat info) {
+ int rowIndexGuess = canScrollVertically() ? getPosition(host) : 0;
+ int columnIndexGuess = canScrollHorizontally() ? getPosition(host) : 0;
+ final AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo =
+ AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(rowIndexGuess, 1,
+ columnIndexGuess, 1, false, false);
+ info.setCollectionItemInfo(itemInfo);
}
/**
@@ -11231,7 +11237,10 @@
* @return The number of rows in LayoutManager for accessibility.
*/
public int getRowCountForAccessibility(@NonNull Recycler recycler, @NonNull State state) {
- return -1;
+ if (mRecyclerView == null || mRecyclerView.mAdapter == null) {
+ return 1;
+ }
+ return canScrollVertically() ? mRecyclerView.mAdapter.getItemCount() : 1;
}
/**
@@ -11248,7 +11257,10 @@
*/
public int getColumnCountForAccessibility(@NonNull Recycler recycler,
@NonNull State state) {
- return -1;
+ if (mRecyclerView == null || mRecyclerView.mAdapter == null) {
+ return 1;
+ }
+ return canScrollHorizontally() ? mRecyclerView.mAdapter.getItemCount() : 1;
}
/**
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java
index 0387e96..73f1d50 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java
@@ -34,6 +34,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.view.ViewCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import java.util.ArrayList;
import java.util.Arrays;
@@ -1290,6 +1291,26 @@
}
@Override
+ public void onInitializeAccessibilityNodeInfoForItem(@NonNull RecyclerView.Recycler recycler,
+ @NonNull RecyclerView.State state, @NonNull View host,
+ @NonNull AccessibilityNodeInfoCompat info) {
+ ViewGroup.LayoutParams lp = host.getLayoutParams();
+ if (!(lp instanceof LayoutParams)) {
+ super.onInitializeAccessibilityNodeInfoForItem(host, info);
+ return;
+ }
+ LayoutParams sglp = (LayoutParams) lp;
+ if (mOrientation == HORIZONTAL) {
+ info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
+ sglp.getSpanIndex(), sglp.mFullSpan ? mSpanCount : 1,
+ -1, -1, false, false));
+ } else { // VERTICAL
+ info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
+ -1, -1,
+ sglp.getSpanIndex(), sglp.mFullSpan ? mSpanCount : 1, false, false));
+ }
+ }
+ @Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
if (getChildCount() > 0) {
@@ -1321,6 +1342,24 @@
return first == null ? RecyclerView.NO_POSITION : getPosition(first);
}
+ @Override
+ public int getRowCountForAccessibility(@NonNull RecyclerView.Recycler recycler,
+ @NonNull RecyclerView.State state) {
+ if (mOrientation == HORIZONTAL) {
+ return mSpanCount;
+ }
+ return super.getRowCountForAccessibility(recycler, state);
+ }
+
+ @Override
+ public int getColumnCountForAccessibility(@NonNull RecyclerView.Recycler recycler,
+ @NonNull RecyclerView.State state) {
+ if (mOrientation == VERTICAL) {
+ return mSpanCount;
+ }
+ return super.getColumnCountForAccessibility(recycler, state);
+ }
+
/**
* This is for internal use. Not necessarily the child closest to start but the first child
* we find that matches the criteria.
diff --git a/room/room-common/src/main/java/androidx/room/Database.kt b/room/room-common/src/main/java/androidx/room/Database.kt
index 4b5eac1..0edb540 100644
--- a/room/room-common/src/main/java/androidx/room/Database.kt
+++ b/room/room-common/src/main/java/androidx/room/Database.kt
@@ -29,7 +29,7 @@
*
* ```
* // Song and Album are classes annotated with @Entity.
- * @Database(version = 1, entities = {Song.class, Album.class})
+ * @Database(version = 1, entities = [Song::class, Album::class])
* abstract class MusicDatabase : RoomDatabase {
* // SongDao is a class annotated with @Dao.
* abstract fun getSongDao(): SongDao
diff --git a/settings.gradle b/settings.gradle
index 2f66253..1a537d8 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -639,7 +639,6 @@
includeProject(":enterprise:enterprise-feedback", [BuildType.MAIN])
includeProject(":enterprise:enterprise-feedback-testing", [BuildType.MAIN])
includeProject(":exifinterface:exifinterface", [BuildType.MAIN])
-includeProject(":fakeannotations", [BuildType.MAIN])
includeProject(":fragment:fragment", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR])
includeProject(":fragment:fragment-ktx", [BuildType.MAIN, BuildType.FLAN])
includeProject(":fragment:fragment-lint", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR])
@@ -1021,7 +1020,6 @@
//
/////////////////////////////
-apply(from: "include-composite-deps.gradle")
File externalRoot = new File(rootDir, "../../external")
includeProject(":icing", new File(externalRoot, "icing"), [BuildType.MAIN])
@@ -1041,4 +1039,5 @@
// for each size and test runner is happy when there is nothing to test.
includeProject(":placeholder-tests")
-includeProject(":fakeannotations")
+// Workaround for b/203825166
+includeBuild("placeholder")
diff --git a/testutils/testutils-macrobenchmark/build.gradle b/testutils/testutils-macrobenchmark/build.gradle
index 3f7f009..62bf7f3 100644
--- a/testutils/testutils-macrobenchmark/build.gradle
+++ b/testutils/testutils-macrobenchmark/build.gradle
@@ -25,7 +25,7 @@
dependencies {
implementation(project(":benchmark:benchmark-macro"))
implementation(project(":benchmark:benchmark-macro-junit4"))
-
+ implementation(project(":tracing:tracing-perfetto-binary"))
implementation(libs.kotlinStdlib)
}
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
index eb3382b..2a02f4a 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
@@ -558,11 +558,6 @@
}
/**
- * @return true if the given line is ellipsized, else false.
- */
- fun isEllipsisApplied(lineIndex: Int): Boolean = layout.getEllipsisCount(lineIndex) > 0
-
- /**
* Fills the bounding boxes for characters within the [startOffset] (inclusive) and [endOffset]
* (exclusive). The array is filled starting from [arrayStart] (inclusive). The coordinates are
* in local text layout coordinates.
diff --git a/tracing/tracing-ktx/api/current.txt b/tracing/tracing-ktx/api/current.txt
index 0cb3ea1..13d99d1 100644
--- a/tracing/tracing-ktx/api/current.txt
+++ b/tracing/tracing-ktx/api/current.txt
@@ -3,7 +3,9 @@
public final class TraceKt {
method public static inline <T> T! trace(String label, kotlin.jvm.functions.Function0<? extends T> block);
+ method public static inline <T> T! trace(kotlin.jvm.functions.Function0<java.lang.String> lazyLabel, kotlin.jvm.functions.Function0<? extends T> block);
method public static suspend inline <T> Object? traceAsync(String methodName, int cookie, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method public static inline <T> T! traceAsync(kotlin.jvm.functions.Function0<java.lang.String> lazyMethodName, kotlin.jvm.functions.Function0<java.lang.Integer> lazyCookie, kotlin.jvm.functions.Function0<? extends T> block);
}
}
diff --git a/tracing/tracing-ktx/api/public_plus_experimental_current.txt b/tracing/tracing-ktx/api/public_plus_experimental_current.txt
index 0cb3ea1..13d99d1 100644
--- a/tracing/tracing-ktx/api/public_plus_experimental_current.txt
+++ b/tracing/tracing-ktx/api/public_plus_experimental_current.txt
@@ -3,7 +3,9 @@
public final class TraceKt {
method public static inline <T> T! trace(String label, kotlin.jvm.functions.Function0<? extends T> block);
+ method public static inline <T> T! trace(kotlin.jvm.functions.Function0<java.lang.String> lazyLabel, kotlin.jvm.functions.Function0<? extends T> block);
method public static suspend inline <T> Object? traceAsync(String methodName, int cookie, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method public static inline <T> T! traceAsync(kotlin.jvm.functions.Function0<java.lang.String> lazyMethodName, kotlin.jvm.functions.Function0<java.lang.Integer> lazyCookie, kotlin.jvm.functions.Function0<? extends T> block);
}
}
diff --git a/tracing/tracing-ktx/api/restricted_current.txt b/tracing/tracing-ktx/api/restricted_current.txt
index 0cb3ea1..13d99d1 100644
--- a/tracing/tracing-ktx/api/restricted_current.txt
+++ b/tracing/tracing-ktx/api/restricted_current.txt
@@ -3,7 +3,9 @@
public final class TraceKt {
method public static inline <T> T! trace(String label, kotlin.jvm.functions.Function0<? extends T> block);
+ method public static inline <T> T! trace(kotlin.jvm.functions.Function0<java.lang.String> lazyLabel, kotlin.jvm.functions.Function0<? extends T> block);
method public static suspend inline <T> Object? traceAsync(String methodName, int cookie, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T>);
+ method public static inline <T> T! traceAsync(kotlin.jvm.functions.Function0<java.lang.String> lazyMethodName, kotlin.jvm.functions.Function0<java.lang.Integer> lazyCookie, kotlin.jvm.functions.Function0<? extends T> block);
}
}
diff --git a/tracing/tracing-ktx/src/androidTest/java/androidx/tracing/TraceTestKt.kt b/tracing/tracing-ktx/src/androidTest/java/androidx/tracing/TraceTestKt.kt
index f328564..f192bb3 100644
--- a/tracing/tracing-ktx/src/androidTest/java/androidx/tracing/TraceTestKt.kt
+++ b/tracing/tracing-ktx/src/androidTest/java/androidx/tracing/TraceTestKt.kt
@@ -21,6 +21,7 @@
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
import org.junit.Test
import org.junit.runner.RunWith
@@ -36,6 +37,23 @@
}
@Test
+ fun traceLazyTest() {
+ assertFalse(
+ "This test expects to be run without tracing enabled in this process",
+ Trace.isEnabled()
+ )
+
+ val x = trace(
+ lazyLabel = {
+ throw IllegalStateException("tracing should be disabled, with message not used")
+ }
+ ) {
+ 10
+ }
+ assertEquals(10, x)
+ }
+
+ @Test
fun asyncTraceTest() {
runBlocking {
val x = traceAsync("test", 0) {
@@ -45,4 +63,26 @@
assertEquals(10, x)
}
}
+
+ @Test
+ fun asyncTraceLazyTest() {
+ assertFalse(
+ "This test expects to be run without tracing enabled in this process",
+ Trace.isEnabled()
+ )
+ runBlocking {
+ val x = traceAsync(
+ lazyMethodName = {
+ throw IllegalStateException("tracing should be disabled, with message not used")
+ },
+ lazyCookie = {
+ throw IllegalStateException("tracing should be disabled, with message not used")
+ }
+ ) {
+ delay(1)
+ 10
+ }
+ assertEquals(10, x)
+ }
+ }
}
diff --git a/tracing/tracing-ktx/src/main/java/androidx/tracing/Trace.kt b/tracing/tracing-ktx/src/main/java/androidx/tracing/Trace.kt
index 134f60cf..68ec25d 100644
--- a/tracing/tracing-ktx/src/main/java/androidx/tracing/Trace.kt
+++ b/tracing/tracing-ktx/src/main/java/androidx/tracing/Trace.kt
@@ -24,8 +24,8 @@
* @param block A block of code which is being traced.
*/
public inline fun <T> trace(label: String, crossinline block: () -> T): T {
+ Trace.beginSection(label)
try {
- Trace.beginSection(label)
return block()
} finally {
Trace.endSection()
@@ -33,6 +33,33 @@
}
/**
+ * Wrap the specified [block] in calls to [Trace.beginSection] (with a lazy-computed
+ * [lazyLabel], only if tracing is enabled - [Trace.isEnabled]) and [Trace.endSection].
+ *
+ * This variant allows you to build a dynamic label, but only when tracing is enabled, avoiding
+ * the cost of String construction otherwise.
+ *
+ * @param lazyLabel A name of the code section to appear in the trace, computed lazily if needed.
+ * @param block A block of code which is being traced.
+ */
+public inline fun <T> trace(
+ lazyLabel: () -> String,
+ block: () -> T
+): T {
+ val isEnabled = Trace.isEnabled()
+ if (isEnabled) {
+ Trace.beginSection(lazyLabel())
+ }
+ try {
+ return block()
+ } finally {
+ if (isEnabled) {
+ Trace.endSection()
+ }
+ }
+}
+
+/**
* Wrap the specified [block] in calls to [Trace.beginAsyncSection] (with the supplied [methodName]
* and [cookie]) and [Trace.endAsyncSection].
*
@@ -44,10 +71,39 @@
cookie: Int,
crossinline block: suspend () -> T
): T {
+ Trace.beginAsyncSection(methodName, cookie)
try {
- Trace.beginAsyncSection(methodName, cookie)
return block()
} finally {
Trace.endAsyncSection(methodName, cookie)
}
}
+
+/**
+ * Wrap the specified [block] in calls to [Trace.beginAsyncSection] and [Trace.endAsyncSection],
+ * with a lazy-computed [lazyMethodName] and [lazyCookie], only if tracing is
+ * enabled - [Trace.isEnabled].
+ *
+ * @param lazyMethodName The method name to appear in the trace, computed lazily if needed.
+ * @param lazyCookie Unique identifier for distinguishing simultaneous , computed lazily if needed.
+ */
+public inline fun <T> traceAsync(
+ lazyMethodName: () -> String,
+ lazyCookie: () -> Int,
+ block: () -> T
+): T {
+ var methodName: String? = null
+ var cookie = 0
+ if (Trace.isEnabled()) {
+ methodName = lazyMethodName()
+ cookie = lazyCookie()
+ Trace.beginAsyncSection(methodName, cookie)
+ }
+ try {
+ return block()
+ } finally {
+ if (methodName != null) {
+ Trace.endAsyncSection(methodName, cookie)
+ }
+ }
+}
diff --git a/tracing/tracing-perfetto/build.gradle b/tracing/tracing-perfetto/build.gradle
index bd462a7..ca9af2c 100644
--- a/tracing/tracing-perfetto/build.gradle
+++ b/tracing/tracing-perfetto/build.gradle
@@ -26,6 +26,14 @@
android {
sourceSets {
test.resources {
+ // Allows for testing that tracing-perfetto-binary in the prebuilts folder matches
+ // checksums in `PerfettoNative.Metadata`.
+ // Note that due to `TRACING_PERFETTO_REUSE_PREBUILTS_AAR` flag in
+ // `tracing:tracing-perfetto-binary`, `tracing:tracing-perfetto-binary` returns
+ // prebuilts AAR instead of being built from source code. See documentation for
+ // `TRACING_PERFETTO_REUSE_PREBUILTS_AAR` for more information.
+ // TODO(239041710): create a test that verifies that `tracing:tracing-perfetto-binary`
+ // matches its source code.
srcDirs += SupportConfigKt
.getPrebuiltsRoot(project).toPath()
.resolve(
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index 26f3b10..efa62ed 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -147,17 +147,21 @@
}
public final class CurvedTextStyle {
- ctor public CurvedTextStyle(optional long background, optional long color, optional long fontSize);
+ ctor public CurvedTextStyle(optional long background, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight);
ctor public CurvedTextStyle(androidx.compose.ui.text.TextStyle style);
- method public androidx.wear.compose.foundation.CurvedTextStyle copy(optional long background, optional long color, optional long fontSize);
+ ctor @Deprecated public CurvedTextStyle(optional long background, optional long color, optional long fontSize);
+ method @Deprecated public androidx.wear.compose.foundation.CurvedTextStyle copy(optional long background, optional long color, optional long fontSize);
+ method public androidx.wear.compose.foundation.CurvedTextStyle copy(optional long background, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight);
method public long getBackground();
method public long getColor();
method public long getFontSize();
+ method public androidx.compose.ui.text.font.FontWeight? getFontWeight();
method public androidx.wear.compose.foundation.CurvedTextStyle merge(optional androidx.wear.compose.foundation.CurvedTextStyle? other);
method public operator androidx.wear.compose.foundation.CurvedTextStyle plus(androidx.wear.compose.foundation.CurvedTextStyle other);
property public final long background;
property public final long color;
property public final long fontSize;
+ property public final androidx.compose.ui.text.font.FontWeight? fontWeight;
}
public final class CurvedTextStyleKt {
diff --git a/wear/compose/compose-foundation/api/public_plus_experimental_current.txt b/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
index 26f3b10..efa62ed 100644
--- a/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
@@ -147,17 +147,21 @@
}
public final class CurvedTextStyle {
- ctor public CurvedTextStyle(optional long background, optional long color, optional long fontSize);
+ ctor public CurvedTextStyle(optional long background, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight);
ctor public CurvedTextStyle(androidx.compose.ui.text.TextStyle style);
- method public androidx.wear.compose.foundation.CurvedTextStyle copy(optional long background, optional long color, optional long fontSize);
+ ctor @Deprecated public CurvedTextStyle(optional long background, optional long color, optional long fontSize);
+ method @Deprecated public androidx.wear.compose.foundation.CurvedTextStyle copy(optional long background, optional long color, optional long fontSize);
+ method public androidx.wear.compose.foundation.CurvedTextStyle copy(optional long background, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight);
method public long getBackground();
method public long getColor();
method public long getFontSize();
+ method public androidx.compose.ui.text.font.FontWeight? getFontWeight();
method public androidx.wear.compose.foundation.CurvedTextStyle merge(optional androidx.wear.compose.foundation.CurvedTextStyle? other);
method public operator androidx.wear.compose.foundation.CurvedTextStyle plus(androidx.wear.compose.foundation.CurvedTextStyle other);
property public final long background;
property public final long color;
property public final long fontSize;
+ property public final androidx.compose.ui.text.font.FontWeight? fontWeight;
}
public final class CurvedTextStyleKt {
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index 26f3b10..efa62ed 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -147,17 +147,21 @@
}
public final class CurvedTextStyle {
- ctor public CurvedTextStyle(optional long background, optional long color, optional long fontSize);
+ ctor public CurvedTextStyle(optional long background, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight);
ctor public CurvedTextStyle(androidx.compose.ui.text.TextStyle style);
- method public androidx.wear.compose.foundation.CurvedTextStyle copy(optional long background, optional long color, optional long fontSize);
+ ctor @Deprecated public CurvedTextStyle(optional long background, optional long color, optional long fontSize);
+ method @Deprecated public androidx.wear.compose.foundation.CurvedTextStyle copy(optional long background, optional long color, optional long fontSize);
+ method public androidx.wear.compose.foundation.CurvedTextStyle copy(optional long background, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight);
method public long getBackground();
method public long getColor();
method public long getFontSize();
+ method public androidx.compose.ui.text.font.FontWeight? getFontWeight();
method public androidx.wear.compose.foundation.CurvedTextStyle merge(optional androidx.wear.compose.foundation.CurvedTextStyle? other);
method public operator androidx.wear.compose.foundation.CurvedTextStyle plus(androidx.wear.compose.foundation.CurvedTextStyle other);
property public final long background;
property public final long color;
property public final long fontSize;
+ property public final androidx.compose.ui.text.font.FontWeight? fontWeight;
}
public final class CurvedTextStyleKt {
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedTextStyle.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedTextStyle.kt
index d262933..9652417 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedTextStyle.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedTextStyle.kt
@@ -28,10 +28,9 @@
internal val DefaultCurvedTextStyles = CurvedTextStyle(
color = Color.Black,
fontSize = 14.sp,
- background = Color.Transparent
-).also {
- it.fontWeight = FontWeight.Normal
-}
+ background = Color.Transparent,
+ fontWeight = FontWeight.Normal
+)
/**
* Styling configuration for a curved text.
@@ -42,15 +41,33 @@
* @param color The text color.
* @param fontSize The size of glyphs (in logical pixels) to use when painting the text. This
* may be [TextUnit.Unspecified] for inheriting from another [CurvedTextStyle].
- *
+ * @param fontWeight The thickness of the glyphs, in a range of [1, 1000]. see [FontWeight]
*/
class CurvedTextStyle(
val background: Color = Color.Unspecified,
val color: Color = Color.Unspecified,
val fontSize: TextUnit = TextUnit.Unspecified,
+ val fontWeight: FontWeight? = null
) {
- // Temporary added as internal field until we can add to the public API in 1.1
- internal var fontWeight: FontWeight? = null
+
+ /**
+ * Styling configuration for a curved text.
+ *
+ * @sample androidx.wear.compose.foundation.samples.CurvedAndNormalText
+ *
+ * @param background The background color for the text.
+ * @param color The text color.
+ * @param fontSize The size of glyphs (in logical pixels) to use when painting the text. This
+ * may be [TextUnit.Unspecified] for inheriting from another [CurvedTextStyle].
+ */
+ @Deprecated("This overload is provided for backwards compatibility with Compose for " +
+ "Wear OS 1.0. A newer overload is available with an additional fontWeight parameter.",
+ level = DeprecationLevel.HIDDEN)
+ constructor(
+ background: Color = Color.Unspecified,
+ color: Color = Color.Unspecified,
+ fontSize: TextUnit = TextUnit.Unspecified
+ ) : this(background, color, fontSize, null)
/**
* Create a curved text style from the given text style.
@@ -58,9 +75,12 @@
* Note that not all parameters in the text style will be used, only [TextStyle.color],
* [TextStyle.fontSize], [TextStyle.background] and [TextStyle.fontWeight]
*/
- constructor(style: TextStyle) : this(style.background, style.color, style.fontSize) {
- fontWeight = style.fontWeight
- }
+ constructor(style: TextStyle) : this(
+ style.background,
+ style.color,
+ style.fontSize,
+ style.fontWeight
+ )
/**
* Returns a new curved text style that is a combination of this style and the given
@@ -79,9 +99,8 @@
color = other.color.takeOrElse { this.color },
fontSize = if (!other.fontSize.isUnspecified) other.fontSize else this.fontSize,
background = other.background.takeOrElse { this.background },
- ).also {
- it.fontWeight = other.fontWeight ?: this.fontWeight
- }
+ fontWeight = other.fontWeight ?: this.fontWeight
+ )
}
/**
@@ -89,6 +108,8 @@
*/
operator fun plus(other: CurvedTextStyle): CurvedTextStyle = this.merge(other)
+ @Deprecated("This overload is provided for backwards compatibility with Compose for " +
+ "Wear OS 1.0. A newer overload is available with an additional fontWeight parameter.")
fun copy(
background: Color = this.background,
color: Color = this.color,
@@ -98,9 +119,22 @@
background = background,
color = color,
fontSize = fontSize,
- ).also {
- it.fontWeight = this.fontWeight
- }
+ fontWeight = this.fontWeight
+ )
+ }
+
+ fun copy(
+ background: Color = this.background,
+ color: Color = this.color,
+ fontSize: TextUnit = this.fontSize,
+ fontWeight: FontWeight? = this.fontWeight
+ ): CurvedTextStyle {
+ return CurvedTextStyle(
+ background = background,
+ color = color,
+ fontSize = fontSize,
+ fontWeight = fontWeight
+ )
}
override fun equals(other: Any?): Boolean {
diff --git a/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/CurvedTextStyleTest.kt b/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/CurvedTextStyleTest.kt
index 565170c..a68526a 100644
--- a/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/CurvedTextStyleTest.kt
+++ b/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/CurvedTextStyleTest.kt
@@ -17,11 +17,13 @@
package androidx.wear.compose.foundation
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.isUnspecified
import androidx.compose.ui.unit.sp
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
+import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -35,6 +37,7 @@
assertEquals(style.color, Color.Unspecified)
assertTrue(style.fontSize.isUnspecified)
assertEquals(style.background, Color.Unspecified)
+ assertNull(style.fontWeight)
}
@Test
@@ -65,6 +68,15 @@
}
@Test
+ fun `constructor with customized font weight`() {
+ val fontWeight = FontWeight.Bold
+
+ val style = CurvedTextStyle(fontWeight = fontWeight)
+
+ assertEquals(style.fontWeight, fontWeight)
+ }
+
+ @Test
fun `merge with empty other should return this`() {
val style = CurvedTextStyle()
@@ -129,4 +141,23 @@
assertEquals(newStyle.background, otherStyle.background)
}
+
+ @Test
+ fun `merge with other's font weight is unspecified should use this' font weight`() {
+ val style = CurvedTextStyle(fontWeight = FontWeight.ExtraBold)
+
+ val newStyle = style.merge(CurvedTextStyle(fontWeight = null))
+
+ assertEquals(newStyle.fontWeight, style.fontWeight)
+ }
+
+ @Test
+ fun `merge with other's font weight is set should use other's font weight`() {
+ val style = CurvedTextStyle(fontWeight = FontWeight.ExtraBold)
+ val otherStyle = CurvedTextStyle(fontWeight = FontWeight.Light)
+
+ val newStyle = style.merge(otherStyle)
+
+ assertEquals(newStyle.fontWeight, otherStyle.fontWeight)
+ }
}
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index 6ff9573..bb7e74a 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -356,12 +356,12 @@
}
@androidx.compose.runtime.Stable public interface RadioButtonColors {
- method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> circleColor(boolean enabled, boolean selected);
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> dotColor(boolean enabled, boolean selected);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> ringColor(boolean enabled, boolean selected);
}
public final class RadioButtonDefaults {
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material.RadioButtonColors colors(optional long selectedCircleColor, optional long selectedDotColor, optional long unselectedCircleColor, optional long unselectedDotColor);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material.RadioButtonColors colors(optional long selectedRingColor, optional long selectedDotColor, optional long unselectedRingColor, optional long unselectedDotColor);
field public static final androidx.wear.compose.material.RadioButtonDefaults INSTANCE;
}
@@ -506,9 +506,9 @@
}
public final class ScrollAwayKt {
- method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState scrollState, optional int offset);
- method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.lazy.LazyListState scrollState, optional int itemIndex, optional int offset);
- method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.material.ScalingLazyListState scrollState, optional int itemIndex, optional int offset);
+ method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState scrollState, optional float offset);
+ method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.lazy.LazyListState scrollState, optional int itemIndex, optional float offset);
+ method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.material.ScalingLazyListState scrollState, optional int itemIndex, optional float offset);
}
@androidx.compose.runtime.Immutable public final class Shapes {
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index f07d9a9..61feb7a 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -371,12 +371,12 @@
}
@androidx.compose.runtime.Stable public interface RadioButtonColors {
- method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> circleColor(boolean enabled, boolean selected);
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> dotColor(boolean enabled, boolean selected);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> ringColor(boolean enabled, boolean selected);
}
public final class RadioButtonDefaults {
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material.RadioButtonColors colors(optional long selectedCircleColor, optional long selectedDotColor, optional long unselectedCircleColor, optional long unselectedDotColor);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material.RadioButtonColors colors(optional long selectedRingColor, optional long selectedDotColor, optional long unselectedRingColor, optional long unselectedDotColor);
field public static final androidx.wear.compose.material.RadioButtonDefaults INSTANCE;
}
@@ -532,9 +532,9 @@
}
public final class ScrollAwayKt {
- method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState scrollState, optional int offset);
- method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.lazy.LazyListState scrollState, optional int itemIndex, optional int offset);
- method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.material.ScalingLazyListState scrollState, optional int itemIndex, optional int offset);
+ method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState scrollState, optional float offset);
+ method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.lazy.LazyListState scrollState, optional int itemIndex, optional float offset);
+ method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.material.ScalingLazyListState scrollState, optional int itemIndex, optional float offset);
}
@androidx.compose.runtime.Immutable public final class Shapes {
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index 6ff9573..bb7e74a 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -356,12 +356,12 @@
}
@androidx.compose.runtime.Stable public interface RadioButtonColors {
- method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> circleColor(boolean enabled, boolean selected);
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> dotColor(boolean enabled, boolean selected);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> ringColor(boolean enabled, boolean selected);
}
public final class RadioButtonDefaults {
- method @androidx.compose.runtime.Composable public androidx.wear.compose.material.RadioButtonColors colors(optional long selectedCircleColor, optional long selectedDotColor, optional long unselectedCircleColor, optional long unselectedDotColor);
+ method @androidx.compose.runtime.Composable public androidx.wear.compose.material.RadioButtonColors colors(optional long selectedRingColor, optional long selectedDotColor, optional long unselectedRingColor, optional long unselectedDotColor);
field public static final androidx.wear.compose.material.RadioButtonDefaults INSTANCE;
}
@@ -506,9 +506,9 @@
}
public final class ScrollAwayKt {
- method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState scrollState, optional int offset);
- method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.lazy.LazyListState scrollState, optional int itemIndex, optional int offset);
- method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.material.ScalingLazyListState scrollState, optional int itemIndex, optional int offset);
+ method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState scrollState, optional float offset);
+ method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.lazy.LazyListState scrollState, optional int itemIndex, optional float offset);
+ method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.material.ScalingLazyListState scrollState, optional int itemIndex, optional float offset);
}
@androidx.compose.runtime.Immutable public final class Shapes {
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorScreenshotTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorScreenshotTest.kt
index ad7f6e0..2734d79 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorScreenshotTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorScreenshotTest.kt
@@ -31,6 +31,7 @@
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
@@ -91,6 +92,7 @@
.assertAgainstGolden(screenshotRule, testName.methodName)
}
+ @Ignore("Disable broken test: b/249447057")
@Test
fun determinate_progress_indicator_no_gap() {
rule.setContentWithTheme {
@@ -109,6 +111,7 @@
.assertAgainstGolden(screenshotRule, testName.methodName)
}
+ @Ignore("Disable broken test: b/249447057")
@Test
fun determinate_progress_indicator_with_gap() {
rule.setContentWithTheme {
@@ -129,6 +132,7 @@
.assertAgainstGolden(screenshotRule, testName.methodName)
}
+ @Ignore("Disable broken test: b/249447057")
@Test
fun determinate_progress_indicator_custom_stroke_width() {
rule.setContentWithTheme {
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlScreenshotTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlScreenshotTest.kt
index d0c4163..494693d 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlScreenshotTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlScreenshotTest.kt
@@ -17,9 +17,11 @@
package androidx.wear.compose.material
import android.os.Build
+import androidx.compose.foundation.background
import androidx.compose.runtime.Composable
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
@@ -28,6 +30,7 @@
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.test.screenshot.matchers.MSSIMMatcher
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
@@ -50,84 +53,88 @@
@Test
fun checkbox_checked_enabled() =
verifyScreenshot {
- Checkbox(checked = true, enabled = true, modifier = Modifier.testTag(TEST_TAG))
+ Checkbox(checked = true, enabled = true, modifier = testBackgroundModifier())
}
@Test
fun checkbox_unchecked_enabled() =
verifyScreenshot {
- Checkbox(checked = false, enabled = true, modifier = Modifier.testTag(TEST_TAG))
+ Checkbox(checked = false, enabled = true, modifier = testBackgroundModifier())
}
@Test
fun checkbox_checked_disabled() =
verifyScreenshot {
- Checkbox(checked = true, enabled = false, modifier = Modifier.testTag(TEST_TAG))
+ Checkbox(checked = true, enabled = false, modifier = testBackgroundModifier())
}
@Test
fun checkbox_unchecked_disabled() =
verifyScreenshot {
- Checkbox(checked = false, enabled = false, modifier = Modifier.testTag(TEST_TAG))
+ Checkbox(checked = false, enabled = false, modifier = testBackgroundModifier())
}
@Test
fun switch_checked_enabled() =
verifyScreenshot {
- Switch(checked = true, enabled = true, modifier = Modifier.testTag(TEST_TAG))
+ Switch(checked = true, enabled = true, modifier = testBackgroundModifier())
}
@Test
fun switch_unchecked_enabled() =
verifyScreenshot {
- Switch(checked = false, enabled = true, modifier = Modifier.testTag(TEST_TAG))
+ Switch(checked = false, enabled = true, modifier = testBackgroundModifier())
}
@Test
fun switch_checked_disabled() =
verifyScreenshot {
- Switch(checked = true, enabled = false, modifier = Modifier.testTag(TEST_TAG))
+ Switch(checked = true, enabled = false, modifier = testBackgroundModifier())
}
@Test
fun switch_unchecked_disabled() =
verifyScreenshot {
- Switch(checked = false, enabled = false, modifier = Modifier.testTag(TEST_TAG))
+ Switch(checked = false, enabled = false, modifier = testBackgroundModifier())
}
@Test
fun radiobutton_checked_enabled() =
verifyScreenshot {
- RadioButton(selected = true, enabled = true, modifier = Modifier.testTag(TEST_TAG))
+ RadioButton(selected = true, enabled = true, modifier = testBackgroundModifier())
}
@Test
fun radiobutton_unchecked_enabled() =
verifyScreenshot {
- RadioButton(selected = false, enabled = true, modifier = Modifier.testTag(TEST_TAG))
+ RadioButton(selected = false, enabled = true, modifier = testBackgroundModifier())
}
@Test
fun radiobutton_checked_disabled() =
verifyScreenshot {
- RadioButton(selected = true, enabled = false, modifier = Modifier.testTag(TEST_TAG))
+ RadioButton(selected = true, enabled = false, modifier = testBackgroundModifier())
}
@Test
fun radiobutton_unchecked_disabled() =
verifyScreenshot {
- RadioButton(selected = false, enabled = false, modifier = Modifier.testTag(TEST_TAG))
+ RadioButton(selected = false, enabled = false, modifier = testBackgroundModifier())
}
private fun verifyScreenshot(
+ threshold: Double = 0.98,
content: @Composable () -> Unit
) {
- rule.setContentWithTheme {
- content()
- }
-
+ rule.setContentWithTheme(composable = content)
rule.onNodeWithTag(TEST_TAG)
.captureToImage()
- .assertAgainstGolden(screenshotRule, testName.methodName)
+ .assertAgainstGolden(screenshotRule, testName.methodName, MSSIMMatcher(threshold))
}
+ @Composable
+ private fun testBackgroundModifier(): Modifier =
+ Modifier.testTag(TEST_TAG).background(
+ MaterialTheme.colors.primary.copy(alpha = 0.5f)
+ .compositeOver(MaterialTheme.colors.surface)
+ )
}
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlTest.kt
index 26445ae..ba65438 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ToggleControlTest.kt
@@ -16,11 +16,14 @@
package androidx.wear.compose.material
+import android.os.Build
+import androidx.annotation.RequiresApi
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.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsProperties
@@ -36,6 +39,7 @@
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.isSelectable
import androidx.compose.ui.test.isToggleable
import androidx.compose.ui.test.junit4.createComposeRule
@@ -225,6 +229,47 @@
.assertIsOff()
}
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ fun checkbox_checked_colors_are_customisable() {
+ val boxColor = Color.Green
+ val checkmarkColor = Color.Blue
+ rule.setContentWithTheme {
+ Checkbox(
+ checked = true,
+ colors = CheckboxDefaults.colors(
+ checkedBoxColor = boxColor,
+ checkedCheckmarkColor = checkmarkColor
+ ),
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+
+ val checkboxImage = rule.onNodeWithTag(TEST_TAG).captureToImage()
+ checkboxImage.assertContainsColor(boxColor, minPercent = 1f)
+ checkboxImage.assertContainsColor(checkmarkColor, minPercent = 1f)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ fun checkbox_unchecked_colors_are_customisable() {
+ // NB checkmark is erased during animation, so we don't test uncheckedCheckmarkColor
+ // as it is just used as a target color for the animation.
+ val boxColor = Color.Green
+ rule.setContentWithTheme {
+ Checkbox(
+ checked = false,
+ colors = CheckboxDefaults.colors(
+ uncheckedBoxColor = boxColor,
+ ),
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+
+ val checkboxImage = rule.onNodeWithTag(TEST_TAG).captureToImage()
+ checkboxImage.assertContainsColor(boxColor, minPercent = 1f)
+ }
+
@Test
fun switch_supports_testtag() {
rule.setContentWithTheme {
@@ -400,6 +445,48 @@
.assertIsOff()
}
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ fun switch_checked_colors_are_customisable() {
+ val thumbColor = Color.Green
+ val trackColor = Color.Blue
+ rule.setContentWithTheme {
+ Switch(
+ checked = true,
+ colors = SwitchDefaults.colors(
+ checkedThumbColor = thumbColor,
+ checkedTrackColor = trackColor
+ ),
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+
+ val image = rule.onNodeWithTag(TEST_TAG).captureToImage()
+ image.assertContainsColor(thumbColor, minPercent = 1f)
+ image.assertContainsColor(trackColor, minPercent = 1f)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ fun switch_unchecked_colors_are_customisable() {
+ val thumbColor = Color.Green
+ val trackColor = Color.Blue
+ rule.setContentWithTheme {
+ Switch(
+ checked = false,
+ colors = SwitchDefaults.colors(
+ uncheckedThumbColor = thumbColor,
+ uncheckedTrackColor = trackColor
+ ),
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+
+ val image = rule.onNodeWithTag(TEST_TAG).captureToImage()
+ image.assertContainsColor(thumbColor, minPercent = 1f)
+ image.assertContainsColor(trackColor, minPercent = 1f)
+ }
+
@Test
fun radiobutton_supports_testtag() {
rule.setContentWithTheme {
@@ -575,4 +662,45 @@
.performClick()
.assertIsNotSelected()
}
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ fun radiobutton_selected_colors_are_customisable() {
+ val ringColor = Color.Green
+ val dotColor = Color.Blue
+ rule.setContentWithTheme {
+ RadioButton(
+ selected = true,
+ colors = RadioButtonDefaults.colors(
+ selectedRingColor = ringColor,
+ selectedDotColor = dotColor
+ ),
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+
+ val image = rule.onNodeWithTag(TEST_TAG).captureToImage()
+ image.assertContainsColor(ringColor, minPercent = 1f)
+ image.assertContainsColor(dotColor, minPercent = 1f)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Test
+ fun radiobutton_unselected_colors_are_customisable() {
+ // NB Dot is erased during animation, so we don't test uncheckedDotColor
+ // as it is just used as a target color for the animation.
+ val ringColor = Color.Green
+ rule.setContentWithTheme {
+ RadioButton(
+ selected = false,
+ colors = RadioButtonDefaults.colors(
+ unselectedRingColor = ringColor,
+ ),
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+
+ val image = rule.onNodeWithTag(TEST_TAG).captureToImage()
+ image.assertContainsColor(ringColor, minPercent = 1f)
+ }
}
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
index 81b971ae..d4f0e89 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
@@ -18,24 +18,17 @@
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.graphics.TransformOrigin
-import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
@@ -51,19 +44,8 @@
*/
public fun Modifier.scrollAway(
scrollState: ScrollState,
- offset: Int = 0,
-): Modifier = composed(
- inspectorInfo = debugInspectorInfo {
- name = "scrollAway"
- properties["scrollState"] = scrollState
- properties["offset"] = offset
- }
-) {
- val y = with(LocalDensity.current) {
- (scrollState.value - offset).toDp()
- }
- scrollAway(y)
-}
+ offset: Dp = 0.dp,
+): Modifier = scrollAway { scrollState.value - offset.toPx() }
/**
* Scroll an item vertically in/out of view based on a [LazyListState].
@@ -78,25 +60,11 @@
public fun Modifier.scrollAway(
scrollState: LazyListState,
itemIndex: Int = 0,
- offset: Int = 0,
-): Modifier = composed(
- inspectorInfo = debugInspectorInfo {
- name = "scrollAway"
- properties["scrollState"] = scrollState
- properties["itemIndex"] = itemIndex
- properties["offset"] = offset
- }
-) {
- val targetItem by remember {
- derivedStateOf {
- scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }
- }
- }
- if (targetItem != null) {
- val y = with(LocalDensity.current) {
- (-targetItem!!.offset - offset).toDp()
- }
- scrollAway(y)
+ offset: Dp = 0.dp,
+): Modifier {
+ val targetItem = scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }
+ return if (targetItem != null) {
+ scrollAway { -targetItem.offset - offset.toPx() }
} else {
ignore()
}
@@ -115,45 +83,39 @@
public fun Modifier.scrollAway(
scrollState: ScalingLazyListState,
itemIndex: Int = 1,
- offset: Int = 0,
-): Modifier =
- composed(
- inspectorInfo = debugInspectorInfo {
- name = "scrollAway"
- properties["scrollState"] = scrollState
- properties["itemIndex"] = itemIndex
- properties["offset"] = offset
- }
- ) {
- val targetItem by remember {
- derivedStateOf {
- scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }
+ offset: Dp = 0.dp,
+): Modifier {
+ val targetItem = scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }
+ return if (targetItem != null) {
+ scrollAway { -targetItem.offset - offset.toPx() }
+ } else {
+ ignore()
+ }
+}
+
+private fun Modifier.scrollAway(yPxFn: Density.() -> Float): Modifier = this.then(
+ object : LayoutModifier {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) {
+ val yPx = yPxFn()
+ val progress: Float = (yPx / maxScrollOut.toPx()).coerceIn(0f, 1f)
+ val motionFraction: Float = lerp(minMotionOut, maxMotionOut, progress)
+ val offsetY = -(maxOffset.toPx() * progress).toInt()
+
+ placeable.placeWithLayer(0, offsetY) {
+ alpha = motionFraction
+ scaleX = motionFraction
+ scaleY = motionFraction
+ transformOrigin = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0.0f)
+ }
}
}
- if (targetItem != null) {
- val y = with(LocalDensity.current) {
- (-targetItem!!.offset - offset).toDp()
- }
- scrollAway(y)
- } else {
- ignore()
- }
}
-
-private fun Modifier.scrollAway(y: Dp): Modifier {
- val progress: Float = (y / maxScrollOut).coerceIn(0f, 1f)
- val motionFraction: Float = lerp(minMotionOut, maxMotionOut, progress)
- val offset = -(maxOffset * progress)
-
- return this
- .offset(y = offset)
- .graphicsLayer {
- alpha = motionFraction
- scaleX = motionFraction
- scaleY = motionFraction
- transformOrigin = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0.0f)
- }
-}
+)
// Trivial modifier that neither measures nor places the content.
private fun Modifier.ignore(): Modifier = this.then(
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ToggleControl.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ToggleControl.kt
index 0a278bf..fd4d7d1 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ToggleControl.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ToggleControl.kt
@@ -229,7 +229,7 @@
val circleColor = animateColor(
transition,
- colorFn = { e, s -> colors.circleColor(enabled = e, selected = s) },
+ colorFn = { e, s -> colors.ringColor(enabled = e, selected = s) },
enabled
)
val dotRadiusProgress = animateProgress(
@@ -333,17 +333,17 @@
@Stable
public interface RadioButtonColors {
/**
- * Represents the circle color for this [RadioButton], depending on
+ * Represents the outer ring color for this [RadioButton], depending on
* the [enabled] and [selected] properties.
*
* @param enabled Whether the [RadioButton] is enabled
* @param selected Whether the [RadioButton] is currently selected or unselected
*/
@Composable
- public fun circleColor(enabled: Boolean, selected: Boolean): State<Color>
+ public fun ringColor(enabled: Boolean, selected: Boolean): State<Color>
/**
- * Represents the dot color for this [RadioButton], depending on
+ * Represents the inner dot color for this [RadioButton], depending on
* the [enabled] and [selected] properties.
*
* @param enabled Whether the [RadioButton] is enabled
@@ -409,9 +409,10 @@
@Composable
public fun colors(
checkedThumbColor: Color = MaterialTheme.colors.secondary,
- checkedTrackColor: Color = checkedThumbColor,
- uncheckedThumbColor: Color = MaterialTheme.colors.onSurface.copy(0.6f),
- uncheckedTrackColor: Color = uncheckedThumbColor,
+ checkedTrackColor: Color = checkedThumbColor.copy(alpha = ContentAlpha.disabled),
+ uncheckedThumbColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
+ uncheckedTrackColor: Color =
+ uncheckedThumbColor.copy(alpha = uncheckedThumbColor.alpha * ContentAlpha.disabled),
): SwitchColors {
return DefaultSwitchColors(
checkedThumbColor = checkedThumbColor,
@@ -419,11 +420,13 @@
uncheckedThumbColor = uncheckedThumbColor,
uncheckedTrackColor = uncheckedTrackColor,
disabledCheckedThumbColor = checkedThumbColor.copy(alpha = ContentAlpha.disabled),
- disabledCheckedTrackColor = checkedTrackColor.copy(alpha = ContentAlpha.disabled),
+ disabledCheckedTrackColor = checkedTrackColor.copy(
+ alpha = checkedTrackColor.alpha * ContentAlpha.disabled
+ ),
disabledUncheckedThumbColor =
- uncheckedThumbColor.copy(alpha = ContentAlpha.disabled),
+ uncheckedThumbColor.copy(alpha = uncheckedThumbColor.alpha * ContentAlpha.disabled),
disabledUncheckedTrackColor =
- uncheckedTrackColor.copy(alpha = ContentAlpha.disabled),
+ uncheckedTrackColor.copy(alpha = uncheckedTrackColor.alpha * ContentAlpha.disabled),
)
}
}
@@ -435,20 +438,20 @@
/**
* Creates a [RadioButtonColors] for use in a [RadioButton].
*
- * @param selectedCircleColor The outer circle color of this [RadioButton] when enabled
+ * @param selectedRingColor The outer ring color of this [RadioButton] when enabled
* and selected.
* @param selectedDotColor The inner dot color of this [RadioButton] when enabled
* and selected.
- * @param unselectedCircleColor The outer circle color of this [RadioButton] when enabled
+ * @param unselectedRingColor The outer ring color of this [RadioButton] when enabled
* and unselected.
* @param unselectedDotColor The inner dot color of this [RadioButton] when enabled
* and unselected.
*/
@Composable
public fun colors(
- selectedCircleColor: Color = MaterialTheme.colors.secondary,
+ selectedRingColor: Color = MaterialTheme.colors.secondary,
selectedDotColor: Color = MaterialTheme.colors.secondary,
- unselectedCircleColor: Color = contentColorFor(
+ unselectedRingColor: Color = contentColorFor(
MaterialTheme.colors.primary.copy(alpha = 0.5f)
.compositeOver(MaterialTheme.colors.surface)
),
@@ -458,14 +461,14 @@
),
): RadioButtonColors {
return DefaultRadioButtonColors(
- selectedCircleColor = selectedCircleColor,
+ selectedRingColor = selectedRingColor,
selectedDotColor = selectedDotColor,
- unselectedCircleColor = unselectedCircleColor,
+ unselectedRingColor = unselectedRingColor,
unselectedDotColor = unselectedDotColor,
- disabledSelectedCircleColor = selectedCircleColor.copy(alpha = ContentAlpha.disabled),
+ disabledSelectedRingColor = selectedRingColor.copy(alpha = ContentAlpha.disabled),
disabledSelectedDotColor = selectedDotColor.copy(alpha = ContentAlpha.disabled),
- disabledUnselectedCircleColor =
- unselectedCircleColor.copy(alpha = ContentAlpha.disabled),
+ disabledUnselectedRingColor =
+ unselectedRingColor.copy(alpha = ContentAlpha.disabled),
disabledUnselectedDotColor = unselectedDotColor.copy(alpha = ContentAlpha.disabled),
)
}
@@ -637,7 +640,7 @@
private fun DrawScope.drawTrack(
color: Color,
switchTrackLengthPx: Float,
- switchTrackHeightPx: Float
+ switchTrackHeightPx: Float,
) {
val path = Path()
val strokeRadius = switchTrackHeightPx / 2f
@@ -646,7 +649,6 @@
drawPath(
path = path,
color = color,
- alpha = 0.38f,
style = Stroke(width = switchTrackHeightPx, cap = StrokeCap.Round)
)
}
@@ -792,22 +794,22 @@
*/
@Immutable
private class DefaultRadioButtonColors(
- private val selectedCircleColor: Color,
+ private val selectedRingColor: Color,
private val selectedDotColor: Color,
- private val unselectedCircleColor: Color,
+ private val unselectedRingColor: Color,
private val unselectedDotColor: Color,
- private val disabledSelectedCircleColor: Color,
+ private val disabledSelectedRingColor: Color,
private val disabledSelectedDotColor: Color,
- private val disabledUnselectedCircleColor: Color,
+ private val disabledUnselectedRingColor: Color,
private val disabledUnselectedDotColor: Color,
) : RadioButtonColors {
@Composable
- override fun circleColor(enabled: Boolean, selected: Boolean): State<Color> {
+ override fun ringColor(enabled: Boolean, selected: Boolean): State<Color> {
return rememberUpdatedState(
if (enabled) {
- if (selected) selectedCircleColor else unselectedCircleColor
+ if (selected) selectedRingColor else unselectedRingColor
} else {
- if (selected) disabledSelectedCircleColor else disabledUnselectedCircleColor
+ if (selected) disabledSelectedRingColor else disabledUnselectedRingColor
}
)
}
@@ -830,26 +832,26 @@
other as DefaultRadioButtonColors
- if (selectedCircleColor != other.selectedCircleColor) return false
+ if (selectedRingColor != other.selectedRingColor) return false
if (selectedDotColor != other.selectedDotColor) return false
- if (unselectedCircleColor != other.unselectedCircleColor) return false
+ if (unselectedRingColor != other.unselectedRingColor) return false
if (unselectedDotColor != other.unselectedDotColor) return false
- if (disabledSelectedCircleColor != other.disabledSelectedCircleColor) return false
+ if (disabledSelectedRingColor != other.disabledSelectedRingColor) return false
if (disabledSelectedDotColor != other.disabledSelectedDotColor) return false
- if (disabledUnselectedCircleColor != other.disabledUnselectedCircleColor) return false
+ if (disabledUnselectedRingColor != other.disabledUnselectedRingColor) return false
if (disabledUnselectedDotColor != other.disabledUnselectedDotColor) return false
return true
}
override fun hashCode(): Int {
- var result = selectedCircleColor.hashCode()
+ var result = selectedRingColor.hashCode()
result = 31 * result + selectedDotColor.hashCode()
- result = 31 * result + unselectedCircleColor.hashCode()
+ result = 31 * result + unselectedRingColor.hashCode()
result = 31 * result + unselectedDotColor.hashCode()
- result = 31 * result + disabledSelectedCircleColor.hashCode()
+ result = 31 * result + disabledSelectedRingColor.hashCode()
result = 31 * result + disabledSelectedDotColor.hashCode()
- result = 31 * result + disabledUnselectedCircleColor.hashCode()
+ result = 31 * result + disabledUnselectedRingColor.hashCode()
result = 31 * result + disabledUnselectedDotColor.hashCode()
return result
}
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
index 2f9a6bf..36c223f 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
@@ -31,6 +31,7 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.AutoCenteringParams
import androidx.wear.compose.material.Card
@@ -47,31 +48,35 @@
import androidx.wear.compose.material.scrollAway
@Composable
-fun ScrollAwayColumnDemo() { ColumnCardDemo(0) }
+fun ScrollAwayColumnDemo() { ColumnCardDemo(0.dp) }
@Composable
-fun ScrollAwayColumnDelayDemo() { ColumnCardDemo(offset = 36) }
+fun ScrollAwayColumnDelayDemo() { ColumnCardDemo(offset = 20.dp) }
@Composable
fun ScrollAwayLazyColumnDemo() {
- LazyColumnCardDemo(offset = 0, itemIndex = 0, initialVisibleItemIndex = 0)
+ LazyColumnCardDemo(offset = 0.dp, itemIndex = 0, initialVisibleItemIndex = 0)
}
@Composable
fun ScrollAwayLazyColumnDemo2() {
- LazyColumnCardDemo(offset = -350, itemIndex = 2, initialVisibleItemIndex = 2)
+ LazyColumnCardDemo(
+ offset = -195.dp,
+ itemIndex = 2,
+ initialVisibleItemIndex = 2
+ )
}
@Composable
fun ScrollAwayLazyColumnDelayDemo() {
- LazyColumnCardDemo(offset = 36, itemIndex = 0, initialVisibleItemIndex = 0)
+ LazyColumnCardDemo(offset = 20.dp, itemIndex = 0, initialVisibleItemIndex = 0)
}
@Composable
fun ScrollAwayScalingLazyColumnCardDemo() {
ScalingLazyColumnCardDemo(
itemIndex = 1,
- offset = 0,
+ offset = 0.dp,
initialCenterItemIndex = 1,
)
}
@@ -80,7 +85,7 @@
fun ScrollAwayScalingLazyColumnCardDemo2() {
ScalingLazyColumnCardDemo(
itemIndex = 2,
- offset = -180,
+ offset = -95.dp,
initialCenterItemIndex = 2,
)
}
@@ -89,7 +94,7 @@
fun ScrollAwayScalingLazyColumnCardDemoMismatch() {
ScalingLazyColumnCardDemo(
itemIndex = 0,
- offset = 120,
+ offset = 75.dp,
initialCenterItemIndex = 1,
)
}
@@ -98,7 +103,7 @@
fun ScrollAwayScalingLazyColumnChipDemo() {
ScalingLazyColumnChipDemo(
itemIndex = 1,
- offset = 20,
+ offset = 10.dp,
initialCenterItemIndex = 1,
)
}
@@ -107,13 +112,13 @@
fun ScrollAwayScalingLazyColumnChipDemo2() {
ScalingLazyColumnChipDemo(
itemIndex = 2,
- offset = -100,
+ offset = -50.dp,
initialCenterItemIndex = 2,
)
}
@Composable
-private fun ColumnCardDemo(offset: Int) {
+private fun ColumnCardDemo(offset: Dp) {
val scrollState = rememberScrollState()
Scaffold(
@@ -143,7 +148,7 @@
}
@Composable
-private fun LazyColumnCardDemo(offset: Int, itemIndex: Int, initialVisibleItemIndex: Int) {
+private fun LazyColumnCardDemo(offset: Dp, itemIndex: Int, initialVisibleItemIndex: Int) {
val scrollState = rememberLazyListState(initialFirstVisibleItemIndex = initialVisibleItemIndex)
Scaffold(
@@ -172,7 +177,7 @@
@Composable
private fun ScalingLazyColumnCardDemo(
- offset: Int,
+ offset: Dp,
itemIndex: Int,
initialCenterItemIndex: Int,
) {
@@ -214,7 +219,7 @@
@Composable
private fun ScalingLazyColumnChipDemo(
- offset: Int,
+ offset: Dp,
itemIndex: Int,
initialCenterItemIndex: Int,
) {
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt
index a3a52b8..b5c6dfe 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ToggleChipDemo.kt
@@ -140,13 +140,6 @@
ToggleChip(
label = { Text("SwitchIcon") },
checked = switchIconChecked,
- // For Switch toggle controls the Wear Material UX guidance is to set the
- // unselected toggle control color to
- // ToggleChipDefaults.switchUncheckedIconColor() rather than the default.
- colors = ToggleChipDefaults.toggleChipColors(
- uncheckedToggleControlColor = ToggleChipDefaults
- .SwitchUncheckedIconColor
- ),
toggleControl = {
Switch(
checked = switchIconChecked,
@@ -167,13 +160,6 @@
label = { Text("SwitchIcon") },
secondaryLabel = { Text("Custom color") },
checked = switchIconCustomColorChecked,
- // For Switch toggle controls the Wear Material UX guidance is to set the
- // unselected toggle control color to
- // ToggleChipDefaults.switchUncheckedIconColor() rather than the default.
- colors = ToggleChipDefaults.toggleChipColors(
- uncheckedToggleControlColor = ToggleChipDefaults
- .SwitchUncheckedIconColor
- ),
toggleControl = {
Switch(
colors = SwitchDefaults.colors(
@@ -228,7 +214,7 @@
)
},
secondaryLabel = {
- Text("Custom Color", maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text("CustomColor", maxLines = 1, overflow = TextOverflow.Ellipsis)
},
checked = radioIconWithSecondaryChecked,
toggleControl = {
@@ -236,9 +222,9 @@
selected = radioIconWithSecondaryChecked,
enabled = enabled,
colors = RadioButtonDefaults.colors(
- selectedCircleColor = MaterialTheme.colors.primary,
+ selectedRingColor = MaterialTheme.colors.primary,
selectedDotColor = Color.Green,
- unselectedCircleColor = Color.Magenta,
+ unselectedRingColor = Color.Magenta,
unselectedDotColor = Color.Red,
),
modifier = Modifier.semantics {
diff --git a/wear/watchface/watchface-client/api/current.txt b/wear/watchface/watchface-client/api/current.txt
index 19433ab..b596ff3 100644
--- a/wear/watchface/watchface-client/api/current.txt
+++ b/wear/watchface/watchface-client/api/current.txt
@@ -71,6 +71,12 @@
public final class DeviceConfigKt {
}
+ public final class DisconnectReasons {
+ field public static final int ENGINE_DETACHED = 2; // 0x2
+ field public static final int ENGINE_DIED = 1; // 0x1
+ field public static final androidx.wear.watchface.client.DisconnectReasons INSTANCE;
+ }
+
public interface EditorListener {
method public void onEditorStateChanged(androidx.wear.watchface.client.EditorState editorState);
}
diff --git a/wear/watchface/watchface-client/api/public_plus_experimental_current.txt b/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
index ca839d5..9e0648e 100644
--- a/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
@@ -75,6 +75,12 @@
public final class DeviceConfigKt {
}
+ public final class DisconnectReasons {
+ field public static final int ENGINE_DETACHED = 2; // 0x2
+ field public static final int ENGINE_DIED = 1; // 0x1
+ field public static final androidx.wear.watchface.client.DisconnectReasons INSTANCE;
+ }
+
public interface EditorListener {
method public void onEditorStateChanged(androidx.wear.watchface.client.EditorState editorState);
}
diff --git a/wear/watchface/watchface-client/api/restricted_current.txt b/wear/watchface/watchface-client/api/restricted_current.txt
index 19433ab..b596ff3 100644
--- a/wear/watchface/watchface-client/api/restricted_current.txt
+++ b/wear/watchface/watchface-client/api/restricted_current.txt
@@ -71,6 +71,12 @@
public final class DeviceConfigKt {
}
+ public final class DisconnectReasons {
+ field public static final int ENGINE_DETACHED = 2; // 0x2
+ field public static final int ENGINE_DIED = 1; // 0x1
+ field public static final androidx.wear.watchface.client.DisconnectReasons INSTANCE;
+ }
+
public interface EditorListener {
method public void onEditorStateChanged(androidx.wear.watchface.client.EditorState editorState);
}
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index 60695d5..30d5c48 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -67,6 +67,7 @@
import androidx.wear.watchface.WatchState
import androidx.wear.watchface.client.DeviceConfig
import androidx.wear.watchface.client.DisconnectReason
+import androidx.wear.watchface.client.DisconnectReasons
import androidx.wear.watchface.client.HeadlessWatchFaceClient
import androidx.wear.watchface.client.InteractiveWatchFaceClient
import androidx.wear.watchface.client.WatchFaceClientExperimental
@@ -1926,7 +1927,7 @@
// Simulate detach.
engine.onDestroy()
- assertThat(lastDisconnectReason).isEqualTo(DisconnectReason.ENGINE_DETACHED)
+ assertThat(lastDisconnectReason).isEqualTo(DisconnectReasons.ENGINE_DETACHED)
}
}
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt
index cf8cd53..08e68ed 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt
@@ -56,29 +56,34 @@
/** @hide */
@IntDef(
value = [
- DisconnectReason.ENGINE_DIED,
- DisconnectReason.ENGINE_DETACHED
+ DisconnectReasons.ENGINE_DIED,
+ DisconnectReasons.ENGINE_DETACHED
]
)
-public annotation class DisconnectReason {
- public companion object {
- /**
- * The underlying engine died, probably because the watch face was killed or crashed.
- * Sometimes this is due to memory pressure and it's not the watch face's fault. Usually in
- * response a new [InteractiveWatchFaceClient] should be created (see
- * [WatchFaceControlClient.getOrCreateInteractiveWatchFaceClient]), however if this new
- * client also disconnects due to [ENGINE_DIED] within a few seconds the watchface is
- * probably bad and it's recommended to switch to a safe system default watch face.
- */
- public const val ENGINE_DIED: Int = 1
+public annotation class DisconnectReason
- /**
- * Wallpaper service detached from the engine, which is now defunct. The watch face itself
- * has no control over this. Usually in response a new [InteractiveWatchFaceClient]
- * should be created (see [WatchFaceControlClient.getOrCreateInteractiveWatchFaceClient]).
- */
- public const val ENGINE_DETACHED: Int = 2
- }
+/**
+ * Disconnect reasons for
+ * [InteractiveWatchFaceClient.ClientDisconnectListener.onClientDisconnected].
+ */
+public object DisconnectReasons {
+
+ /**
+ * The underlying engine died, probably because the watch face was killed or crashed.
+ * Sometimes this is due to memory pressure and it's not the watch face's fault. Usually in
+ * response a new [InteractiveWatchFaceClient] should be created (see
+ * [WatchFaceControlClient.getOrCreateInteractiveWatchFaceClient]), however if this new
+ * client also disconnects due to [ENGINE_DIED] within a few seconds the watchface is
+ * probably bad and it's recommended to switch to a safe system default watch face.
+ */
+ public const val ENGINE_DIED: Int = 1
+
+ /**
+ * Wallpaper service detached from the engine, which is now defunct. The watch face itself
+ * has no control over this. Usually in response a new [InteractiveWatchFaceClient]
+ * should be created (see [WatchFaceControlClient.getOrCreateInteractiveWatchFaceClient]).
+ */
+ public const val ENGINE_DETACHED: Int = 2
}
/**
@@ -88,6 +93,7 @@
* Note clients should call [close] when finished.
*/
public interface InteractiveWatchFaceClient : AutoCloseable {
+
/**
* Sends new [ComplicationData] to the watch face. Note this doesn't have to be a full update,
* it's possible to update just one complication at a time, but doing so may result in a less
@@ -250,7 +256,8 @@
"Deprecated, use an overload that passes the disconnectReason",
ReplaceWith("onClientDisconnected(Int)")
)
- public fun onClientDisconnected() {}
+ public fun onClientDisconnected() {
+ }
/**
* The client disconnected, due to [disconnectReason].
@@ -321,7 +328,8 @@
public fun addOnWatchFaceColorsListener(
executor: Executor,
listener: Consumer<WatchFaceColors?>
- ) {}
+ ) {
+ }
/**
* Stops listening for events registered by [addOnWatchFaceColorsListener].
@@ -380,14 +388,14 @@
}
override fun onEngineDetached() {
- sendDisconnectNotification(DisconnectReason.ENGINE_DETACHED)
+ sendDisconnectNotification(DisconnectReasons.ENGINE_DETACHED)
}
}
init {
iInteractiveWatchFace.asBinder().linkToDeath(
{
- sendDisconnectNotification(DisconnectReason.ENGINE_DIED)
+ sendDisconnectNotification(DisconnectReasons.ENGINE_DIED)
},
0
)
diff --git a/wear/watchface/watchface-client/src/test/java/androidx/wear/watchface/client/InteractiveWatchFaceClientTest.kt b/wear/watchface/watchface-client/src/test/java/androidx/wear/watchface/client/InteractiveWatchFaceClientTest.kt
index 784c538..8a468f0 100644
--- a/wear/watchface/watchface-client/src/test/java/androidx/wear/watchface/client/InteractiveWatchFaceClientTest.kt
+++ b/wear/watchface/watchface-client/src/test/java/androidx/wear/watchface/client/InteractiveWatchFaceClientTest.kt
@@ -47,8 +47,8 @@
client.addClientDisconnectListener(listener, { it.run() })
// Simulate multiple disconnect notifications.
- client.sendDisconnectNotification(DisconnectReason.ENGINE_DETACHED)
- client.sendDisconnectNotification(DisconnectReason.ENGINE_DIED)
+ client.sendDisconnectNotification(DisconnectReasons.ENGINE_DETACHED)
+ client.sendDisconnectNotification(DisconnectReasons.ENGINE_DIED)
// But only one should be sent to the listener.
verify(listener, times(1)).onClientDisconnected(any())
diff --git a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt
index 035c75f..99d37bb 100644
--- a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt
+++ b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/ComplicationDataSourceInfoRetriever.kt
@@ -51,6 +51,7 @@
import java.lang.IllegalArgumentException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
+import kotlinx.coroutines.CancellableContinuation
private typealias WireComplicationProviderInfo =
android.support.wearable.complications.ComplicationProviderInfo
@@ -209,44 +210,53 @@
}
return suspendCancellableCoroutine { continuation ->
- val deathObserver = IBinder.DeathRecipient {
- continuation.resumeWithException(ServiceDisconnectedException())
- }
- service.asBinder().linkToDeath(deathObserver, 0)
-
- // Not a huge deal but we might as well unlink the deathObserver.
- continuation.invokeOnCancellation {
- safeUnlinkToDeath(service, deathObserver)
- }
-
+ val callback = PreviewComplicationDataCallback(service, continuation)
if (!service.requestPreviewComplicationData(
complicationDataSourceComponent,
complicationType.toWireComplicationType(),
- object : IPreviewComplicationDataCallback.Stub() {
- override fun updateComplicationData(
- data: android.support.wearable.complications.ComplicationData?
- ) {
- safeUnlinkToDeath(service, deathObserver)
- continuation.resume(data?.toApiComplicationData())
- }
- }
+ callback
)
) {
- safeUnlinkToDeath(service, deathObserver)
+ callback.safeUnlinkToDeath()
continuation.resume(null)
}
}
}
- internal fun safeUnlinkToDeath(
- service: IProviderInfoService,
- deathObserver: IBinder.DeathRecipient
- ) {
- try {
- service.asBinder().unlinkToDeath(deathObserver, 0)
- } catch (e: NoSuchElementException) {
- // This really shouldn't happen.
- Log.w(TAG, "retrievePreviewComplicationData encountered", e)
+ private class PreviewComplicationDataCallback(
+ val service: IProviderInfoService,
+ var continuation: CancellableContinuation<ComplicationData?>?
+ ) : IPreviewComplicationDataCallback.Stub() {
+ val deathObserver: IBinder.DeathRecipient = IBinder.DeathRecipient {
+ continuation?.resumeWithException(ServiceDisconnectedException())
+ }
+
+ init {
+ service.asBinder().linkToDeath(deathObserver, 0)
+
+ // Not a huge deal but we might as well unlink the deathObserver.
+ continuation?.invokeOnCancellation {
+ safeUnlinkToDeath()
+ }
+ }
+
+ override fun updateComplicationData(
+ data: android.support.wearable.complications.ComplicationData?
+ ) {
+ safeUnlinkToDeath()
+ continuation!!.resume(data?.toApiComplicationData())
+
+ // Re http://b/249121838 this is important, it prevents a memory leak.
+ continuation = null
+ }
+
+ internal fun safeUnlinkToDeath() {
+ try {
+ service.asBinder().unlinkToDeath(deathObserver, 0)
+ } catch (e: NoSuchElementException) {
+ // This really shouldn't happen.
+ Log.w(TAG, "retrievePreviewComplicationData encountered", e)
+ }
}
}
diff --git a/wear/watchface/watchface-editor/build.gradle b/wear/watchface/watchface-editor/build.gradle
index eb5993b..fcd5e21 100644
--- a/wear/watchface/watchface-editor/build.gradle
+++ b/wear/watchface/watchface-editor/build.gradle
@@ -33,6 +33,7 @@
api(libs.kotlinCoroutinesAndroid)
implementation("androidx.core:core:1.1.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
implementation("androidx.lifecycle:lifecycle-common-java8:2.3.1")
androidTestImplementation(project(":wear:watchface:watchface-complications-rendering"))
diff --git a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
index cc749b9..ae4c81f 100644
--- a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
+++ b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
@@ -120,12 +120,15 @@
import org.mockito.Mockito.`when`
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
import java.lang.IllegalArgumentException
import java.time.Instant
import java.time.ZonedDateTime
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.MILLISECONDS
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
public const val LEFT_COMPLICATION_ID: Int = 1000
public const val RIGHT_COMPLICATION_ID: Int = 1001
@@ -2048,6 +2051,46 @@
EditorService.globalEditorService.unregisterObserver(observerId)
}
+ @Test
+ @Suppress("Deprecation") // userStyleSettings
+ public fun observedDeathForceClosesEditorSession() {
+ val scenario = createOnWatchFaceEditingTestActivity(
+ listOf(colorStyleSetting, watchHandStyleSetting),
+ listOf(leftComplication, rightComplication)
+ )
+
+ val editorObserver = Mockito.mock(IEditorObserver::class.java)
+ val mockBinder = Mockito.mock(IBinder::class.java)
+ `when`(editorObserver.asBinder()).thenReturn(mockBinder)
+
+ val observerId = EditorService.globalEditorService.registerObserver(editorObserver)
+
+ val deathRecipientCaptor = ArgumentCaptor.forClass(IBinder.DeathRecipient::class.java)
+ verify(mockBinder).linkToDeath(deathRecipientCaptor.capture(), anyInt())
+
+ scenario.onActivity { activity ->
+ // Select [blueStyleOption] and [gothicStyleOption].
+ val mutableUserStyle = activity.editorSession.userStyle.value.toMutableUserStyle()
+ for (userStyleSetting in activity.editorSession.userStyleSchema.userStyleSettings) {
+ mutableUserStyle[userStyleSetting] = userStyleSetting.options.last()
+ }
+ activity.editorSession.userStyle.value = mutableUserStyle.toUserStyle()
+ }
+
+ // Pretend the binder died, this should force close the editor session.
+ deathRecipientCaptor.value.binderDied()
+
+ assertTrue(onDestroyLatch.await(5L, TimeUnit.SECONDS))
+
+ // The style change should not have been applied to the watchface.
+ assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
+ .isEqualTo(redStyleOption.id.value)
+ assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
+ .isEqualTo(classicStyleOption.id.value)
+
+ EditorService.globalEditorService.unregisterObserver(observerId)
+ }
+
@SuppressLint("NewApi") // EditorRequest
@Test
public fun closeEditorSessionBeforeInitCompleted() {
diff --git a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
index cde44f8..da41032 100644
--- a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
+++ b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
@@ -37,6 +37,7 @@
import androidx.annotation.UiThread
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
import androidx.wear.watchface.complications.ComplicationDataSourceInfo
import androidx.wear.watchface.complications.ComplicationDataSourceInfoRetriever
import androidx.wear.watchface.complications.data.ComplicationData
@@ -66,7 +67,6 @@
import androidx.wear.watchface.style.UserStyleSchema
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.android.asCoroutineDispatcher
@@ -289,7 +289,6 @@
"EditorSession.createOnWatchEditorSessionAsyncImpl"
).use {
try {
- val coroutineScope = CoroutineScope(Dispatchers.Main.immediate)
val editorRequest = editIntent.getParcelableExtra<ComponentName>(
Constants.EXTRA_WATCH_FACE_COMPONENT
)?.let {
@@ -307,13 +306,13 @@
editorRequest.watchFaceId,
editorRequest.initialUserStyle,
complicationDataSourceInfoRetrieverProvider,
- coroutineScope,
+ activity.lifecycleScope,
editorRequest.previewScreenshotParams
)
// But full initialization has to be deferred because
// [WatchFace.getOrCreateEditorDelegate] is async.
// Resolve only after init has been completed.
- withContext(coroutineScope.coroutineContext) {
+ withContext(activity.lifecycleScope.coroutineContext) {
withTimeout(EDITING_SESSION_TIMEOUT.toMillis()) {
session.setEditorDelegate(
// Either create a delegate for a new headless client or await an
@@ -744,7 +743,6 @@
releaseResources()
closed = true
editorSessionTraceEvent.close()
- coroutineScope.cancel()
activity = null
complicationDataSourceInfoRetrieverProvider = null
chooseComplicationDataSource = null
@@ -760,7 +758,6 @@
releaseResources()
EditorService.globalEditorService.removeCloseCallback(closeCallback)
editorSessionTraceEvent.close()
- coroutineScope.cancel()
activity?.finish()
activity = null
complicationDataSourceInfoRetrieverProvider = null
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/editor/EditorService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/editor/EditorService.kt
index 0facd80..d613398 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/editor/EditorService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/editor/EditorService.kt
@@ -48,7 +48,14 @@
synchronized(lock) {
val id = nextId++
observers[id] = observer
- val deathObserver = IBinder.DeathRecipient { unregisterObserver(id) }
+ val deathObserver = IBinder.DeathRecipient {
+ Log.w(TAG, "observer died, closing editor")
+ // If SysUI dies we should close the editor too, otherwise the watchface could get
+ // left in an inconsistent state where it has local edits that were not persisted by
+ // the system.
+ closeEditor()
+ unregisterObserver(id)
+ }
observer.asBinder().linkToDeath(deathObserver, 0)
deathObservers[id] = deathObserver
return id
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java
index 8d66c85..807d6aa 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java
@@ -132,14 +132,6 @@
/**
* Adapter method for
- * {@link androidx.webkit.WebSettingsCompat#setRequestedWithHeaderMode(android.webkit.WebSettings, int)}
- */
- public void setRequestedWithHeaderMode(int requestedWithHeaderMode) {
- mBoundaryInterface.setRequestedWithHeaderMode(requestedWithHeaderMode);
- }
-
- /**
- * Adapter method for
* {@link androidx.webkit.WebSettingsCompat#setEnterpriseAuthenticationAppLinkPolicyEnabled}.
*/
public void setEnterpriseAuthenticationAppLinkPolicyEnabled(boolean enabled) {
@@ -153,12 +145,4 @@
public boolean getEnterpriseAuthenticationAppLinkPolicyEnabled() {
return mBoundaryInterface.getEnterpriseAuthenticationAppLinkPolicyEnabled();
}
-
- /**
- * Adapter method for
- * {@link androidx.webkit.WebSettingsCompat#getRequestedWithHeaderMode(android.webkit.WebSettings)}
- */
- public int getRequestedWithHeaderMode() {
- return mBoundaryInterface.getRequestedWithHeaderMode();
- }
}