Merge "Use NoActionBar theme for default test activity" into androidx-main
diff --git a/appcompat/appcompat-benchmark/build.gradle b/appcompat/appcompat-benchmark/build.gradle
index 601c7ad..f1d3c0e 100644
--- a/appcompat/appcompat-benchmark/build.gradle
+++ b/appcompat/appcompat-benchmark/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import androidx.build.LibraryType
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -34,3 +36,7 @@
android {
namespace = "androidx.appcompat.benchmark"
}
+
+androidx {
+ type = LibraryType.BENCHMARK
+}
diff --git a/appsearch/appsearch-builtin-types/api/1.1.0-beta01.txt b/appsearch/appsearch-builtin-types/api/1.1.0-beta01.txt
index afd7275..911bcf5 100644
--- a/appsearch/appsearch-builtin-types/api/1.1.0-beta01.txt
+++ b/appsearch/appsearch-builtin-types/api/1.1.0-beta01.txt
@@ -164,7 +164,7 @@
method public android.net.Uri? getIconUri();
method public String getPackageName();
method public byte[] getSha256Certificate();
- method public long getUpdatedTimestamp();
+ method public long getUpdatedTimestampMillis();
}
public static final class MobileApplication.Builder {
@@ -186,7 +186,7 @@
method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setImage(String?);
method public androidx.appsearch.builtintypes.MobileApplication.Builder setName(String?);
method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setPotentialActions(java.util.List<androidx.appsearch.builtintypes.PotentialAction!>?);
- method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestamp(long);
+ method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestampMillis(long);
method public androidx.appsearch.builtintypes.MobileApplication.Builder setUrl(String?);
}
diff --git a/appsearch/appsearch-builtin-types/api/current.txt b/appsearch/appsearch-builtin-types/api/current.txt
index afd7275..911bcf5 100644
--- a/appsearch/appsearch-builtin-types/api/current.txt
+++ b/appsearch/appsearch-builtin-types/api/current.txt
@@ -164,7 +164,7 @@
method public android.net.Uri? getIconUri();
method public String getPackageName();
method public byte[] getSha256Certificate();
- method public long getUpdatedTimestamp();
+ method public long getUpdatedTimestampMillis();
}
public static final class MobileApplication.Builder {
@@ -186,7 +186,7 @@
method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setImage(String?);
method public androidx.appsearch.builtintypes.MobileApplication.Builder setName(String?);
method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setPotentialActions(java.util.List<androidx.appsearch.builtintypes.PotentialAction!>?);
- method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestamp(long);
+ method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestampMillis(long);
method public androidx.appsearch.builtintypes.MobileApplication.Builder setUrl(String?);
}
diff --git a/appsearch/appsearch-builtin-types/api/restricted_1.1.0-beta01.txt b/appsearch/appsearch-builtin-types/api/restricted_1.1.0-beta01.txt
index a929d58..c61ebfd 100644
--- a/appsearch/appsearch-builtin-types/api/restricted_1.1.0-beta01.txt
+++ b/appsearch/appsearch-builtin-types/api/restricted_1.1.0-beta01.txt
@@ -166,7 +166,7 @@
method public android.net.Uri? getIconUri();
method public String getPackageName();
method public byte[] getSha256Certificate();
- method public long getUpdatedTimestamp();
+ method public long getUpdatedTimestampMillis();
}
public static final class MobileApplication.Builder {
@@ -188,7 +188,7 @@
method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setImage(String?);
method public androidx.appsearch.builtintypes.MobileApplication.Builder setName(String?);
method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setPotentialActions(java.util.List<androidx.appsearch.builtintypes.PotentialAction!>?);
- method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestamp(long);
+ method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestampMillis(long);
method public androidx.appsearch.builtintypes.MobileApplication.Builder setUrl(String?);
}
diff --git a/appsearch/appsearch-builtin-types/api/restricted_current.txt b/appsearch/appsearch-builtin-types/api/restricted_current.txt
index a929d58..c61ebfd 100644
--- a/appsearch/appsearch-builtin-types/api/restricted_current.txt
+++ b/appsearch/appsearch-builtin-types/api/restricted_current.txt
@@ -166,7 +166,7 @@
method public android.net.Uri? getIconUri();
method public String getPackageName();
method public byte[] getSha256Certificate();
- method public long getUpdatedTimestamp();
+ method public long getUpdatedTimestampMillis();
}
public static final class MobileApplication.Builder {
@@ -188,7 +188,7 @@
method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setImage(String?);
method public androidx.appsearch.builtintypes.MobileApplication.Builder setName(String?);
method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setPotentialActions(java.util.List<androidx.appsearch.builtintypes.PotentialAction!>?);
- method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestamp(long);
+ method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestampMillis(long);
method public androidx.appsearch.builtintypes.MobileApplication.Builder setUrl(String?);
}
diff --git a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/MobileApplicationTest.java b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/MobileApplicationTest.java
index fc262f2..9195d39 100644
--- a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/MobileApplicationTest.java
+++ b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/MobileApplicationTest.java
@@ -51,7 +51,7 @@
builder.setDisplayName("display name");
builder.setAlternateNames(Arrays.asList("alternate name 1", "alternate name 2"));
builder.setIconUri(Uri.parse("android.resource://com.example.app/drawable/12345"));
- builder.setUpdatedTimestamp(1234567890L);
+ builder.setUpdatedTimestampMillis(1234567890L);
builder.setClassName("com.example.app.MainActivity");
MobileApplication mobileApplication = builder.build();
@@ -63,7 +63,7 @@
.containsExactly("alternate name 1", "alternate name 2");
assertThat(mobileApplication.getIconUri())
.isEqualTo(Uri.parse("android.resource://com.example.app/drawable/12345"));
- assertThat(mobileApplication.getUpdatedTimestamp()).isEqualTo(1234567890L);
+ assertThat(mobileApplication.getUpdatedTimestampMillis()).isEqualTo(1234567890L);
assertThat(mobileApplication.getClassName()).isEqualTo("com.example.app.MainActivity");
}
@@ -83,7 +83,7 @@
.setDisplayName(name)
.setAlternateNames(alternateNames)
.setIconUri(iconUri)
- .setUpdatedTimestamp(updatedTimestamp)
+ .setUpdatedTimestampMillis(updatedTimestamp)
.setClassName(className)
.build();
@@ -92,7 +92,7 @@
assertThat(mobileApplication.getAlternateNames()).isEqualTo(alternateNames);
assertThat(mobileApplication.getIconUri()).isEqualTo(iconUri);
assertThat(mobileApplication.getSha256Certificate()).isEqualTo(sha256Certificate);
- assertThat(mobileApplication.getUpdatedTimestamp()).isEqualTo(updatedTimestamp);
+ assertThat(mobileApplication.getUpdatedTimestampMillis()).isEqualTo(updatedTimestamp);
assertThat(mobileApplication.getClassName()).isEqualTo(className);
}
@@ -103,7 +103,7 @@
builder.setDisplayName("display name");
builder.setAlternateNames(Arrays.asList("alternate name 1", "alternate name 2"));
builder.setIconUri(Uri.parse("android.resource://com.example.app/drawable/12345"));
- builder.setUpdatedTimestamp(1234567890L);
+ builder.setUpdatedTimestampMillis(1234567890L);
builder.setClassName("com.example.app.MainActivity");
MobileApplication mobileApplication = builder.build();
@@ -111,7 +111,7 @@
.setDisplayName("new display name")
.setAlternateNames(Arrays.asList("new alternate name 1", "new alternate name 2"))
.setIconUri(Uri.parse("android.resource://com.example.app/drawable/98765"))
- .setUpdatedTimestamp(9876543210L)
+ .setUpdatedTimestampMillis(9876543210L)
.setClassName("com.example.app.NewMainActivity");
// assert the original hasn't changed
@@ -119,7 +119,7 @@
.containsExactly("alternate name 1", "alternate name 2");
assertThat(mobileApplication.getIconUri())
.isEqualTo(Uri.parse("android.resource://com.example.app/drawable/12345"));
- assertThat(mobileApplication.getUpdatedTimestamp()).isEqualTo(1234567890L);
+ assertThat(mobileApplication.getUpdatedTimestampMillis()).isEqualTo(1234567890L);
assertThat(mobileApplication.getClassName()).isEqualTo("com.example.app.MainActivity");
}
@@ -140,7 +140,7 @@
.setDisplayName(name)
.setAlternateNames(alternateNames)
.setIconUri(iconUri)
- .setUpdatedTimestamp(updatedTimestamp)
+ .setUpdatedTimestampMillis(updatedTimestamp)
.setClassName(className)
.build();
@@ -217,7 +217,7 @@
assertThat(mobileApplication.getDisplayName()).isEqualTo("display name");
assertThat(mobileApplication.getIconUri().toString())
.isEqualTo("android.resource://com.example.app/drawable/12345");
- assertThat(mobileApplication.getUpdatedTimestamp()).isEqualTo(1234567890L);
+ assertThat(mobileApplication.getUpdatedTimestampMillis()).isEqualTo(1234567890L);
assertThat(mobileApplication.getClassName())
.isEqualTo("com.example.app.MainActivity");
} finally {
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/MobileApplication.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/MobileApplication.java
index c7cc01a..625dc0b 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/MobileApplication.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/MobileApplication.java
@@ -57,8 +57,10 @@
@Document.BytesProperty private final byte[] mSha256Certificate;
- @Document.LongProperty(indexingType = LongPropertyConfig.INDEXING_TYPE_RANGE)
- private final long mUpdatedTimestamp;
+ // Property name set to update to match framework
+ @Document.LongProperty(name = "updatedTimestamp",
+ indexingType = LongPropertyConfig.INDEXING_TYPE_RANGE)
+ private final long mUpdatedTimestampMillis;
@Document.StringProperty private final String mClassName;
@@ -79,7 +81,7 @@
@Nullable String displayName,
@Nullable Uri iconUri,
byte @NonNull [] sha256Certificate,
- long updatedTimestamp,
+ long updatedTimestampMillis,
@Nullable String className) {
super(namespace, id, documentScore, creationTimestampMillis, documentTtlMillis, name,
alternateNames, description, image, url, potentialActions);
@@ -88,7 +90,7 @@
mAlternateNames = Preconditions.checkNotNull(alternateNames);
mIconUri = iconUri;
mSha256Certificate = Preconditions.checkNotNull(sha256Certificate);
- mUpdatedTimestamp = updatedTimestamp;
+ mUpdatedTimestampMillis = updatedTimestampMillis;
mClassName = className;
}
@@ -133,8 +135,8 @@
/** Returns the last time the app was installed or updated on the device. */
@CurrentTimeMillisLong
- public long getUpdatedTimestamp() {
- return mUpdatedTimestamp;
+ public long getUpdatedTimestampMillis() {
+ return mUpdatedTimestampMillis;
}
/**
@@ -185,7 +187,7 @@
private String mDisplayName;
private Uri mIconUri;
private final byte[] mSha256Certificate;
- private long mUpdatedTimestamp;
+ private long mUpdatedTimestampMillis;
private String mClassName;
private boolean mBuilt = false;
@@ -203,7 +205,7 @@
mDisplayName = mobileApplication.mDisplayName;
mIconUri = mobileApplication.mIconUri;
mSha256Certificate = mobileApplication.mSha256Certificate;
- mUpdatedTimestamp = mobileApplication.mUpdatedTimestamp;
+ mUpdatedTimestampMillis = mobileApplication.mUpdatedTimestampMillis;
mClassName = mobileApplication.mClassName;
}
@@ -222,9 +224,10 @@
}
/** Sets the last time the app was installed or updated on the device. */
- public @NonNull T setUpdatedTimestamp(@CurrentTimeMillisLong long updatedTimestamp) {
+ public @NonNull T setUpdatedTimestampMillis(
+ @CurrentTimeMillisLong long updatedTimestampMillis) {
resetIfBuilt();
- mUpdatedTimestamp = updatedTimestamp;
+ mUpdatedTimestampMillis = updatedTimestampMillis;
return (T) this;
}
@@ -265,7 +268,7 @@
mDisplayName,
mIconUri,
mSha256Certificate,
- mUpdatedTimestamp,
+ mUpdatedTimestampMillis,
mClassName);
}
}
diff --git a/benchmark/benchmark-darwin-core/build.gradle b/benchmark/benchmark-darwin-core/build.gradle
index b9c1d95..d54e610 100644
--- a/benchmark/benchmark-darwin-core/build.gradle
+++ b/benchmark/benchmark-darwin-core/build.gradle
@@ -6,7 +6,7 @@
* modifying its settings.
*/
import androidx.build.PlatformIdentifier
-import androidx.build.Publish
+import androidx.build.LibraryType
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.mpp.BitcodeEmbeddingMode
@@ -54,7 +54,7 @@
androidx {
name = "Benchmarks - Darwin Core"
+ type = LibraryType.SNAPSHOT_ONLY_LIBRARY
inceptionYear = "2022"
description = "AndroidX Benchmarks - Darwin Core"
- publish = Publish.SNAPSHOT_ONLY
}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/build.gradle b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
index 1566464..627198b 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/build.gradle
+++ b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
@@ -50,7 +50,7 @@
androidx {
name = "Benchmarks - Darwin Gradle Plugin"
- type = LibraryType.GRADLE_PLUGIN
+ type = LibraryType.INTERNAL_GRADLE_PLUGIN
inceptionYear = "2022"
description = "AndroidX Benchmarks - Darwin Gradle Plugin"
}
diff --git a/benchmark/benchmark-darwin/build.gradle b/benchmark/benchmark-darwin/build.gradle
index 9f57fa2..67a7e43 100644
--- a/benchmark/benchmark-darwin/build.gradle
+++ b/benchmark/benchmark-darwin/build.gradle
@@ -6,7 +6,7 @@
* modifying its settings.
*/
import androidx.build.PlatformIdentifier
-import androidx.build.Publish
+import androidx.build.LibraryType
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.mpp.BitcodeEmbeddingMode
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkConfig
@@ -87,5 +87,5 @@
name = "Benchmarks - Darwin"
inceptionYear = "2022"
description = "AndroidX Benchmarks - Darwin"
- publish = Publish.SNAPSHOT_ONLY
+ type = LibraryType.SNAPSHOT_ONLY_LIBRARY
}
diff --git a/benchmark/benchmark/build.gradle b/benchmark/benchmark/build.gradle
index d3c0e4e..6857553 100644
--- a/benchmark/benchmark/build.gradle
+++ b/benchmark/benchmark/build.gradle
@@ -56,5 +56,5 @@
}
androidx {
- type = LibraryType.INTERNAL_TEST_LIBRARY
+ type = LibraryType.BENCHMARK
}
diff --git a/benchmark/integration-tests/dry-run-benchmark/build.gradle b/benchmark/integration-tests/dry-run-benchmark/build.gradle
index 38997e5..d910c48 100644
--- a/benchmark/integration-tests/dry-run-benchmark/build.gradle
+++ b/benchmark/integration-tests/dry-run-benchmark/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import androidx.build.LibraryType
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -32,3 +34,7 @@
android {
namespace = "androidx.benchmark.integration.dryrun.benchmark"
}
+
+androidx {
+ type = LibraryType.BENCHMARK
+}
\ No newline at end of file
diff --git a/benchmark/integration-tests/startup-benchmark/build.gradle b/benchmark/integration-tests/startup-benchmark/build.gradle
index f008e06..dca2751 100644
--- a/benchmark/integration-tests/startup-benchmark/build.gradle
+++ b/benchmark/integration-tests/startup-benchmark/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import androidx.build.LibraryType
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -32,3 +34,7 @@
android {
namespace = "androidx.benchmark.integration.startup.benchmark"
}
+
+androidx {
+ type = LibraryType.BENCHMARK
+}
diff --git a/bluetooth/integration-tests/testapp/build.gradle b/bluetooth/integration-tests/testapp/build.gradle
index 1344503..843b63a 100644
--- a/bluetooth/integration-tests/testapp/build.gradle
+++ b/bluetooth/integration-tests/testapp/build.gradle
@@ -5,7 +5,6 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
/*
* Copyright 2022 The Android Open Source Project
diff --git a/browser/browser/api/aidlRelease/current/android/support/customtabs/IAuthTabCallback.aidl b/browser/browser/api/aidlRelease/current/android/support/customtabs/IAuthTabCallback.aidl
new file mode 100644
index 0000000..d9236cd
--- /dev/null
+++ b/browser/browser/api/aidlRelease/current/android/support/customtabs/IAuthTabCallback.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.support.customtabs;
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+interface IAuthTabCallback {
+ oneway void onNavigationEvent(int navigationEvent, in android.os.Bundle extras) = 1;
+ oneway void onExtraCallback(String callbackName, in android.os.Bundle args) = 2;
+ android.os.Bundle onExtraCallbackWithResult(String callbackName, in android.os.Bundle args) = 3;
+ oneway void onWarmupCompleted(in android.os.Bundle extras) = 4;
+}
diff --git a/browser/browser/api/aidlRelease/current/android/support/customtabs/ICustomTabsService.aidl b/browser/browser/api/aidlRelease/current/android/support/customtabs/ICustomTabsService.aidl
index 668a8a6..7662a12 100644
--- a/browser/browser/api/aidlRelease/current/android/support/customtabs/ICustomTabsService.aidl
+++ b/browser/browser/api/aidlRelease/current/android/support/customtabs/ICustomTabsService.aidl
@@ -50,4 +50,5 @@
boolean isEngagementSignalsApiAvailable(in android.support.customtabs.ICustomTabsCallback customTabsCallback, in android.os.Bundle extras) = 12;
boolean setEngagementSignalsCallback(in android.support.customtabs.ICustomTabsCallback customTabsCallback, in IBinder callback, in android.os.Bundle extras) = 13;
boolean isEphemeralBrowsingSupported(in android.os.Bundle extras) = 16;
+ boolean newAuthTabSession(in android.support.customtabs.IAuthTabCallback callback, in android.os.Bundle extras) = 17;
}
diff --git a/browser/browser/api/api_lint.ignore b/browser/browser/api/api_lint.ignore
index 4ce666c..5882aac 100644
--- a/browser/browser/api/api_lint.ignore
+++ b/browser/browser/api/api_lint.ignore
@@ -99,6 +99,12 @@
Invalid nullability on parameter `name` in method `onServiceDisconnected`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
+ListenerLast: androidx.browser.customtabs.CustomTabsClient#newAuthTabSession(androidx.browser.auth.AuthTabCallback, java.util.concurrent.Executor) parameter #1:
+ Listeners should always be at end of argument list (method `newAuthTabSession`)
+ListenerLast: androidx.browser.customtabs.CustomTabsClient#newAuthTabSession(androidx.browser.auth.AuthTabCallback, java.util.concurrent.Executor, int) parameter #1:
+ Listeners should always be at end of argument list (method `newAuthTabSession`)
+ListenerLast: androidx.browser.customtabs.CustomTabsClient#newAuthTabSession(androidx.browser.auth.AuthTabCallback, java.util.concurrent.Executor, int) parameter #2:
+ Listeners should always be at end of argument list (method `newAuthTabSession`)
ListenerLast: androidx.browser.customtabs.CustomTabsClient#newPendingSession(android.content.Context, androidx.browser.customtabs.CustomTabsCallback, int) parameter #2:
Listeners should always be at end of argument list (method `newPendingSession`)
ListenerLast: androidx.browser.customtabs.CustomTabsClient#newSession(androidx.browser.customtabs.CustomTabsCallback, int) parameter #1:
diff --git a/browser/browser/api/current.txt b/browser/browser/api/current.txt
index f31423d..f229789 100644
--- a/browser/browser/api/current.txt
+++ b/browser/browser/api/current.txt
@@ -1,6 +1,13 @@
// Signature format: 4.0
package androidx.browser.auth {
+ @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public interface AuthTabCallback {
+ method public void onExtraCallback(String, android.os.Bundle);
+ method public android.os.Bundle onExtraCallbackWithResult(String, android.os.Bundle);
+ method public void onNavigationEvent(int, android.os.Bundle);
+ method public void onWarmupCompleted(android.os.Bundle);
+ }
+
public final class AuthTabColorSchemeParams {
method @ColorInt public Integer? getNavigationBarColor();
method @ColorInt public Integer? getNavigationBarDividerColor();
@@ -17,6 +24,8 @@
@SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public class AuthTabIntent {
method public static androidx.browser.auth.AuthTabColorSchemeParams getColorSchemeParams(android.content.Intent, @IntRange(from=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT, to=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK) int);
+ method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.auth.AuthTabSession.PendingSession? getPendingSession();
+ method public androidx.browser.auth.AuthTabSession? getSession();
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalEphemeralBrowsing public boolean isEphemeralBrowsingEnabled();
method public void launch(androidx.activity.result.ActivityResultLauncher<android.content.Intent!>, android.net.Uri, String);
method public void launch(androidx.activity.result.ActivityResultLauncher<android.content.Intent!>, android.net.Uri, String, String);
@@ -45,6 +54,22 @@
method public androidx.browser.auth.AuthTabIntent.Builder setColorSchemeParams(@IntRange(from=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT, to=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK) int, androidx.browser.auth.AuthTabColorSchemeParams);
method public androidx.browser.auth.AuthTabIntent.Builder setDefaultColorSchemeParams(androidx.browser.auth.AuthTabColorSchemeParams);
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalEphemeralBrowsing public androidx.browser.auth.AuthTabIntent.Builder setEphemeralBrowsingEnabled(boolean);
+ method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.auth.AuthTabIntent.Builder setPendingSession(androidx.browser.auth.AuthTabSession.PendingSession);
+ method public androidx.browser.auth.AuthTabIntent.Builder setSession(androidx.browser.auth.AuthTabSession);
+ }
+
+ @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public final class AuthTabSession {
+ }
+
+ @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public static class AuthTabSession.PendingSession {
+ }
+
+ @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public class AuthTabSessionToken {
+ method public androidx.browser.auth.AuthTabCallback? getCallback();
+ method public android.app.PendingIntent? getId();
+ method public static androidx.browser.auth.AuthTabSessionToken? getSessionTokenFromIntent(android.content.Intent);
+ method public boolean hasId();
+ method public boolean isAssociatedWith(androidx.browser.auth.AuthTabSession);
}
@SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAuthTab {
@@ -150,6 +175,7 @@
}
public class CustomTabsClient {
+ method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.auth.AuthTabSession? attachAuthTabSession(androidx.browser.auth.AuthTabSession.PendingSession);
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.customtabs.CustomTabsSession? attachSession(androidx.browser.customtabs.CustomTabsSession.PendingSession);
method public static boolean bindCustomTabsService(android.content.Context, String?, androidx.browser.customtabs.CustomTabsServiceConnection);
method public static boolean bindCustomTabsServicePreservePriority(android.content.Context, String?, androidx.browser.customtabs.CustomTabsServiceConnection);
@@ -158,6 +184,9 @@
method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?);
method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?, boolean);
method public static boolean isSetNetworkSupported(android.content.Context, String);
+ method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public androidx.browser.auth.AuthTabSession? newAuthTabSession(androidx.browser.auth.AuthTabCallback?, java.util.concurrent.Executor?);
+ method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public androidx.browser.auth.AuthTabSession? newAuthTabSession(androidx.browser.auth.AuthTabCallback?, java.util.concurrent.Executor?, int);
+ method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab @androidx.browser.customtabs.ExperimentalPendingSession public static androidx.browser.auth.AuthTabSession.PendingSession newPendingAuthTabSession(android.content.Context, int, java.util.concurrent.Executor?, androidx.browser.auth.AuthTabCallback?);
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public static androidx.browser.customtabs.CustomTabsSession.PendingSession newPendingSession(android.content.Context, androidx.browser.customtabs.CustomTabsCallback?, int);
method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?);
method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?, int);
@@ -313,11 +342,13 @@
public abstract class CustomTabsService extends android.app.Service {
ctor public CustomTabsService();
+ method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab protected boolean cleanUpSession(androidx.browser.auth.AuthTabSessionToken);
method protected boolean cleanUpSession(androidx.browser.customtabs.CustomTabsSessionToken);
method protected abstract android.os.Bundle? extraCommand(String, android.os.Bundle?);
method protected boolean isEngagementSignalsApiAvailable(androidx.browser.customtabs.CustomTabsSessionToken, android.os.Bundle);
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalEphemeralBrowsing protected boolean isEphemeralBrowsingSupported(android.os.Bundle);
method protected abstract boolean mayLaunchUrl(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri?, android.os.Bundle?, java.util.List<android.os.Bundle!>?);
+ method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab protected boolean newAuthTabSession(androidx.browser.auth.AuthTabSessionToken);
method protected abstract boolean newSession(androidx.browser.customtabs.CustomTabsSessionToken);
method public android.os.IBinder onBind(android.content.Intent?);
method @androidx.browser.customtabs.CustomTabsService.Result protected abstract int postMessage(androidx.browser.customtabs.CustomTabsSessionToken, String, android.os.Bundle?);
diff --git a/browser/browser/api/restricted_current.txt b/browser/browser/api/restricted_current.txt
index ecc3b90..a0ad85a 100644
--- a/browser/browser/api/restricted_current.txt
+++ b/browser/browser/api/restricted_current.txt
@@ -1,6 +1,13 @@
// Signature format: 4.0
package androidx.browser.auth {
+ @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public interface AuthTabCallback {
+ method public void onExtraCallback(String, android.os.Bundle);
+ method public android.os.Bundle onExtraCallbackWithResult(String, android.os.Bundle);
+ method public void onNavigationEvent(int, android.os.Bundle);
+ method public void onWarmupCompleted(android.os.Bundle);
+ }
+
public final class AuthTabColorSchemeParams {
method @ColorInt public Integer? getNavigationBarColor();
method @ColorInt public Integer? getNavigationBarDividerColor();
@@ -17,6 +24,8 @@
@SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public class AuthTabIntent {
method public static androidx.browser.auth.AuthTabColorSchemeParams getColorSchemeParams(android.content.Intent, @IntRange(from=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT, to=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK) int);
+ method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.auth.AuthTabSession.PendingSession? getPendingSession();
+ method public androidx.browser.auth.AuthTabSession? getSession();
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalEphemeralBrowsing public boolean isEphemeralBrowsingEnabled();
method public void launch(androidx.activity.result.ActivityResultLauncher<android.content.Intent!>, android.net.Uri, String);
method public void launch(androidx.activity.result.ActivityResultLauncher<android.content.Intent!>, android.net.Uri, String, String);
@@ -45,6 +54,22 @@
method public androidx.browser.auth.AuthTabIntent.Builder setColorSchemeParams(@IntRange(from=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT, to=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK) int, androidx.browser.auth.AuthTabColorSchemeParams);
method public androidx.browser.auth.AuthTabIntent.Builder setDefaultColorSchemeParams(androidx.browser.auth.AuthTabColorSchemeParams);
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalEphemeralBrowsing public androidx.browser.auth.AuthTabIntent.Builder setEphemeralBrowsingEnabled(boolean);
+ method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.auth.AuthTabIntent.Builder setPendingSession(androidx.browser.auth.AuthTabSession.PendingSession);
+ method public androidx.browser.auth.AuthTabIntent.Builder setSession(androidx.browser.auth.AuthTabSession);
+ }
+
+ @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public final class AuthTabSession {
+ }
+
+ @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public static class AuthTabSession.PendingSession {
+ }
+
+ @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public class AuthTabSessionToken {
+ method public androidx.browser.auth.AuthTabCallback? getCallback();
+ method public android.app.PendingIntent? getId();
+ method public static androidx.browser.auth.AuthTabSessionToken? getSessionTokenFromIntent(android.content.Intent);
+ method public boolean hasId();
+ method public boolean isAssociatedWith(androidx.browser.auth.AuthTabSession);
}
@SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAuthTab {
@@ -161,6 +186,7 @@
}
public class CustomTabsClient {
+ method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.auth.AuthTabSession? attachAuthTabSession(androidx.browser.auth.AuthTabSession.PendingSession);
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.customtabs.CustomTabsSession? attachSession(androidx.browser.customtabs.CustomTabsSession.PendingSession);
method public static boolean bindCustomTabsService(android.content.Context, String?, androidx.browser.customtabs.CustomTabsServiceConnection);
method public static boolean bindCustomTabsServicePreservePriority(android.content.Context, String?, androidx.browser.customtabs.CustomTabsServiceConnection);
@@ -169,6 +195,9 @@
method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?);
method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?, boolean);
method public static boolean isSetNetworkSupported(android.content.Context, String);
+ method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public androidx.browser.auth.AuthTabSession? newAuthTabSession(androidx.browser.auth.AuthTabCallback?, java.util.concurrent.Executor?);
+ method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public androidx.browser.auth.AuthTabSession? newAuthTabSession(androidx.browser.auth.AuthTabCallback?, java.util.concurrent.Executor?, int);
+ method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab @androidx.browser.customtabs.ExperimentalPendingSession public static androidx.browser.auth.AuthTabSession.PendingSession newPendingAuthTabSession(android.content.Context, int, java.util.concurrent.Executor?, androidx.browser.auth.AuthTabCallback?);
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public static androidx.browser.customtabs.CustomTabsSession.PendingSession newPendingSession(android.content.Context, androidx.browser.customtabs.CustomTabsCallback?, int);
method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?);
method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?, int);
@@ -324,11 +353,13 @@
public abstract class CustomTabsService extends android.app.Service {
ctor public CustomTabsService();
+ method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab protected boolean cleanUpSession(androidx.browser.auth.AuthTabSessionToken);
method protected boolean cleanUpSession(androidx.browser.customtabs.CustomTabsSessionToken);
method protected abstract android.os.Bundle? extraCommand(String, android.os.Bundle?);
method protected boolean isEngagementSignalsApiAvailable(androidx.browser.customtabs.CustomTabsSessionToken, android.os.Bundle);
method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalEphemeralBrowsing protected boolean isEphemeralBrowsingSupported(android.os.Bundle);
method protected abstract boolean mayLaunchUrl(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri?, android.os.Bundle?, java.util.List<android.os.Bundle!>?);
+ method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab protected boolean newAuthTabSession(androidx.browser.auth.AuthTabSessionToken);
method protected abstract boolean newSession(androidx.browser.customtabs.CustomTabsSessionToken);
method public android.os.IBinder onBind(android.content.Intent?);
method @androidx.browser.customtabs.CustomTabsService.Result protected abstract int postMessage(androidx.browser.customtabs.CustomTabsSessionToken, String, android.os.Bundle?);
diff --git a/browser/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsService.java b/browser/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsService.java
index 9b0aa7e..3831801 100644
--- a/browser/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsService.java
+++ b/browser/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsService.java
@@ -24,6 +24,7 @@
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
+import android.support.customtabs.IAuthTabCallback;
import android.support.customtabs.ICustomTabsCallback;
import android.support.customtabs.ICustomTabsService;
@@ -151,6 +152,12 @@
public boolean isEphemeralBrowsingSupported(Bundle extras) throws RemoteException {
return mMock.isEphemeralBrowsingSupported(extras);
}
+
+ @Override
+ public boolean newAuthTabSession(IAuthTabCallback callback, Bundle extras)
+ throws RemoteException {
+ return false;
+ }
};
@Override
diff --git a/browser/browser/src/main/java/androidx/browser/auth/AuthTabCallback.java b/browser/browser/src/main/java/androidx/browser/auth/AuthTabCallback.java
new file mode 100644
index 0000000..633eb08
--- /dev/null
+++ b/browser/browser/src/main/java/androidx/browser/auth/AuthTabCallback.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.browser.auth;
+
+import android.os.Bundle;
+
+import androidx.browser.customtabs.CustomTabsCallback;
+import androidx.browser.customtabs.CustomTabsClient;
+import androidx.browser.customtabs.CustomTabsService;
+
+import org.jspecify.annotations.NonNull;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A callback class for Auth Tab clients to get messages regarding events in their Auth Tabs. In the
+ * implementation, all callbacks are sent to the {@link Executor} provided by the client to
+ * {@link CustomTabsClient#newAuthTabSession} or its UI thread if one is not provided.
+ */
+@ExperimentalAuthTab
+public interface AuthTabCallback {
+ /**
+ * To be called when a navigation event happens.
+ *
+ * @param navigationEvent The code corresponding to the navigation event.
+ * @param extras Reserved for future use.
+ */
+ void onNavigationEvent(@CustomTabsCallback.NavigationEvent int navigationEvent,
+ @NonNull Bundle extras);
+
+ /**
+ * Unsupported callbacks that may be provided by the implementation.
+ *
+ * <p>
+ * <strong>Note:</strong>Clients should <strong>never</strong> rely on this callback to be
+ * called and/or to have a defined behavior, as it is entirely implementation-defined and not
+ * supported.
+ *
+ * <p> This can be used by implementations to add extra callbacks, for testing or experimental
+ * purposes.
+ *
+ * @param callbackName Name of the extra callback.
+ * @param args Arguments for the callback
+ */
+ void onExtraCallback(@NonNull String callbackName, @NonNull Bundle args);
+
+ /**
+ * The same as {@link #onExtraCallback}, except that this method allows the Auth Tab provider to
+ * return a result.
+ *
+ * A return value of {@link Bundle#EMPTY} will be used to signify that the client does not know
+ * how to handle the callback.
+ *
+ * As optional best practices, {@link CustomTabsService#KEY_SUCCESS} could be use to identify
+ * that callback was *successfully* handled. For example, when returning a message with result:
+ * <pre><code>
+ * Bundle result = new Bundle();
+ * result.putString("message", message);
+ * if (success)
+ * result.putBoolean(CustomTabsService#KEY_SUCCESS, true);
+ * return result;
+ * </code></pre>
+ * The caller side:
+ * <pre><code>
+ * Bundle result = extraCallbackWithResult(callbackName, args);
+ * if (result.getBoolean(CustomTabsService#KEY_SUCCESS)) {
+ * // callback was successfully handled
+ * }
+ * </code></pre>
+ */
+ @NonNull
+ Bundle onExtraCallbackWithResult(@NonNull String callbackName, @NonNull Bundle args);
+
+ /**
+ * Called when the browser process finished warming up initiated by
+ * {@link CustomTabsClient#warmup()}.
+ *
+ * @param extras Reserved for future use.
+ */
+ void onWarmupCompleted(@NonNull Bundle extras);
+}
diff --git a/browser/browser/src/main/java/androidx/browser/auth/AuthTabIntent.java b/browser/browser/src/main/java/androidx/browser/auth/AuthTabIntent.java
index 25a7667..43c0e43 100644
--- a/browser/browser/src/main/java/androidx/browser/auth/AuthTabIntent.java
+++ b/browser/browser/src/main/java/androidx/browser/auth/AuthTabIntent.java
@@ -23,12 +23,15 @@
import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_COLOR_SCHEME_PARAMS;
import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_ENABLE_EPHEMERAL_BROWSING;
import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_SESSION;
+import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_SESSION_ID;
import android.app.Activity;
+import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
+import android.os.IBinder;
import android.util.SparseArray;
import androidx.activity.result.ActivityResultCallback;
@@ -40,6 +43,7 @@
import androidx.annotation.RestrictTo;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.ExperimentalEphemeralBrowsing;
+import androidx.browser.customtabs.ExperimentalPendingSession;
import androidx.core.os.BundleCompat;
import org.jspecify.annotations.NonNull;
@@ -147,6 +151,9 @@
/** An {@link Intent} used to start the Auth Tab Activity. */
public final @NonNull Intent intent;
+ private final @Nullable AuthTabSession mSession;
+ private final AuthTabSession.@Nullable PendingSession mPendingSession;
+
/**
* Launches an Auth Tab Activity. Must be used for flows that result in a redirect with a custom
* scheme.
@@ -220,8 +227,21 @@
return defaults;
}
- private AuthTabIntent(@NonNull Intent intent) {
+ private AuthTabIntent(@NonNull Intent intent, @Nullable AuthTabSession session,
+ AuthTabSession.@Nullable PendingSession pendingSession) {
this.intent = intent;
+ mSession = session;
+ mPendingSession = pendingSession;
+ }
+
+ @Nullable
+ public AuthTabSession getSession() {
+ return mSession;
+ }
+
+ @ExperimentalPendingSession
+ public AuthTabSession.@Nullable PendingSession getPendingSession() {
+ return mPendingSession;
}
/**
@@ -233,11 +253,48 @@
new AuthTabColorSchemeParams.Builder();
private @Nullable SparseArray<Bundle> mColorSchemeParamBundles;
private @Nullable Bundle mDefaultColorSchemeBundle;
+ private @Nullable AuthTabSession mSession;
+ private AuthTabSession.@Nullable PendingSession mPendingSession;
public Builder() {
}
/**
+ * Associates the {@link Intent} with the given {@link AuthTabSession}.
+ *
+ * Guarantees that the {@link Intent} will be sent to the same component as the one the
+ * session is associated with.
+ */
+ public @NonNull Builder setSession(@NonNull AuthTabSession session) {
+ mSession = session;
+ mIntent.setPackage(session.getComponentName().getPackageName());
+ setSessionParameters(session.getBinder(), session.getId());
+ return this;
+ }
+
+ /**
+ * Associates the {@link Intent} with the given {@link AuthTabSession.PendingSession}.
+ * Overrides the effect of {@link #setSession}.
+ */
+ @ExperimentalPendingSession
+ public @NonNull Builder setPendingSession(AuthTabSession.@NonNull PendingSession session) {
+ mPendingSession = session;
+ setSessionParameters(null, session.getId());
+ return this;
+ }
+
+ private void setSessionParameters(@Nullable IBinder binder,
+ @Nullable PendingIntent sessionId) {
+ Bundle bundle = new Bundle();
+ bundle.putBinder(EXTRA_SESSION, binder);
+ if (sessionId != null) {
+ bundle.putParcelable(EXTRA_SESSION_ID, sessionId);
+ }
+
+ mIntent.putExtras(bundle);
+ }
+
+ /**
* Sets whether to enable ephemeral browsing within the Auth Tab. If ephemeral browsing is
* enabled, and the browser supports it, the Auth Tab does not share cookies or other data
* with the browser that handles the auth session.
@@ -363,7 +420,7 @@
mIntent.putExtras(bundle);
}
- return new AuthTabIntent(mIntent);
+ return new AuthTabIntent(mIntent, mSession, mPendingSession);
}
}
diff --git a/browser/browser/src/main/java/androidx/browser/auth/AuthTabSession.java b/browser/browser/src/main/java/androidx/browser/auth/AuthTabSession.java
new file mode 100644
index 0000000..a9e30e9
--- /dev/null
+++ b/browser/browser/src/main/java/androidx/browser/auth/AuthTabSession.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.browser.auth;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.os.IBinder;
+import android.support.customtabs.IAuthTabCallback;
+
+import androidx.annotation.RestrictTo;
+import androidx.browser.customtabs.CustomTabsClient;
+import androidx.browser.customtabs.ExperimentalPendingSession;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A class to be used for AuthTab related communication. Clients that want to launch Auth Tabs can
+ * use this class exclusively to handle all related communication.
+ *
+ * Use {@link CustomTabsClient#newAuthTabSession} to create an instance.
+ */
+@ExperimentalAuthTab
+public final class AuthTabSession {
+ private final IAuthTabCallback mCallback;
+ private final ComponentName mComponentName;
+
+ /**
+ * The session ID represented by {@link PendingIntent}. Other apps cannot forge
+ * {@link PendingIntent}. The {@link PendingIntent#equals(Object)} method considers two
+ * {@link PendingIntent} objects equal if their action, data, type, class and category are the
+ * same (even across a process being killed).
+ *
+ * {@see Intent#filterEquals()}
+ */
+ @Nullable
+ private final PendingIntent mId;
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public AuthTabSession(@NonNull IAuthTabCallback callback, @NonNull ComponentName componentName,
+ @Nullable PendingIntent sessionId) {
+ mCallback = callback;
+ mComponentName = componentName;
+ mId = sessionId;
+ }
+
+ /* package */ IBinder getBinder() {
+ return mCallback.asBinder();
+ }
+
+ /* package */ ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ @Nullable
+ /* package */ PendingIntent getId() {
+ return mId;
+ }
+
+ /**
+ * A class to be used instead of {@link AuthTabSession} when an Auth Tab is launched before a
+ * Service connection is established.
+ *
+ * Use {@link CustomTabsClient#newPendingAuthTabSession} to create an instance.
+ * Use {@link CustomTabsClient#attachAuthTabSession(PendingSession)} to get an
+ * {@link AuthTabSession}.
+ */
+ @ExperimentalPendingSession
+ public static class PendingSession {
+ @Nullable
+ private final PendingIntent mId;
+ @Nullable
+ private final Executor mExecutor;
+ @Nullable
+ private final AuthTabCallback mCallback;
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public PendingSession(@Nullable PendingIntent sessionId, @Nullable Executor executor,
+ @Nullable AuthTabCallback callback) {
+ mId = sessionId;
+ mExecutor = executor;
+ mCallback = callback;
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Nullable
+ public PendingIntent getId() {
+ return mId;
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Nullable
+ public Executor getExecutor() {
+ return mExecutor;
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Nullable
+ public AuthTabCallback getCallback() {
+ return mCallback;
+ }
+ }
+}
diff --git a/browser/browser/src/main/java/androidx/browser/auth/AuthTabSessionToken.java b/browser/browser/src/main/java/androidx/browser/auth/AuthTabSessionToken.java
new file mode 100644
index 0000000..ff60fab
--- /dev/null
+++ b/browser/browser/src/main/java/androidx/browser/auth/AuthTabSessionToken.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.browser.auth;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.support.customtabs.IAuthTabCallback;
+import android.util.Log;
+
+import androidx.annotation.RestrictTo;
+import androidx.browser.customtabs.CustomTabsIntent;
+import androidx.core.content.IntentCompat;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Wrapper class that can be used as a unique identifier for a session. Also contains an accessor
+ * for the {@link AuthTabCallback} for the session if there was any.
+ */
+@ExperimentalAuthTab
+public class AuthTabSessionToken {
+ private static final String TAG = "AuthTabSessionToken";
+
+ /**
+ * Both {@link #mCallbackBinder} and {@link #mSessionId} are used as session ID.
+ * At least one of the ID should be not null. If {@link #mSessionId} is null,
+ * the session will be invalidated as soon as the client goes away.
+ * Otherwise the browser will attempt to keep the session parameters,
+ * but it might drop them to reclaim resources
+ */
+ @Nullable
+ private final IAuthTabCallback mCallbackBinder;
+ @Nullable
+ private final PendingIntent mSessionId;
+ @Nullable
+ private final AuthTabCallback mCallback;
+
+ /**
+ * Constructs a new session token.
+ *
+ * Both {@code callbackBinder} and {@code sessionId} are used as session ID. At least one of the
+ * IDs should be not null. If {@code sessionId} is null, the session will be invalidated as soon
+ * as the client goes away. Otherwise the browser will attempt to keep the session parameters,
+ * but it might drop them to reclaim resources.
+ *
+ * @param callbackBinder The {@link IAuthTabCallback} associated with this session.
+ * @param sessionId The {@link PendingIntent} associated with this session.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public AuthTabSessionToken(@Nullable IAuthTabCallback callbackBinder,
+ @Nullable PendingIntent sessionId) {
+ if (callbackBinder == null && sessionId == null) {
+ throw new IllegalStateException(
+ "AuthTabSessionToken must have either a session id or a callback (or both).");
+ }
+
+ mCallbackBinder = callbackBinder;
+ mSessionId = sessionId;
+
+ mCallback = mCallbackBinder == null ? null : new AuthTabCallback() {
+ @Override
+ public void onNavigationEvent(int navigationEvent, @NonNull Bundle extras) {
+ try {
+ mCallbackBinder.onNavigationEvent(navigationEvent, extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException during IAuthTabCallback transaction");
+ }
+ }
+
+ @Override
+ public void onExtraCallback(@NonNull String callbackName, @NonNull Bundle args) {
+ try {
+ mCallbackBinder.onExtraCallback(callbackName, args);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException during IAuthTabCallback transaction");
+ }
+ }
+
+ @NonNull
+ @Override
+ public Bundle onExtraCallbackWithResult(@NonNull String callbackName,
+ @NonNull Bundle args) {
+ try {
+ return mCallbackBinder.onExtraCallbackWithResult(callbackName, args);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException during IAuthTabCallback transaction");
+ return Bundle.EMPTY;
+ }
+ }
+
+ @Override
+ public void onWarmupCompleted(@NonNull Bundle extras) {
+ try {
+ mCallbackBinder.onWarmupCompleted(extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException during IAuthTabCallback transaction");
+ }
+ }
+ };
+ }
+
+ /**
+ * Obtain an {@link AuthTabSessionToken} from an intent. See {@link AuthTabIntent.Builder}
+ * for ways to generate an intent for Auth Tabs.
+ *
+ * @param intent The intent to generate the token from. This has to include an extra for
+ * {@link CustomTabsIntent#EXTRA_SESSION}.
+ * @return The token that was generated.
+ */
+ public static @Nullable AuthTabSessionToken getSessionTokenFromIntent(@NonNull Intent intent) {
+ Bundle b = intent.getExtras();
+ if (b == null) return null;
+ IBinder binder = b.getBinder(CustomTabsIntent.EXTRA_SESSION);
+ PendingIntent sessionId = IntentCompat.getParcelableExtra(intent,
+ CustomTabsIntent.EXTRA_SESSION_ID, PendingIntent.class);
+ if (binder == null && sessionId == null) return null;
+ IAuthTabCallback callback = binder == null ? null : IAuthTabCallback.Stub.asInterface(
+ binder);
+ return new AuthTabSessionToken(callback, sessionId);
+ }
+
+ /**
+ * @return {@link AuthTabCallback} corresponding to this session if there was any non-null
+ * callbacks passed by the client.
+ */
+ @Nullable
+ public AuthTabCallback getCallback() {
+ return mCallback;
+ }
+
+ /**
+ * @return The callback binder corresponding to this session, null if there is none.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Nullable
+ public IBinder getCallbackBinder() {
+ if (mCallbackBinder == null) return null;
+ return mCallbackBinder.asBinder();
+ }
+
+ /** @return The session id corresponding to this session, null if there is none. */
+ @Nullable
+ public PendingIntent getId() {
+ return mSessionId;
+ }
+
+ /** @return Whether this token is associated with a session id. */
+ public boolean hasId() {
+ return mSessionId != null;
+ }
+
+ @Override
+ public int hashCode() {
+ if (mSessionId != null) return mSessionId.hashCode();
+
+ return getCallbackBinderAssertNotNull().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof AuthTabSessionToken)) return false;
+ AuthTabSessionToken other = (AuthTabSessionToken) o;
+
+ PendingIntent otherSessionId = other.getId();
+ // If one object has a session id and the other one doesn't, they're not equal.
+ if ((mSessionId == null) != (otherSessionId == null)) return false;
+
+ // If both objects have an id, check that they are equal.
+ if (mSessionId != null) return mSessionId.equals(otherSessionId);
+
+ // Otherwise check for binder equality.
+ return getCallbackBinderAssertNotNull().equals(other.getCallbackBinderAssertNotNull());
+ }
+
+ private IBinder getCallbackBinderAssertNotNull() {
+ if (mCallbackBinder == null) {
+ throw new IllegalStateException(
+ "AuthTabSessionToken must have valid binder or pending session");
+ }
+ return mCallbackBinder.asBinder();
+ }
+
+ /**
+ * @return Whether this token is associated with the given session.
+ */
+ public boolean isAssociatedWith(@NonNull AuthTabSession session) {
+ return session.getBinder().equals(mCallbackBinder);
+ }
+
+ static class MockCallback extends IAuthTabCallback.Stub {
+ @Override
+ public void onNavigationEvent(int navigationEvent, Bundle extras) throws RemoteException {
+ }
+
+ @Override
+ public void onExtraCallback(String callbackName, Bundle args) throws RemoteException {
+ }
+
+ @Override
+ public Bundle onExtraCallbackWithResult(String callbackName, Bundle args)
+ throws RemoteException {
+ return Bundle.EMPTY;
+ }
+
+ @Override
+ public void onWarmupCompleted(Bundle extras) throws RemoteException {
+ }
+ }
+}
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsCallback.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsCallback.java
index dcac37a..990fe57 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsCallback.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsCallback.java
@@ -38,6 +38,15 @@
*/
public class CustomTabsCallback {
/**
+ * To be called when a navigation event happens.
+ *
+ * @param navigationEvent The code corresponding to the navigation event.
+ * @param extras Reserved for future use.
+ */
+ public void onNavigationEvent(@NavigationEvent int navigationEvent, @Nullable Bundle extras) {
+ }
+
+ /**
* Sent when the tab has started loading a page.
*/
public static final int NAVIGATION_STARTED = 1;
@@ -68,6 +77,14 @@
*/
public static final int TAB_HIDDEN = 6;
+ /** Possible navigation event values. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @IntDef({NAVIGATION_STARTED, NAVIGATION_FINISHED, NAVIGATION_FAILED, NAVIGATION_ABORTED,
+ TAB_SHOWN, TAB_HIDDEN})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface NavigationEvent {
+ }
+
/**
* Key for the extra included in {@link #onRelationshipValidationResult} {@code extras}
* containing whether the verification was performed while the device was online. This may be
@@ -77,14 +94,6 @@
public static final String ONLINE_EXTRAS_KEY = "online";
/**
- * To be called when a navigation event happens.
- *
- * @param navigationEvent The code corresponding to the navigation event.
- * @param extras Reserved for future use.
- */
- public void onNavigationEvent(int navigationEvent, @Nullable Bundle extras) {}
-
- /**
* Unsupported callbacks that may be provided by the implementation.
*
* <p>
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
index 2305715..2be2101 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
@@ -26,26 +26,35 @@
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.net.Uri;
+import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
+import android.support.customtabs.IAuthTabCallback;
import android.support.customtabs.ICustomTabsCallback;
import android.support.customtabs.ICustomTabsService;
import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.OptIn;
+import androidx.browser.auth.AuthTabCallback;
+import androidx.browser.auth.AuthTabSession;
+import androidx.browser.auth.ExperimentalAuthTab;
+
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executor;
/**
* Class to communicate with a {@link CustomTabsService} and create
* {@link CustomTabsSession} from it.
*/
+@OptIn(markerClass = ExperimentalAuthTab.class)
public class CustomTabsClient {
private static final String TAG = "CustomTabsClient";
@@ -285,6 +294,156 @@
return new CustomTabsSession.PendingSession(callback, sessionId);
}
+ /**
+ * Creates a new pending session with an optional callback. This session can be converted to
+ * a standard session using {@link #attachSession} after connection.
+ *
+ * {@see PendingSession}
+ */
+ @ExperimentalAuthTab
+ @ExperimentalPendingSession
+ public static AuthTabSession.@NonNull PendingSession newPendingAuthTabSession(
+ @NonNull Context context, int id, @Nullable Executor executor,
+ @Nullable AuthTabCallback callback) {
+ PendingIntent sessionId = createSessionId(context, id);
+
+ return new AuthTabSession.PendingSession(sessionId, executor, callback);
+ }
+
+ /**
+ * Creates a new session through an ICustomTabsService with the optional callback. This session
+ * can be used to associate any related communication through the service with an intent and
+ * then later with a Custom Tab. The client can then send later service calls or intents to
+ * through same session-intent-Custom Tab association.
+ *
+ * @param callback The callback through which the client will receive updates about the created
+ * session. Can be null.
+ * @param executor The {@link Executor} to be used to execute the callbacks. If null, the
+ * callbacks will be received on the UI thread.
+ * @return The session object that was created as a result of the transaction. The client can
+ * use this to relay session specific calls. Null if the service failed to respond
+ * (threw a RemoteException).
+ */
+ @ExperimentalAuthTab
+ @Nullable
+ public AuthTabSession newAuthTabSession(@Nullable AuthTabCallback callback,
+ @Nullable Executor executor) {
+ return newAuthTabSessionInternal(callback, executor, null);
+ }
+
+ /**
+ * Creates a new session through an ICustomTabsService with the optional callback. This session
+ * can be used to associate any related communication through the service with an intent and
+ * then later with a Custom Tab. The client can then send later service calls or intents to
+ * through same session-intent-Custom Tab association.
+ *
+ * @param callback The callback through which the client will receive updates about the created
+ * session. Can be null.
+ * @param executor The {@link Executor} to be used to execute the callbacks. If null, the
+ * callbacks will be received on the UI thread.
+ * @param id The session id. If the session with the specified id already exists for
+ * the given client application, the new callback is supplied to that session
+ * and
+ * further attempts to launch URLs using that session will update the
+ * existing Auth
+ * Tab instead of launching a new one.
+ * @return The session object that was created as a result of the transaction. The client can
+ * use this to relay session specific calls. Null if the service failed to respond
+ * (threw a RemoteException).
+ */
+ @ExperimentalAuthTab
+ @Nullable
+ public AuthTabSession newAuthTabSession(@Nullable AuthTabCallback callback,
+ @Nullable Executor executor, int id) {
+ return newAuthTabSessionInternal(callback, executor,
+ createSessionId(mApplicationContext, id));
+ }
+
+ @Nullable
+ private AuthTabSession newAuthTabSessionInternal(@Nullable AuthTabCallback callback,
+ @Nullable Executor executor, @Nullable PendingIntent sessionId) {
+ IAuthTabCallback.Stub wrapper = createAuthTabCallbackWrapper(callback, executor);
+
+ try {
+ boolean success;
+
+ Bundle extras = new Bundle();
+ extras.putParcelable(CustomTabsIntent.EXTRA_SESSION_ID, sessionId);
+ success = mService.newAuthTabSession(wrapper, extras);
+
+ if (!success) return null;
+ } catch (RemoteException e) {
+ return null;
+ }
+ return new AuthTabSession(wrapper, mServiceComponentName, sessionId);
+ }
+
+ private IAuthTabCallback.Stub createAuthTabCallbackWrapper(@Nullable AuthTabCallback callback,
+ @Nullable Executor executor) {
+ return new IAuthTabCallback.Stub() {
+ private final Executor mExecutor = executor != null ? executor : new Handler(
+ Looper.getMainLooper())::post;
+
+ @Override
+ public void onNavigationEvent(int navigationEvent, Bundle extras)
+ throws RemoteException {
+ if (callback == null) return;
+ long identity = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> callback.onNavigationEvent(navigationEvent, extras));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void onExtraCallback(String callbackName, Bundle args) throws RemoteException {
+ if (callback == null) return;
+ long identity = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> callback.onExtraCallback(callbackName, args));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public Bundle onExtraCallbackWithResult(String callbackName, Bundle args)
+ throws RemoteException {
+ if (callback == null) return Bundle.EMPTY;
+ long identity = Binder.clearCallingIdentity();
+ try {
+ return callback.onExtraCallbackWithResult(callbackName, args);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void onWarmupCompleted(Bundle extras) throws RemoteException {
+ if (callback == null) return;
+ long identity = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> callback.onWarmupCompleted(extras));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ };
+ }
+
+ /**
+ * Associate {@link AuthTabSession.PendingSession} with the service and turn it into an
+ * {@link AuthTabSession}.
+ */
+ @ExperimentalAuthTab
+ @ExperimentalPendingSession
+ @Nullable
+ public AuthTabSession attachAuthTabSession(AuthTabSession.@NonNull PendingSession session) {
+ return newAuthTabSessionInternal(session.getCallback(), session.getExecutor(),
+ session.getId());
+ }
+
private @Nullable CustomTabsSession newSessionInternal(
final @Nullable CustomTabsCallback callback, @Nullable PendingIntent sessionId) {
ICustomTabsCallback.Stub wrapper = createCallbackWrapper(callback);
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
index dd2faf2..95ef578 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
@@ -25,11 +25,14 @@
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
+import android.support.customtabs.IAuthTabCallback;
import android.support.customtabs.ICustomTabsCallback;
import android.support.customtabs.ICustomTabsService;
import androidx.annotation.IntDef;
import androidx.annotation.RestrictTo;
+import androidx.browser.auth.AuthTabSessionToken;
+import androidx.browser.auth.ExperimentalAuthTab;
import androidx.collection.SimpleArrayMap;
import org.jspecify.annotations.NonNull;
@@ -333,6 +336,23 @@
return bundle.getParcelable(CustomTabsSession.TARGET_ORIGIN_KEY);
}
}
+
+ @ExperimentalAuthTab
+ @Override
+ public boolean newAuthTabSession(IAuthTabCallback callback, Bundle extras) {
+ PendingIntent sessionId = getSessionIdFromBundle(extras);
+ AuthTabSessionToken sessionToken = new AuthTabSessionToken(callback, sessionId);
+ try {
+ DeathRecipient deathRecipient = () -> cleanUpSession(sessionToken);
+ synchronized (mDeathRecipientMap) {
+ callback.asBinder().linkToDeath(deathRecipient, 0);
+ mDeathRecipientMap.put(callback.asBinder(), deathRecipient);
+ }
+ return CustomTabsService.this.newAuthTabSession(sessionToken);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
};
@Override
@@ -365,6 +385,31 @@
}
/**
+ * Called when the client side {@link IBinder} for this {@link AuthTabSessionToken} is dead.
+ * Can also be used to clean up {@link DeathRecipient} instances allocated for the given token.
+ *
+ * @param sessionToken The session token for which the {@link DeathRecipient} call has been
+ * received.
+ * @return Whether the clean up was successful. Multiple calls with two tokens holdings the
+ * same binder will return false.
+ */
+ @ExperimentalAuthTab
+ protected boolean cleanUpSession(@NonNull AuthTabSessionToken sessionToken) {
+ try {
+ synchronized (mDeathRecipientMap) {
+ IBinder binder = sessionToken.getCallbackBinder();
+ if (binder == null) return false;
+ DeathRecipient deathRecipient = mDeathRecipientMap.get(binder);
+ binder.unlinkToDeath(deathRecipient, 0);
+ mDeathRecipientMap.remove(binder);
+ }
+ } catch (NoSuchElementException e) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
* Warms up the browser process asynchronously.
*
* @param flags Reserved for future use.
@@ -621,4 +666,20 @@
protected boolean isEphemeralBrowsingSupported(@NonNull Bundle extras) {
return false;
}
+
+ /**
+ * Creates a new Auth Tab session through an ICustomTabsService with the optional callback. This
+ * session can be used to associate any related communication through the service with an intent
+ * and then later with an Auth Tab. The client can then send later service calls or intents
+ * through the same session-intent-Auth Tab association.
+ *
+ * @param sessionToken Session token to be used as a unique identifier. This also has access
+ * to the {@link AuthTabCallback} passed from the client side through
+ * {@link AuthTabSessionToken#getCallback()}.
+ * @return Whether a new session was successfully created.
+ */
+ @ExperimentalAuthTab
+ protected boolean newAuthTabSession(@NonNull AuthTabSessionToken sessionToken) {
+ return false;
+ }
}
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
index 0dc92fa..c68384f 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
@@ -26,6 +26,7 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
+import android.support.customtabs.IAuthTabCallback;
import android.support.customtabs.ICustomTabsCallback;
import android.support.customtabs.ICustomTabsService;
import android.support.customtabs.IEngagementSignalsCallback;
@@ -702,5 +703,11 @@
public boolean isEphemeralBrowsingSupported(Bundle extras) throws RemoteException {
return false;
}
+
+ @Override
+ public boolean newAuthTabSession(IAuthTabCallback callback, Bundle extras)
+ throws RemoteException {
+ return false;
+ }
}
}
diff --git a/browser/browser/src/main/stableAidl/android/support/customtabs/IAuthTabCallback.aidl b/browser/browser/src/main/stableAidl/android/support/customtabs/IAuthTabCallback.aidl
new file mode 100644
index 0000000..379e46f
--- /dev/null
+++ b/browser/browser/src/main/stableAidl/android/support/customtabs/IAuthTabCallback.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.customtabs;
+
+import android.os.Bundle;
+
+/**
+ * Interface to an AuthTabCallback.
+ */
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+interface IAuthTabCallback {
+ oneway void onNavigationEvent(int navigationEvent, in Bundle extras) = 1;
+ oneway void onExtraCallback(String callbackName, in Bundle args) = 2;
+ Bundle onExtraCallbackWithResult(String callbackName, in Bundle args) = 3;
+ oneway void onWarmupCompleted(in Bundle extras) = 4;
+}
\ No newline at end of file
diff --git a/browser/browser/src/main/stableAidl/android/support/customtabs/ICustomTabsService.aidl b/browser/browser/src/main/stableAidl/android/support/customtabs/ICustomTabsService.aidl
index 84a6a4a..67645fc 100644
--- a/browser/browser/src/main/stableAidl/android/support/customtabs/ICustomTabsService.aidl
+++ b/browser/browser/src/main/stableAidl/android/support/customtabs/ICustomTabsService.aidl
@@ -19,6 +19,7 @@
import android.content.ComponentName;
import android.net.Uri;
import android.os.Bundle;
+import android.support.customtabs.IAuthTabCallback;
import android.support.customtabs.ICustomTabsCallback;
import android.support.customtabs.IEngagementSignalsCallback;
@@ -46,4 +47,5 @@
boolean isEngagementSignalsApiAvailable(in ICustomTabsCallback customTabsCallback, in Bundle extras) = 12;
boolean setEngagementSignalsCallback(in ICustomTabsCallback customTabsCallback, in IBinder callback, in Bundle extras) = 13;
boolean isEphemeralBrowsingSupported(in Bundle extras) = 16;
+ boolean newAuthTabSession(in IAuthTabCallback callback, in Bundle extras) = 17;
}
diff --git a/browser/browser/src/test/java/androidx/browser/auth/AuthTabIntentTest.java b/browser/browser/src/test/java/androidx/browser/auth/AuthTabIntentTest.java
index 8c26499..9b1d422 100644
--- a/browser/browser/src/test/java/androidx/browser/auth/AuthTabIntentTest.java
+++ b/browser/browser/src/test/java/androidx/browser/auth/AuthTabIntentTest.java
@@ -37,6 +37,7 @@
import androidx.activity.result.ActivityResultLauncher;
import androidx.browser.customtabs.CustomTabsIntent;
+import androidx.browser.customtabs.TestUtil;
import org.junit.Rule;
import org.junit.Test;
@@ -166,4 +167,48 @@
.intent;
assertTrue(intent.getBooleanExtra(CustomTabsIntent.EXTRA_ENABLE_EPHEMERAL_BROWSING, false));
}
+
+ @Test
+ public void testPutsNullSessionExtra_WhenBuiltWithDefaultConstructor() {
+ Intent intent = new AuthTabIntent.Builder().build().intent;
+ assertNullSessionInExtras(intent);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testPutsSessionBinderAndId_IfSuppliedInConstructor() {
+ AuthTabSession session = TestUtil.makeMockAuthTabSession();
+ Intent intent = new AuthTabIntent.Builder().setSession(session).build().intent;
+ assertEquals(session.getBinder(),
+ intent.getExtras().getBinder(CustomTabsIntent.EXTRA_SESSION));
+ assertEquals(session.getId(), intent.getParcelableExtra(CustomTabsIntent.EXTRA_SESSION_ID));
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testPutsSessionBinderAndId_IfSuppliedInSetter() {
+ AuthTabSession session = TestUtil.makeMockAuthTabSession();
+ AuthTabIntent authTabIntent = new AuthTabIntent.Builder().setSession(session).build();
+ assertEquals(session, authTabIntent.getSession());
+ assertEquals(session.getBinder(),
+ authTabIntent.intent.getExtras().getBinder(CustomTabsIntent.EXTRA_SESSION));
+ assertEquals(session.getId(),
+ authTabIntent.intent.getParcelableExtra(CustomTabsIntent.EXTRA_SESSION_ID));
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testPutsPendingSessionId() {
+ AuthTabSession.PendingSession pendingSession = TestUtil.makeMockPendingAuthTabSession();
+ AuthTabIntent authTabIntent = new AuthTabIntent.Builder().setPendingSession(
+ pendingSession).build();
+ assertEquals(pendingSession, authTabIntent.getPendingSession());
+ assertEquals(pendingSession.getId(),
+ authTabIntent.intent.getParcelableExtra(CustomTabsIntent.EXTRA_SESSION_ID));
+ }
+
+ private void assertNullSessionInExtras(Intent intent) {
+ assertTrue(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION));
+ assertNull(intent.getExtras().getBinder(CustomTabsIntent.EXTRA_SESSION));
+ }
}
diff --git a/browser/browser/src/test/java/androidx/browser/auth/AuthTabSessionTokenTest.java b/browser/browser/src/test/java/androidx/browser/auth/AuthTabSessionTokenTest.java
new file mode 100644
index 0000000..01087e5
--- /dev/null
+++ b/browser/browser/src/test/java/androidx/browser/auth/AuthTabSessionTokenTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.browser.auth;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.support.customtabs.IAuthTabCallback;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+/**
+ * Tests for {@link AuthTabSessionToken}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class AuthTabSessionTokenTest {
+ private Context mContext;
+
+ @Before
+ public void setup() {
+ mContext = ApplicationProvider.getApplicationContext();
+ }
+
+ @Test
+ public void testEquality_withId() {
+ AuthTabSessionToken token1 = new AuthTabSessionToken(new AuthTabSessionToken.MockCallback(),
+ createSessionId(27));
+
+ AuthTabSessionToken token2 = new AuthTabSessionToken(new AuthTabSessionToken.MockCallback(),
+ createSessionId(27));
+
+ assertEquals(token1, token2);
+ }
+
+ @Test
+ public void testNonEquality_withId() {
+ // Using the same binder to ensure only the id matters.
+ IAuthTabCallback.Stub binder = new AuthTabSessionToken.MockCallback();
+
+ AuthTabSessionToken token1 = new AuthTabSessionToken(binder, createSessionId(10));
+ AuthTabSessionToken token2 = new AuthTabSessionToken(binder, createSessionId(20));
+
+ assertNotEquals(token1, token2);
+ }
+
+ @Test
+ public void testEquality_withBinder() {
+ IAuthTabCallback.Stub binder = new AuthTabSessionToken.MockCallback();
+
+ AuthTabSessionToken token1 = new AuthTabSessionToken(binder, null);
+ AuthTabSessionToken token2 = new AuthTabSessionToken(binder, null);
+
+ assertEquals(token1, token2);
+ }
+
+ @Test
+ public void testNonEquality_withBinder() {
+ IAuthTabCallback.Stub binder1 = new AuthTabSessionToken.MockCallback();
+ IAuthTabCallback.Stub binder2 = new AuthTabSessionToken.MockCallback();
+
+ AuthTabSessionToken token1 = new AuthTabSessionToken(binder1, null);
+ AuthTabSessionToken token2 = new AuthTabSessionToken(binder2, null);
+
+ assertNotEquals(token1, token2);
+ }
+
+ @Test
+ public void testNonEquality_mixedIdAndBinder() {
+ // Using the same binder to ensure only the id matters.
+ IAuthTabCallback.Stub binder = new AuthTabSessionToken.MockCallback();
+
+ AuthTabSessionToken token1 = new AuthTabSessionToken(binder, createSessionId(10));
+ // Tokens cannot be mixed if only one has an id even if the binder is the same.
+ AuthTabSessionToken token2 = new AuthTabSessionToken(binder, null);
+
+ assertNotEquals(token1, token2);
+ }
+
+ // This code does the same as CustomTabsClient#createSessionId but that is not necessary for the
+ // test, we just need to create a PendingIntent that uses sessionId as the requestCode.
+ private PendingIntent createSessionId(int sessionId) {
+ return PendingIntent.getActivity(mContext, sessionId, new Intent(), 0);
+ }
+
+ private void assertEquals(AuthTabSessionToken token1, AuthTabSessionToken token2) {
+ Assert.assertEquals(token1, token2);
+ Assert.assertEquals(token2, token1);
+
+ Assert.assertEquals(token1.hashCode(), token2.hashCode());
+ }
+
+ private void assertNotEquals(AuthTabSessionToken token1, AuthTabSessionToken token2) {
+ Assert.assertNotEquals(token1, token2);
+ Assert.assertNotEquals(token2, token1);
+
+ // I guess technically this could be flaky, but let's hope not...
+ Assert.assertNotEquals(token1.hashCode(), token2.hashCode());
+ }
+}
diff --git a/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java b/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java
index c020c0d..2e6315f 100644
--- a/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java
+++ b/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java
@@ -23,11 +23,17 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.support.customtabs.IAuthTabCallback;
import android.support.customtabs.ICustomTabsCallback;
import android.support.customtabs.ICustomTabsService;
+import androidx.browser.auth.AuthTabCallback;
+import androidx.browser.auth.AuthTabSession;
+
import org.jspecify.annotations.NonNull;
+import java.util.concurrent.Executor;
+
/**
* Utilities for unit testing Custom Tabs.
*/
@@ -56,4 +62,17 @@
CustomTabsIntent.EXTRA_SESSION));
assertEquals(session.getId(), intent.getParcelableExtra(CustomTabsIntent.EXTRA_SESSION_ID));
}
+
+ /** Create s a mock {@link AuthTabSession} for testing. */
+ @NonNull
+ public static AuthTabSession makeMockAuthTabSession() {
+ return new AuthTabSession(mock(IAuthTabCallback.class),
+ new ComponentName("", ""), makeMockPendingIntent());
+ }
+
+ /** Creates a mock {@link AuthTabSession.PendingSession} for testing. */
+ public static AuthTabSession.@NonNull PendingSession makeMockPendingAuthTabSession() {
+ return new AuthTabSession.PendingSession(makeMockPendingIntent(), mock(Executor.class),
+ mock(AuthTabCallback.class));
+ }
}
diff --git a/buildSrc-tests/build.gradle b/buildSrc-tests/build.gradle
index f2e88f3..d70a688 100644
--- a/buildSrc-tests/build.gradle
+++ b/buildSrc-tests/build.gradle
@@ -19,7 +19,6 @@
import androidx.build.LibraryType
import androidx.build.KotlinTarget
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
@@ -90,8 +89,7 @@
}
androidx {
- type = LibraryType.GRADLE_PLUGIN
- publish = Publish.NONE
+ type = LibraryType.INTERNAL_GRADLE_PLUGIN
kotlinTarget = KotlinTarget.KOTLIN_1_9
}
diff --git a/buildSrc-tests/lint-baseline.xml b/buildSrc-tests/lint-baseline.xml
index f24bb50..b7dd377 100644
--- a/buildSrc-tests/lint-baseline.xml
+++ b/buildSrc-tests/lint-baseline.xml
@@ -66,15 +66,6 @@
<issue
id="EagerGradleConfiguration"
- message="Avoid using method get"
- errorLine1=" val verifyOutputsTask = verifyOutputs.get()"
- errorLine2=" ~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/Release.kt"/>
- </issue>
-
- <issue
- id="EagerGradleConfiguration"
message="Use configureEach instead of whenObjectAdded"
errorLine1=" configurations.whenObjectAdded {"
errorLine2=" ~~~~~~~~~~~~~~~">
@@ -256,7 +247,7 @@
<issue
id="GradleProjectIsolation"
message="Use isolated.rootProject instead of getRootProject"
- errorLine1=" rootProject.layout.buildDirectory.dir("privacysandbox-files")"
+ errorLine1=" rootProject.layout.buildDirectory.dir("app-apks-files")"
errorLine2=" ~~~~~~~~~~~">
<location
file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/BuildServerConfiguration.kt"/>
@@ -273,6 +264,15 @@
<issue
id="GradleProjectIsolation"
+ message="Avoid using method getParent"
+ errorLine1=" ${project.parent}."
+ errorLine2=" ~~~~~~">
+ <location
+ file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/checkapi/CompilationInputs.kt"/>
+ </issue>
+
+ <issue
+ id="GradleProjectIsolation"
message="Use isolated.rootProject instead of getRootProject"
errorLine1=" rootProject.tasks.named(CREATE_AGGREGATE_BUILD_INFO_FILES_TASK).configure {"
errorLine2=" ~~~~~~~~~~~">
@@ -345,15 +345,6 @@
<issue
id="GradleProjectIsolation"
- message="Avoid using method getParent"
- errorLine1=" ${project.parent}."
- errorLine2=" ~~~~~~">
- <location
- file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/java/JavaCompileInputs.kt"/>
- </issue>
-
- <issue
- id="GradleProjectIsolation"
message="Use isolated.rootProject instead of getRootProject"
errorLine1=" val rootBaseDir = if (compilerDaemonDisabled) projectDir else rootProject.projectDir"
errorLine2=" ~~~~~~~~~~~">
diff --git a/buildSrc-tests/max-dep-versions/buildSrc-tests-max-dep-versions-dep/build.gradle b/buildSrc-tests/max-dep-versions/buildSrc-tests-max-dep-versions-dep/build.gradle
index 92f2564..38cc6f4 100644
--- a/buildSrc-tests/max-dep-versions/buildSrc-tests-max-dep-versions-dep/build.gradle
+++ b/buildSrc-tests/max-dep-versions/buildSrc-tests-max-dep-versions-dep/build.gradle
@@ -6,7 +6,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
index 4d328d0..90069e4 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
@@ -24,7 +24,12 @@
/**
* Whether to enable constraints for projects in same-version groups
*
- * This is default true.
+ * This is expected to be true during builds that publish artifacts externally This is expected to
+ * be false during most other builds because: Developers may be interested in including only a
+ * subset of projects in ANDROIDX_PROJECTS to make Studio run more quickly. If a build contains only
+ * a subset of projects, we cannot necessarily add constraints between all pairs of projects in the
+ * same group. We want most builds to have high remote cache usage, so we want constraints to be
+ * similar across most builds See go/androidx-group-constraints for more information
*/
const val ADD_GROUP_CONSTRAINTS = "androidx.constraints"
@@ -177,8 +182,7 @@
* Whether to enable constraints for projects in same-version groups See the property definition for
* more details
*/
-fun Project.shouldAddGroupConstraints() =
- project.providers.gradleProperty(ADD_GROUP_CONSTRAINTS).map { s -> s.toBoolean() }.orElse(true)
+fun Project.shouldAddGroupConstraints() = booleanPropertyProvider(ADD_GROUP_CONSTRAINTS)
/**
* Returns alternative project url that will be used as "url" property in publishing maven artifact
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index a016ea3..4dc9c6e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -727,6 +727,7 @@
project.afterEvaluate {
project.addToBuildOnServer("assembleAndroidMain")
project.addToBuildOnServer("lint")
+ // Created to be consumed by docs-tip-of-tree
project.configurations.create("androidIntermediates") {
it.isVisible = false
it.isCanBeResolved = false
@@ -1062,8 +1063,7 @@
val isProbablyPublished =
androidXExtension.type == LibraryType.PUBLISHED_LIBRARY ||
androidXExtension.type ==
- LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS ||
- androidXExtension.type == LibraryType.UNSET
+ LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
if (mavenGroup != null && isProbablyPublished && androidXExtension.shouldPublish()) {
validateProjectMavenGroup(mavenGroup.group)
validateProjectMavenName(androidXExtension.name.get(), mavenGroup.group)
@@ -1310,7 +1310,7 @@
) {
if (buildFeatures.isIsolatedProjectsEnabled()) return
afterEvaluate {
- if (androidXExtension.type !in listOf(LibraryType.UNSET, LibraryType.SAMPLES)) {
+ if (androidXExtension.type.requiresDependencyVerification()) {
val verifyDependencyVersionsTask = project.createVerifyDependencyVersionsTask()
if (verifyDependencyVersionsTask != null) {
taskConfigurator(verifyDependencyVersionsTask)
@@ -1635,10 +1635,6 @@
"$errorPrefix Incorrectly computed libraryType = ${parsed.libraryType} " +
"instead of ${androidXExtension.type}"
}
- check(androidXExtension.publish == parsed.publish) {
- "$errorPrefix Incorrectly computed publish = ${parsed.publish} " +
- "instead of ${androidXExtension.publish}"
- }
check(androidXExtension.shouldPublish() == parsed.shouldPublish()) {
"$errorPrefix Incorrectly computed shouldPublish() = ${parsed.shouldPublish()} " +
"instead of ${androidXExtension.shouldPublish()}"
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
index 005f95f..044bcbb 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
@@ -16,7 +16,7 @@
package androidx.build
-import androidx.build.java.JavaCompileInputs
+import androidx.build.checkapi.CompilationInputs
import com.android.build.api.variant.AndroidComponentsExtension
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
@@ -58,7 +58,7 @@
makeKmpErrorProneTask(
COMPILE_JAVA_TASK_NAME,
jvmJarProvider,
- JavaCompileInputs.fromKmpJvmTarget(project)
+ CompilationInputs.fromKmpJvmTarget(project)
)
} else {
makeErrorProneTask(COMPILE_JAVA_TASK_NAME)
@@ -275,12 +275,12 @@
* Note: Since ErrorProne only understands Java files which may be dependent on Kotlin source, using
* this method to register ErrorProne task causes it to be dependent on jvmJar task.
*
- * @param jvmCompileInputs [JavaCompileInputs] that specifies jvm source including Kotlin sources.
+ * @param jvmCompileInputs [CompilationInputs] that specifies jvm source including Kotlin sources.
*/
private fun Project.makeKmpErrorProneTask(
compileTaskName: String,
jvmJarTaskProvider: TaskProvider<Jar>,
- jvmCompileInputs: JavaCompileInputs
+ jvmCompileInputs: CompilationInputs
) {
makeErrorProneTask(compileTaskName) { errorProneTask ->
// ErrorProne doesn't understand Kotlin source, so first let kotlinCompile finish, then
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index d26533a..812d98f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -164,7 +164,10 @@
val lintChecksProject = findLintProject(":lint-checks") ?: return
project.dependencies.add("lintChecks", lintChecksProject)
- if (extension.type == LibraryType.GRADLE_PLUGIN) {
+ if (
+ extension.type == LibraryType.GRADLE_PLUGIN ||
+ extension.type == LibraryType.INTERNAL_GRADLE_PLUGIN
+ ) {
project.rootProject.findProject(":lint:lint-gradle")?.let {
project.dependencies.add("lintChecks", it)
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt b/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt
index 0cb1030..3d34b79 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt
@@ -42,36 +42,13 @@
if (line.contains("mavenVersion =")) specifiesVersion = true
}
val libraryTypeEnum = libraryType?.let { LibraryType.valueOf(it) } ?: LibraryType.UNSET
- val publishEnum = publish?.let { Publish.valueOf(it) } ?: Publish.UNSET
- return ParsedProject(
- libraryType = libraryTypeEnum,
- publish = publishEnum,
- specifiesVersion = specifiesVersion
- )
+ return ParsedProject(libraryType = libraryTypeEnum, specifiesVersion = specifiesVersion)
}
- data class ParsedProject(
- val libraryType: LibraryType,
- val publish: Publish,
- val specifiesVersion: Boolean
- ) {
- fun shouldPublish(): Boolean =
- if (publish != Publish.UNSET) {
- publish.shouldPublish()
- } else if (libraryType != LibraryType.UNSET) {
- libraryType.publish.shouldPublish()
- } else {
- false
- }
+ data class ParsedProject(val libraryType: LibraryType, val specifiesVersion: Boolean) {
+ fun shouldPublish(): Boolean = libraryType.publish.shouldPublish()
- fun shouldRelease(): Boolean =
- if (publish != Publish.UNSET) {
- publish.shouldRelease()
- } else if (libraryType != LibraryType.UNSET) {
- libraryType.publish.shouldRelease()
- } else {
- false
- }
+ fun shouldRelease(): Boolean = libraryType.publish.shouldRelease()
}
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/Release.kt b/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
index d3b8452..bfbb70c 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
@@ -114,13 +114,6 @@
)
return
}
- if (!androidXExtension.isPublishConfigured()) {
- project.logger.info(
- "project ${project.name} isn't part of release, because" +
- " it does not set the \"publish\" property."
- )
- return
- }
if (!androidXExtension.shouldRelease() && !isSnapshotBuild()) {
project.logger.info(
"project ${project.name} isn't part of release, because its" +
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt b/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt
index 17d960d..24bc918 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt
@@ -19,11 +19,9 @@
import androidx.build.AndroidXExtension
import androidx.build.Release
import androidx.build.RunApiTasks
-import androidx.build.Version
import androidx.build.binarycompatibilityvalidator.BinaryCompatibilityValidation
import androidx.build.getSupportRootFolder
import androidx.build.isWriteVersionedApiFilesEnabled
-import androidx.build.java.JavaCompileInputs
import androidx.build.metalava.MetalavaTasks
import androidx.build.multiplatformExtension
import androidx.build.resources.ResourceTasks
@@ -61,63 +59,21 @@
)
}
- // API behavior is default for type
- if (type.checkApi is RunApiTasks.No && runApiTasks is RunApiTasks.No) {
- project.logger.info("Projects of type ${type.name} do not track API.")
- return false
- }
-
- when (runApiTasks) {
- // API behavior for type must have been overridden, because previous check did not trigger
+ return when (type.checkApi) {
is RunApiTasks.No -> {
- project.logger.info(
- "Project ${project.name} has explicitly disabled API tasks with " +
- "reason: ${(runApiTasks as RunApiTasks.No).reason}"
- )
- return false
+ project.logger.info("Projects of type ${type.name} do not track API.")
+ false
}
is RunApiTasks.Yes -> {
- // API behavior is default for type; not overridden
- if (type.checkApi is RunApiTasks.Yes) {
- return true
- }
- // API behavior for type is overridden
- (runApiTasks as RunApiTasks.Yes).reason?.let { reason ->
+ (type.checkApi as RunApiTasks.Yes).reason?.let { reason ->
project.logger.info(
"Project ${project.name} has explicitly enabled API tasks " +
"with reason: $reason"
)
}
- return true
+ true
}
- else -> {}
}
-
- if (project.version !is Version) {
- project.logger.info("Project ${project.name} has no version set, ignoring API tasks.")
- return false
- }
-
- // If the project has an "api" directory, either because they used to track APIs or they
- // added one manually to force tracking (as recommended below), continue tracking APIs.
- if (project.hasApiFileDirectory() && !shouldRelease()) {
- project.logger.error(
- "Project ${project.name} is not published, but has an existing API " +
- "directory. Forcing API tasks enabled. Please migrate to runApiTasks=Yes."
- )
- return true
- }
-
- if (!shouldRelease()) {
- project.logger.info(
- "Project ${project.name} is not published, ignoring API tasks. " +
- "If you still want to track APIs, create an \"api\" directory in your project" +
- " root and run the updateApi task."
- )
- return false
- }
-
- return true
}
/**
@@ -162,14 +118,14 @@
listOf(currentApiLocation)
}
- val (javaInputs, androidManifest) =
- configureJavaInputsAndManifest(config) ?: return@afterEvaluate
+ val (compilationInputs, androidManifest) =
+ configureCompilationInputsAndManifest(config) ?: return@afterEvaluate
val baselinesApiLocation = ApiBaselinesLocation.fromApiLocation(currentApiLocation)
val generateApiDependencies = createReleaseApiConfiguration()
MetalavaTasks.setupProject(
project,
- javaInputs,
+ compilationInputs,
generateApiDependencies,
extension,
androidManifest,
@@ -204,27 +160,27 @@
}
}
-internal fun Project.configureJavaInputsAndManifest(
+internal fun Project.configureCompilationInputsAndManifest(
config: ApiTaskConfig
-): Pair<JavaCompileInputs, Provider<RegularFile>?>? {
+): Pair<CompilationInputs, Provider<RegularFile>?>? {
return when (config) {
is LibraryApiTaskConfig -> {
if (config.variant.name != Release.DEFAULT_PUBLISH_CONFIG) {
return null
}
- JavaCompileInputs.fromLibraryVariant(config.variant, project) to
+ CompilationInputs.fromLibraryVariant(config.variant, project) to
config.variant.artifacts.get(SingleArtifact.MERGED_MANIFEST)
}
is AndroidMultiplatformApiTaskConfig -> {
- JavaCompileInputs.fromKmpAndroidTarget(project) to null
+ CompilationInputs.fromKmpAndroidTarget(project) to null
}
is KmpApiTaskConfig -> {
- JavaCompileInputs.fromKmpJvmTarget(project) to null
+ CompilationInputs.fromKmpJvmTarget(project) to null
}
is JavaApiTaskConfig -> {
val javaExtension = extensions.getByType<JavaPluginExtension>()
val mainSourceSet = javaExtension.sourceSets.getByName("main")
- JavaCompileInputs.fromSourceSet(mainSourceSet, this) to null
+ CompilationInputs.fromSourceSet(mainSourceSet, this) to null
}
}
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/checkapi/CompilationInputs.kt b/buildSrc/private/src/main/kotlin/androidx/build/checkapi/CompilationInputs.kt
new file mode 100644
index 0000000..ff46bb5
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/checkapi/CompilationInputs.kt
@@ -0,0 +1,317 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.checkapi
+
+import androidx.build.getAndroidJar
+import androidx.build.multiplatformExtension
+import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
+import com.android.build.api.variant.LibraryAndroidComponentsExtension
+import com.android.build.api.variant.LibraryVariant
+import org.gradle.api.Project
+import org.gradle.api.attributes.Attribute
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.FileCollection
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.SourceSet
+import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
+import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
+
+/**
+ * [CompilationInputs] contains the information required to compile Java/Kotlin code. This can be
+ * helpful for creating Metalava and Kzip tasks with the same settings.
+ *
+ * There are two implementations: [StandardCompilationInputs] for non-multiplatform projects and
+ * [MultiplatformCompilationInputs] for multiplatform projects.
+ */
+internal sealed interface CompilationInputs {
+ /** Source files to process */
+ val sourcePaths: FileCollection
+
+ /** Source files from the KMP common module of this project */
+ val commonModuleSourcePaths: FileCollection
+
+ /** Dependencies (compiled classes) of [sourcePaths]. */
+ val dependencyClasspath: FileCollection
+
+ /** Android's boot classpath. */
+ val bootClasspath: FileCollection
+
+ companion object {
+ /** Constructs a [CompilationInputs] from a library and its variant */
+ fun fromLibraryVariant(variant: LibraryVariant, project: Project): CompilationInputs {
+ // The boot classpath is common to both multiplatform and standard configurations.
+ val bootClasspath =
+ project.files(
+ project.extensions
+ .findByType(LibraryAndroidComponentsExtension::class.java)!!
+ .sdkComponents
+ .bootClasspath
+ )
+
+ // If this is a multiplatform project, set up inputs for the androidJvm compilation
+ val multiplatformExtension = project.multiplatformExtension
+ if (multiplatformExtension != null) {
+ val androidJvmTarget =
+ multiplatformExtension.targets
+ .requirePlatform(KotlinPlatformType.androidJvm)
+ .findCompilation(compilationName = variant.name)
+
+ return MultiplatformCompilationInputs.fromCompilation(
+ project = project,
+ compilationProvider = androidJvmTarget,
+ bootClasspath = bootClasspath,
+ )
+ }
+
+ // Not a multiplatform project, set up standard inputs
+ val kotlinCollection = project.files(variant.sources.kotlin?.all)
+ val javaCollection = project.files(variant.sources.java?.all)
+ val sourceCollection = kotlinCollection + javaCollection
+
+ return StandardCompilationInputs(
+ sourcePaths = sourceCollection,
+ commonModuleSourcePaths = project.files(),
+ dependencyClasspath = variant.compileClasspath,
+ bootClasspath = bootClasspath
+ )
+ }
+
+ /**
+ * Returns the CompilationInputs for the `jvm` target of a KMP project.
+ *
+ * @param project The project whose main jvm target inputs will be returned.
+ */
+ fun fromKmpJvmTarget(project: Project): CompilationInputs {
+ val kmpExtension =
+ checkNotNull(project.multiplatformExtension) {
+ """
+ ${project.path} needs to have Kotlin Multiplatform Plugin applied to obtain its
+ jvm source sets.
+ """
+ .trimIndent()
+ }
+ val jvmTarget = kmpExtension.targets.requirePlatform(KotlinPlatformType.jvm)
+ val jvmCompilation =
+ jvmTarget.findCompilation(compilationName = KotlinCompilation.MAIN_COMPILATION_NAME)
+
+ return MultiplatformCompilationInputs.fromCompilation(
+ project = project,
+ compilationProvider = jvmCompilation,
+ bootClasspath = project.getAndroidJar()
+ )
+ }
+
+ /**
+ * Returns the CompilationInputs for the `android` target of a KMP project.
+ *
+ * @param project The project whose main android target inputs will be returned.
+ */
+ fun fromKmpAndroidTarget(project: Project): CompilationInputs {
+ val kmpExtension =
+ checkNotNull(project.multiplatformExtension) {
+ """
+ ${project.path} needs to have Kotlin Multiplatform Plugin applied to obtain its
+ android source sets.
+ """
+ .trimIndent()
+ }
+ val target =
+ kmpExtension.targets
+ .withType(KotlinMultiplatformAndroidLibraryTarget::class.java)
+ .single()
+ val compilation = target.findCompilation(KotlinCompilation.MAIN_COMPILATION_NAME)
+
+ return MultiplatformCompilationInputs.fromCompilation(
+ project = project,
+ compilationProvider = compilation,
+ bootClasspath = project.getAndroidJar()
+ )
+ }
+
+ /** Constructs a [CompilationInputs] from a sourceset */
+ fun fromSourceSet(sourceSet: SourceSet, project: Project): CompilationInputs {
+ val sourcePaths: FileCollection =
+ project.files(project.provider { sourceSet.allSource.srcDirs })
+ val dependencyClasspath = sourceSet.compileClasspath
+ return StandardCompilationInputs(
+ sourcePaths = sourcePaths,
+ commonModuleSourcePaths = project.files(),
+ dependencyClasspath = dependencyClasspath,
+ bootClasspath = project.getAndroidJar()
+ )
+ }
+
+ /**
+ * Returns the list of Files (might be directories) that are included in the compilation of
+ * this target.
+ *
+ * @param compilationName The name of the compilation. A target might have separate
+ * compilations (e.g. main vs test for jvm or debug vs release for Android)
+ */
+ private fun KotlinTarget.findCompilation(
+ compilationName: String
+ ): Provider<KotlinCompilation<*>> {
+ return project.provider {
+ val selectedCompilation =
+ checkNotNull(compilations.findByName(compilationName)) {
+ """
+ Cannot find $compilationName compilation configuration of $name in
+ ${project.parent}.
+ Available compilations: ${compilations.joinToString(", ") { it.name }}
+ """
+ .trimIndent()
+ }
+ selectedCompilation
+ }
+ }
+
+ /**
+ * Returns the [KotlinTarget] that targets the given platform type.
+ *
+ * This method will throw if there are no matching targets or there are more than 1 matching
+ * target.
+ */
+ private fun Collection<KotlinTarget>.requirePlatform(
+ expectedPlatformType: KotlinPlatformType
+ ): KotlinTarget {
+ return this.singleOrNull { it.platformType == expectedPlatformType }
+ ?: error(
+ """
+ Expected 1 and only 1 kotlin target with $expectedPlatformType. Found $size.
+ Matching compilation targets:
+ ${joinToString(",") { it.name }}
+ All compilation targets:
+ ${[email protected](",") { it.name }}
+ """
+ .trimIndent()
+ )
+ }
+ }
+}
+
+/** Compile inputs for a regular (non-multiplatform) project */
+internal data class StandardCompilationInputs(
+ override val sourcePaths: FileCollection,
+ override val dependencyClasspath: FileCollection,
+ override val bootClasspath: FileCollection,
+ override val commonModuleSourcePaths: FileCollection,
+) : CompilationInputs
+
+/** Compile inputs for a single source set from a multiplatform project. */
+internal data class SourceSetInputs(
+ /** Name of the source set, e.g. "androidMain" */
+ val sourceSetName: String,
+ /** Names of other source sets that this one depends on */
+ val dependsOnSourceSets: List<String>,
+ /** Source files of this source set */
+ val sourcePaths: FileCollection,
+ /** Compile dependencies for this source set */
+ val dependencyClasspath: FileCollection,
+)
+
+/** Inputs for a single compilation of a multiplatform project (just the android or jvm target) */
+internal class MultiplatformCompilationInputs(
+ project: Project,
+ /**
+ * The [SourceSetInputs] for this project's source sets. This is a [Provider] because not all
+ * relationships between source sets will be loaded at configuration time.
+ */
+ val sourceSets: Provider<List<SourceSetInputs>>,
+ override val bootClasspath: FileCollection,
+ override val commonModuleSourcePaths: FileCollection,
+) : CompilationInputs {
+ // Aggregate sources and classpath from all source sets
+ override val sourcePaths: ConfigurableFileCollection =
+ project.files(sourceSets.map { it.map { sourceSet -> sourceSet.sourcePaths } })
+ override val dependencyClasspath: ConfigurableFileCollection =
+ project.files(sourceSets.map { it.map { sourceSet -> sourceSet.dependencyClasspath } })
+
+ companion object {
+ /** Creates inputs based on one compilation of a multiplatform project. */
+ fun fromCompilation(
+ project: Project,
+ compilationProvider: Provider<KotlinCompilation<*>>,
+ bootClasspath: FileCollection,
+ ): MultiplatformCompilationInputs {
+ val compileDependencies =
+ compilationProvider.map { compilation ->
+ // Sometimes an Android source set has the jvm platform type instead of
+ // androidJvm
+ val platformType =
+ if (compilation.defaultSourceSet.name == "androidMain") {
+ KotlinPlatformType.androidJvm
+ } else {
+ compilation.platformType
+ }
+
+ project.configurations
+ .named(compilation.compileDependencyConfigurationName)
+ .map { config ->
+ // AGP adds files from many configurations to the
+ // compileDependencyFiles,
+ // so it needs to be filtered to avoid variant resolution errors.
+ config.incoming
+ .artifactView {
+ val artifactType =
+ if (platformType == KotlinPlatformType.androidJvm) {
+ "android-classes"
+ } else {
+ "jar"
+ }
+ it.attributes.attribute(
+ Attribute.of("artifactType", String::class.java),
+ artifactType
+ )
+ }
+ .files
+ }
+ }
+ val sourceSets =
+ compilationProvider.map { compilation ->
+ compilation.allKotlinSourceSets.map { sourceSet ->
+ SourceSetInputs(
+ sourceSet.name,
+ sourceSet.dependsOn.map { it.name },
+ sourceSet.kotlin.sourceDirectories,
+ project.files(compileDependencies),
+ )
+ }
+ }
+ return MultiplatformCompilationInputs(
+ project,
+ sourceSets,
+ bootClasspath,
+ project.commonModuleSourcePaths(compilationProvider)
+ )
+ }
+
+ private fun Project.commonModuleSourcePaths(
+ kotlinCompilation: Provider<KotlinCompilation<*>>
+ ): ConfigurableFileCollection {
+ return project.files(
+ project.provider {
+ kotlinCompilation
+ .get()
+ .allKotlinSourceSets
+ .filter { it.dependsOn.isEmpty() }
+ .flatMap { it.kotlin.sourceDirectories.files }
+ }
+ )
+ }
+ }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt b/buildSrc/private/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt
deleted file mode 100644
index cb3d5ae..0000000
--- a/buildSrc/private/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt
+++ /dev/null
@@ -1,247 +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.java
-
-import androidx.build.getAndroidJar
-import androidx.build.multiplatformExtension
-import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
-import com.android.build.api.variant.LibraryAndroidComponentsExtension
-import com.android.build.api.variant.LibraryVariant
-import org.gradle.api.Project
-import org.gradle.api.file.ConfigurableFileCollection
-import org.gradle.api.file.FileCollection
-import org.gradle.api.provider.Provider
-import org.gradle.api.tasks.SourceSet
-import org.gradle.kotlin.dsl.get
-import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
-import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
-import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
-
-// JavaCompileInputs contains the information required to compile Java/Kotlin code
-// This can be helpful for creating Metalava and Dokka tasks with the same settings
-data class JavaCompileInputs(
- // Source files to process
- val sourcePaths: FileCollection,
-
- // Source files from the KMP common module of this project
- val commonModuleSourcePaths: FileCollection,
-
- // Dependencies (compiled classes) of [sourcePaths].
- val dependencyClasspath: FileCollection,
-
- // Android's boot classpath.
- val bootClasspath: FileCollection
-) {
- companion object {
- // Constructs a JavaCompileInputs from a library and its variant
- fun fromLibraryVariant(variant: LibraryVariant, project: Project): JavaCompileInputs {
- val kotlinCollection = project.files(variant.sources.kotlin?.all)
- val javaCollection = project.files(variant.sources.java?.all)
-
- val androidJvmTarget =
- project.multiplatformExtension
- ?.targets
- ?.requirePlatform(KotlinPlatformType.androidJvm)
- ?.findCompilation(compilationName = variant.name)
-
- val sourceCollection =
- androidJvmTarget?.let { project.files(project.sourceFiles(it)) }
- ?: (kotlinCollection + javaCollection)
-
- val commonModuleSourceCollection =
- project
- .files(androidJvmTarget?.let { project.commonModuleSourcePaths(it) })
- .builtBy(
- // Remove task dependency when b/332711506 is fixed, which should get us an
- // API to get all sources (static and generated)
- project.tasks.named("compileReleaseJavaWithJavac")
- )
-
- val bootClasspath =
- project.extensions
- .findByType(LibraryAndroidComponentsExtension::class.java)!!
- .sdkComponents
- .bootClasspath
-
- return JavaCompileInputs(
- sourcePaths = sourceCollection,
- commonModuleSourcePaths = commonModuleSourceCollection,
- dependencyClasspath = variant.compileClasspath,
- bootClasspath = project.files(bootClasspath)
- )
- }
-
- /**
- * Returns the JavaCompileInputs for the `jvm` target of a KMP project.
- *
- * @param project The project whose main jvm target inputs will be returned.
- */
- fun fromKmpJvmTarget(project: Project): JavaCompileInputs {
- val kmpExtension =
- checkNotNull(project.multiplatformExtension) {
- """
- ${project.path} needs to have Kotlin Multiplatform Plugin applied to obtain its
- jvm source sets.
- """
- .trimIndent()
- }
- val jvmTarget = kmpExtension.targets.requirePlatform(KotlinPlatformType.jvm)
- val jvmCompilation =
- jvmTarget.findCompilation(compilationName = KotlinCompilation.MAIN_COMPILATION_NAME)
-
- val sourceCollection = project.sourceFiles(jvmCompilation)
-
- val commonModuleSourcePaths = project.commonModuleSourcePaths(jvmCompilation)
-
- return JavaCompileInputs(
- sourcePaths = sourceCollection,
- commonModuleSourcePaths = commonModuleSourcePaths,
- dependencyClasspath =
- jvmTarget.compilations[KotlinCompilation.MAIN_COMPILATION_NAME]
- .compileDependencyFiles,
- bootClasspath = project.getAndroidJar()
- )
- }
-
- /**
- * Returns the JavaCompileInputs for the `android` target of a KMP project.
- *
- * @param project The project whose main android target inputs will be returned.
- */
- fun fromKmpAndroidTarget(project: Project): JavaCompileInputs {
- val kmpExtension =
- checkNotNull(project.multiplatformExtension) {
- """
- ${project.path} needs to have Kotlin Multiplatform Plugin applied to obtain its
- android source sets.
- """
- .trimIndent()
- }
- val target =
- kmpExtension.targets
- .withType(KotlinMultiplatformAndroidLibraryTarget::class.java)
- .single()
- val compilation = target.findCompilation(KotlinCompilation.MAIN_COMPILATION_NAME)
- val sourceCollection = project.sourceFiles(compilation)
-
- val commonModuleSourcePaths = project.commonModuleSourcePaths(compilation)
-
- return JavaCompileInputs(
- sourcePaths = sourceCollection,
- commonModuleSourcePaths = commonModuleSourcePaths,
- dependencyClasspath =
- target.compilations[KotlinCompilation.MAIN_COMPILATION_NAME]
- .compileDependencyFiles,
- bootClasspath = project.getAndroidJar()
- )
- }
-
- // Constructs a JavaCompileInputs from a sourceset
- fun fromSourceSet(sourceSet: SourceSet, project: Project): JavaCompileInputs {
- val sourcePaths: FileCollection =
- project.files(project.provider { sourceSet.allSource.srcDirs })
- val dependencyClasspath = sourceSet.compileClasspath
- return JavaCompileInputs(
- sourcePaths = sourcePaths,
- commonModuleSourcePaths = project.files(),
- dependencyClasspath = dependencyClasspath,
- bootClasspath = project.getAndroidJar()
- )
- }
-
- /**
- * Returns the list of Files (might be directories) that are included in the compilation of
- * this target.
- *
- * @param compilationName The name of the compilation. A target might have separate
- * compilations (e.g. main vs test for jvm or debug vs release for Android)
- */
- private fun KotlinTarget.findCompilation(
- compilationName: String
- ): Provider<KotlinCompilation<*>> {
- return project.provider {
- val selectedCompilation =
- checkNotNull(compilations.findByName(compilationName)) {
- """
- Cannot find $compilationName compilation configuration of $name in
- ${project.parent}.
- Available compilations: ${compilations.joinToString(", ") { it.name }}
- """
- .trimIndent()
- }
- selectedCompilation
- }
- }
-
- private fun Project.sourceFiles(
- kotlinCompilation: Provider<KotlinCompilation<*>>
- ): ConfigurableFileCollection {
- return project.files(
- project.provider {
- kotlinCompilation
- .get()
- .allKotlinSourceSets
- .flatMap { it.kotlin.sourceDirectories }
- .also {
- require(it.isNotEmpty()) {
- """
- Didn't find any source sets for $kotlinCompilation in ${project.path}.
- """
- .trimIndent()
- }
- }
- }
- )
- }
-
- private fun Project.commonModuleSourcePaths(
- kotlinCompilation: Provider<KotlinCompilation<*>>
- ): ConfigurableFileCollection {
- return project.files(
- project.provider {
- kotlinCompilation
- .get()
- .allKotlinSourceSets
- .filter { it.dependsOn.isEmpty() }
- .flatMap { it.kotlin.sourceDirectories.files }
- }
- )
- }
-
- /**
- * Returns the [KotlinTarget] that targets the given platform type.
- *
- * This method will throw if there are no matching targets or there are more than 1 matching
- * target.
- */
- private fun Collection<KotlinTarget>.requirePlatform(
- expectedPlatformType: KotlinPlatformType
- ): KotlinTarget {
- return this.singleOrNull { it.platformType == expectedPlatformType }
- ?: error(
- """
- Expected 1 and only 1 kotlin target with $expectedPlatformType. Found $size.
- Matching compilation targets:
- ${joinToString(",") { it.name }}
- All compilation targets:
- ${[email protected](",") { it.name }}
- """
- .trimIndent()
- )
- }
- }
-}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateJavaKzipTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateJavaKzipTask.kt
index 656e1b4..2177809 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateJavaKzipTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateJavaKzipTask.kt
@@ -16,9 +16,9 @@
package androidx.build.kythe
import androidx.build.addToBuildOnServer
+import androidx.build.checkapi.CompilationInputs
import androidx.build.getCheckoutRoot
import androidx.build.getPrebuiltsRoot
-import androidx.build.java.JavaCompileInputs
import java.io.File
import javax.inject.Inject
import org.gradle.api.DefaultTask
@@ -136,7 +136,7 @@
internal companion object {
fun setupProject(
project: Project,
- javaInputs: JavaCompileInputs,
+ compilationInputs: CompilationInputs,
compiledSources: Configuration,
) {
val annotationProcessorPaths =
@@ -162,10 +162,10 @@
"build-tools/common/javac_extractor.jar"
)
)
- sourcePaths.setFrom(javaInputs.sourcePaths)
+ sourcePaths.setFrom(compilationInputs.sourcePaths)
vnamesJson.set(project.getVnamesJson())
dependencyClasspath.setFrom(
- javaInputs.dependencyClasspath + javaInputs.bootClasspath
+ compilationInputs.dependencyClasspath + compilationInputs.bootClasspath
)
this.compiledSources.setFrom(compiledSources)
kzipOutputFile.set(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateKotlinKzipTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateKotlinKzipTask.kt
index 536f9cc..6a9d62a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateKotlinKzipTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateKotlinKzipTask.kt
@@ -19,10 +19,10 @@
import androidx.build.KotlinTarget
import androidx.build.OperatingSystem
import androidx.build.addToBuildOnServer
+import androidx.build.checkapi.CompilationInputs
import androidx.build.getCheckoutRoot
import androidx.build.getOperatingSystem
import androidx.build.getPrebuiltsRoot
-import androidx.build.java.JavaCompileInputs
import androidx.build.multiplatformExtension
import java.io.File
import java.util.jar.JarOutputStream
@@ -188,7 +188,7 @@
internal companion object {
fun setupProject(
project: Project,
- javaInputs: JavaCompileInputs,
+ compilationInputs: CompilationInputs,
compiledSources: Configuration,
kotlinTarget: Property<KotlinTarget>,
javaVersion: JavaVersion,
@@ -208,11 +208,11 @@
"build-tools/${osName()}/bin/kotlinc_extractor"
)
)
- sourcePaths.setFrom(javaInputs.sourcePaths)
- commonModuleSourcePaths.from(javaInputs.commonModuleSourcePaths)
+ sourcePaths.setFrom(compilationInputs.sourcePaths)
+ commonModuleSourcePaths.from(compilationInputs.commonModuleSourcePaths)
vnamesJson.set(project.getVnamesJson())
dependencyClasspath.setFrom(
- javaInputs.dependencyClasspath + javaInputs.bootClasspath
+ compilationInputs.dependencyClasspath + compilationInputs.bootClasspath
)
this.compiledSources.setFrom(compiledSources)
this.kotlinTarget.set(kotlinTarget)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/kythe/KzipTasks.kt b/buildSrc/private/src/main/kotlin/androidx/build/kythe/KzipTasks.kt
index 481acc4..56d327b 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/kythe/KzipTasks.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/kythe/KzipTasks.kt
@@ -19,7 +19,7 @@
import androidx.build.AndroidXExtension
import androidx.build.ProjectLayoutType
import androidx.build.checkapi.ApiTaskConfig
-import androidx.build.checkapi.configureJavaInputsAndManifest
+import androidx.build.checkapi.configureCompilationInputsAndManifest
import androidx.build.checkapi.createReleaseApiConfiguration
import androidx.build.getDefaultTargetJavaVersion
import androidx.build.getSupportRootFolder
@@ -58,18 +58,19 @@
// afterEvaluate required to read extension properties
afterEvaluate {
- val (javaInputs, _) = configureJavaInputsAndManifest(config) ?: return@afterEvaluate
+ val (compilationInputs, _) =
+ configureCompilationInputsAndManifest(config) ?: return@afterEvaluate
val compiledSources = createReleaseApiConfiguration()
GenerateKotlinKzipTask.setupProject(
project,
- javaInputs,
+ compilationInputs,
compiledSources,
extension.kotlinTarget,
getDefaultTargetJavaVersion(extension.type, project.name)
)
- GenerateJavaKzipTask.setupProject(project, javaInputs, compiledSources)
+ GenerateJavaKzipTask.setupProject(project, compilationInputs, compiledSources)
}
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt
index 806aa75..7024071 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt
@@ -19,7 +19,7 @@
import androidx.build.Version
import androidx.build.checkapi.ApiBaselinesLocation
import androidx.build.checkapi.ApiLocation
-import androidx.build.java.JavaCompileInputs
+import androidx.build.checkapi.StandardCompilationInputs
import java.io.File
import javax.inject.Inject
import org.gradle.api.file.Directory
@@ -95,7 +95,7 @@
}
val inputs =
- JavaCompileInputs(
+ StandardCompilationInputs(
sourcePaths = sourcePaths,
commonModuleSourcePaths = commonModuleSourcePaths,
dependencyClasspath = dependencyClasspath,
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
index 82770b8..730d085 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
@@ -18,8 +18,8 @@
import androidx.build.Version
import androidx.build.checkapi.ApiLocation
+import androidx.build.checkapi.CompilationInputs
import androidx.build.getLibraryByName
-import androidx.build.java.JavaCompileInputs
import androidx.build.logging.TERMINAL_RED
import androidx.build.logging.TERMINAL_RESET
import java.io.ByteArrayOutputStream
@@ -240,9 +240,9 @@
/**
* Generates all of the specified api files, as well as a version history JSON for the public API.
*/
-fun generateApi(
+internal fun generateApi(
metalavaClasspath: FileCollection,
- files: JavaCompileInputs,
+ files: CompilationInputs,
apiLocation: ApiLocation,
apiLintMode: ApiLintMode,
includeRestrictToLibraryGroupApis: Boolean,
@@ -284,7 +284,7 @@
*/
private fun generateApi(
metalavaClasspath: FileCollection,
- files: JavaCompileInputs,
+ files: CompilationInputs,
outputLocation: ApiLocation,
generateApiMode: GenerateApiMode,
apiLintMode: ApiLintMode,
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt
index f385c0f..544231d 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt
@@ -22,8 +22,8 @@
import androidx.build.addToCheckTask
import androidx.build.checkapi.ApiBaselinesLocation
import androidx.build.checkapi.ApiLocation
+import androidx.build.checkapi.CompilationInputs
import androidx.build.checkapi.getRequiredCompatibilityApiLocation
-import androidx.build.java.JavaCompileInputs
import androidx.build.uptodatedness.cacheEvenIfNoOutputs
import androidx.build.version
import org.gradle.api.Project
@@ -33,11 +33,11 @@
import org.gradle.api.tasks.TaskProvider
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
-object MetalavaTasks {
+internal object MetalavaTasks {
fun setupProject(
project: Project,
- javaCompileInputs: JavaCompileInputs,
+ compilationInputs: CompilationInputs,
generateApiDependencies: Configuration,
extension: AndroidXExtension,
androidManifest: Provider<RegularFile>?,
@@ -71,7 +71,7 @@
task.currentVersion.set(version)
androidManifest?.let { task.manifestPath.set(it) }
- applyInputs(javaCompileInputs, task, generateApiDependencies)
+ applyInputs(compilationInputs, task, generateApiDependencies)
// If we will be updating the api lint baselines, then we should do that before
// using it to validate the generated api
task.mustRunAfter("updateApiLintBaseline")
@@ -92,8 +92,8 @@
task.baselines.set(baselinesApiLocation)
task.api.set(builtApiLocation)
task.version.set(version)
- task.dependencyClasspath = javaCompileInputs.dependencyClasspath
- task.bootClasspath = javaCompileInputs.bootClasspath
+ task.dependencyClasspath = compilationInputs.dependencyClasspath
+ task.bootClasspath = compilationInputs.bootClasspath
task.k2UastEnabled.set(extension.metalavaK2UastEnabled)
task.kotlinSourceLevel.set(kotlinSourceLevel)
task.cacheEvenIfNoOutputs()
@@ -108,8 +108,8 @@
task.baselines.set(checkApiRelease!!.flatMap { it.baselines })
task.api.set(builtApiLocation)
task.version.set(version)
- task.dependencyClasspath = javaCompileInputs.dependencyClasspath
- task.bootClasspath = javaCompileInputs.bootClasspath
+ task.dependencyClasspath = compilationInputs.dependencyClasspath
+ task.bootClasspath = compilationInputs.bootClasspath
task.k2UastEnabled.set(extension.metalavaK2UastEnabled)
task.kotlinSourceLevel.set(kotlinSourceLevel)
task.dependsOn(generateApi)
@@ -127,7 +127,7 @@
task.k2UastEnabled.set(extension.metalavaK2UastEnabled)
task.kotlinSourceLevel.set(kotlinSourceLevel)
androidManifest?.let { task.manifestPath.set(it) }
- applyInputs(javaCompileInputs, task, generateApiDependencies)
+ applyInputs(compilationInputs, task, generateApiDependencies)
}
// Policy: All changes to API surfaces for which compatibility is enforced must be
@@ -208,7 +208,7 @@
}
private fun applyInputs(
- inputs: JavaCompileInputs,
+ inputs: CompilationInputs,
task: MetalavaTask,
generateApiDependencies: Configuration
) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt
index 70390ec..cbb8eda 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt
@@ -18,13 +18,14 @@
import androidx.build.Version
import androidx.build.checkapi.ApiLocation
+import androidx.build.checkapi.CompilationInputs
+import androidx.build.checkapi.StandardCompilationInputs
import androidx.build.checkapi.getApiFileVersion
import androidx.build.checkapi.getRequiredCompatibilityApiLocation
import androidx.build.checkapi.getVersionedApiLocation
import androidx.build.checkapi.isValidArtifactVersion
import androidx.build.getAndroidJar
import androidx.build.getCheckoutRoot
-import androidx.build.java.JavaCompileInputs
import java.io.File
import javax.inject.Inject
import org.gradle.api.DefaultTask
@@ -159,7 +160,7 @@
outputApiLocation: ApiLocation,
) {
val mavenId = "$groupId:$artifactId:$version"
- val inputs: JavaCompileInputs?
+ val inputs: CompilationInputs?
try {
inputs = getFiles(runnerProject, mavenId)
} catch (e: TypedResolveException) {
@@ -185,14 +186,13 @@
}
}
- private fun getFiles(runnerProject: Project, mavenId: String): JavaCompileInputs {
+ private fun getFiles(runnerProject: Project, mavenId: String): CompilationInputs {
val jars = getJars(runnerProject, mavenId)
val sources = getSources(runnerProject, "$mavenId:sources")
- return JavaCompileInputs(
+ // TODO(b/330721660) parse META-INF/kotlin-project-structure-metadata.json for KMP projects
+ return StandardCompilationInputs(
sourcePaths = sources,
- // TODO(b/330721660) parse META-INF/kotlin-project-structure-metadata.json for
- // common sources
commonModuleSourcePaths = project.files(),
dependencyClasspath = jars,
bootClasspath = project.getAndroidJar()
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/AndroidXExtension.kt b/buildSrc/public/src/main/kotlin/androidx/build/AndroidXExtension.kt
index 99b1d02..d3272c5 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/AndroidXExtension.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/AndroidXExtension.kt
@@ -294,26 +294,9 @@
private var extraLicenses: MutableCollection<License> = ArrayList()
- // Should only be used to override LibraryType.publish, if a library isn't ready to publish yet
- var publish: Publish = Publish.UNSET
+ fun shouldPublish(): Boolean = type.publish.shouldPublish()
- fun shouldPublish(): Boolean =
- if (publish != Publish.UNSET) {
- publish.shouldPublish()
- } else if (type != LibraryType.UNSET) {
- type.publish.shouldPublish()
- } else {
- false
- }
-
- fun shouldRelease(): Boolean =
- if (publish != Publish.UNSET) {
- publish.shouldRelease()
- } else if (type != LibraryType.UNSET) {
- type.publish.shouldRelease()
- } else {
- false
- }
+ fun shouldRelease(): Boolean = type.publish.shouldRelease()
fun ifReleasing(action: () -> Unit) {
project.afterEvaluate {
@@ -323,31 +306,12 @@
}
}
- fun isPublishConfigured(): Boolean = (publish != Publish.UNSET || type.publish != Publish.UNSET)
-
fun shouldPublishSbom(): Boolean {
if (isIsolatedProjectsEnabled()) return false
// IDE plugins are used by and ship inside Studio
return shouldPublish() || type == LibraryType.IDE_PLUGIN
}
- /**
- * Whether to run API tasks such as tracking and linting. The default value is
- * [RunApiTasks.Auto], which automatically picks based on the project's properties.
- */
- // TODO: decide whether we want to support overriding runApiTasks
- // @Deprecated("Replaced with AndroidXExtension.type: LibraryType.runApiTasks")
- var runApiTasks: RunApiTasks = RunApiTasks.Auto
- get() = if (field == RunApiTasks.Auto && type != LibraryType.UNSET) type.checkApi else field
- set(value) {
- if (value is RunApiTasks.No) {
- throw GradleException(
- "runApiTasks cannot be disabled from the AndroidX extension. Ensure you're using the correct library type if you really do not need API tracking"
- )
- }
- field = value
- }
-
var doNotDocumentReason: String? = null
var type: LibraryType = LibraryType.UNSET
@@ -387,7 +351,7 @@
}
fun shouldEnforceKotlinStrictApiMode(): Boolean {
- return !legacyDisableKotlinStrictApiMode && runApiTasks is RunApiTasks.Yes
+ return !legacyDisableKotlinStrictApiMode && type.checkApi is RunApiTasks.Yes
}
fun extraLicense(closure: Closure<Any>): License {
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
index b0be6fb..8347d6d 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
@@ -16,205 +16,304 @@
package androidx.build
+import androidx.build.LibraryType.Companion.BENCHMARK
+import androidx.build.LibraryType.Companion.SAMPLES
+import androidx.build.LibraryType.Companion.TEST_APPLICATION
+import androidx.build.LibraryType.Companion.UNSET
+import kotlin.collections.contains
+
/**
- * LibraryType represents the purpose and type of a library, whether it is a conventional library, a
- * set of samples showing how to use a conventional library, a set of lint rules for using a
- * conventional library, or any other type of published project.
+ * Represents the purpose and configuration of a library, including how it is published, whether it
+ * enforces API compatibility checks, and which environment it targets. By using [LibraryType],
+ * developers can select from predefined library configurations or create their own through
+ * [ConfigurableLibrary]. This reduces complexity by capturing a library's behavior and rationale in
+ * one place, rather than requiring manual configuration of multiple independent properties.
*
- * LibraryType collects a set of properties together, to make the "why" more clear and to simplify
- * setting these properties for library developers, so that only a single enum inferrable from the
- * purpose of the library needs to be set, rather than a variety of more arcane options.
+ * The key properties controlled by [LibraryType] are:
+ * - [publish]: Defines how (or if) the library is published to external repositories (e.g.,
+ * GMaven).
+ * - [checkApi]: Determines whether API compatibility tasks are run, which enforce semantic
+ * versioning and API stability.
+ * - [compilationTarget]: Specifies whether the library runs on a host machine or an Android device.
+ * - [allowCallingVisibleForTestsApis]: Indicates whether calling `@VisibleForTesting` APIs is
+ * allowed, useful for test libraries.
+ * - [targetsKotlinConsumersOnly]: When `true`, the library is intended for Kotlin consumers only,
+ * allowing for more Kotlin-centric API design.
*
- * These properties are as follows: LibraryType.publish represents how the library is published to
- * GMaven LibraryType.checkApi represents whether we enforce API compatibility of the library
- * according to our semantic versioning protocol
+ * [LibraryType] includes a variety of predefined configurations commonly used in Android libraries:
+ * - Conventional published libraries ([PUBLISHED_LIBRARY], [PUBLISHED_PROTO_LIBRARY], etc.)
+ * - Internal libraries not published externally ([INTERNAL_TEST_LIBRARY],
+ * [INTERNAL_HOST_TEST_LIBRARY])
+ * - Test libraries that allow testing internal or unstable APIs ([PUBLISHED_TEST_LIBRARY],
+ * [INTERNAL_TEST_LIBRARY])
+ * - Lint rule sets ([LINT], [STANDALONE_PUBLISHED_LINT]) for guiding correct usage of a library
+ * - Libraries containing samples to supplement documentation ([SAMPLES])
+ * - Host-only libraries such as Gradle plugins, annotation processors, and code generators
+ * ([GRADLE_PLUGIN], [ANNOTATION_PROCESSOR], [OTHER_CODE_PROCESSOR])
+ * - Libraries specifically meant for IDE consumption ([IDE_PLUGIN])
+ * - Snapshot-only libraries for early access or development use cases
+ * ([SNAPSHOT_ONLY_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS], etc.)
+ * - Libraries that do not publish artifacts but still run API tasks, or vice versa
+ * ([INTERNAL_LIBRARY_WITH_API_TASKS], [SNAPSHOT_ONLY_LIBRARY_WITH_API_TASKS])
+ * - [UNSET]: a default or transitional state indicating the library's type isn't fully determined
*
- * The possible values of LibraryType are as follows:
- * - [PUBLISHED_LIBRARY]: a conventional library published, sourced, documented, and versioned.
- * - [PUBLISHED_TEST_LIBRARY]: [PUBLISHED_LIBRARY], but allows calling `@VisibleForTesting` API.
- * Used for libraries that allow developers to test code that uses your library. Often provides
- * test fakes.
- * - [INTERNAL_TEST_LIBRARY]: unpublished, untracked, undocumented. Used in internal tests. Usually
- * contains integration tests, but is _not_ an app. Runs device tests.
- * - [INTERNAL_HOST_TEST_LIBRARY]: as [INTERNAL_TEST_LIBRARY], but runs host tests instead. Avoid
- * mixing host tests and device tests in the same library, for performance / test-result-caching
- * reasons.
- * - [SAMPLES]: a library containing sample code referenced in your library's documentation with
- * `@sampled`, published as a documentation-related supplement to a conventional library.
- * - [LINT]: a library of lint rules for using a conventional library. Published through lintPublish
- * as part of an AAR, not published standalone.
- * - [GRADLE_PLUGIN]: a library that is a gradle plugin.
- * - [ANNOTATION_PROCESSOR]: a library consisting of an annotation processor. Used only while
- * compiling.
- * - [ANNOTATION_PROCESSOR_UTILS]: contains reference code for understanding an annotation
- * processor. Publishes source jars, but does not track API.
- * - [OTHER_CODE_PROCESSOR]: a library that algorithmically generates and/or alters code but not
- * through hooking into custom annotations or the kotlin compiler. For example,
- * navigation:safe-args-generator or Jetifier.
- * - [IDE_PLUGIN]: a library that should only ever be downloaded by studio. Unfortunately, we don't
- * yet have a good way to track API for these. b/281843422
- * - [UNSET]: a library that has not yet been migrated to using LibraryType. Should never be used.
+ * Although predefined library types cover many common scenarios, you can create new
+ * [ConfigurableLibrary] instances if your project requires a unique combination of publish
+ * settings, API checking, and compilation targeting. In doing so, you ensure the project's
+ * configuration is concise, clear, and consistently applied.
*/
sealed class LibraryType(
+ val name: String,
val publish: Publish = Publish.NONE,
val checkApi: RunApiTasks = RunApiTasks.No("Unknown Library Type"),
val compilationTarget: CompilationTarget = CompilationTarget.DEVICE,
val allowCallingVisibleForTestsApis: Boolean = false,
val targetsKotlinConsumersOnly: Boolean = false
) {
- val name: String
- get() = javaClass.simpleName
-
- companion object {
- @JvmStatic val ANNOTATION_PROCESSOR = AnnotationProcessor()
- @JvmStatic val ANNOTATION_PROCESSOR_UTILS = AnnotationProcessorUtils()
- @JvmStatic val GRADLE_PLUGIN = GradlePlugin()
- @JvmStatic val IDE_PLUGIN = IdePlugin()
- @JvmStatic val INTERNAL_TEST_LIBRARY = InternalTestLibrary()
- @JvmStatic val INTERNAL_HOST_TEST_LIBRARY = InternalHostTestLibrary()
- @JvmStatic val LINT = Lint()
- @JvmStatic val STANDALONE_PUBLISHED_LINT = StandalonePublishedLint()
- @JvmStatic val PUBLISHED_LIBRARY = PublishedLibrary()
- @JvmStatic
- val PUBLISHED_PROTO_LIBRARY =
- PublishedLibrary(
- checkApi =
- RunApiTasks.No("Metalava doesn't properly parse the proto sources b/180579063")
- )
- @JvmStatic
- val PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS =
- PublishedLibrary(targetsKotlinConsumersOnly = true)
- @JvmStatic val PUBLISHED_TEST_LIBRARY = PublishedTestLibrary()
- @JvmStatic
- val PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY =
- PublishedTestLibrary(targetsKotlinConsumersOnly = true)
- @JvmStatic val SAMPLES = Samples()
- @JvmStatic val OTHER_CODE_PROCESSOR = OtherCodeProcessor()
- val UNSET = Unset()
-
- private val allTypes =
- mapOf(
- "PUBLISHED_LIBRARY" to PUBLISHED_LIBRARY,
- "PUBLISHED_PROTO_LIBRARY" to PUBLISHED_PROTO_LIBRARY,
- "PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS" to
- PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS,
- "PUBLISHED_TEST_LIBRARY" to PUBLISHED_TEST_LIBRARY,
- "PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY" to PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY,
- "INTERNAL_TEST_LIBRARY" to INTERNAL_TEST_LIBRARY,
- "INTERNAL_HOST_TEST_LIBRARY" to INTERNAL_HOST_TEST_LIBRARY,
- "SAMPLES" to SAMPLES,
- "LINT" to LINT,
- "STANDALONE_PUBLISHED_LINT" to STANDALONE_PUBLISHED_LINT,
- "GRADLE_PLUGIN" to GRADLE_PLUGIN,
- "ANNOTATION_PROCESSOR" to ANNOTATION_PROCESSOR,
- "ANNOTATION_PROCESSOR_UTILS" to ANNOTATION_PROCESSOR_UTILS,
- "OTHER_CODE_PROCESSOR" to OTHER_CODE_PROCESSOR,
- "IDE_PLUGIN" to IDE_PLUGIN,
- "UNSET" to UNSET,
- )
-
- fun valueOf(name: String): LibraryType {
- val result = allTypes[name]
- check(result != null) { "LibraryType with name $name not found" }
- return result
- }
- }
-
- open class PublishedLibrary(
- checkApi: RunApiTasks = RunApiTasks.Yes(),
+ class ConfigurableLibrary(
+ name: String,
+ publish: Publish = Publish.NONE,
+ checkApi: RunApiTasks = RunApiTasks.No("Unknown Library Type"),
+ compilationTarget: CompilationTarget = CompilationTarget.DEVICE,
allowCallingVisibleForTestsApis: Boolean = false,
targetsKotlinConsumersOnly: Boolean = false
) :
LibraryType(
- publish = Publish.SNAPSHOT_AND_RELEASE,
- checkApi = checkApi,
- allowCallingVisibleForTestsApis = allowCallingVisibleForTestsApis,
- targetsKotlinConsumersOnly = targetsKotlinConsumersOnly
+ name,
+ publish,
+ checkApi,
+ compilationTarget,
+ allowCallingVisibleForTestsApis,
+ targetsKotlinConsumersOnly
)
- open class InternalLibrary(
- compilationTarget: CompilationTarget = CompilationTarget.DEVICE,
- allowCallingVisibleForTestsApis: Boolean = false
- ) :
- LibraryType(
- checkApi = RunApiTasks.No("Internal Library"),
- compilationTarget = compilationTarget,
- allowCallingVisibleForTestsApis = allowCallingVisibleForTestsApis
- )
+ companion object {
+ // Host-only tooling libraries
+ val ANNOTATION_PROCESSOR =
+ ConfigurableLibrary(
+ name = "ANNOTATION_PROCESSOR",
+ publish = Publish.SNAPSHOT_AND_RELEASE,
+ checkApi = RunApiTasks.No("Annotation Processor"),
+ compilationTarget = CompilationTarget.HOST
+ )
- class PublishedTestLibrary(targetsKotlinConsumersOnly: Boolean = false) :
- PublishedLibrary(
- allowCallingVisibleForTestsApis = true,
- targetsKotlinConsumersOnly = targetsKotlinConsumersOnly
- )
+ val ANNOTATION_PROCESSOR_UTILS =
+ ConfigurableLibrary(
+ name = "ANNOTATION_PROCESSOR_UTILS",
+ publish = Publish.SNAPSHOT_AND_RELEASE,
+ checkApi = RunApiTasks.No("Annotation Processor Helper Library"),
+ compilationTarget = CompilationTarget.HOST
+ )
- class InternalTestLibrary() : InternalLibrary(allowCallingVisibleForTestsApis = true)
+ val GRADLE_PLUGIN =
+ ConfigurableLibrary(
+ name = "GRADLE_PLUGIN",
+ publish = Publish.SNAPSHOT_AND_RELEASE,
+ checkApi = RunApiTasks.No("Gradle Plugin (Host-only)"),
+ compilationTarget = CompilationTarget.HOST
+ )
- class InternalHostTestLibrary() : InternalLibrary(CompilationTarget.HOST)
+ val OTHER_CODE_PROCESSOR =
+ ConfigurableLibrary(
+ name = "OTHER_CODE_PROCESSOR",
+ publish = Publish.SNAPSHOT_AND_RELEASE,
+ checkApi = RunApiTasks.No("Code Processor (Host-only)"),
+ compilationTarget = CompilationTarget.HOST
+ )
- class Samples :
- LibraryType(
- publish = Publish.SNAPSHOT_AND_RELEASE,
- checkApi = RunApiTasks.No("Sample Library")
- )
+ // Lint libraries
+ val LINT =
+ ConfigurableLibrary(
+ name = "LINT",
+ checkApi = RunApiTasks.No("Lint Library"),
+ compilationTarget = CompilationTarget.HOST
+ )
- class Lint :
- LibraryType(
- publish = Publish.NONE,
- checkApi = RunApiTasks.No("Lint Library"),
- compilationTarget = CompilationTarget.HOST
- )
+ val STANDALONE_PUBLISHED_LINT =
+ ConfigurableLibrary(
+ name = "STANDALONE_PUBLISHED_LINT",
+ publish = Publish.SNAPSHOT_AND_RELEASE,
+ checkApi = RunApiTasks.No("Lint Library"),
+ compilationTarget = CompilationTarget.HOST
+ )
- class StandalonePublishedLint :
- LibraryType(
- publish = Publish.SNAPSHOT_AND_RELEASE,
- checkApi = RunApiTasks.No("Lint Library"),
- compilationTarget = CompilationTarget.HOST
- )
+ // Published libraries
+ val PUBLISHED_LIBRARY =
+ ConfigurableLibrary(
+ name = "PUBLISHED_LIBRARY",
+ publish = Publish.SNAPSHOT_AND_RELEASE,
+ checkApi = RunApiTasks.Yes()
+ )
- class GradlePlugin :
- LibraryType(
- Publish.SNAPSHOT_AND_RELEASE,
- RunApiTasks.No("Gradle Plugin (Host-only)"),
- CompilationTarget.HOST
- )
+ val PUBLISHED_PROTO_LIBRARY =
+ ConfigurableLibrary(
+ name = "PUBLISHED_PROTO_LIBRARY",
+ publish = Publish.SNAPSHOT_AND_RELEASE,
+ checkApi =
+ RunApiTasks.No("Metalava doesn't properly parse the proto sources b/180579063")
+ )
- class AnnotationProcessor :
- LibraryType(
- publish = Publish.SNAPSHOT_AND_RELEASE,
- checkApi = RunApiTasks.No("Annotation Processor"),
- compilationTarget = CompilationTarget.HOST
- )
+ val PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS =
+ ConfigurableLibrary(
+ name = "PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS",
+ publish = Publish.SNAPSHOT_AND_RELEASE,
+ checkApi = RunApiTasks.Yes(),
+ targetsKotlinConsumersOnly = true
+ )
- class AnnotationProcessorUtils :
- LibraryType(
- publish = Publish.SNAPSHOT_AND_RELEASE,
- checkApi = RunApiTasks.No("Annotation Processor Helper Library"),
- compilationTarget = CompilationTarget.HOST
- )
+ // Published test libraries
+ val PUBLISHED_TEST_LIBRARY =
+ ConfigurableLibrary(
+ name = "PUBLISHED_TEST_LIBRARY",
+ publish = Publish.SNAPSHOT_AND_RELEASE,
+ checkApi = RunApiTasks.Yes(),
+ allowCallingVisibleForTestsApis = true
+ )
- class OtherCodeProcessor(publish: Publish = Publish.SNAPSHOT_AND_RELEASE) :
- LibraryType(
- publish = publish,
- checkApi = RunApiTasks.No("Code Processor (Host-only)"),
- compilationTarget = CompilationTarget.HOST
- )
+ val PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY =
+ ConfigurableLibrary(
+ name = "PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY",
+ publish = Publish.SNAPSHOT_AND_RELEASE,
+ checkApi = RunApiTasks.Yes(),
+ allowCallingVisibleForTestsApis = true,
+ targetsKotlinConsumersOnly = true
+ )
- class IdePlugin :
- LibraryType(
- publish = Publish.NONE,
- // TODO: figure out a way to make sure we don't break Studio
- checkApi = RunApiTasks.No("IDE Plugin (consumed only by Android Studio"),
- // This is a bit complicated. IDE plugins usually have an on-device component installed
- // by
- // Android Studio, rather than by a client of the library, but also a host-side
- // component.
- compilationTarget = CompilationTarget.DEVICE
- )
+ // Snapshot-only libraries
+ val SNAPSHOT_ONLY_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS =
+ ConfigurableLibrary(
+ name = "SNAPSHOT_ONLY_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS",
+ publish = Publish.SNAPSHOT_ONLY,
+ checkApi = RunApiTasks.Yes(),
+ targetsKotlinConsumersOnly = true
+ )
- class Unset : LibraryType()
+ val SNAPSHOT_ONLY_TEST_LIBRARY_WITH_API_TASKS =
+ ConfigurableLibrary(
+ name = "SNAPSHOT_ONLY_TEST_LIBRARY_WITH_API_TASKS",
+ publish = Publish.SNAPSHOT_ONLY,
+ checkApi = RunApiTasks.Yes(),
+ allowCallingVisibleForTestsApis = true
+ )
+
+ val SNAPSHOT_ONLY_LIBRARY_WITH_API_TASKS =
+ ConfigurableLibrary(
+ name = "SNAPSHOT_ONLY_LIBRARY_WITH_API_TASKS",
+ publish = Publish.SNAPSHOT_ONLY,
+ checkApi = RunApiTasks.Yes("Snapshot-only library that runs API tasks")
+ )
+
+ val SNAPSHOT_ONLY_LIBRARY =
+ ConfigurableLibrary(
+ name = "SNAPSHOT_ONLY_LIBRARY",
+ publish = Publish.SNAPSHOT_ONLY,
+ checkApi = RunApiTasks.No("Snapshot-only library that does not run API tasks")
+ )
+
+ // Samples library
+ val SAMPLES =
+ ConfigurableLibrary(
+ name = "SAMPLES",
+ publish = Publish.SNAPSHOT_AND_RELEASE,
+ checkApi = RunApiTasks.No("Sample Library")
+ )
+
+ // IDE libraries
+ val IDE_PLUGIN =
+ ConfigurableLibrary(
+ name = "IDE_PLUGIN",
+ checkApi = RunApiTasks.No("IDE Plugin (consumed only by Android Studio)"),
+ compilationTarget = CompilationTarget.DEVICE
+ )
+
+ // Internal libraries
+ val INTERNAL_GRADLE_PLUGIN =
+ ConfigurableLibrary(
+ name = "INTERNAL_GRADLE_PLUGIN",
+ checkApi = RunApiTasks.No("Internal Gradle Plugin"),
+ compilationTarget = CompilationTarget.HOST
+ )
+
+ val INTERNAL_HOST_TEST_LIBRARY =
+ ConfigurableLibrary(
+ name = "INTERNAL_HOST_TEST_LIBRARY",
+ checkApi = RunApiTasks.No("Internal Library"),
+ compilationTarget = CompilationTarget.HOST
+ )
+
+ val INTERNAL_LIBRARY_WITH_API_TASKS =
+ ConfigurableLibrary(
+ name = "INTERNAL_LIBRARY_WITH_API_TASKS",
+ checkApi = RunApiTasks.Yes("Always run API tasks even if not published")
+ )
+
+ val INTERNAL_OTHER_CODE_PROCESSOR =
+ ConfigurableLibrary(
+ name = "INTERNAL_OTHER_CODE_PROCESSOR",
+ checkApi = RunApiTasks.No("Code Processor (Host-only)"),
+ compilationTarget = CompilationTarget.HOST
+ )
+
+ val INTERNAL_TEST_LIBRARY =
+ ConfigurableLibrary(
+ name = "INTERNAL_TEST_LIBRARY",
+ checkApi = RunApiTasks.No("Internal Library"),
+ allowCallingVisibleForTestsApis = true
+ )
+
+ // Misc libraries
+ val BENCHMARK =
+ ConfigurableLibrary(
+ name = "BENCHMARK",
+ checkApi = RunApiTasks.No("Benchmark Library"),
+ allowCallingVisibleForTestsApis = true
+ )
+
+ val TEST_APPLICATION =
+ ConfigurableLibrary(name = "TEST_APPLICATION", checkApi = RunApiTasks.No("Test App"))
+
+ val UNSET = ConfigurableLibrary(name = "UNSET")
+
+ private val allTypes: Map<String, LibraryType> by lazy {
+ listOf(
+ PUBLISHED_LIBRARY,
+ PUBLISHED_PROTO_LIBRARY,
+ PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS,
+ PUBLISHED_TEST_LIBRARY,
+ PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY,
+ INTERNAL_GRADLE_PLUGIN,
+ INTERNAL_HOST_TEST_LIBRARY,
+ INTERNAL_LIBRARY_WITH_API_TASKS,
+ INTERNAL_OTHER_CODE_PROCESSOR,
+ INTERNAL_TEST_LIBRARY,
+ SAMPLES,
+ SNAPSHOT_ONLY_LIBRARY,
+ SNAPSHOT_ONLY_LIBRARY_WITH_API_TASKS,
+ SNAPSHOT_ONLY_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS,
+ SNAPSHOT_ONLY_TEST_LIBRARY_WITH_API_TASKS,
+ TEST_APPLICATION,
+ LINT,
+ STANDALONE_PUBLISHED_LINT,
+ GRADLE_PLUGIN,
+ ANNOTATION_PROCESSOR,
+ ANNOTATION_PROCESSOR_UTILS,
+ BENCHMARK,
+ OTHER_CODE_PROCESSOR,
+ IDE_PLUGIN,
+ UNSET
+ )
+ .associateBy { it.name }
+ }
+
+ fun valueOf(name: String): LibraryType {
+ return requireNotNull(allTypes[name]) { "LibraryType with name $name not found" }
+ }
+ }
}
+fun LibraryType.requiresDependencyVerification(): Boolean =
+ this !in listOf(BENCHMARK, SAMPLES, TEST_APPLICATION, UNSET)
+
enum class CompilationTarget {
/** This library is meant to run on the host machine (like an annotation processor). */
HOST,
@@ -226,17 +325,11 @@
* Publish Enum: Publish.NONE -> Generates no artifacts; does not generate snapshot artifacts or
* releasable maven artifacts Publish.SNAPSHOT_ONLY -> Only generates snapshot artifacts
* Publish.SNAPSHOT_AND_RELEASE -> Generates both snapshot artifacts and releasable maven artifact
- * Publish.UNSET -> Do the default, based on LibraryType. If LibraryType.UNSET -> Publish.NONE
- *
- * TODO: should we introduce a Publish.lintPublish?
- * TODO: remove Publish.UNSET once we remove LibraryType.UNSET. It is necessary now in order to be
- * able to override LibraryType.publish (with Publish.None)
*/
enum class Publish {
NONE,
SNAPSHOT_ONLY,
- SNAPSHOT_AND_RELEASE,
- UNSET;
+ SNAPSHOT_AND_RELEASE;
fun shouldRelease() = this == SNAPSHOT_AND_RELEASE
@@ -244,8 +337,6 @@
}
sealed class RunApiTasks {
- /** Automatically determine whether API tasks should be run. */
- object Auto : RunApiTasks()
/** Always run API tasks regardless of other project properties. */
data class Yes(val reason: String? = null) : RunApiTasks()
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt
index a9321e0..8fc9568 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt
@@ -236,7 +236,9 @@
override fun onActive(session: CameraCaptureSessionWrapper) {}
}
- if (!cameraDeviceWrapper.createCaptureSession(listOf(surface), callback)) {
+ if (cameraDeviceWrapper.createCaptureSession(listOf(surface), callback)) {
+ sessionConfigured.await()
+ } else {
Log.error {
"Failed to create a blank capture session! " +
"Surfaces may not be disconnected properly."
@@ -246,7 +248,6 @@
surfaceTexture.release()
}
}
- sessionConfigured.await()
}
companion object {
diff --git a/camera/camera-extensions-stub/build.gradle b/camera/camera-extensions-stub/build.gradle
index 67cafd2..5386fd8 100644
--- a/camera/camera-extensions-stub/build.gradle
+++ b/camera/camera-extensions-stub/build.gradle
@@ -13,7 +13,7 @@
*/
- import androidx.build.Publish
+ import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -27,8 +27,7 @@
androidx {
name = "Camera OEM Extensions Stub"
- publish = Publish.NONE
-
+ type = LibraryType.UNSET
inceptionYear = "2019"
description = "OEM Extensions stub implementation for the Jetpack Camera Library, a library providing interfaces" +
" to integrate with OEM specific camera features."
diff --git a/camera/camera-testlib-extensions/build.gradle b/camera/camera-testlib-extensions/build.gradle
index ebec508..d0e83eeb 100644
--- a/camera/camera-testlib-extensions/build.gradle
+++ b/camera/camera-testlib-extensions/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -44,7 +44,7 @@
androidx {
name = "Camera Extensions Example"
- publish = Publish.NONE
+ type = LibraryType.UNSET
inceptionYear = "2019"
description = "Example extension implementation for the Jetpack Camera Library, a library providing a " +
"consistent and reliable camera foundation that enables great camera driven " +
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/PersistentRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/PersistentRecordingTest.kt
new file mode 100644
index 0000000..59c3bb5
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/PersistentRecordingTest.kt
@@ -0,0 +1,437 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video
+
+import android.Manifest
+import android.content.Context
+import android.os.Build
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraControl
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.impl.AndroidUtil.isEmulator
+import androidx.camera.testing.impl.AndroidUtil.skipVideoRecordingTestIfNotSupportedByEmulator
+import androidx.camera.testing.impl.CameraPipeConfigTestRule
+import androidx.camera.testing.impl.CameraTaskTrackingExecutor
+import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.InternalTestConvenience.ignoreTestForCameraPipe
+import androidx.camera.testing.impl.SurfaceTextureProvider
+import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.camera.testing.impl.video.AudioChecker
+import androidx.camera.testing.impl.video.RecordingSession
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = 21)
+class PersistentRecordingTest(
+ private val implName: String,
+ private var cameraSelector: CameraSelector,
+ private val cameraConfig: CameraXConfig,
+ private val forceEnableStreamSharing: Boolean,
+) {
+
+ @get:Rule
+ val cameraPipeConfigTestRule =
+ CameraPipeConfigTestRule(
+ active = implName.contains(CameraPipeConfig::class.simpleName!!),
+ )
+
+ @get:Rule
+ val cameraRule =
+ CameraUtil.grantCameraPermissionAndPreTestAndPostTest(
+ CameraUtil.PreTestCameraIdList(cameraConfig)
+ )
+
+ @get:Rule
+ val temporaryFolder =
+ TemporaryFolder(ApplicationProvider.getApplicationContext<Context>().cacheDir)
+
+ @get:Rule
+ val permissionRule: GrantPermissionRule =
+ GrantPermissionRule.grant(Manifest.permission.RECORD_AUDIO)
+
+ companion object {
+
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data(): Collection<Array<Any>> {
+ return listOf(
+ arrayOf(
+ "back+" + Camera2Config::class.simpleName,
+ CameraSelector.DEFAULT_BACK_CAMERA,
+ Camera2Config.defaultConfig(),
+ /*forceEnableStreamSharing=*/ false,
+ ),
+ arrayOf(
+ "front+" + Camera2Config::class.simpleName,
+ CameraSelector.DEFAULT_FRONT_CAMERA,
+ Camera2Config.defaultConfig(),
+ /*forceEnableStreamSharing=*/ false,
+ ),
+ arrayOf(
+ "back+" + Camera2Config::class.simpleName + "+streamSharing",
+ CameraSelector.DEFAULT_BACK_CAMERA,
+ Camera2Config.defaultConfig(),
+ /*forceEnableStreamSharing=*/ true,
+ ),
+ arrayOf(
+ "back+" + CameraPipeConfig::class.simpleName,
+ CameraSelector.DEFAULT_BACK_CAMERA,
+ CameraPipeConfig.defaultConfig(),
+ /*forceEnableStreamSharing=*/ false,
+ ),
+ arrayOf(
+ "front+" + CameraPipeConfig::class.simpleName,
+ CameraSelector.DEFAULT_FRONT_CAMERA,
+ CameraPipeConfig.defaultConfig(),
+ /*forceEnableStreamSharing=*/ false,
+ ),
+ arrayOf(
+ "back+" + CameraPipeConfig::class.simpleName + "+streamSharing",
+ CameraSelector.DEFAULT_BACK_CAMERA,
+ CameraPipeConfig.defaultConfig(),
+ /*forceEnableStreamSharing=*/ true,
+ ),
+ )
+ }
+ }
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val context: Context = ApplicationProvider.getApplicationContext()
+ private lateinit var cameraProvider: ProcessCameraProviderWrapper
+ private lateinit var lifecycleOwner: FakeLifecycleOwner
+ private lateinit var preview: Preview
+ private lateinit var cameraInfo: CameraInfo
+ private lateinit var videoCapabilities: VideoCapabilities
+ private lateinit var camera: Camera
+ private lateinit var videoCapture: VideoCapture<Recorder>
+ private lateinit var recordingSession: RecordingSession
+ private lateinit var cameraExecutor: CameraTaskTrackingExecutor
+
+ private val oppositeCameraSelector: CameraSelector by lazy {
+ if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA)
+ CameraSelector.DEFAULT_FRONT_CAMERA
+ else CameraSelector.DEFAULT_BACK_CAMERA
+ }
+
+ private val oppositeCamera: Camera by lazy {
+ lateinit var camera: Camera
+ instrumentation.runOnMainSync {
+ camera = cameraProvider.bindToLifecycle(lifecycleOwner, oppositeCameraSelector)
+ }
+ camera
+ }
+
+ private val audioStreamAvailable by lazy {
+ AudioChecker.canAudioStreamBeStarted(videoCapabilities, Recorder.DEFAULT_QUALITY_SELECTOR)
+ }
+
+ @Before
+ fun setUp() {
+ assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
+ skipVideoRecordingTestIfNotSupportedByEmulator()
+
+ // Skip for b/264902324
+ assumeFalse(
+ "Emulator API 30 crashes running this test.",
+ Build.VERSION.SDK_INT == 30 && isEmulator()
+ )
+
+ cameraExecutor = CameraTaskTrackingExecutor()
+ val cameraXConfig =
+ CameraXConfig.Builder.fromConfig(cameraConfig).setCameraExecutor(cameraExecutor).build()
+
+ ProcessCameraProvider.configureInstance(cameraXConfig)
+
+ cameraProvider =
+ ProcessCameraProviderWrapper(
+ ProcessCameraProvider.getInstance(context).get(),
+ forceEnableStreamSharing
+ )
+ lifecycleOwner = FakeLifecycleOwner()
+ lifecycleOwner.startAndResume()
+
+ // Add extra Preview to provide an additional surface for b/168187087.
+ preview = Preview.Builder().build()
+ videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
+
+ instrumentation.runOnMainSync {
+ // Sets surface provider to preview
+ preview.surfaceProvider = SurfaceTextureProvider.createSurfaceTextureProvider()
+
+ // Retrieves the target testing camera and camera info
+ camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector)
+ cameraInfo = camera.cameraInfo
+ videoCapabilities = Recorder.getVideoCapabilities(cameraInfo)
+ }
+
+ recordingSession =
+ RecordingSession(
+ RecordingSession.Defaults(
+ context = context,
+ recorder = videoCapture.output,
+ outputOptionsProvider = {
+ FileOutputOptions.Builder(temporaryFolder.newFile()).build()
+ },
+ withAudio = audioStreamAvailable,
+ )
+ )
+ }
+
+ @After
+ fun tearDown() {
+ if (this::recordingSession.isInitialized) {
+ recordingSession.release(timeoutMs = 5000)
+ }
+ if (this::cameraProvider.isInitialized) {
+ cameraProvider.shutdownAsync()[10, TimeUnit.SECONDS]
+ }
+ }
+
+ @Test
+ fun persistentRecording_canContinueRecordingAfterRebind() {
+ assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
+
+ // TODO(b/340406044): Enable the test for stream sharing use case.
+ assumeFalse(
+ "The test is temporarily ignored when stream sharing is enabled.",
+ forceEnableStreamSharing
+ )
+
+ checkAndBindUseCases(preview, videoCapture)
+
+ // TODO(b/340406044): Enable the test for stream sharing use case.
+ // Bypass stream sharing if it's enforced on the device. Like quirks in
+ // androidx.camera.core.internal.compat.workaround.StreamSharingForceEnabler.
+ assumeFalse(
+ "The test is temporarily ignored when the video capture requires transformation.",
+ isStreamSharingEnabled(videoCapture)
+ )
+
+ val recording =
+ recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
+
+ instrumentation.runOnMainSync { cameraProvider.unbindAll() }
+ checkAndBindUseCases(preview, videoCapture)
+
+ recording.clearEvents()
+ recording.verifyStatus()
+
+ recording.stopAndVerify()
+ }
+
+ @Test
+ fun persistentRecording_canContinueRecordingPausedAfterRebind() {
+ assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
+
+ // TODO(b/340406044): Enable the test for stream sharing use case.
+ assumeFalse(
+ "The test is temporarily ignored when stream sharing is enabled.",
+ forceEnableStreamSharing
+ )
+
+ checkAndBindUseCases(preview, videoCapture)
+
+ // TODO(b/340406044): Enable the test for stream sharing use case.
+ // Bypass stream sharing if it's enforced on the device. Like quirks in
+ // androidx.camera.core.internal.compat.workaround.StreamSharingForceEnabler.
+ assumeFalse(
+ "The test is temporarily ignored when the video capture requires transformation.",
+ isStreamSharingEnabled(videoCapture)
+ )
+
+ val recording =
+ recordingSession
+ .createRecording(asPersistentRecording = true)
+ .startAndVerify()
+ .pauseAndVerify()
+
+ instrumentation.runOnMainSync { cameraProvider.unbindAll() }
+ checkAndBindUseCases(preview, videoCapture)
+
+ recording.resumeAndVerify().stopAndVerify()
+ }
+
+ @Test
+ fun persistentRecording_canStopAfterUnbind() {
+ assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
+
+ // TODO(b/353113961): Enable the test for camera pipe implementation.
+ implName.ignoreTestForCameraPipe(
+ "The test is temporarily ignored for camera pipe implementation.",
+ true
+ )
+
+ // TODO(b/340406044): Enable the test for stream sharing use case.
+ assumeFalse(
+ "The test is temporarily ignored when stream sharing is enabled.",
+ forceEnableStreamSharing
+ )
+
+ checkAndBindUseCases(preview, videoCapture)
+
+ // TODO(b/340406044): Enable the test for stream sharing use case.
+ // Bypass stream sharing if it's enforced on the device. Like quirks in
+ // androidx.camera.core.internal.compat.workaround.StreamSharingForceEnabler.
+ assumeFalse(
+ "The test is temporarily ignored when the video capture requires transformation.",
+ isStreamSharingEnabled(videoCapture)
+ )
+
+ val recording =
+ recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
+
+ instrumentation.runOnMainSync { cameraProvider.unbindAll() }
+
+ recording.stopAndVerify()
+ }
+
+ @Test
+ fun updateVideoUsage_whenUseCaseUnboundAndReboundForPersistentRecording(): Unit = runBlocking {
+ assumeFalse(
+ "TODO: b/340406044 - Temporarily ignored when stream sharing is enabled.",
+ forceEnableStreamSharing
+ )
+
+ checkAndBindUseCases(preview, videoCapture)
+ val recording =
+ recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
+
+ // Act 1 - unbind VideoCapture before recording completes, isRecording should be false.
+ instrumentation.runOnMainSync { cameraProvider.unbind(videoCapture) }
+
+ camera.cameraControl.verifyIfInVideoUsage(
+ false,
+ "VideoCapture unbound but camera still in video usage"
+ )
+
+ // Act 2 - rebind VideoCapture, isRecording should be true.
+ checkAndBindUseCases(videoCapture)
+
+ camera.cameraControl.verifyIfInVideoUsage(
+ true,
+ "VideoCapture re-bound but camera still not in video usage"
+ )
+
+ // TODO(b/382158668): Remove the check for the status events.
+ recording.clearEvents()
+ recording.verifyStatus()
+ recording.stopAndVerify()
+ }
+
+ @Test
+ fun updateVideoUsage_whenUseCaseBoundToNewCameraForPersistentRecording(): Unit = runBlocking {
+ assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
+
+ assumeFalse(
+ "TODO: b/340406044 - Temporarily ignored when stream sharing is enabled.",
+ forceEnableStreamSharing
+ )
+
+ checkAndBindUseCases(preview, videoCapture)
+ val recording =
+ recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
+
+ // Act 1 - unbind before recording completes, isRecording should be false.
+ instrumentation.runOnMainSync { cameraProvider.unbindAll() }
+
+ camera.cameraControl.verifyIfInVideoUsage(
+ false,
+ "VideoCapture unbound but camera still in video usage"
+ )
+
+ // Act 2 - rebind VideoCapture to opposite camera, isRecording should be true.
+ checkAndBindUseCases(preview, videoCapture, useOppositeCamera = true)
+
+ oppositeCamera.cameraControl.verifyIfInVideoUsage(
+ true,
+ "VideoCapture re-bound but camera still not in video usage"
+ )
+
+ // TODO(b/382158668): Remove the check for the status events.
+ recording.clearEvents()
+ recording.verifyStatus()
+ recording.stopAndVerify()
+ }
+
+ private fun getCameraSelector(useOppositeCamera: Boolean): CameraSelector =
+ if (!useOppositeCamera) cameraSelector else oppositeCameraSelector
+
+ private fun getCamera(useOppositeCamera: Boolean): Camera =
+ if (!useOppositeCamera) camera else oppositeCamera
+
+ private fun isUseCasesCombinationSupported(
+ vararg useCases: UseCase,
+ useOppositeCamera: Boolean = false,
+ ) = getCamera(useOppositeCamera).isUseCasesCombinationSupported(*useCases)
+
+ private fun checkAndBindUseCases(
+ vararg useCases: UseCase,
+ useOppositeCamera: Boolean = false,
+ ) {
+ assumeTrue(isUseCasesCombinationSupported(*useCases, useOppositeCamera = useOppositeCamera))
+
+ instrumentation.runOnMainSync {
+ cameraProvider.bindToLifecycle(
+ lifecycleOwner,
+ getCameraSelector(useOppositeCamera),
+ *useCases
+ )
+ }
+ }
+
+ private suspend fun CameraControl.verifyIfInVideoUsage(
+ expected: Boolean,
+ message: String = ""
+ ) {
+ instrumentation.waitForIdleSync() // VideoCapture observes Recorder in main thread
+ // VideoUsage is updated in camera thread. So, we should ensure all tasks already submitted
+ // to camera thread are completed before checking isInVideoUsage
+ cameraExecutor.awaitIdle()
+ assertWithMessage(message).that((this as CameraControlInternal).isInVideoUsage).apply {
+ if (expected) {
+ isTrue()
+ } else {
+ isFalse()
+ }
+ }
+ }
+}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index 269f4ea..a93558e 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -19,11 +19,9 @@
import android.Manifest
import android.content.Context
import android.graphics.Rect
-import android.graphics.SurfaceTexture
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
-import android.util.Log
import android.util.Rational
import android.util.Size
import android.view.Surface
@@ -37,12 +35,8 @@
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraXConfig
import androidx.camera.core.DynamicRange
-import androidx.camera.core.ImageAnalysis
-import androidx.camera.core.ImageCapture
-import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.core.UseCase
-import androidx.camera.core.UseCaseGroup
import androidx.camera.core.impl.CameraControlInternal
import androidx.camera.core.impl.SessionConfig
import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9
@@ -53,27 +47,22 @@
import androidx.camera.core.impl.utils.TransformUtils.rectToSize
import androidx.camera.core.impl.utils.TransformUtils.rotateSize
import androidx.camera.core.impl.utils.TransformUtils.within360
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.impl.AndroidUtil.isEmulator
import androidx.camera.testing.impl.AndroidUtil.skipVideoRecordingTestIfNotSupportedByEmulator
import androidx.camera.testing.impl.CameraPipeConfigTestRule
import androidx.camera.testing.impl.CameraTaskTrackingExecutor
import androidx.camera.testing.impl.CameraUtil
-import androidx.camera.testing.impl.InternalTestConvenience.ignoreTestForCameraPipe
-import androidx.camera.testing.impl.StreamSharingForceEnabledEffect
import androidx.camera.testing.impl.SurfaceTextureProvider
import androidx.camera.testing.impl.WakelockEmptyActivityRule
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
import androidx.camera.testing.impl.getRotatedAspectRatio
import androidx.camera.testing.impl.getRotation
-import androidx.camera.testing.impl.mocks.MockScreenFlash
import androidx.camera.testing.impl.useAndRelease
import androidx.camera.testing.impl.video.AudioChecker
import androidx.camera.testing.impl.video.Recording
import androidx.camera.testing.impl.video.RecordingSession
import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE
-import androidx.lifecycle.LifecycleOwner
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -83,7 +72,6 @@
import com.google.common.truth.Truth.assertWithMessage
import com.google.common.util.concurrent.ListenableFuture
import java.io.File
-import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.runBlocking
import org.junit.After
@@ -226,7 +214,10 @@
ProcessCameraProvider.configureInstance(cameraXConfig)
cameraProvider =
- ProcessCameraProviderWrapper(ProcessCameraProvider.getInstance(context).get())
+ ProcessCameraProviderWrapper(
+ ProcessCameraProvider.getInstance(context).get(),
+ forceEnableStreamSharing
+ )
lifecycleOwner = FakeLifecycleOwner()
lifecycleOwner.startAndResume()
@@ -236,7 +227,7 @@
instrumentation.runOnMainSync {
// Sets surface provider to preview
- preview.surfaceProvider = getSurfaceProvider()
+ preview.surfaceProvider = SurfaceTextureProvider.createSurfaceTextureProvider()
// Retrieves the target testing camera and camera info
camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector)
@@ -438,50 +429,6 @@
}
@Test
- fun recordingWithPreviewAndImageAnalysis() {
- // Arrange.
- val analysis = ImageAnalysis.Builder().build()
- val latchForImageAnalysis = CountDownLatch(5)
- analysis.setAnalyzer(CameraXExecutors.directExecutor()) {
- latchForImageAnalysis.countDown()
- it.close()
- }
- checkAndBindUseCases(preview, videoCapture, analysis)
-
- // Act.
- recordingSession.createRecording().recordAndVerify()
-
- // Verify.
- assertThat(latchForImageAnalysis.await(10, TimeUnit.SECONDS)).isTrue()
- }
-
- @Test
- fun recordingWithPreviewAndImageCapture() {
- // Arrange.
- val imageCapture = ImageCapture.Builder().build()
- checkAndBindUseCases(preview, videoCapture, imageCapture)
-
- // Act.
- recordingSession.createRecording().recordAndVerify()
-
- // Verify.
- completeImageCapture(imageCapture)
- }
-
- @Test
- fun recordingWithPreviewAndFlashImageCapture() {
- // Arrange.
- val imageCapture = ImageCapture.Builder().build()
- checkAndBindUseCases(preview, videoCapture, imageCapture)
-
- // Act.
- recordingSession.createRecording().recordAndVerify()
-
- // Verify.
- completeImageCapture(imageCapture, useFlash = true)
- }
-
- @Test
fun recordingWithPreview_boundSeparately() {
assumeTrue(camera.isUseCasesCombinationSupported(preview, videoCapture))
@@ -577,26 +524,6 @@
}
@Test
- fun boundButNotRecordingDuringCapture_withPreviewAndImageCapture() {
- // Arrange.
- val imageCapture = ImageCapture.Builder().build()
- checkAndBindUseCases(preview, videoCapture, imageCapture)
-
- // Act & verify.
- completeImageCapture(imageCapture)
- }
-
- @Test
- fun boundButNotRecordingDuringFlashCapture_withPreviewAndImageCapture() {
- // Arrange.
- val imageCapture = ImageCapture.Builder().build()
- checkAndBindUseCases(preview, videoCapture, imageCapture)
-
- // Act & verify.
- completeImageCapture(imageCapture, useFlash = true)
- }
-
- @Test
fun canRecordMultipleFilesInARow() {
checkAndBindUseCases(preview, videoCapture)
recordingSession.createRecording().recordAndVerify()
@@ -726,104 +653,6 @@
}
@Test
- fun persistentRecording_canContinueRecordingAfterRebind() {
- assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
-
- // TODO(b/340406044): Enable the test for stream sharing use case.
- assumeFalse(
- "The test is temporarily ignored when stream sharing is enabled.",
- forceEnableStreamSharing
- )
-
- checkAndBindUseCases(preview, videoCapture)
-
- // TODO(b/340406044): Enable the test for stream sharing use case.
- // Bypass stream sharing if it's enforced on the device. Like quirks in
- // androidx.camera.core.internal.compat.workaround.StreamSharingForceEnabler.
- assumeFalse(
- "The test is temporarily ignored when the video capture requires transformation.",
- isStreamSharingEnabled(videoCapture)
- )
-
- val recording =
- recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
-
- instrumentation.runOnMainSync { cameraProvider.unbindAll() }
- checkAndBindUseCases(preview, videoCapture)
-
- recording.clearEvents()
- recording.verifyStatus()
-
- recording.stopAndVerify()
- }
-
- @Test
- fun persistentRecording_canContinueRecordingPausedAfterRebind() {
- assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
-
- // TODO(b/340406044): Enable the test for stream sharing use case.
- assumeFalse(
- "The test is temporarily ignored when stream sharing is enabled.",
- forceEnableStreamSharing
- )
-
- checkAndBindUseCases(preview, videoCapture)
-
- // TODO(b/340406044): Enable the test for stream sharing use case.
- // Bypass stream sharing if it's enforced on the device. Like quirks in
- // androidx.camera.core.internal.compat.workaround.StreamSharingForceEnabler.
- assumeFalse(
- "The test is temporarily ignored when the video capture requires transformation.",
- isStreamSharingEnabled(videoCapture)
- )
-
- val recording =
- recordingSession
- .createRecording(asPersistentRecording = true)
- .startAndVerify()
- .pauseAndVerify()
-
- instrumentation.runOnMainSync { cameraProvider.unbindAll() }
- checkAndBindUseCases(preview, videoCapture)
-
- recording.resumeAndVerify().stopAndVerify()
- }
-
- @Test
- fun persistentRecording_canStopAfterUnbind() {
- assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
-
- // TODO(b/353113961): Enable the test for camera pipe implementation.
- implName.ignoreTestForCameraPipe(
- "The test is temporarily ignored for camera pipe implementation.",
- true
- )
-
- // TODO(b/340406044): Enable the test for stream sharing use case.
- assumeFalse(
- "The test is temporarily ignored when stream sharing is enabled.",
- forceEnableStreamSharing
- )
-
- checkAndBindUseCases(preview, videoCapture)
-
- // TODO(b/340406044): Enable the test for stream sharing use case.
- // Bypass stream sharing if it's enforced on the device. Like quirks in
- // androidx.camera.core.internal.compat.workaround.StreamSharingForceEnabler.
- assumeFalse(
- "The test is temporarily ignored when the video capture requires transformation.",
- isStreamSharingEnabled(videoCapture)
- )
-
- val recording =
- recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
-
- instrumentation.runOnMainSync { cameraProvider.unbindAll() }
-
- recording.stopAndVerify()
- }
-
- @Test
fun canRecordWithCorrectTransformation() {
// Act.
checkAndBindUseCases(preview, videoCapture)
@@ -924,74 +753,6 @@
)
}
- @Test
- fun updateVideoUsage_whenUseCaseUnboundAndReboundForPersistentRecording(): Unit = runBlocking {
- assumeFalse(
- "TODO: b/340406044 - Temporarily ignored when stream sharing is enabled.",
- forceEnableStreamSharing
- )
-
- checkAndBindUseCases(preview, videoCapture)
- val recording =
- recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
-
- // Act 1 - unbind VideoCapture before recording completes, isRecording should be false.
- instrumentation.runOnMainSync { cameraProvider.unbind(videoCapture) }
-
- camera.cameraControl.verifyIfInVideoUsage(
- false,
- "VideoCapture unbound but camera still in video usage"
- )
-
- // Act 2 - rebind VideoCapture, isRecording should be true.
- checkAndBindUseCases(videoCapture)
-
- camera.cameraControl.verifyIfInVideoUsage(
- true,
- "VideoCapture re-bound but camera still not in video usage"
- )
-
- // TODO(b/382158668): Remove the check for the status events.
- recording.clearEvents()
- recording.verifyStatus()
- recording.stopAndVerify()
- }
-
- @Test
- fun updateVideoUsage_whenUseCaseBoundToNewCameraForPersistentRecording(): Unit = runBlocking {
- assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
-
- assumeFalse(
- "TODO: b/340406044 - Temporarily ignored when stream sharing is enabled.",
- forceEnableStreamSharing
- )
-
- checkAndBindUseCases(preview, videoCapture)
- val recording =
- recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
-
- // Act 1 - unbind before recording completes, isRecording should be false.
- instrumentation.runOnMainSync { cameraProvider.unbindAll() }
-
- camera.cameraControl.verifyIfInVideoUsage(
- false,
- "VideoCapture unbound but camera still in video usage"
- )
-
- // Act 2 - rebind VideoCapture to opposite camera, isRecording should be true.
- checkAndBindUseCases(preview, videoCapture, useOppositeCamera = true)
-
- oppositeCamera.cameraControl.verifyIfInVideoUsage(
- true,
- "VideoCapture re-bound but camera still not in video usage"
- )
-
- // TODO(b/382158668): Remove the check for the status events.
- recording.clearEvents()
- recording.verifyStatus()
- recording.stopAndVerify()
- }
-
// TODO: b/341691683 - Add tests for multiple VideoCapture bound and recording concurrently
private fun getCameraSelector(useOppositeCamera: Boolean): CameraSelector =
@@ -1020,35 +781,6 @@
}
}
- private fun completeImageCapture(
- imageCapture: ImageCapture,
- imageFile: File = temporaryFolder.newFile(),
- useFlash: Boolean = false
- ) {
- val savedCallback = ImageSavedCallback()
-
- if (useFlash) {
- if (cameraSelector.lensFacing == CameraSelector.LENS_FACING_FRONT) {
- imageCapture.screenFlash = MockScreenFlash()
- imageCapture.flashMode = ImageCapture.FLASH_MODE_SCREEN
- } else {
- imageCapture.flashMode = ImageCapture.FLASH_MODE_ON
- }
- } else {
- imageCapture.flashMode = ImageCapture.FLASH_MODE_OFF
- }
-
- imageCapture.takePicture(
- ImageCapture.OutputFileOptions.Builder(imageFile).build(),
- CameraXExecutors.ioExecutor(),
- savedCallback
- )
- savedCallback.verifyCaptureResult()
-
- // Just in case same imageCapture is bound to rear camera later
- imageCapture.screenFlash = null
- }
-
data class ExpectedRotation(val contentRotation: Int, val metadataRotation: Int)
private fun getExpectedRotation(
@@ -1106,82 +838,10 @@
}
}
- private fun getSurfaceProvider(): Preview.SurfaceProvider {
- return SurfaceTextureProvider.createSurfaceTextureProvider(
- object : SurfaceTextureProvider.SurfaceTextureCallback {
- override fun onSurfaceTextureReady(
- surfaceTexture: SurfaceTexture,
- resolution: Size
- ) {
- // No-op
- }
-
- override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
- surfaceTexture.release()
- }
- }
- )
- }
-
private fun assumeExtraCroppingQuirk() {
assumeExtraCroppingQuirk(implName)
}
- private inner class ProcessCameraProviderWrapper(val cameraProvider: ProcessCameraProvider) {
-
- fun bindToLifecycle(
- lifecycleOwner: LifecycleOwner,
- cameraSelector: CameraSelector,
- vararg useCases: UseCase
- ): Camera {
- if (useCases.isEmpty()) {
- return cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, *useCases)
- }
- val useCaseGroup =
- UseCaseGroup.Builder()
- .apply {
- useCases.forEach { useCase -> addUseCase(useCase) }
- if (forceEnableStreamSharing) {
- addEffect(StreamSharingForceEnabledEffect())
- }
- }
- .build()
- return cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCaseGroup)
- }
-
- fun unbind(vararg useCases: UseCase) {
- cameraProvider.unbind(*useCases)
- }
-
- fun unbindAll() {
- cameraProvider.unbindAll()
- }
-
- fun shutdownAsync(): ListenableFuture<Void> = cameraProvider.shutdownAsync()
- }
-
- private class ImageSavedCallback : ImageCapture.OnImageSavedCallback {
-
- private val latch = CountDownLatch(1)
- val results = mutableListOf<ImageCapture.OutputFileResults>()
- val errors = mutableListOf<ImageCaptureException>()
-
- override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
- results.add(outputFileResults)
- latch.countDown()
- }
-
- override fun onError(exception: ImageCaptureException) {
- errors.add(exception)
- Log.e(TAG, "OnImageSavedCallback.onError: ${exception.message}")
- latch.countDown()
- }
-
- fun verifyCaptureResult() {
- assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue()
- }
- }
-
private suspend fun CameraControl.verifyIfInVideoUsage(
expected: Boolean,
message: String = ""
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt
index 4781265..e5e9d50 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt
@@ -26,13 +26,20 @@
import androidx.camera.camera2.pipe.integration.CameraPipeConfig
import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks as PipeDeviceQuirks
import androidx.camera.camera2.pipe.integration.compat.quirk.ExtraCroppingQuirk as PipeExtraCroppingQuirk
+import androidx.camera.core.Camera
import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
import androidx.camera.core.UseCase
+import androidx.camera.core.UseCaseGroup
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.impl.StreamSharingForceEnabledEffect
import androidx.camera.testing.impl.getRotatedResolution
import androidx.camera.testing.impl.useAndRelease
import androidx.camera.video.internal.compat.quirk.DeviceQuirks
import androidx.camera.video.internal.compat.quirk.StopCodecAfterSurfaceRemovalCrashMediaServerQuirk
+import androidx.lifecycle.LifecycleOwner
import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.ListenableFuture
import java.io.File
import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue
@@ -88,3 +95,39 @@
fun isSurfaceProcessingEnabled(videoCapture: VideoCapture<*>) =
videoCapture.node != null || isStreamSharingEnabled(videoCapture)
+
+class ProcessCameraProviderWrapper(
+ private val cameraProvider: ProcessCameraProvider,
+ private val forceEnableStreamSharing: Boolean
+) {
+
+ fun bindToLifecycle(
+ lifecycleOwner: LifecycleOwner,
+ cameraSelector: CameraSelector,
+ vararg useCases: UseCase
+ ): Camera {
+ if (useCases.isEmpty()) {
+ return cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, *useCases)
+ }
+ val useCaseGroup =
+ UseCaseGroup.Builder()
+ .apply {
+ useCases.forEach { useCase -> addUseCase(useCase) }
+ if (forceEnableStreamSharing) {
+ addEffect(StreamSharingForceEnabledEffect())
+ }
+ }
+ .build()
+ return cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCaseGroup)
+ }
+
+ fun unbind(vararg useCases: UseCase) {
+ cameraProvider.unbind(*useCases)
+ }
+
+ fun unbindAll() {
+ cameraProvider.unbindAll()
+ }
+
+ fun shutdownAsync(): ListenableFuture<Void> = cameraProvider.shutdownAsync()
+}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
index defa125..518bae0 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
@@ -38,6 +38,7 @@
import androidx.camera.testing.impl.SurfaceTextureProvider.createAutoDrainingSurfaceTextureProvider
import androidx.camera.testing.impl.WakelockEmptyActivityRule
import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.camera.testing.impl.mocks.MockScreenFlash
import androidx.camera.testing.impl.video.AudioChecker
import androidx.camera.testing.impl.video.RecordingSession
import androidx.camera.video.FileOutputOptions
@@ -95,7 +96,6 @@
@get:Rule val wakelockEmptyActivityRule = WakelockEmptyActivityRule()
companion object {
- private const val TAG = "UseCaseCombinationTest"
@JvmStatic
@Parameterized.Parameters(name = "{0}")
@@ -317,7 +317,18 @@
}
@Test
- fun previewCombinesVideoCaptureAndImageCapture() {
+ fun previewCombinesVideoCaptureAndImageCapture_withoutRecording() {
+ // Arrange.
+ checkAndPrepareVideoCaptureSources()
+ checkAndBindUseCases(preview, videoCapture, imageCapture)
+
+ // Assert.
+ previewMonitor.waitForStream()
+ imageCapture.waitForCapturing()
+ }
+
+ @Test
+ fun previewCombinesVideoCaptureAndImageCapture_withRecording() {
// Arrange.
checkAndPrepareVideoCaptureSources()
checkAndBindUseCases(preview, videoCapture, imageCapture)
@@ -329,6 +340,29 @@
}
@Test
+ fun previewCombinesVideoCaptureAndFlashImageCapture_withoutRecording() {
+ // Arrange.
+ checkAndPrepareVideoCaptureSources()
+ checkAndBindUseCases(preview, videoCapture, imageCapture)
+
+ // Assert.
+ previewMonitor.waitForStream()
+ imageCapture.waitForCapturing(useFlash = true)
+ }
+
+ @Test
+ fun previewCombinesVideoCaptureAndFlashImageCapture_withRecording() {
+ // Arrange.
+ checkAndPrepareVideoCaptureSources()
+ checkAndBindUseCases(preview, videoCapture, imageCapture)
+
+ // Assert.
+ previewMonitor.waitForStream()
+ recordingSession.createRecording().recordAndVerify()
+ imageCapture.waitForCapturing(useFlash = true)
+ }
+
+ @Test
fun previewCombinesVideoCaptureAndImageAnalysis() {
// Arrange.
checkAndPrepareVideoCaptureSources()
@@ -487,7 +521,7 @@
return ImageCapture.Builder().build()
}
- private fun ImageCapture.waitForCapturing(timeMillis: Long = 5000) {
+ private fun ImageCapture.waitForCapturing(timeMillis: Long = 10000, useFlash: Boolean = false) {
val callback =
object : ImageCapture.OnImageCapturedCallback() {
val latch = CountDownLatch(1)
@@ -504,12 +538,26 @@
}
}
+ if (useFlash) {
+ if (cameraSelector.lensFacing == CameraSelector.LENS_FACING_FRONT) {
+ screenFlash = MockScreenFlash()
+ flashMode = ImageCapture.FLASH_MODE_SCREEN
+ } else {
+ flashMode = ImageCapture.FLASH_MODE_ON
+ }
+ } else {
+ flashMode = ImageCapture.FLASH_MODE_OFF
+ }
+
takePicture(Dispatchers.Main.asExecutor(), callback)
assertThat(
callback.latch.await(timeMillis, TimeUnit.MILLISECONDS) && callback.errors.isEmpty()
)
.isTrue()
+
+ // Just in case same imageCapture is bound to rear camera later
+ screenFlash = null
}
class PreviewMonitor {
diff --git a/car/app/app-samples/navigation/automotive/build.gradle b/car/app/app-samples/navigation/automotive/build.gradle
index c7e3a8a..c75e16c 100644
--- a/car/app/app-samples/navigation/automotive/build.gradle
+++ b/car/app/app-samples/navigation/automotive/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
@@ -52,6 +51,5 @@
}
androidx {
- type = LibraryType.SAMPLES
- publish = Publish.NONE
+ type = LibraryType.TEST_APPLICATION
}
diff --git a/car/app/app-samples/navigation/common/build.gradle b/car/app/app-samples/navigation/common/build.gradle
index b3be643..5977ad6 100644
--- a/car/app/app-samples/navigation/common/build.gradle
+++ b/car/app/app-samples/navigation/common/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
@@ -46,6 +45,7 @@
}
androidx {
- type = LibraryType.SAMPLES
- publish = Publish.NONE
+ type = LibraryType.TEST_APPLICATION
+ // TODO: b/326456246
+ optOutJSpecify = true
}
diff --git a/collection/collection-benchmark-kmp/build.gradle b/collection/collection-benchmark-kmp/build.gradle
index 2c5b34e..a284d14 100644
--- a/collection/collection-benchmark-kmp/build.gradle
+++ b/collection/collection-benchmark-kmp/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget
@@ -71,6 +71,6 @@
}
androidx {
- publish = Publish.NONE
+ type = LibraryType.UNSET
}
diff --git a/collection/collection-benchmark/build.gradle b/collection/collection-benchmark/build.gradle
index 2f5bdef..59b2ca4 100644
--- a/collection/collection-benchmark/build.gradle
+++ b/collection/collection-benchmark/build.gradle
@@ -21,6 +21,8 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
+
+import androidx.build.LibraryType
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.mpp.BitcodeEmbeddingMode
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkConfig
@@ -110,6 +112,7 @@
androidx {
name = "Collections Benchmarks (Android / iOS)"
+ type = LibraryType.BENCHMARK
inceptionYear = "2022"
description = "AndroidX Collections Benchmarks (Android / iOS)"
}
diff --git a/collection/integration-tests/testapp/build.gradle b/collection/integration-tests/testapp/build.gradle
index 06b5a37..3c90a3b 100644
--- a/collection/integration-tests/testapp/build.gradle
+++ b/collection/integration-tests/testapp/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -36,7 +36,7 @@
androidx {
name = "Collection Integration Tests"
- publish = Publish.NONE
+ type = LibraryType.TEST_APPLICATION
inceptionYear = "2021"
description = "AndroidX Collection Integration Tests"
// TODO: b/326456246
diff --git a/compose/animation/animation-core/benchmark/build.gradle b/compose/animation/animation-core/benchmark/build.gradle
index e9ef268..dfe4a41 100644
--- a/compose/animation/animation-core/benchmark/build.gradle
+++ b/compose/animation/animation-core/benchmark/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import androidx.build.LibraryType
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -39,3 +41,7 @@
namespace = "androidx.compose.animation.core.benchmark"
}
+
+androidx {
+ type = LibraryType.BENCHMARK
+}
diff --git a/compose/benchmark-utils/benchmark/build.gradle b/compose/benchmark-utils/benchmark/build.gradle
index 0ab9324..388dd5b 100644
--- a/compose/benchmark-utils/benchmark/build.gradle
+++ b/compose/benchmark-utils/benchmark/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import androidx.build.LibraryType
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -37,3 +39,7 @@
compileSdk = 35
namespace = "androidx.compose.benchmarkutils.benchmark"
}
+
+androidx {
+ type = LibraryType.BENCHMARK
+}
diff --git a/compose/foundation/foundation-layout/benchmark/build.gradle b/compose/foundation/foundation-layout/benchmark/build.gradle
index 2b41dc0..0cbdfed 100644
--- a/compose/foundation/foundation-layout/benchmark/build.gradle
+++ b/compose/foundation/foundation-layout/benchmark/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import androidx.build.LibraryType
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -42,3 +44,7 @@
compileSdk = 35
namespace = "androidx.compose.foundation.layout.benchmark"
}
+
+androidx {
+ type = LibraryType.BENCHMARK
+}
diff --git a/compose/foundation/foundation/benchmark/build.gradle b/compose/foundation/foundation/benchmark/build.gradle
index 7fdab92..2e666b3 100644
--- a/compose/foundation/foundation/benchmark/build.gradle
+++ b/compose/foundation/foundation/benchmark/build.gradle
@@ -66,5 +66,5 @@
}
androidx {
- type = LibraryType.INTERNAL_TEST_LIBRARY
+ type = LibraryType.BENCHMARK
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldLayoutPhaseToggleTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldLayoutPhaseToggleTest.kt
index da1ea4fb..52fb64a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldLayoutPhaseToggleTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldLayoutPhaseToggleTest.kt
@@ -18,6 +18,9 @@
import android.os.Build
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.TEST_FONT_FAMILY
import androidx.compose.foundation.text.matchers.assertThat
@@ -31,12 +34,17 @@
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.test.filters.SdkSuppress
+import com.google.common.collect.Range
+import com.google.common.truth.IntegerSubject
import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
import org.junit.Rule
import org.junit.Test
@@ -102,4 +110,45 @@
assertThat(secondTextLayoutResult.layoutInput.style.textAlign).isEqualTo(TextAlign.Start)
assertThat(firstBitmap).isNotEqualToBitmap(secondBitmap)
}
+
+ @Test
+ fun constraintsMinWidthDecrease_textLayoutReflects() {
+ state = TextFieldState("abc")
+ var leftBoxSize by mutableStateOf(100.dp)
+ var textLayoutResult: TextLayoutResult? = null
+ rule.setContent {
+ Row(Modifier.size(200.dp)) {
+ Box(Modifier.size(leftBoxSize))
+ BasicTextField(
+ state = state,
+ textStyle = textStyle,
+ modifier = Modifier.weight(1f),
+ lineLimits = TextFieldLineLimits.SingleLine,
+ onTextLayout = { textLayoutResult = it.invoke() }
+ )
+ }
+ }
+
+ with(rule.density) {
+ rule.runOnIdle {
+ val width =
+ maxOf(textLayoutResult!!.multiParagraph.maxIntrinsicWidth, 100.dp.toPx())
+ assertThat(textLayoutResult).isNotNull()
+ assertThat(textLayoutResult?.size?.width).isEqualTo(width.roundToInt(), 1)
+ assertThat(textLayoutResult?.multiParagraph?.width).isWithin(1f).of(width)
+ }
+
+ leftBoxSize = 150.dp
+
+ rule.runOnIdle {
+ val width = maxOf(textLayoutResult!!.multiParagraph.maxIntrinsicWidth, 50.dp.toPx())
+ assertThat(textLayoutResult?.size?.width).isEqualTo(width.roundToInt(), 1)
+ assertThat(textLayoutResult?.multiParagraph?.width).isWithin(1f).of(width)
+ }
+ }
+ }
+}
+
+internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
+ isIn(Range.closed(expected - tolerance, expected + tolerance))
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
index 3bf9852..634c4a0 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
@@ -223,7 +223,6 @@
}
}
- @SdkSuppress(minSdkVersion = 22) // b/266742195
@Test
fun textInputStarted_forFieldInActivity_whenFocusRequestedImmediately_fromLaunchedEffect() {
textInputStarted_whenFocusRequestedImmediately_fromEffect(
@@ -231,7 +230,6 @@
)
}
- @SdkSuppress(minSdkVersion = 22) // b/266742195
@Test
fun textInputStarted_forFieldInActivity_whenFocusRequestedImmediately_fromDisposableEffect() {
textInputStarted_whenFocusRequestedImmediately_fromEffect(
@@ -295,7 +293,6 @@
inputMethodInterceptor.assertSessionActive()
}
- @SdkSuppress(minSdkVersion = 22) // b/266742195
@Test
fun basicTextField_checkFocusNavigation_onDPadLeft_DPadDevice() {
setupAndEnableBasicTextField()
@@ -311,7 +308,6 @@
rule.onNodeWithTag("test-button-left").assertIsFocused()
}
- @SdkSuppress(minSdkVersion = 22) // b/266742195
@Test
fun basicTextField_checkFocusNavigation_onDPadRight_DPadDevice() {
setupAndEnableBasicTextField()
@@ -327,7 +323,6 @@
rule.onNodeWithTag("test-button-right").assertIsFocused()
}
- @SdkSuppress(minSdkVersion = 22) // b/266742195
@Test
fun basicTextField_checkFocusNavigation_onDPadUp_DPadDevice() {
setupAndEnableBasicTextField()
@@ -343,7 +338,6 @@
rule.onNodeWithTag("test-button-top").assertIsFocused()
}
- @SdkSuppress(minSdkVersion = 22) // b/266742195
@Test
fun basicTextField_checkFocusNavigation_onDPadDown_DPadDevice() {
setupAndEnableBasicTextField()
@@ -375,7 +369,6 @@
}
@Ignore("339495780")
- @SdkSuppress(minSdkVersion = 22) // b/266742195
@Test
fun basicTextField_checkFocusNavigation_onDPadLeft_hardwareKeyboard() {
setupAndEnableBasicTextField()
@@ -396,7 +389,6 @@
}
@Ignore("339495780")
- @SdkSuppress(minSdkVersion = 22) // b/266742195
@Test
fun basicTextField_checkFocusNavigation_onDPadRight_hardwareKeyboard() {
setupAndEnableBasicTextField()
@@ -419,7 +411,6 @@
}
@Ignore("339495780")
- @SdkSuppress(minSdkVersion = 22) // b/266742195
@Test
fun basicTextField_checkFocusNavigation_onDPadUp_hardwareKeyboard() {
setupAndEnableBasicTextField()
@@ -440,7 +431,6 @@
}
@Ignore("339495780")
- @SdkSuppress(minSdkVersion = 22) // b/266742195
@Test
fun basicTextField_checkFocusNavigation_onDPadDown_hardwareKeyboard() {
setupAndEnableBasicTextField()
@@ -462,7 +452,6 @@
rule.onNodeWithTag("test-text-field-1").assertSelection(TextRange(2))
}
- @SdkSuppress(minSdkVersion = 22) // b/266742195
@Test
fun basicTextField_checkFocusNavigation_onTab() {
setupAndEnableBasicTextField(singleLine = true)
@@ -475,7 +464,6 @@
rule.onNodeWithTag("test-button-right").assertIsFocused()
}
- @SdkSuppress(minSdkVersion = 22) // b/266742195
@Test
fun basicTextField_withImeActionNext_checkFocusNavigation_onEnter() {
setupAndEnableBasicTextField(singleLine = true)
@@ -488,7 +476,6 @@
rule.onNodeWithTag("test-button-right").assertIsFocused()
}
- @SdkSuppress(minSdkVersion = 22) // b/266742195
@Test
fun basicTextField_checkFocusNavigation_onShiftTab() {
setupAndEnableBasicTextField(singleLine = true)
diff --git a/compose/integration-tests/docs-snippets/build.gradle b/compose/integration-tests/docs-snippets/build.gradle
index d43b9a5..19a718c 100644
--- a/compose/integration-tests/docs-snippets/build.gradle
+++ b/compose/integration-tests/docs-snippets/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
@@ -65,7 +65,7 @@
androidx {
name = "Compose Documentation Snippets"
- publish = Publish.NONE
+ type = LibraryType.TEST_APPLICATION
description = "Compose Documentation Snippets on developer.android.com"
}
diff --git a/compose/integration-tests/hero/jetsnack/jetsnack-microbenchmark/build.gradle b/compose/integration-tests/hero/jetsnack/jetsnack-microbenchmark/build.gradle
index fc2f32b9..bcca37d 100644
--- a/compose/integration-tests/hero/jetsnack/jetsnack-microbenchmark/build.gradle
+++ b/compose/integration-tests/hero/jetsnack/jetsnack-microbenchmark/build.gradle
@@ -62,5 +62,5 @@
}
androidx {
- type = LibraryType.INTERNAL_TEST_LIBRARY
+ type = LibraryType.BENCHMARK
}
diff --git a/compose/integration-tests/material-catalog/build.gradle b/compose/integration-tests/material-catalog/build.gradle
index ca73ac9..fc97e89 100644
--- a/compose/integration-tests/material-catalog/build.gradle
+++ b/compose/integration-tests/material-catalog/build.gradle
@@ -22,7 +22,7 @@
* modifying its settings.
*/
import androidx.build.ApkCopyHelperKt
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -73,7 +73,7 @@
androidx {
name = "Compose Material Catalog app"
- publish = Publish.NONE
+ type = LibraryType.TEST_APPLICATION
inceptionYear = "2021"
description = "This is a project for the Compose Material Catalog app."
}
diff --git a/compose/material/material-ripple/benchmark/build.gradle b/compose/material/material-ripple/benchmark/build.gradle
index 4d59f30..b2d5bb9 100644
--- a/compose/material/material-ripple/benchmark/build.gradle
+++ b/compose/material/material-ripple/benchmark/build.gradle
@@ -52,5 +52,5 @@
}
androidx {
- type = LibraryType.INTERNAL_TEST_LIBRARY
+ type = LibraryType.BENCHMARK
}
diff --git a/compose/material/material/benchmark/build.gradle b/compose/material/material/benchmark/build.gradle
index 64803b4..97ae9aa 100644
--- a/compose/material/material/benchmark/build.gradle
+++ b/compose/material/material/benchmark/build.gradle
@@ -54,5 +54,5 @@
}
androidx {
- type = LibraryType.INTERNAL_TEST_LIBRARY
+ type = LibraryType.BENCHMARK
}
diff --git a/compose/material/material/integration-tests/material-catalog/build.gradle b/compose/material/material/integration-tests/material-catalog/build.gradle
index 29c6f75..2db1be6 100644
--- a/compose/material/material/integration-tests/material-catalog/build.gradle
+++ b/compose/material/material/integration-tests/material-catalog/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -43,7 +43,7 @@
androidx {
name = "Compose Material Catalog"
- publish = Publish.NONE
+ type = LibraryType.TEST_APPLICATION
inceptionYear = "2021"
description = "This is a project for the Compose Material Catalog."
}
diff --git a/compose/material/material/integration-tests/material-demos/build.gradle b/compose/material/material/integration-tests/material-demos/build.gradle
index 5d92707..209c329 100644
--- a/compose/material/material/integration-tests/material-demos/build.gradle
+++ b/compose/material/material/integration-tests/material-demos/build.gradle
@@ -5,7 +5,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -31,7 +31,7 @@
androidx {
name = "Compose Material Demos"
- publish = Publish.NONE
+ type = LibraryType.TEST_APPLICATION
inceptionYear = "2019"
description = "This is a project for Material demos."
}
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/SystemBarsDefaultInsets.android.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/SystemBarsDefaultInsets.android.kt
index 6977a047..b69db8d 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/SystemBarsDefaultInsets.android.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/SystemBarsDefaultInsets.android.kt
@@ -17,8 +17,10 @@
package androidx.compose.material
import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.union
import androidx.compose.runtime.Composable
internal actual val WindowInsets.Companion.systemBarsForVisualComponents: WindowInsets
- @Composable get() = systemBars
+ @Composable get() = systemBars.union(displayCutout)
diff --git a/compose/material3/adaptive/benchmark/build.gradle b/compose/material3/adaptive/benchmark/build.gradle
index 54b0da4..f63172f 100644
--- a/compose/material3/adaptive/benchmark/build.gradle
+++ b/compose/material3/adaptive/benchmark/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import androidx.build.LibraryType
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -37,3 +39,7 @@
compileSdk = 35
namespace = "androidx.compose.material3.adaptive.benchmark"
}
+
+androidx {
+ type = LibraryType.BENCHMARK
+}
diff --git a/compose/material3/benchmark/build.gradle b/compose/material3/benchmark/build.gradle
index 618db30..c2a930a 100644
--- a/compose/material3/benchmark/build.gradle
+++ b/compose/material3/benchmark/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import androidx.build.LibraryType
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -40,4 +42,8 @@
android {
compileSdk = 35
namespace = "androidx.compose.material3.benchmark"
-}
\ No newline at end of file
+}
+
+androidx {
+ type = LibraryType.BENCHMARK
+}
diff --git a/compose/material3/material3-common/build.gradle b/compose/material3/material3-common/build.gradle
index 3eac2d7..372a968 100644
--- a/compose/material3/material3-common/build.gradle
+++ b/compose/material3/material3-common/build.gradle
@@ -25,7 +25,6 @@
import androidx.build.KotlinTarget
import androidx.build.LibraryType
import androidx.build.PlatformIdentifier
-import androidx.build.Publish
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index c5a99fc..2f679dbf 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -3119,19 +3119,6 @@
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @kotlin.jvm.JvmInline public final value class WideNavigationRailArrangement {
- field public static final androidx.compose.material3.WideNavigationRailArrangement.Companion Companion;
- }
-
- public static final class WideNavigationRailArrangement.Companion {
- method public int getBottom();
- method public int getCenter();
- method public int getTop();
- property public final int Bottom;
- property public final int Center;
- property public final int Top;
- }
-
@androidx.compose.runtime.Immutable public final class WideNavigationRailColors {
ctor public WideNavigationRailColors(long containerColor, long contentColor, long modalContainerColor, long modalScrimColor);
method public androidx.compose.material3.WideNavigationRailColors copy(optional long containerColor, optional long contentColor, optional long modalContainerColor, optional long modalScrimColor);
@@ -3148,11 +3135,11 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WideNavigationRailDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.WideNavigationRailColors colors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.WideNavigationRailColors colors(optional long containerColor, optional long contentColor, optional long modalContainerColor, optional long modalScrimColor);
- method public int getArrangement();
+ method public androidx.compose.foundation.layout.Arrangement.Vertical getArrangement();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getContainerShape();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getModalContainerShape();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getWindowInsets();
- property public final int Arrangement;
+ property public final androidx.compose.foundation.layout.Arrangement.Vertical arrangement;
property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape containerShape;
property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape modalContainerShape;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets windowInsets;
@@ -3167,8 +3154,8 @@
}
public final class WideNavigationRailKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ModalWideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional boolean hideOnCollapse, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.ui.graphics.Shape expandedShape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional float expandedHeaderTopPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, optional androidx.compose.material3.ModalWideNavigationRailProperties expandedProperties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ModalWideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional boolean hideOnCollapse, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.ui.graphics.Shape expandedShape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional float expandedHeaderTopPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.foundation.layout.Arrangement.Vertical arrangement, optional androidx.compose.material3.ModalWideNavigationRailProperties expandedProperties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.foundation.layout.Arrangement.Vertical arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRailItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean railExpanded, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index c5a99fc..2f679dbf 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -3119,19 +3119,6 @@
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
}
- @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @kotlin.jvm.JvmInline public final value class WideNavigationRailArrangement {
- field public static final androidx.compose.material3.WideNavigationRailArrangement.Companion Companion;
- }
-
- public static final class WideNavigationRailArrangement.Companion {
- method public int getBottom();
- method public int getCenter();
- method public int getTop();
- property public final int Bottom;
- property public final int Center;
- property public final int Top;
- }
-
@androidx.compose.runtime.Immutable public final class WideNavigationRailColors {
ctor public WideNavigationRailColors(long containerColor, long contentColor, long modalContainerColor, long modalScrimColor);
method public androidx.compose.material3.WideNavigationRailColors copy(optional long containerColor, optional long contentColor, optional long modalContainerColor, optional long modalScrimColor);
@@ -3148,11 +3135,11 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WideNavigationRailDefaults {
method @androidx.compose.runtime.Composable public androidx.compose.material3.WideNavigationRailColors colors();
method @androidx.compose.runtime.Composable public androidx.compose.material3.WideNavigationRailColors colors(optional long containerColor, optional long contentColor, optional long modalContainerColor, optional long modalScrimColor);
- method public int getArrangement();
+ method public androidx.compose.foundation.layout.Arrangement.Vertical getArrangement();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getContainerShape();
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getModalContainerShape();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getWindowInsets();
- property public final int Arrangement;
+ property public final androidx.compose.foundation.layout.Arrangement.Vertical arrangement;
property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape containerShape;
property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape modalContainerShape;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets windowInsets;
@@ -3167,8 +3154,8 @@
}
public final class WideNavigationRailKt {
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ModalWideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional boolean hideOnCollapse, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.ui.graphics.Shape expandedShape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional float expandedHeaderTopPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, optional androidx.compose.material3.ModalWideNavigationRailProperties expandedProperties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ModalWideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional boolean hideOnCollapse, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.ui.graphics.Shape expandedShape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional float expandedHeaderTopPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.foundation.layout.Arrangement.Vertical arrangement, optional androidx.compose.material3.ModalWideNavigationRailProperties expandedProperties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.foundation.layout.Arrangement.Vertical arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRailItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean railExpanded, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
diff --git a/compose/material3/material3/integration-tests/material3-catalog/build.gradle b/compose/material3/material3/integration-tests/material3-catalog/build.gradle
index 2dfcfc5..123bb8a 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/build.gradle
+++ b/compose/material3/material3/integration-tests/material3-catalog/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
@@ -49,7 +49,7 @@
androidx {
name = "Compose Material3 Catalog"
- publish = Publish.NONE
+ type = LibraryType.TEST_APPLICATION
inceptionYear = "2021"
description = "This is a project for the Compose Material You Catalog."
}
diff --git a/compose/material3/material3/integration-tests/material3-demos/build.gradle b/compose/material3/material3/integration-tests/material3-demos/build.gradle
index 510bd2e..0890c10 100644
--- a/compose/material3/material3/integration-tests/material3-demos/build.gradle
+++ b/compose/material3/material3/integration-tests/material3-demos/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -49,7 +49,7 @@
androidx {
name = "Compose Material3 Components Demos"
- publish = Publish.NONE
+ type = LibraryType.TEST_APPLICATION
inceptionYear = "2022"
description = "Contains the demo code for the AndroidX Compose Material 3 components."
}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationRailSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationRailSamples.kt
index d221c20..3aee1b1 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationRailSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationRailSamples.kt
@@ -19,6 +19,7 @@
import android.app.Activity
import android.content.pm.ActivityInfo
import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -43,7 +44,6 @@
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.Text
import androidx.compose.material3.WideNavigationRail
-import androidx.compose.material3.WideNavigationRailArrangement
import androidx.compose.material3.WideNavigationRailItem
import androidx.compose.material3.WideNavigationRailValue
import androidx.compose.material3.rememberWideNavigationRailState
@@ -336,7 +336,9 @@
val selectedIcons = listOf(Icons.Filled.Home, Icons.Filled.Favorite, Icons.Filled.Star)
val unselectedIcons =
listOf(Icons.Outlined.Home, Icons.Outlined.FavoriteBorder, Icons.Outlined.StarBorder)
- WideNavigationRail(state = rememberWideNavigationRailState()) {
+ WideNavigationRail(
+ state = rememberWideNavigationRailState(initialValue = WideNavigationRailValue.Expanded)
+ ) {
items.forEachIndexed { index, item ->
WideNavigationRailItem(
railExpanded = true,
@@ -365,7 +367,7 @@
listOf(Icons.Outlined.Home, Icons.Outlined.FavoriteBorder, Icons.Outlined.StarBorder)
val state = rememberWideNavigationRailState()
val scope = rememberCoroutineScope()
- var arrangement by remember { mutableStateOf(WideNavigationRailArrangement.Center) }
+ var arrangement: Arrangement.Vertical by remember { mutableStateOf(Arrangement.Center) }
Row(Modifier.fillMaxWidth()) {
WideNavigationRail(
@@ -419,7 +421,7 @@
}
}
- val isArrangementCenter = arrangement == WideNavigationRailArrangement.Center
+ val isArrangementCenter = arrangement == Arrangement.Center
val changeToString = if (isArrangementCenter) "Bottom" else "Center"
Column(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) {
Text(modifier = Modifier.padding(16.dp), text = "Change arrangement to:")
@@ -427,9 +429,9 @@
modifier = Modifier.padding(4.dp),
onClick = {
if (isArrangementCenter) {
- arrangement = WideNavigationRailArrangement.Bottom
+ arrangement = Arrangement.Bottom
} else {
- arrangement = WideNavigationRailArrangement.Center
+ arrangement = Arrangement.Center
}
}
) {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialComponentsInsetSupportTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialComponentsInsetSupportTest.kt
index 3ff91fa..41088c5 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialComponentsInsetSupportTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialComponentsInsetSupportTest.kt
@@ -19,8 +19,10 @@
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.union
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.test.junit4.createAndroidComposeRule
@@ -51,7 +53,9 @@
var expected: WindowInsets? = null
rule.setContent {
expected =
- WindowInsets.systemBars.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
+ WindowInsets.systemBars
+ .union(WindowInsets.displayCutout)
+ .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
contentPadding = TopAppBarDefaults.windowInsets
}
@@ -64,9 +68,9 @@
var expected: WindowInsets? = null
rule.setContent {
expected =
- WindowInsets.systemBars.only(
- WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
- )
+ WindowInsets.systemBars
+ .union(WindowInsets.displayCutout)
+ .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom)
contentPadding = BottomAppBarDefaults.windowInsets
}
@@ -82,7 +86,9 @@
var expected: WindowInsets? = null
rule.setContent {
expected =
- WindowInsets.systemBars.only(WindowInsetsSides.Start + WindowInsetsSides.Vertical)
+ WindowInsets.systemBars
+ .union(WindowInsets.displayCutout)
+ .only(WindowInsetsSides.Start + WindowInsetsSides.Vertical)
contentPadding = DrawerDefaults.windowInsets
}
@@ -95,9 +101,9 @@
var expected: WindowInsets? = null
rule.setContent {
expected =
- WindowInsets.systemBars.only(
- WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal
- )
+ WindowInsets.systemBars
+ .union(WindowInsets.displayCutout)
+ .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal)
contentPadding = NavigationBarDefaults.windowInsets
}
@@ -110,7 +116,9 @@
var expected: WindowInsets? = null
rule.setContent {
expected =
- WindowInsets.systemBars.only(WindowInsetsSides.Start + WindowInsetsSides.Vertical)
+ WindowInsets.systemBars
+ .union(WindowInsets.displayCutout)
+ .only(WindowInsetsSides.Start + WindowInsetsSides.Vertical)
contentPadding = NavigationRailDefaults.windowInsets
}
@@ -124,7 +132,10 @@
var layoutDirection: LayoutDirection? = null
rule.setContent {
layoutDirection = LocalLayoutDirection.current
- expected = WindowInsets.systemBars.asPaddingValues(LocalDensity.current)
+ expected =
+ WindowInsets.systemBars
+ .union(WindowInsets.displayCutout)
+ .asPaddingValues(LocalDensity.current)
Scaffold { paddingValues -> contentPadding = paddingValues }
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailScreenshotTest.kt
index bf6d8bd..fc8f93e 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailScreenshotTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@@ -157,7 +158,7 @@
DefaultWideNavigationRail(
interactionSource,
expanded = scheme.expanded,
- arrangement = WideNavigationRailArrangement.Center
+ arrangement = Arrangement.Center
)
}
@@ -182,7 +183,7 @@
DefaultWideNavigationRail(
interactionSource,
expanded = scheme.expanded,
- arrangement = WideNavigationRailArrangement.Bottom
+ arrangement = Arrangement.Bottom
)
}
@@ -273,7 +274,7 @@
* @param interactionSource the [MutableInteractionSource] for the first [WideNavigationRailItem],
* to control its visual state
* @param expanded whether the rail is expanded
- * @param arrangement the [WideNavigationRailArrangement] of the rail
+ * @param arrangement the [Arrangement.Vertical] of the rail
* @param withHeader when true, shows a [FloatingActionButton] as the header
* @param setUnselectedItemsAsDisabled when true, marks unselected items as disabled
*/
@@ -282,7 +283,7 @@
private fun DefaultWideNavigationRail(
interactionSource: MutableInteractionSource,
expanded: Boolean = false,
- arrangement: WideNavigationRailArrangement = WideNavigationRailArrangement.Top,
+ arrangement: Arrangement.Vertical = Arrangement.Top,
withHeader: Boolean = false,
setUnselectedItemsAsDisabled: Boolean = false,
) {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailTest.kt
index 521309b..56c08e6 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.material3
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
@@ -538,7 +539,7 @@
rule.setMaterialContent(lightColorScheme()) {
WideNavigationRail(
modifier = Modifier.testTag("rail"),
- arrangement = WideNavigationRailArrangement.Center,
+ arrangement = Arrangement.Center,
header = { Box(Modifier.testTag("header").size(10.dp)) }
) {
WideNavigationRailItem(
@@ -570,7 +571,7 @@
rule.setMaterialContent(lightColorScheme()) {
WideNavigationRail(
modifier = Modifier.testTag("rail"),
- arrangement = WideNavigationRailArrangement.Bottom,
+ arrangement = Arrangement.Bottom,
header = { Box(Modifier.testTag("header").size(10.dp)) }
) {
WideNavigationRailItem(
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.android.kt
index fe4e4a7..5299afb 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.android.kt
@@ -17,8 +17,10 @@
package androidx.compose.material3.internal
import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.union
import androidx.compose.runtime.Composable
internal actual val WindowInsets.Companion.systemBarsForVisualComponents: WindowInsets
- @Composable get() = systemBars
+ @Composable get() = systemBars.union(displayCutout)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
index da4cab6..d6b0f7f 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
@@ -27,13 +27,13 @@
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
@@ -94,11 +94,9 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.offset
import androidx.compose.ui.util.fastFirst
-import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastMap
-import androidx.compose.ui.util.fastSumBy
import androidx.compose.ui.util.lerp
-import kotlin.jvm.JvmInline
import kotlin.math.min
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
@@ -132,9 +130,8 @@
*
* For a modal variation of the wide navigation rail, see [ModalWideNavigationRail].
*
- * Finally, the [WideNavigationRail] supports setting a [WideNavigationRailArrangement] for the
- * items, so that the items can be grouped at the top (the default), at the middle, or at the bottom
- * of the rail. The header will always be at the top.
+ * Finally, the [WideNavigationRail] supports setting an [Arrangement.Vertical] for the items, with
+ * [Arrangement.Top] being the default. The header will always be at the top.
*
* See [WideNavigationRailItem] for configuration specific to each item, and not the overall
* [WideNavigationRail] component.
@@ -146,7 +143,9 @@
* wide navigation rail. See [WideNavigationRailDefaults.colors]
* @param header optional header that may hold a [FloatingActionButton] or a logo
* @param windowInsets a window insets of the wide navigation rail
- * @param arrangement the [WideNavigationRailArrangement] of this wide navigation rail
+ * @param arrangement the [Arrangement.Vertical] of this wide navigation rail for its content. Note
+ * that if there's a header present, the items will be arranged on the remaining space below it,
+ * except for the center arrangement which considers the entire height of the container
* @param content the content of this wide navigation rail, typically [WideNavigationRailItem]s
*/
@ExperimentalMaterial3ExpressiveApi
@@ -158,7 +157,7 @@
colors: WideNavigationRailColors = WideNavigationRailDefaults.colors(),
header: @Composable (() -> Unit)? = null,
windowInsets: WindowInsets = WideNavigationRailDefaults.windowInsets,
- arrangement: WideNavigationRailArrangement = WideNavigationRailDefaults.Arrangement,
+ arrangement: Arrangement.Vertical = WideNavigationRailDefaults.arrangement,
content: @Composable () -> Unit
) {
WideNavigationRailLayout(
@@ -174,7 +173,6 @@
)
}
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun WideNavigationRailLayout(
modifier: Modifier,
@@ -184,7 +182,7 @@
shape: Shape,
header: @Composable (() -> Unit)?,
windowInsets: WindowInsets,
- arrangement: WideNavigationRailArrangement,
+ arrangement: Arrangement.Vertical,
content: @Composable () -> Unit
) {
var currentWidth by remember { mutableIntStateOf(0) }
@@ -345,34 +343,39 @@
currentWidth = width
return layout(width, height) {
- var y = 0
- var headerHeight = 0
+ val railHeight = height - WNRVerticalPadding.roundToPx()
+ var headerOffset = 0
if (headerPlaceable != null && headerPlaceable.height > 0) {
- headerPlaceable.placeRelative(0, y)
- headerHeight = headerPlaceable.height
- if (arrangement == WideNavigationRailArrangement.Top) {
- y += headerHeight + WNRHeaderPadding.roundToPx()
- }
+ headerPlaceable.placeRelative(0, 0)
+ headerOffset +=
+ headerPlaceable.height + WNRHeaderPadding.roundToPx()
}
- val itemsHeight = itemsPlaceables?.fastSumBy { it.height } ?: 0
- val verticalPadding = itemVerticalSpacedBy.roundToPx()
- if (arrangement == WideNavigationRailArrangement.Center) {
- y =
- (height -
- WNRVerticalPadding.roundToPx() -
- (itemsHeight + (itemsCount - 1) * verticalPadding)) / 2
- y = y.coerceAtLeast(headerHeight)
- } else if (arrangement == WideNavigationRailArrangement.Bottom) {
- y =
- height -
- WNRVerticalPadding.roundToPx() -
- (itemsHeight + (itemsCount - 1) * verticalPadding)
- y = y.coerceAtLeast(headerHeight)
- }
- itemsPlaceables?.fastForEach { item ->
- item.placeRelative(0, y)
- y += item.height + verticalPadding
+ if (itemsPlaceables != null) {
+ val layoutSize =
+ if (arrangement == Arrangement.Center) {
+ // For centered arrangement the items will be centered in
+ // the container, not in the remaining space below the
+ // header.
+ railHeight
+ } else {
+ railHeight - headerOffset
+ }
+ val sizes = IntArray(itemsPlaceables.size)
+ itemsPlaceables.fastForEachIndexed { index, item ->
+ sizes[index] = item.height
+ if (index < itemsPlaceables.size - 1) {
+ sizes[index] += itemVerticalSpacedBy.roundToPx()
+ }
+ }
+ val y = IntArray(itemsPlaceables.size)
+ with(arrangement) { arrange(layoutSize, sizes, y) }
+
+ val offset =
+ if (arrangement == Arrangement.Center) 0 else headerOffset
+ itemsPlaceables.fastForEachIndexed { index, item ->
+ item.placeRelative(0, y[index] + offset)
+ }
}
}
}
@@ -419,7 +422,7 @@
* @param expandedHeaderTopPadding the padding to be applied to the top of the rail. It's usually
* needed in order to align the content of the rail between the collapsed and expanded animation
* @param windowInsets a window insets of the wide navigation rail
- * @param arrangement the [WideNavigationRailArrangement] of this wide navigation rail
+ * @param arrangement the [Arrangement.Vertical] of this wide navigation rail
* @param expandedProperties [ModalWideNavigationRailProperties] for further customization of the
* expanded modal wide navigation rail's window behavior
* @param content the content of this modal wide navigation rail, usually [WideNavigationRailItem]s
@@ -436,7 +439,7 @@
header: @Composable (() -> Unit)? = null,
expandedHeaderTopPadding: Dp = 0.dp,
windowInsets: WindowInsets = WideNavigationRailDefaults.windowInsets,
- arrangement: WideNavigationRailArrangement = WideNavigationRailDefaults.Arrangement,
+ arrangement: Arrangement.Vertical = WideNavigationRailDefaults.arrangement,
expandedProperties: ModalWideNavigationRailProperties =
ModalWideNavigationRailDefaults.Properties,
content: @Composable () -> Unit
@@ -654,30 +657,6 @@
)
}
-/** Class that describes the different supported item arrangements of the [WideNavigationRail]. */
-@ExperimentalMaterial3ExpressiveApi
-@JvmInline
-value class WideNavigationRailArrangement private constructor(private val value: Int) {
- companion object {
- /* The items are grouped at the top on the wide navigation Rail. */
- val Top = WideNavigationRailArrangement(0)
-
- /* The items are centered on the wide navigation Rail. */
- val Center = WideNavigationRailArrangement(1)
-
- /* The items are grouped at the bottom on the wide navigation Rail. */
- val Bottom = WideNavigationRailArrangement(2)
- }
-
- override fun toString() =
- when (this) {
- Top -> "Top"
- Center -> "Center"
- Bottom -> "Bottom"
- else -> "Unknown"
- }
-}
-
/**
* Represents the colors of the various elements of a wide navigation rail.
*
@@ -749,8 +728,8 @@
@Composable get() = NavigationRailExpandedTokens.ModalContainerShape.value
/** Default arrangement for a wide navigation rail. */
- val Arrangement: WideNavigationRailArrangement
- get() = WideNavigationRailArrangement.Top
+ val arrangement: Arrangement.Vertical
+ get() = Arrangement.Top
/** Default window insets for a wide navigation rail. */
val windowInsets: WindowInsets
@@ -932,7 +911,7 @@
header: @Composable (() -> Unit)?,
windowInsets: WindowInsets,
gesturesEnabled: Boolean,
- arrangement: WideNavigationRailArrangement,
+ arrangement: Arrangement.Vertical,
content: @Composable () -> Unit
) {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
@@ -1094,7 +1073,6 @@
private val CollapsedRailWidth = NavigationRailCollapsedTokens.ContainerWidth
private val ExpandedRailMinWidth = NavigationRailExpandedTokens.ContainerWidthMinimum
private val ExpandedRailMaxWidth = NavigationRailExpandedTokens.ContainerWidthMaximum
-private val ItemMinWidth = NavigationRailCollapsedTokens.ContainerWidth
private val TopIconItemMinHeight = NavigationRailBaselineItemTokens.ContainerHeight
private val ItemTopIconIndicatorVerticalPadding =
(NavigationRailVerticalItemTokens.ActiveIndicatorHeight -
diff --git a/compose/runtime/runtime-test-utils/build.gradle b/compose/runtime/runtime-test-utils/build.gradle
index f9f82e1..0b326a2 100644
--- a/compose/runtime/runtime-test-utils/build.gradle
+++ b/compose/runtime/runtime-test-utils/build.gradle
@@ -15,7 +15,6 @@
*/
import androidx.build.LibraryType
import androidx.build.PlatformIdentifier
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
@@ -75,9 +74,9 @@
}
androidx {
+ // This library is consumed by Kotlin CI to run Compose runtime test with the latest compiler.
name = "Compose Internal Test Utils"
- type = LibraryType.INTERNAL_TEST_LIBRARY
- publish = Publish.SNAPSHOT_ONLY
+ type = LibraryType.SNAPSHOT_ONLY_LIBRARY
inceptionYear = "2024"
description = "Compose runtime test utils shared between runtime and compiler tests."
}
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
index ce213f7..1a6f050 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
+++ b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
@@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+import androidx.build.LibraryType
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -58,3 +61,7 @@
// See aosp/1804059
androidTestImplementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
}
+
+androidx {
+ type = LibraryType.BENCHMARK
+}
\ No newline at end of file
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index b8bb931..49de56d 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
diff --git a/compose/ui/ui-graphics/benchmark/build.gradle b/compose/ui/ui-graphics/benchmark/build.gradle
index 6f9f2ec..5023dea 100644
--- a/compose/ui/ui-graphics/benchmark/build.gradle
+++ b/compose/ui/ui-graphics/benchmark/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import androidx.build.LibraryType
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -37,3 +39,7 @@
compileSdk = 35
namespace = "androidx.compose.ui.graphics.benchmark"
}
+
+androidx {
+ type = LibraryType.BENCHMARK
+}
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt
index f9c09ef..f3910b4 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt
@@ -140,4 +140,10 @@
waitUntilDoesNotExist(hasTestTag(TestTag), timeoutMillis = Timeout)
}
}
+
+ // Regression for b/361250553
+ @Test
+ fun waitUntil_succeedsWhen_noRoots() = runComposeUiTest {
+ waitUntilDoesNotExist(hasTestTag(TestTag), Timeout)
+ }
}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
index 987f728a..9e931b1 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
@@ -228,7 +228,9 @@
timeoutMillis: Long = 1_000L
) {
waitUntil("exactly $count nodes match (${matcher.description})", timeoutMillis) {
- onAllNodes(matcher).fetchSemanticsNodes().size == count
+ // Never require the existence of compose roots. Either the current UI or the anticipated UI
+ // might not have any compose at all (i.e. View only).
+ onAllNodes(matcher).fetchSemanticsNodes(atLeastOneRootRequired = false).size == count
}
}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
index b019338..63c3346 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
@@ -60,17 +60,14 @@
atLeastOneRootRequired: Boolean,
errorMessageOnFail: String? = null,
skipDeactivatedNodes: Boolean = true
- ): SelectionResult {
- val nodes =
- testContext.testOwner.getAllSemanticsNodes(
- atLeastOneRootRequired = atLeastOneRootRequired,
- useUnmergedTree = useUnmergedTree,
- skipDeactivatedNodes = skipDeactivatedNodes
- )
- return testContext.testOwner.runOnUiThread {
- selector.map(nodes, errorMessageOnFail.orEmpty())
+ ): SelectionResult =
+ testContext.testOwner.getAllSemanticsNodes(
+ atLeastOneRootRequired = atLeastOneRootRequired,
+ useUnmergedTree = useUnmergedTree,
+ skipDeactivatedNodes = skipDeactivatedNodes
+ ) {
+ selector.map(it, errorMessageOnFail.orEmpty())
}
- }
/**
* Returns the semantics node captured by this object.
@@ -201,13 +198,11 @@
/** If using the merged tree, performs the same search in the unmerged tree. */
private fun getNodesInUnmergedTree(errorMessageOnFail: String?): List<SemanticsNode> {
return if (!useUnmergedTree) {
- val nodes =
- testContext.testOwner.getAllSemanticsNodes(
- atLeastOneRootRequired = true,
- useUnmergedTree = true
- )
- testContext.testOwner.runOnUiThread {
- selector.map(nodes, errorMessageOnFail.orEmpty()).selectedNodes
+ testContext.testOwner.getAllSemanticsNodes(
+ atLeastOneRootRequired = true,
+ useUnmergedTree = true
+ ) {
+ selector.map(it, errorMessageOnFail.orEmpty()).selectedNodes
}
} else {
emptyList()
@@ -234,8 +229,6 @@
internal val useUnmergedTree: Boolean,
internal val selector: SemanticsSelector
) {
- private var nodeIds: List<Int>? = null
-
constructor(
testContext: TestContext,
useUnmergedTree: Boolean,
@@ -258,19 +251,9 @@
atLeastOneRootRequired: Boolean = true,
errorMessageOnFail: String? = null
): List<SemanticsNode> {
- if (nodeIds == null) {
- val nodes =
- testContext.testOwner.getAllSemanticsNodes(atLeastOneRootRequired, useUnmergedTree)
-
- return testContext.testOwner
- .runOnUiThread { selector.map(nodes, errorMessageOnFail.orEmpty()) }
- .apply { nodeIds = selectedNodes.map { it.id }.toList() }
- .selectedNodes
+ return testContext.testOwner.getAllSemanticsNodes(atLeastOneRootRequired, useUnmergedTree) {
+ selector.map(it, errorMessageOnFail.orEmpty()).selectedNodes
}
-
- return testContext.testOwner
- .getAllSemanticsNodes(atLeastOneRootRequired, useUnmergedTree)
- .filter { it.id in nodeIds!! }
}
/**
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
index de45f6e..8560ecd 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
@@ -40,10 +40,10 @@
/**
* Collects all [RootForTest]s from all compose hierarchies.
*
- * This is a blocking call. Returns only after compose is idle.
- *
- * Can crash in case it hits time out. This is not supposed to be handled as it surfaces only in
- * incorrect tests.
+ * This method is the choke point where all assertions and interactions must go through when
+ * testing composables and where we thus have the opportunity to automatically reach quiescence.
+ * This is done by calling [ComposeUiTest.waitForIdle] before getting and returning the
+ * registered roots.
*
* @param atLeastOneRootExpected Whether the caller expects that at least one compose root is
* present in the tested app. This affects synchronization efforts / timeouts of this API.
@@ -52,18 +52,31 @@
}
/**
- * Collects all [SemanticsNode]s from all compose hierarchies.
+ * Collects all [SemanticsNode]s from all compose hierarchies, and returns the [transform]ed
+ * results.
*
- * This is a blocking call. Returns only after compose is idle.
+ * Set [useUnmergedTree] to `true` to search through the unmerged semantics tree.
*
- * Can crash in case it hits time out. This is not supposed to be handled as it surfaces only in
- * incorrect tests.
+ * Set [skipDeactivatedNodes] to `false` to include
+ * [deactivated][androidx.compose.ui.node.LayoutNode.isDeactivated] nodes in the search.
+ *
+ * Use [atLeastOneRootRequired] to treat not finding any compose hierarchies at all as an error. If
+ * no hierarchies are found, we will wait 2 seconds to accommodate cases where composable content is
+ * set asynchronously. On the other hand, if you expect or know that there is no composable content,
+ * set [atLeastOneRootRequired] to `false` and no error will be thrown if there are no compose
+ * roots, and the wait for compose roots will be reduced to .5 seconds.
+ *
+ * This method will wait for quiescence before collecting all SemanticsNodes. Collection happens on
+ * the main thread and the [transform]ation of all SemanticsNodes to a result is done while on the
+ * main thread. This allows us to transform the result using methods that must be called on the main
+ * thread, without switching back and forth between the main thread and the test thread.
*/
-internal fun TestOwner.getAllSemanticsNodes(
+internal fun <R> TestOwner.getAllSemanticsNodes(
atLeastOneRootRequired: Boolean,
useUnmergedTree: Boolean,
- skipDeactivatedNodes: Boolean = true
-): Iterable<SemanticsNode> {
+ skipDeactivatedNodes: Boolean = true,
+ transform: (Iterable<SemanticsNode>) -> R
+): R {
val roots =
getRoots(atLeastOneRootRequired).also {
check(!atLeastOneRootRequired || it.isNotEmpty()) {
@@ -77,11 +90,13 @@
}
return runOnUiThread {
- roots.flatMap {
- it.semanticsOwner.getAllSemanticsNodes(
- mergingEnabled = !useUnmergedTree,
- skipDeactivatedNodes = skipDeactivatedNodes
- )
- }
+ transform.invoke(
+ roots.flatMap {
+ it.semanticsOwner.getAllSemanticsNodes(
+ mergingEnabled = !useUnmergedTree,
+ skipDeactivatedNodes = skipDeactivatedNodes
+ )
+ }
+ )
}
}
diff --git a/compose/ui/ui-text/benchmark/build.gradle b/compose/ui/ui-text/benchmark/build.gradle
index 19f23c8..2f33706 100644
--- a/compose/ui/ui-text/benchmark/build.gradle
+++ b/compose/ui/ui-text/benchmark/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import androidx.build.LibraryType
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -42,3 +44,7 @@
compileSdk = 35
namespace = "androidx.compose.ui.text.benchmark"
}
+
+androidx {
+ type = LibraryType.BENCHMARK
+}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/CacheTextLayoutInputTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/CacheTextLayoutInputTest.kt
index cbd6e80..585ce22 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/CacheTextLayoutInputTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/CacheTextLayoutInputTest.kt
@@ -202,12 +202,12 @@
}
@Test
- fun minConstraints_should_not_differ() {
+ fun minConstraints_should_differ() {
val input1 = cacheTextLayoutInput(constraints = Constraints(minWidth = 10, minHeight = 20))
val input2 = cacheTextLayoutInput(constraints = Constraints(minWidth = 20, minHeight = 10))
- assertThat(input1.hashCode()).isEqualTo(input2.hashCode())
- assertThat(input1).isEqualTo(input2)
+ assertThat(input1.hashCode()).isNotEqualTo(input2.hashCode())
+ assertThat(input1).isNotEqualTo(input2)
}
private fun cacheTextLayoutInput(
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextLayoutCacheTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextLayoutCacheTest.kt
index 819b397..34c02cb 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextLayoutCacheTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextLayoutCacheTest.kt
@@ -175,7 +175,7 @@
}
@Test
- fun constraintsMinChanges_shouldReturnFromCache() {
+ fun constraintsMinChanges_shouldReturnNull() {
val textLayoutCache = TextLayoutCache(16)
val firstInput =
textLayoutInput(
@@ -194,7 +194,7 @@
val textLayoutResult = layoutText(firstInput)
textLayoutCache.put(firstInput, textLayoutResult)
- Truth.assertThat(textLayoutCache.get(secondInput)).isEqualTo(textLayoutResult)
+ Truth.assertThat(textLayoutCache.get(secondInput)).isNull()
}
@Test
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
index 5af082b..d278c21 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
@@ -458,6 +458,31 @@
}
@Test
+ fun decreasingMinWidth_decreasesTheCalculatedWidth() {
+ val textMeasurer = textMeasurer(cacheSize = 8)
+ val firstTextLayout =
+ layoutText(
+ textLayoutInput(
+ constraints = Constraints(minWidth = 1000, maxWidth = Int.MAX_VALUE)
+ ),
+ textMeasurer
+ )
+
+ val secondTextLayout =
+ layoutText(
+ textLayoutInput(
+ constraints = Constraints(minWidth = 500, maxWidth = Int.MAX_VALUE)
+ ),
+ textMeasurer
+ )
+
+ assertThat(firstTextLayout.multiParagraph)
+ .isNotSameInstanceAs(secondTextLayout.multiParagraph)
+ assertThat(firstTextLayout.size.width).isEqualTo(1000)
+ assertThat(secondTextLayout.size.width).isEqualTo(500)
+ }
+
+ @Test
fun emptyConstraints_hugeString_dontCrash() {
val subject = textMeasurer()
subject.measure("A".repeat(100_000), TextStyle.Default)
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
index b35f666..d146fac 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
@@ -421,8 +421,7 @@
result = 31 * result + density.hashCode()
result = 31 * result + layoutDirection.hashCode()
result = 31 * result + fontFamilyResolver.hashCode()
- result = 31 * result + constraints.maxWidth.hashCode()
- result = 31 * result + constraints.maxHeight.hashCode()
+ result = 31 * result + constraints.hashCode()
return result
}
@@ -440,8 +439,7 @@
if (density != other.textLayoutInput.density) return false
if (layoutDirection != other.textLayoutInput.layoutDirection) return false
if (fontFamilyResolver !== other.textLayoutInput.fontFamilyResolver) return false
- if (constraints.maxWidth != other.textLayoutInput.constraints.maxWidth) return false
- if (constraints.maxHeight != other.textLayoutInput.constraints.maxHeight) return false
+ if (constraints != other.textLayoutInput.constraints) return false
}
return true
diff --git a/compose/ui/ui/benchmark/build.gradle b/compose/ui/ui/benchmark/build.gradle
index 216aa33..cddf687 100644
--- a/compose/ui/ui/benchmark/build.gradle
+++ b/compose/ui/ui/benchmark/build.gradle
@@ -55,5 +55,5 @@
}
androidx {
- type = LibraryType.INTERNAL_TEST_LIBRARY
+ type = LibraryType.BENCHMARK
}
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/AndroidAutofillBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/AndroidAutofillBenchmark.kt
index 465fc6b..d4ba384 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/AndroidAutofillBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/AndroidAutofillBenchmark.kt
@@ -21,9 +21,6 @@
import android.view.autofill.AutofillValue
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeatedOnMainThread
-import androidx.compose.ui.autofill.AutofillNode
-import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.autofill.AutofillType
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.platform.LocalAutofillTree
import androidx.compose.ui.platform.LocalView
@@ -44,7 +41,7 @@
@get:Rule val benchmarkRule = BenchmarkRule()
- private lateinit var autofillTree: AutofillTree
+ private lateinit var autofillTree: androidx.compose.ui.autofill.AutofillTree
private lateinit var composeView: View
@Before
@@ -62,9 +59,10 @@
composeTestRule.runOnUiThread {
// Arrange.
val autofillNode =
- AutofillNode(
+ androidx.compose.ui.autofill.AutofillNode(
onFill = {},
- autofillTypes = listOf(AutofillType.PersonFullName),
+ autofillTypes =
+ listOf(androidx.compose.ui.autofill.AutofillType.PersonFullName),
boundingBox = Rect(0f, 0f, 0f, 0f)
)
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index 47bd70f..dc19905 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -18,7 +18,6 @@
import android.os.Build
import android.os.Build.VERSION.SDK_INT
-import android.os.Build.VERSION_CODES.O
import androidx.annotation.RequiresApi
import androidx.compose.foundation.demos.text.SoftwareKeyboardControllerDemo
import androidx.compose.integration.demos.common.ActivityDemo
@@ -38,6 +37,7 @@
import androidx.compose.ui.demos.autofill.BasicTextFieldAutofill
import androidx.compose.ui.demos.autofill.ExplicitAutofillTypesDemo
import androidx.compose.ui.demos.autofill.LegacyTextFieldAutofillDemo
+import androidx.compose.ui.demos.autofill.MixedOldNewAutofillDemo
import androidx.compose.ui.demos.autofill.OutlinedTextFieldAutofillDemo
import androidx.compose.ui.demos.focus.AdjacentScrollablesFocusDemo
import androidx.compose.ui.demos.focus.CancelFocusDemo
@@ -288,7 +288,8 @@
},
ComposableDemo("S: TextField Autofill") { LegacyTextFieldAutofillDemo() },
ComposableDemo("S: OutlinedTextField Autofill") { OutlinedTextFieldAutofillDemo() },
- ComposableDemo("Navigation Sample") { AutofillNavigation() }
+ ComposableDemo("Navigation Sample") { AutofillNavigation() },
+ ComposableDemo("Old and New Autofill Mixed") { MixedOldNewAutofillDemo() }
)
)
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
index 739374e..f9e568a 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
@@ -32,8 +32,6 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.AutofillNode
-import androidx.compose.ui.autofill.AutofillType
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
@@ -43,7 +41,6 @@
import androidx.compose.ui.unit.dp
@Composable
-@OptIn(ExperimentalComposeUiApi::class)
fun ExplicitAutofillTypesDemo() {
var name by
rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
@@ -52,7 +49,7 @@
Column {
Autofill(
- autofillTypes = listOf(AutofillType.PersonFullName),
+ autofillTypes = listOf(androidx.compose.ui.autofill.AutofillType.PersonFullName),
onFill = { name = TextFieldValue(it) }
) {
OutlinedTextField(
@@ -65,7 +62,7 @@
Spacer(Modifier.height(10.dp))
Autofill(
- autofillTypes = listOf(AutofillType.EmailAddress),
+ autofillTypes = listOf(androidx.compose.ui.autofill.AutofillType.EmailAddress),
onFill = { email = TextFieldValue(it) }
) {
OutlinedTextField(
@@ -77,18 +74,20 @@
}
}
-@ExperimentalComposeUiApi
@Composable
private fun Autofill(
- autofillTypes: List<AutofillType>,
+ autofillTypes: List<androidx.compose.ui.autofill.AutofillType>,
onFill: ((String) -> Unit),
content: @Composable BoxScope.() -> Unit
) {
- val autofill = LocalAutofill.current
+ val autofill = @OptIn(ExperimentalComposeUiApi::class) LocalAutofill.current
val autofillTree = LocalAutofillTree.current
val autofillNode =
remember(autofillTypes, onFill) {
- AutofillNode(onFill = onFill, autofillTypes = autofillTypes)
+ androidx.compose.ui.autofill.AutofillNode(
+ onFill = onFill,
+ autofillTypes = autofillTypes
+ )
}
Box(
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/MixedOldNewAutofillDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/MixedOldNewAutofillDemo.kt
new file mode 100644
index 0000000..fd19751
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/MixedOldNewAutofillDemo.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.demos.autofill
+
+import android.annotation.SuppressLint
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.input.TextFieldState
+import androidx.compose.foundation.text.input.rememberTextFieldState
+import androidx.compose.material.Button
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.autofill.ContentType
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalAutofill
+import androidx.compose.ui.platform.LocalAutofillManager
+import androidx.compose.ui.platform.LocalAutofillTree
+import androidx.compose.ui.semantics.contentType
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlin.collections.set
+
+@RequiresApi(Build.VERSION_CODES.O)
+@SuppressLint("NullAnnotationGroup")
+@Preview
+@Composable
+fun MixedOldNewAutofillDemo() {
+ Column(modifier = Modifier.background(color = Color.Black)) {
+ Text(text = "Enter your username and password below.", color = Color.White)
+
+ // Text field using new autofill API.
+ BasicTextField(
+ state = remember { TextFieldState() },
+ modifier =
+ Modifier.fillMaxWidth().border(1.dp, Color.LightGray).semantics {
+ contentType = ContentType.Username
+ },
+ textStyle = MaterialTheme.typography.body1.copy(color = Color.LightGray),
+ cursorBrush = SolidColor(Color.White)
+ )
+
+ // Text field using old autofill API.
+ val autofill = @OptIn(ExperimentalComposeUiApi::class) LocalAutofill.current
+ val autofillTree = LocalAutofillTree.current
+ val textState = rememberTextFieldState()
+ val autofillNode = remember {
+ androidx.compose.ui.autofill.AutofillNode(
+ onFill = { textState.edit { replace(0, length, it) } },
+ autofillTypes = listOf(androidx.compose.ui.autofill.AutofillType.Password),
+ )
+ }
+ BasicTextField(
+ state = textState,
+ modifier =
+ Modifier.fillMaxWidth()
+ .border(1.dp, Color.LightGray)
+ .onGloballyPositioned { autofillNode.boundingBox = it.boundsInWindow() }
+ .onFocusChanged {
+ if (it.isFocused) {
+ autofill?.requestAutofillForNode(autofillNode)
+ } else {
+ autofill?.cancelAutofillForNode(autofillNode)
+ }
+ },
+ textStyle = MaterialTheme.typography.body1.copy(color = Color.LightGray),
+ cursorBrush = SolidColor(Color.White)
+ )
+ DisposableEffect(autofillNode) {
+ autofillTree.children[autofillNode.id] = autofillNode
+ onDispose { autofillTree.children.remove(autofillNode.id) }
+ }
+
+ // Submit button (Only available using the new autofill APIs.
+ val autofillManager = LocalAutofillManager.current
+ Button(onClick = { autofillManager?.commit() }) { Text("Submit credentials") }
+ }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
index 6f44c0a..b3bc836 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
@@ -28,6 +28,7 @@
import androidx.compose.ui.platform.LocalAutofillTree
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
@@ -77,8 +78,6 @@
@SdkSuppress(minSdkVersion = 26)
@Test
fun onProvideAutofillVirtualStructure_populatesViewStructure() {
- // TODO(b/383201236): Ensure the old API works when the new API is enabled.
- if (isSemanticAutofillEnabled) return
// Arrange.
val viewStructure: ViewStructure = FakeViewStructure()
val autofillNode =
@@ -97,14 +96,20 @@
assertThat(viewStructure)
.isEqualTo(
FakeViewStructure().apply {
+ if (isSemanticAutofillEnabled) {
+ autofillId = ownerView.autofillId
+ bounds = android.graphics.Rect(0, 0, 0, 0)
+ packageName = currentPackageName
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+ }
children.add(
FakeViewStructure().apply {
- virtualId = autofillNode.id
+ autofillHints = mutableListOf(AUTOFILL_HINT_PERSON_NAME)
autofillId = ownerView.autofillId
autofillType = View.AUTOFILL_TYPE_TEXT
- autofillHints = mutableListOf(AUTOFILL_HINT_PERSON_NAME)
- packageName = currentPackageName
bounds = android.graphics.Rect(0, 0, 0, 0)
+ packageName = currentPackageName
+ virtualId = autofillNode.id
}
)
}
@@ -114,14 +119,12 @@
@SdkSuppress(minSdkVersion = 26)
@Test
fun autofill_triggersOnFill() {
- // TODO(b/383201236): Ensure the old API works when the new API is enabled.
- if (isSemanticAutofillEnabled) return
// Arrange.
val expectedValue = "PersonName"
- var autofilledValue = ""
+ var autoFilledValue = ""
val autofillNode =
AutofillNode(
- onFill = { autofilledValue = it },
+ onFill = { autoFilledValue = it },
autofillTypes = listOf(AutofillType.PersonFullName),
boundingBox = Rect(0f, 0f, 0f, 0f)
)
@@ -135,6 +138,6 @@
ownerView.autofill(autofillValues)
// Assert.
- assertThat(autofilledValue).isEqualTo(expectedValue)
+ assertThat(autoFilledValue).isEqualTo(expectedValue)
}
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
index fa7cd95..5d4cdac 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
@@ -31,6 +31,7 @@
import androidx.compose.ui.platform.LocalAutofillManager
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.contentDataType
+import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.contentType
import androidx.compose.ui.semantics.editableText
import androidx.compose.ui.semantics.focused
@@ -196,6 +197,34 @@
@Test
@SmallTest
+ fun autofillManager_doNotCallCommit_nonAutofillRelatedNodesAddedAndDisappear() {
+ val am: PlatformAutofillManager = mock()
+ var isVisible by mutableStateOf(true)
+ var semanticsExist by mutableStateOf(false)
+
+ rule.setContent {
+ (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+ if (isVisible) {
+ Box(
+ modifier =
+ Modifier.then(
+ if (semanticsExist)
+ Modifier.semantics { contentDescription = "contentDescription" }
+ else Modifier.size(height, width)
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle { semanticsExist = true }
+ rule.runOnIdle { isVisible = false }
+
+ // Adding in semantics not related to autofill should not trigger commit
+ rule.runOnIdle { verify(am, never()).commit() }
+ }
+
+ @Test
+ @SmallTest
fun autofillManager_callCommit_nodesBecomeAutofillRelatedAndDisappear() {
val am: PlatformAutofillManager = mock()
var isVisible by mutableStateOf(true)
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
index aa3ffa8..a80a5ea 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
@@ -59,7 +59,7 @@
@JvmField var hint: CharSequence? = null,
@JvmField var htmlInfo: HtmlInfo? = null,
@JvmField var inputType: Int = 0,
- @JvmField var isEnabled: Boolean = false,
+ @JvmField var isEnabled: Boolean = true,
@JvmField var isAccessibilityFocused: Boolean = false,
@JvmField var isChecked: Boolean = false,
@JvmField var isClickable: Boolean = false,
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/MixedAutofillTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/MixedAutofillTest.kt
new file mode 100644
index 0000000..a955f9d
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/MixedAutofillTest.kt
@@ -0,0 +1,403 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.autofill
+
+import android.graphics.Rect as AndroidRect
+import android.os.Build
+import android.util.SparseArray
+import android.view.View
+import android.view.View.AUTOFILL_TYPE_TEXT
+import android.view.ViewStructure
+import android.view.autofill.AutofillValue
+import androidx.annotation.RequiresApi
+import androidx.autofill.HintConstants
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalAutofillTree
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.contentType
+import androidx.compose.ui.semantics.onAutofillText
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 26)
+@RequiresApi(Build.VERSION_CODES.O)
+class MixedAutofillTest {
+ @get:Rule val rule = createComposeRule()
+ private val height = 200.dp
+ private val width = 200.dp
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ private val previousFlagValue = ComposeUiFlags.isSemanticAutofillEnabled
+
+ @Before
+ fun enableAutofill() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isSemanticAutofillEnabled = true
+ }
+
+ @After
+ fun disableAutofill() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ ComposeUiFlags.isSemanticAutofillEnabled = previousFlagValue
+ }
+
+ @Test
+ fun populateViewStructure_empty() {
+ // Arrange.
+ lateinit var view: View
+ val viewStructure: ViewStructure = FakeViewStructure()
+ rule.setContent { view = LocalView.current }
+
+ // Act.
+ rule.runOnIdle {
+ // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
+ }
+
+ // Assert.
+ assertThat(viewStructure.childCount).isEqualTo(0)
+ }
+
+ @Test
+ fun autofill_empty() {
+ // Arrange.
+ lateinit var view: View
+ val viewStructure: ViewStructure = FakeViewStructure()
+ rule.setContent { view = LocalView.current }
+
+ // Act.
+ rule.runOnIdle {
+ // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+ view.autofill(
+ SparseArray<AutofillValue>(2).apply {
+ append(1, AutofillValue.forText("any"))
+ append(2, AutofillValue.forText("any"))
+ }
+ )
+ }
+
+ // Assert.
+ assertThat(viewStructure.childCount).isEqualTo(0)
+ }
+
+ @Test
+ fun populateViewStructure_new_old_sameLayoutNode() {
+ // Arrange.
+ lateinit var view: View
+ lateinit var autofillTree: AutofillTree
+ val viewStructure: ViewStructure = FakeViewStructure()
+ lateinit var autofillNode: AutofillNode
+ rule.setContent {
+ view = LocalView.current
+ autofillTree = LocalAutofillTree.current
+ autofillNode = remember {
+ AutofillNode(
+ onFill = {},
+ autofillTypes = listOf(AutofillType.Password),
+ )
+ }
+ Box(
+ Modifier.semantics {
+ testTag = "newApi"
+ contentType = ContentType.Username
+ }
+ .size(height, width)
+ .onGloballyPositioned { autofillNode.boundingBox = it.boundsInWindow() }
+ ) {
+ DisposableEffect(autofillNode) {
+ autofillTree.children[autofillNode.id] = autofillNode
+ onDispose { autofillTree.children.remove(autofillNode.id) }
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
+ }
+
+ // Assert.
+ assertThat(viewStructure)
+ .isEqualTo(
+ FakeViewStructure().apply {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+ packageName = view.context.applicationInfo.packageName
+ bounds = AndroidRect(0, 0, width.dpToPx(), height.dpToPx())
+ autofillId = view.autofillId
+ isEnabled = true
+ children.add(
+ FakeViewStructure().apply {
+ virtualId = rule.onNodeWithTag("newApi").semanticsId()
+ packageName = view.context.applicationInfo.packageName
+ bounds = AndroidRect(0, 0, width.dpToPx(), height.dpToPx())
+ autofillId = view.autofillId
+ isEnabled = true
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ visibility = View.VISIBLE
+ isLongClickable = false
+ isFocusable = false
+ isFocused = false
+ isEnabled = true
+ }
+ )
+ children.add(
+ FakeViewStructure().apply {
+ virtualId = autofillNode.id
+ packageName = view.context.applicationInfo.packageName
+ bounds = AndroidRect(0, 0, width.dpToPx(), height.dpToPx())
+ autofillId = view.autofillId
+ autofillType = AUTOFILL_TYPE_TEXT
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_PASSWORD)
+ visibility = View.VISIBLE
+ isLongClickable = false
+ isFocusable = false
+ isFocused = false
+ }
+ )
+ }
+ )
+ }
+
+ @Test
+ fun populateViewStructure_new_old_differentLayoutNodes() {
+ // Arrange.
+ lateinit var view: View
+ lateinit var autofillTree: AutofillTree
+ val viewStructure: ViewStructure = FakeViewStructure()
+ lateinit var autofillNode: AutofillNode
+ rule.setContent {
+ view = LocalView.current
+ autofillTree = LocalAutofillTree.current
+ autofillNode = remember {
+ AutofillNode(
+ onFill = {},
+ autofillTypes = listOf(AutofillType.Password),
+ )
+ }
+ Column {
+ Box(
+ Modifier.semantics { contentType = ContentType.Username }
+ .size(height, width)
+ .testTag("newApi")
+ )
+ Box(
+ Modifier.size(height, width).onGloballyPositioned {
+ autofillNode.boundingBox = it.boundsInWindow()
+ }
+ ) {
+ DisposableEffect(autofillNode) {
+ autofillTree.children[autofillNode.id] = autofillNode
+ onDispose { autofillTree.children.remove(autofillNode.id) }
+ }
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
+ }
+
+ // Assert.
+ assertThat(viewStructure)
+ .isEqualTo(
+ FakeViewStructure().apply {
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+ packageName = view.context.applicationInfo.packageName
+ bounds = AndroidRect(0, 0, width.dpToPx(), 2 * height.dpToPx())
+ autofillId = view.autofillId
+ isEnabled = true
+ children.add(
+ FakeViewStructure().apply {
+ virtualId = rule.onNodeWithTag("newApi").semanticsId()
+ packageName = view.context.applicationInfo.packageName
+ bounds = AndroidRect(0, 0, width.dpToPx(), height.dpToPx())
+ autofillId = view.autofillId
+ isEnabled = true
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ visibility = View.VISIBLE
+ isLongClickable = false
+ isFocusable = false
+ isFocused = false
+ isEnabled = true
+ }
+ )
+ children.add(
+ FakeViewStructure().apply {
+ virtualId = autofillNode.id
+ packageName = view.context.applicationInfo.packageName
+ bounds =
+ AndroidRect(0, height.dpToPx(), width.dpToPx(), 2 * height.dpToPx())
+ autofillId = view.autofillId
+ autofillType = AUTOFILL_TYPE_TEXT
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_PASSWORD)
+ visibility = View.VISIBLE
+ isLongClickable = false
+ isFocusable = false
+ isFocused = false
+ }
+ )
+ }
+ )
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 26)
+ fun autofill_new_old_sameLayoutNode() {
+ // Arrange.
+ lateinit var view: View
+ lateinit var autofillTree: AutofillTree
+ lateinit var autofillNode: AutofillNode
+ lateinit var autoFilledValueNewApi: String
+ lateinit var autoFilledValueOldApi: String
+ rule.setContent {
+ view = LocalView.current
+ autofillTree = LocalAutofillTree.current
+ autofillNode = remember {
+ AutofillNode(
+ onFill = { autoFilledValueOldApi = it },
+ autofillTypes = listOf(AutofillType.Password),
+ )
+ }
+ Box(
+ Modifier.semantics {
+ testTag = "newApi"
+ contentType = ContentType.Username
+ onAutofillText {
+ autoFilledValueNewApi = it.toString()
+ true
+ }
+ }
+ .onGloballyPositioned { autofillNode.boundingBox = it.boundsInWindow() }
+ .size(height, width)
+ ) {
+ DisposableEffect(autofillNode) {
+ autofillTree.children[autofillNode.id] = autofillNode
+ onDispose { autofillTree.children.remove(autofillNode.id) }
+ }
+ }
+ }
+
+ // Act.
+ val newApiSemanticsId = rule.onNodeWithTag("newApi").semanticsId()
+ rule.runOnIdle {
+ view.autofill(
+ SparseArray<AutofillValue>(2).apply {
+ append(newApiSemanticsId, AutofillValue.forText("TestUsername"))
+ append(autofillNode.id, AutofillValue.forText("TestPassword"))
+ }
+ )
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(autoFilledValueNewApi).isEqualTo("TestUsername")
+ assertThat(autoFilledValueOldApi).isEqualTo("TestPassword")
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 26)
+ fun autofill_new_old_differentLayoutNodes() {
+ // Arrange.
+ lateinit var view: View
+ lateinit var autofillTree: AutofillTree
+ lateinit var autofillNode: AutofillNode
+ lateinit var autoFilledValueNewApi: String
+ lateinit var autoFilledValueOldApi: String
+ rule.setContent {
+ view = LocalView.current
+ autofillTree = LocalAutofillTree.current
+ autofillNode = remember {
+ AutofillNode(
+ onFill = { autoFilledValueOldApi = it },
+ autofillTypes = listOf(AutofillType.Password),
+ )
+ }
+ Column {
+ Box(
+ Modifier.semantics {
+ contentType = ContentType.Username
+ onAutofillText {
+ autoFilledValueNewApi = it.toString()
+ true
+ }
+ }
+ .size(height, width)
+ .testTag("newApi")
+ )
+ Box(
+ Modifier.size(height, width).onGloballyPositioned {
+ autofillNode.boundingBox = it.boundsInWindow()
+ }
+ ) {
+ DisposableEffect(autofillNode) {
+ autofillTree.children[autofillNode.id] = autofillNode
+ onDispose { autofillTree.children.remove(autofillNode.id) }
+ }
+ }
+ }
+ }
+
+ // Act.
+ val newApiSemanticsId = rule.onNodeWithTag("newApi").semanticsId()
+ rule.runOnIdle {
+ view.autofill(
+ SparseArray<AutofillValue>(2).apply {
+ append(newApiSemanticsId, AutofillValue.forText("TestUsername"))
+ append(autofillNode.id, AutofillValue.forText("TestPassword"))
+ }
+ )
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(autoFilledValueNewApi).isEqualTo("TestUsername")
+ assertThat(autoFilledValueOldApi).isEqualTo("TestPassword")
+ }
+ }
+
+ private fun Dp.dpToPx() = with(rule.density) { [email protected]() }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/PerformAndroidAutofillManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/PerformAndroidAutofillManagerTest.kt
index c878248..4d142e5 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/PerformAndroidAutofillManagerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/PerformAndroidAutofillManagerTest.kt
@@ -47,6 +47,7 @@
import androidx.compose.ui.semantics.contentDataType
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.contentType
+import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.maxTextLength
import androidx.compose.ui.semantics.onLongClick
@@ -171,26 +172,25 @@
assertThat(viewStructure)
.isEqualTo(
FakeViewStructure().apply {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
- packageName = view.context.applicationInfo.packageName
- bounds = Rect(0, 0, width.dpToPx(), height.dpToPx())
autofillId = view.autofillId
- isEnabled = true
+ bounds = Rect(0, 0, width.dpToPx(), height.dpToPx())
children.add(
FakeViewStructure().apply {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
- packageName = view.context.applicationInfo.packageName
- bounds = Rect(0, 0, width.dpToPx(), height.dpToPx())
- autofillId = view.autofillId
- isEnabled = true
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
- visibility = View.VISIBLE
- isLongClickable = false
+ autofillId = view.autofillId
+ bounds = Rect(0, 0, width.dpToPx(), height.dpToPx())
+ isEnabled = true
isFocusable = false
isFocused = false
- isEnabled = true
+ isLongClickable = false
+ packageName = view.context.applicationInfo.packageName
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ visibility = View.VISIBLE
}
)
+ isEnabled = true
+ packageName = view.context.applicationInfo.packageName
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -221,13 +221,13 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -258,13 +258,13 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillType = AUTOFILL_TYPE_TEXT
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -296,15 +296,15 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
isClickable = true
isFocusable = true
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -338,14 +338,14 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
contentDescription = contentTag
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -384,16 +384,16 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
isClickable = true
isFocusable = true
isSelected = true
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -432,19 +432,19 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
className = "android.widget.RadioButton"
- isClickable = true
- isFocusable = true
isCheckable = true
isChecked = true
+ isClickable = true
+ isFocusable = true
isSelected = true
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -483,19 +483,19 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
className = "android.widget.Spinner"
- isClickable = true
- isFocusable = true
isCheckable = true
isChecked = true
+ isClickable = true
+ isFocusable = true
isSelected = true
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -534,19 +534,19 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
className = "android.widget.NumberPicker"
- isClickable = true
- isFocusable = true
isCheckable = true
isChecked = true
+ isClickable = true
+ isFocusable = true
isSelected = true
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -581,14 +581,14 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
visibility = View.VISIBLE
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -621,14 +621,14 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
visibility = View.INVISIBLE
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -660,14 +660,14 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
visibility = View.VISIBLE
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -703,8 +703,8 @@
virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
visibility = View.INVISIBLE
}
)
@@ -741,14 +741,14 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
isLongClickable = true
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -782,14 +782,14 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
isFocusable = true
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -822,15 +822,15 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
isFocusable = true
isFocused = true
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -845,9 +845,47 @@
rule.setContent {
view = LocalView.current
Box(
+ Modifier.semantics { contentType = ContentType.Username }
+ .size(width, height)
+ .testTag(contentTag)
+ )
+ }
+
+ // Act.
+ rule.runOnIdle {
+ // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+ view.onProvideAutofillVirtualStructure(viewStructure, 0)
+ }
+
+ // Assert.
+ assertThat(viewStructure)
+ .isEqualTo(
+ ViewStructure(view) {
+ children.add(
+ ViewStructure(view) {
+ autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+ isEnabled = true
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+ }
+ )
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+ }
+ )
+ }
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 26)
+ fun populateViewStructure_disabled() {
+ // Arrange.
+ lateinit var view: View
+ val viewStructure: ViewStructure = FakeViewStructure()
+ rule.setContent {
+ view = LocalView.current
+ Box(
Modifier.semantics {
contentType = ContentType.Username
- isEnabled()
+ disabled()
}
.size(width, height)
.testTag(contentTag)
@@ -864,14 +902,14 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
- isEnabled = true
+ isEnabled = false
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -906,16 +944,16 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
- autofillType = AUTOFILL_TYPE_TEXT
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
- maxTextLength = 5
+ autofillType = AUTOFILL_TYPE_TEXT
className = "android.widget.EditText"
+ maxTextLength = 5
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -949,14 +987,14 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
maxTextLength = -1
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -990,15 +1028,15 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
autofillType = View.AUTOFILL_TYPE_TOGGLE
isCheckable = true
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -1032,16 +1070,16 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
autofillType = View.AUTOFILL_TYPE_TOGGLE
isCheckable = true
isChecked = true
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -1073,18 +1111,18 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
autofillType = View.AUTOFILL_TYPE_TOGGLE
isCheckable = true
isChecked = true
- isFocusable = true
isClickable = true
+ isFocusable = true
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -1119,11 +1157,8 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
- text = ""
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
autofillType = AUTOFILL_TYPE_TEXT
autofillValue = AutofillValue.forText("")
@@ -1131,9 +1166,12 @@
isClickable = true
isFocusable = true
isLongClickable = true
+ text = ""
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
visibility = View.VISIBLE
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -1168,11 +1206,8 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
- text = ""
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
autofillType = AUTOFILL_TYPE_TEXT
autofillValue = AutofillValue.forText("testUsername")
@@ -1180,9 +1215,12 @@
isClickable = true
isFocusable = true
isLongClickable = true
+ text = ""
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
visibility = View.VISIBLE
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -1219,24 +1257,24 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
- text = ""
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_PASSWORD)
autofillType = AUTOFILL_TYPE_TEXT
autofillValue = AutofillValue.forText("")
className = "android.widget.EditText"
- isClickable = true
dataIsSensitive = true
inputType =
InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
+ isClickable = true
isFocusable = true
isLongClickable = true
+ text = ""
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
visibility = View.VISIBLE
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -1271,24 +1309,24 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
- text = ""
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_PASSWORD)
autofillType = AUTOFILL_TYPE_TEXT
autofillValue = AutofillValue.forText("testPassword")
className = "android.widget.EditText"
- isClickable = true
dataIsSensitive = true
inputType =
InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
+ isClickable = true
isFocusable = true
isLongClickable = true
+ text = ""
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
visibility = View.VISIBLE
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -1324,14 +1362,14 @@
assertThat(viewStructure)
.isEqualTo(
ViewStructure(view) {
- virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
children.add(
ViewStructure(view) {
- virtualId = rule.onNodeWithTag(contentTag).semanticsId()
autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
dataIsSensitive = true
+ virtualId = rule.onNodeWithTag(contentTag).semanticsId()
}
)
+ virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
)
}
@@ -1437,10 +1475,10 @@
block: FakeViewStructure.() -> Unit
): FakeViewStructure {
return FakeViewStructure().apply {
- packageName = view.context.applicationInfo.packageName
- bounds = Rect(0, 0, width.dpToPx(), height.dpToPx())
autofillId = view.autofillId
+ bounds = Rect(0, 0, width.dpToPx(), height.dpToPx())
isEnabled = true
+ packageName = view.context.applicationInfo.packageName
block()
}
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt
index 1a3e98e..680dcbd 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt
@@ -83,6 +83,7 @@
*/
@RequiresApi(Build.VERSION_CODES.O)
internal fun AndroidAutofill.populateViewStructure(root: ViewStructure) {
+ if (autofillTree.children.isEmpty()) return
// Add child nodes. The function returns the index to the first item.
var index = AutofillApi26Helper.addChildCount(root, autofillTree.children.count())
@@ -123,6 +124,8 @@
/** Triggers onFill() in response to a request from the autofill framework. */
@RequiresApi(Build.VERSION_CODES.O)
internal fun AndroidAutofill.performAutofill(values: SparseArray<AutofillValue>) {
+ if (autofillTree.children.isEmpty()) return
+
for (index in 0 until values.size()) {
val itemId = values.keyAt(index)
val value = values[itemId]
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
index 421f7f2..3a2f75d 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
@@ -130,10 +130,10 @@
}
// Update currentlyDisplayedIDs if relevance to Autofill has changed.
- val prevRelatedToAutofill = prevConfig?.isRelatedToAutofill()
- val currRelatedToAutofill = config?.isRelatedToAutofill()
+ val prevRelatedToAutofill = prevConfig?.isRelatedToAutofill() ?: false
+ val currRelatedToAutofill = config?.isRelatedToAutofill() ?: false
if (prevRelatedToAutofill != currRelatedToAutofill) {
- if (currRelatedToAutofill == true) {
+ if (currRelatedToAutofill) {
currentlyDisplayedIDs.add(semanticsId)
} else {
currentlyDisplayedIDs.remove(semanticsId)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.android.kt
index f2f7b6f..cb92103 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.android.kt
@@ -52,42 +52,6 @@
import androidx.autofill.HintConstants.AUTOFILL_HINT_POSTAL_CODE
import androidx.autofill.HintConstants.AUTOFILL_HINT_SMS_OTP
import androidx.autofill.HintConstants.AUTOFILL_HINT_USERNAME
-import androidx.compose.ui.autofill.AutofillType.AddressAuxiliaryDetails
-import androidx.compose.ui.autofill.AutofillType.AddressCountry
-import androidx.compose.ui.autofill.AutofillType.AddressLocality
-import androidx.compose.ui.autofill.AutofillType.AddressRegion
-import androidx.compose.ui.autofill.AutofillType.AddressStreet
-import androidx.compose.ui.autofill.AutofillType.BirthDateDay
-import androidx.compose.ui.autofill.AutofillType.BirthDateFull
-import androidx.compose.ui.autofill.AutofillType.BirthDateMonth
-import androidx.compose.ui.autofill.AutofillType.BirthDateYear
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationDate
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationDay
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationMonth
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationYear
-import androidx.compose.ui.autofill.AutofillType.CreditCardNumber
-import androidx.compose.ui.autofill.AutofillType.CreditCardSecurityCode
-import androidx.compose.ui.autofill.AutofillType.EmailAddress
-import androidx.compose.ui.autofill.AutofillType.Gender
-import androidx.compose.ui.autofill.AutofillType.NewPassword
-import androidx.compose.ui.autofill.AutofillType.NewUsername
-import androidx.compose.ui.autofill.AutofillType.Password
-import androidx.compose.ui.autofill.AutofillType.PersonFirstName
-import androidx.compose.ui.autofill.AutofillType.PersonFullName
-import androidx.compose.ui.autofill.AutofillType.PersonLastName
-import androidx.compose.ui.autofill.AutofillType.PersonMiddleInitial
-import androidx.compose.ui.autofill.AutofillType.PersonMiddleName
-import androidx.compose.ui.autofill.AutofillType.PersonNamePrefix
-import androidx.compose.ui.autofill.AutofillType.PersonNameSuffix
-import androidx.compose.ui.autofill.AutofillType.PhoneCountryCode
-import androidx.compose.ui.autofill.AutofillType.PhoneNumber
-import androidx.compose.ui.autofill.AutofillType.PhoneNumberDevice
-import androidx.compose.ui.autofill.AutofillType.PhoneNumberNational
-import androidx.compose.ui.autofill.AutofillType.PostalAddress
-import androidx.compose.ui.autofill.AutofillType.PostalCode
-import androidx.compose.ui.autofill.AutofillType.PostalCodeExtended
-import androidx.compose.ui.autofill.AutofillType.SmsOtpCode
-import androidx.compose.ui.autofill.AutofillType.Username
/**
* Gets the Android specific [AutofillHint][android.view.ViewStructure.setAutofillHints]
@@ -96,47 +60,47 @@
internal val AutofillType.androidType: String
get() {
val androidAutofillType = androidAutofillTypes[this]
- requireNotNull(androidAutofillType, { "Unsupported autofill type" })
+ requireNotNull(androidAutofillType) { "Unsupported autofill type" }
return androidAutofillType
}
/** Maps each [AutofillType] to one of the autofill hints in [androidx.autofill.HintConstants] */
private val androidAutofillTypes: HashMap<AutofillType, String> =
hashMapOf(
- EmailAddress to AUTOFILL_HINT_EMAIL_ADDRESS,
- Username to AUTOFILL_HINT_USERNAME,
- Password to AUTOFILL_HINT_PASSWORD,
- NewUsername to AUTOFILL_HINT_NEW_USERNAME,
- NewPassword to AUTOFILL_HINT_NEW_PASSWORD,
- PostalAddress to AUTOFILL_HINT_POSTAL_ADDRESS,
- PostalCode to AUTOFILL_HINT_POSTAL_CODE,
- CreditCardNumber to AUTOFILL_HINT_CREDIT_CARD_NUMBER,
- CreditCardSecurityCode to AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE,
- CreditCardExpirationDate to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE,
- CreditCardExpirationMonth to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH,
- CreditCardExpirationYear to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR,
- CreditCardExpirationDay to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY,
- AddressCountry to AUTOFILL_HINT_POSTAL_ADDRESS_COUNTRY,
- AddressRegion to AUTOFILL_HINT_POSTAL_ADDRESS_REGION,
- AddressLocality to AUTOFILL_HINT_POSTAL_ADDRESS_LOCALITY,
- AddressStreet to AUTOFILL_HINT_POSTAL_ADDRESS_STREET_ADDRESS,
- AddressAuxiliaryDetails to AUTOFILL_HINT_POSTAL_ADDRESS_EXTENDED_ADDRESS,
- PostalCodeExtended to AUTOFILL_HINT_POSTAL_ADDRESS_EXTENDED_POSTAL_CODE,
- PersonFullName to AUTOFILL_HINT_PERSON_NAME,
- PersonFirstName to AUTOFILL_HINT_PERSON_NAME_GIVEN,
- PersonLastName to AUTOFILL_HINT_PERSON_NAME_FAMILY,
- PersonMiddleName to AUTOFILL_HINT_PERSON_NAME_MIDDLE,
- PersonMiddleInitial to AUTOFILL_HINT_PERSON_NAME_MIDDLE_INITIAL,
- PersonNamePrefix to AUTOFILL_HINT_PERSON_NAME_PREFIX,
- PersonNameSuffix to AUTOFILL_HINT_PERSON_NAME_SUFFIX,
- PhoneNumber to AUTOFILL_HINT_PHONE_NUMBER,
- PhoneNumberDevice to AUTOFILL_HINT_PHONE_NUMBER_DEVICE,
- PhoneCountryCode to AUTOFILL_HINT_PHONE_COUNTRY_CODE,
- PhoneNumberNational to AUTOFILL_HINT_PHONE_NATIONAL,
- Gender to AUTOFILL_HINT_GENDER,
- BirthDateFull to AUTOFILL_HINT_BIRTH_DATE_FULL,
- BirthDateDay to AUTOFILL_HINT_BIRTH_DATE_DAY,
- BirthDateMonth to AUTOFILL_HINT_BIRTH_DATE_MONTH,
- BirthDateYear to AUTOFILL_HINT_BIRTH_DATE_YEAR,
- SmsOtpCode to AUTOFILL_HINT_SMS_OTP
+ AutofillType.EmailAddress to AUTOFILL_HINT_EMAIL_ADDRESS,
+ AutofillType.Username to AUTOFILL_HINT_USERNAME,
+ AutofillType.Password to AUTOFILL_HINT_PASSWORD,
+ AutofillType.NewUsername to AUTOFILL_HINT_NEW_USERNAME,
+ AutofillType.NewPassword to AUTOFILL_HINT_NEW_PASSWORD,
+ AutofillType.PostalAddress to AUTOFILL_HINT_POSTAL_ADDRESS,
+ AutofillType.PostalCode to AUTOFILL_HINT_POSTAL_CODE,
+ AutofillType.CreditCardNumber to AUTOFILL_HINT_CREDIT_CARD_NUMBER,
+ AutofillType.CreditCardSecurityCode to AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE,
+ AutofillType.CreditCardExpirationDate to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE,
+ AutofillType.CreditCardExpirationMonth to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH,
+ AutofillType.CreditCardExpirationYear to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR,
+ AutofillType.CreditCardExpirationDay to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY,
+ AutofillType.AddressCountry to AUTOFILL_HINT_POSTAL_ADDRESS_COUNTRY,
+ AutofillType.AddressRegion to AUTOFILL_HINT_POSTAL_ADDRESS_REGION,
+ AutofillType.AddressLocality to AUTOFILL_HINT_POSTAL_ADDRESS_LOCALITY,
+ AutofillType.AddressStreet to AUTOFILL_HINT_POSTAL_ADDRESS_STREET_ADDRESS,
+ AutofillType.AddressAuxiliaryDetails to AUTOFILL_HINT_POSTAL_ADDRESS_EXTENDED_ADDRESS,
+ AutofillType.PostalCodeExtended to AUTOFILL_HINT_POSTAL_ADDRESS_EXTENDED_POSTAL_CODE,
+ AutofillType.PersonFullName to AUTOFILL_HINT_PERSON_NAME,
+ AutofillType.PersonFirstName to AUTOFILL_HINT_PERSON_NAME_GIVEN,
+ AutofillType.PersonLastName to AUTOFILL_HINT_PERSON_NAME_FAMILY,
+ AutofillType.PersonMiddleName to AUTOFILL_HINT_PERSON_NAME_MIDDLE,
+ AutofillType.PersonMiddleInitial to AUTOFILL_HINT_PERSON_NAME_MIDDLE_INITIAL,
+ AutofillType.PersonNamePrefix to AUTOFILL_HINT_PERSON_NAME_PREFIX,
+ AutofillType.PersonNameSuffix to AUTOFILL_HINT_PERSON_NAME_SUFFIX,
+ AutofillType.PhoneNumber to AUTOFILL_HINT_PHONE_NUMBER,
+ AutofillType.PhoneNumberDevice to AUTOFILL_HINT_PHONE_NUMBER_DEVICE,
+ AutofillType.PhoneCountryCode to AUTOFILL_HINT_PHONE_COUNTRY_CODE,
+ AutofillType.PhoneNumberNational to AUTOFILL_HINT_PHONE_NATIONAL,
+ AutofillType.Gender to AUTOFILL_HINT_GENDER,
+ AutofillType.BirthDateFull to AUTOFILL_HINT_BIRTH_DATE_FULL,
+ AutofillType.BirthDateDay to AUTOFILL_HINT_BIRTH_DATE_DAY,
+ AutofillType.BirthDateMonth to AUTOFILL_HINT_BIRTH_DATE_MONTH,
+ AutofillType.BirthDateYear to AUTOFILL_HINT_BIRTH_DATE_YEAR,
+ AutofillType.SmsOtpCode to AUTOFILL_HINT_SMS_OTP
)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/PopulateViewStructure.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/PopulateViewStructure.android.kt
index db2789e..72cfae7 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/PopulateViewStructure.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/PopulateViewStructure.android.kt
@@ -62,7 +62,6 @@
var toggleableStateProp: ToggleableState? = null
// Semantics properties form merged configuration.
- var disabledMergedProp: Boolean? = null
var textMergedProp: List<AnnotatedString>? = null
// Semantics actions.
@@ -94,7 +93,7 @@
semanticsInfo.mergedSemanticsConfiguration()?.props?.forEach { property, value ->
@Suppress("UNCHECKED_CAST")
when (property) {
- properties.Disabled -> disabledMergedProp = value as Boolean
+ properties.Disabled -> autofillApi.setEnabled(this, false)
properties.Text -> textMergedProp = value as List<AnnotatedString>
}
}
@@ -124,9 +123,6 @@
autofillApi.setDimens(this, left, top, 0, 0, right - left, bottom - top)
}
- // Enabled.
- autofillApi.setEnabled(this, disabledMergedProp != true)
-
// Selected.
selectedProp?.let { autofillApi.setSelected(this, it) }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index dbb41be..604175c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -108,7 +108,6 @@
import androidx.compose.ui.focus.focusRect
import androidx.compose.ui.focus.is1dFocusSearch
import androidx.compose.ui.focus.isBetterCandidate
-import androidx.compose.ui.focus.requestFocus
import androidx.compose.ui.focus.requestInteropFocus
import androidx.compose.ui.focus.toAndroidFocusDirection
import androidx.compose.ui.focus.toFocusDirection
@@ -1905,10 +1904,8 @@
if (autofillSupported() && structure != null) {
if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isSemanticAutofillEnabled) {
_autofillManager?.populateViewStructure(structure)
- } else {
- // TODO(b/383201236): Remove _autofill and route requests through _autofillManager.
- _autofill?.populateViewStructure(structure)
}
+ _autofill?.populateViewStructure(structure)
}
}
@@ -1916,10 +1913,8 @@
if (autofillSupported()) {
if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isSemanticAutofillEnabled) {
_autofillManager?.performAutofill(values)
- } else {
- // TODO(b/383201236): Remove _autofill and route requests through _autofillManager.
- _autofill?.performAutofill(values)
}
+ _autofill?.performAutofill(values)
}
}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt
index 0475d44..b5c8886 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt
@@ -16,42 +16,6 @@
package androidx.compose.ui.autofill
-import androidx.compose.ui.autofill.AutofillType.AddressAuxiliaryDetails
-import androidx.compose.ui.autofill.AutofillType.AddressCountry
-import androidx.compose.ui.autofill.AutofillType.AddressLocality
-import androidx.compose.ui.autofill.AutofillType.AddressRegion
-import androidx.compose.ui.autofill.AutofillType.AddressStreet
-import androidx.compose.ui.autofill.AutofillType.BirthDateDay
-import androidx.compose.ui.autofill.AutofillType.BirthDateFull
-import androidx.compose.ui.autofill.AutofillType.BirthDateMonth
-import androidx.compose.ui.autofill.AutofillType.BirthDateYear
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationDate
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationDay
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationMonth
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationYear
-import androidx.compose.ui.autofill.AutofillType.CreditCardNumber
-import androidx.compose.ui.autofill.AutofillType.CreditCardSecurityCode
-import androidx.compose.ui.autofill.AutofillType.EmailAddress
-import androidx.compose.ui.autofill.AutofillType.Gender
-import androidx.compose.ui.autofill.AutofillType.NewPassword
-import androidx.compose.ui.autofill.AutofillType.NewUsername
-import androidx.compose.ui.autofill.AutofillType.Password
-import androidx.compose.ui.autofill.AutofillType.PersonFirstName
-import androidx.compose.ui.autofill.AutofillType.PersonFullName
-import androidx.compose.ui.autofill.AutofillType.PersonLastName
-import androidx.compose.ui.autofill.AutofillType.PersonMiddleInitial
-import androidx.compose.ui.autofill.AutofillType.PersonMiddleName
-import androidx.compose.ui.autofill.AutofillType.PersonNamePrefix
-import androidx.compose.ui.autofill.AutofillType.PersonNameSuffix
-import androidx.compose.ui.autofill.AutofillType.PhoneCountryCode
-import androidx.compose.ui.autofill.AutofillType.PhoneNumber
-import androidx.compose.ui.autofill.AutofillType.PhoneNumberDevice
-import androidx.compose.ui.autofill.AutofillType.PhoneNumberNational
-import androidx.compose.ui.autofill.AutofillType.PostalAddress
-import androidx.compose.ui.autofill.AutofillType.PostalCode
-import androidx.compose.ui.autofill.AutofillType.PostalCodeExtended
-import androidx.compose.ui.autofill.AutofillType.SmsOtpCode
-import androidx.compose.ui.autofill.AutofillType.Username
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -62,181 +26,186 @@
@Test
fun emailAddress() {
- assertThat(EmailAddress.androidType).isEqualTo("emailAddress")
+ assertThat(AutofillType.EmailAddress.androidType).isEqualTo("emailAddress")
}
@Test
fun username() {
- assertThat(Username.androidType).isEqualTo("username")
+ assertThat(AutofillType.Username.androidType).isEqualTo("username")
}
@Test
fun password() {
- assertThat(Password.androidType).isEqualTo("password")
+ assertThat(AutofillType.Password.androidType).isEqualTo("password")
}
@Test
fun newUsername() {
- assertThat(NewUsername.androidType).isEqualTo("newUsername")
+ assertThat(AutofillType.NewUsername.androidType).isEqualTo("newUsername")
}
@Test
fun newPassword() {
- assertThat(NewPassword.androidType).isEqualTo("newPassword")
+ assertThat(AutofillType.NewPassword.androidType).isEqualTo("newPassword")
}
@Test
fun postalAddress() {
- assertThat(PostalAddress.androidType).isEqualTo("postalAddress")
+ assertThat(AutofillType.PostalAddress.androidType).isEqualTo("postalAddress")
}
@Test
fun postalCode() {
- assertThat(PostalCode.androidType).isEqualTo("postalCode")
+ assertThat(AutofillType.PostalCode.androidType).isEqualTo("postalCode")
}
@Test
fun creditCardNumber() {
- assertThat(CreditCardNumber.androidType).isEqualTo("creditCardNumber")
+ assertThat(AutofillType.CreditCardNumber.androidType).isEqualTo("creditCardNumber")
}
@Test
fun creditCardSecurityCode() {
- assertThat(CreditCardSecurityCode.androidType).isEqualTo("creditCardSecurityCode")
+ assertThat(AutofillType.CreditCardSecurityCode.androidType)
+ .isEqualTo("creditCardSecurityCode")
}
@Test
fun creditCardExpirationDate() {
- assertThat(CreditCardExpirationDate.androidType).isEqualTo("creditCardExpirationDate")
+ assertThat(AutofillType.CreditCardExpirationDate.androidType)
+ .isEqualTo("creditCardExpirationDate")
}
@Test
fun creditCardExpirationMonth() {
- assertThat(CreditCardExpirationMonth.androidType).isEqualTo("creditCardExpirationMonth")
+ assertThat(AutofillType.CreditCardExpirationMonth.androidType)
+ .isEqualTo("creditCardExpirationMonth")
}
@Test
fun creditCardExpirationYear() {
- assertThat(CreditCardExpirationYear.androidType).isEqualTo("creditCardExpirationYear")
+ assertThat(AutofillType.CreditCardExpirationYear.androidType)
+ .isEqualTo("creditCardExpirationYear")
}
@Test
fun creditCardExpirationDay() {
- assertThat(CreditCardExpirationDay.androidType).isEqualTo("creditCardExpirationDay")
+ assertThat(AutofillType.CreditCardExpirationDay.androidType)
+ .isEqualTo("creditCardExpirationDay")
}
@Test
fun addressCountry() {
- assertThat(AddressCountry.androidType).isEqualTo("addressCountry")
+ assertThat(AutofillType.AddressCountry.androidType).isEqualTo("addressCountry")
}
@Test
fun addressRegion() {
- assertThat(AddressRegion.androidType).isEqualTo("addressRegion")
+ assertThat(AutofillType.AddressRegion.androidType).isEqualTo("addressRegion")
}
@Test
fun addressLocality() {
- assertThat(AddressLocality.androidType).isEqualTo("addressLocality")
+ assertThat(AutofillType.AddressLocality.androidType).isEqualTo("addressLocality")
}
@Test
fun addressStreet() {
- assertThat(AddressStreet.androidType).isEqualTo("streetAddress")
+ assertThat(AutofillType.AddressStreet.androidType).isEqualTo("streetAddress")
}
@Test
fun addressAuxiliaryDetails() {
- assertThat(AddressAuxiliaryDetails.androidType).isEqualTo("extendedAddress")
+ assertThat(AutofillType.AddressAuxiliaryDetails.androidType).isEqualTo("extendedAddress")
}
@Test
fun postalCodeExtended() {
- assertThat(PostalCodeExtended.androidType).isEqualTo("extendedPostalCode")
+ assertThat(AutofillType.PostalCodeExtended.androidType).isEqualTo("extendedPostalCode")
}
@Test
fun personFullName() {
- assertThat(PersonFullName.androidType).isEqualTo("personName")
+ assertThat(AutofillType.PersonFullName.androidType).isEqualTo("personName")
}
@Test
fun personFirstName() {
- assertThat(PersonFirstName.androidType).isEqualTo("personGivenName")
+ assertThat(AutofillType.PersonFirstName.androidType).isEqualTo("personGivenName")
}
@Test
fun personLastName() {
- assertThat(PersonLastName.androidType).isEqualTo("personFamilyName")
+ assertThat(AutofillType.PersonLastName.androidType).isEqualTo("personFamilyName")
}
@Test
fun personMiddleName() {
- assertThat(PersonMiddleName.androidType).isEqualTo("personMiddleName")
+ assertThat(AutofillType.PersonMiddleName.androidType).isEqualTo("personMiddleName")
}
@Test
fun personMiddleInitial() {
- assertThat(PersonMiddleInitial.androidType).isEqualTo("personMiddleInitial")
+ assertThat(AutofillType.PersonMiddleInitial.androidType).isEqualTo("personMiddleInitial")
}
@Test
fun personNamePrefix() {
- assertThat(PersonNamePrefix.androidType).isEqualTo("personNamePrefix")
+ assertThat(AutofillType.PersonNamePrefix.androidType).isEqualTo("personNamePrefix")
}
@Test
fun personNameSuffix() {
- assertThat(PersonNameSuffix.androidType).isEqualTo("personNameSuffix")
+ assertThat(AutofillType.PersonNameSuffix.androidType).isEqualTo("personNameSuffix")
}
@Test
fun phoneNumber() {
- assertThat(PhoneNumber.androidType).isEqualTo("phoneNumber")
+ assertThat(AutofillType.PhoneNumber.androidType).isEqualTo("phoneNumber")
}
@Test
fun phoneNumberDevice() {
- assertThat(PhoneNumberDevice.androidType).isEqualTo("phoneNumberDevice")
+ assertThat(AutofillType.PhoneNumberDevice.androidType).isEqualTo("phoneNumberDevice")
}
@Test
fun phoneCountryCode() {
- assertThat(PhoneCountryCode.androidType).isEqualTo("phoneCountryCode")
+ assertThat(AutofillType.PhoneCountryCode.androidType).isEqualTo("phoneCountryCode")
}
@Test
fun phoneNumberNational() {
- assertThat(PhoneNumberNational.androidType).isEqualTo("phoneNational")
+ assertThat(AutofillType.PhoneNumberNational.androidType).isEqualTo("phoneNational")
}
@Test
fun gender() {
- assertThat(Gender.androidType).isEqualTo("gender")
+ assertThat(AutofillType.Gender.androidType).isEqualTo("gender")
}
@Test
fun birthDateFull() {
- assertThat(BirthDateFull.androidType).isEqualTo("birthDateFull")
+ assertThat(AutofillType.BirthDateFull.androidType).isEqualTo("birthDateFull")
}
@Test
fun birthDateDay() {
- assertThat(BirthDateDay.androidType).isEqualTo("birthDateDay")
+ assertThat(AutofillType.BirthDateDay.androidType).isEqualTo("birthDateDay")
}
@Test
fun birthDateMonth() {
- assertThat(BirthDateMonth.androidType).isEqualTo("birthDateMonth")
+ assertThat(AutofillType.BirthDateMonth.androidType).isEqualTo("birthDateMonth")
}
@Test
fun birthDateYear() {
- assertThat(BirthDateYear.androidType).isEqualTo("birthDateYear")
+ assertThat(AutofillType.BirthDateYear.androidType).isEqualTo("birthDateYear")
}
@Test
fun smsOTPCode() {
- assertThat(SmsOtpCode.androidType).isEqualTo("smsOTPCode")
+ assertThat(AutofillType.SmsOtpCode.androidType).isEqualTo("smsOTPCode")
}
}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt
index 8edb3d1..126bfc9 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt
@@ -21,7 +21,7 @@
import android.view.View
import android.view.autofill.AutofillValue
import androidx.compose.ui.geometry.Rect
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -48,10 +48,10 @@
fun performAutofill_name() {
// Arrange.
val expectedValue = "Name"
- var autofilledValue = ""
+ var autoFilledValue = ""
val autofillNode =
AutofillNode(
- onFill = { autofilledValue = it },
+ onFill = { autoFilledValue = it },
autofillTypes = listOf(AutofillType.PersonFullName),
boundingBox = Rect(0f, 0f, 0f, 0f)
)
@@ -66,17 +66,17 @@
androidAutofill.performAutofill(autofillValues)
// Assert.
- Truth.assertThat(autofilledValue).isEqualTo(expectedValue)
+ assertThat(autoFilledValue).isEqualTo(expectedValue)
}
@Test
fun performAutofill_email() {
// Arrange.
val expectedValue = "[email protected]"
- var autofilledValue = ""
+ var autoFilledValue = ""
val autofillNode =
AutofillNode(
- onFill = { autofilledValue = it },
+ onFill = { autoFilledValue = it },
autofillTypes = listOf(AutofillType.EmailAddress),
boundingBox = Rect(0f, 0f, 0f, 0f)
)
@@ -91,6 +91,6 @@
androidAutofill.performAutofill(autofillValues)
// Assert.
- Truth.assertThat(autofilledValue).isEqualTo(expectedValue)
+ assertThat(autoFilledValue).isEqualTo(expectedValue)
}
}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt
index 9a71fe1..debfc97 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt
@@ -120,11 +120,13 @@
val viewEnteredStats = mutableListOf<NotifyViewEntered>()
val viewExitedStats = mutableListOf<NotifyViewExited>()
+ @Suppress("Unused")
@Implementation
fun notifyViewEntered(view: View, virtualId: Int, rect: android.graphics.Rect) {
viewEnteredStats += NotifyViewEntered(view, virtualId, rect.toComposeRect())
}
+ @Suppress("Unused")
@Implementation
fun notifyViewExited(view: View, virtualId: Int) {
viewExitedStats += NotifyViewExited(view, virtualId)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt
index 9c45245..bc0ac620 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt
@@ -16,15 +16,18 @@
package androidx.compose.ui.autofill
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.platform.makeSynchronizedObject
import androidx.compose.ui.platform.synchronized
+import androidx.compose.ui.semantics.generateSemanticsId
/**
* Autofill API.
*
* This interface is available to all composables via a CompositionLocal. The composable can then
- * request or cancel autofill as required. For instance, the [TextField] can call
+ * request or cancel autofill as required. For instance, the TextField can call
* [requestAutofillForNode] when it gains focus, and [cancelAutofillForNode] when it loses focus.
*/
interface Autofill {
@@ -32,7 +35,7 @@
/**
* Request autofill for the specified node.
*
- * @param autofillNode The node that needs to be autofilled.
+ * @param autofillNode The node that needs to be auto-filled.
*
* This function is usually called when an autofillable component gains focus.
*/
@@ -41,7 +44,7 @@
/**
* Cancel a previously supplied autofill request.
*
- * @param autofillNode The node that needs to be autofilled.
+ * @param autofillNode The node that needs to be auto-filled.
*
* This function is usually called when an autofillable component loses focus.
*/
@@ -59,7 +62,7 @@
* list because some fields can have multiple types. For instance, userid in a login form can
* either be a username or an email address. TODO(b/138731416): Check with the autofill service
* team if the order matters, and how duplicate types are handled.
- * @property boundingBox The screen coordinates of the composable being autofilled. This data is
+ * @property boundingBox The screen coordinates of the composable being auto-filled. This data is
* used by the autofill framework to decide where to show the autofill popup.
* @property onFill The callback that is called by the autofill framework to perform autofill.
* @property id A virtual id that is automatically generated for each node.
@@ -78,7 +81,9 @@
private fun generateId() = synchronized(lock) { ++previousId }
}
- val id: Int = generateId()
+ val id: Int =
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (ComposeUiFlags.isSemanticAutofillEnabled) generateSemanticsId() else generateId()
override fun equals(other: Any?): Boolean {
if (this === other) return true
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt
index bbfbafe..8449d95 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt
@@ -23,16 +23,16 @@
*
* Autofill services use the [AutofillType] to determine what value to use to autofill fields
* associated with this type. If the [AutofillType] is not specified, the autofill services have to
- * use heuristics to determine the right value to use while autofilling the corresponding field.
+ * use heuristics to determine the right value to use while auto-filling the corresponding field.
*/
enum class AutofillType {
- /** Indicates that the associated component can be autofilled with an email address. */
+ /** Indicates that the associated component can be auto-filled with an email address. */
EmailAddress,
- /** Indicates that the associated component can be autofilled with a username. */
+ /** Indicates that the associated component can be auto-filled with a username. */
Username,
- /** Indicates that the associated component can be autofilled with a password. */
+ /** Indicates that the associated component can be auto-filled with a password. */
Password,
/**
@@ -47,94 +47,100 @@
*/
NewPassword,
- /** Indicates that the associated component can be autofilled with a postal address. */
+ /** Indicates that the associated component can be auto-filled with a postal address. */
PostalAddress,
- /** Indicates that the associated component can be autofilled with a postal code. */
+ /** Indicates that the associated component can be auto-filled with a postal code. */
PostalCode,
- /** Indicates that the associated component can be autofilled with a credit card number. */
+ /** Indicates that the associated component can be auto-filled with a credit card number. */
CreditCardNumber,
/**
- * Indicates that the associated component can be autofilled with a credit card security code.
+ * Indicates that the associated component can be auto-filled with a credit card security code.
*/
CreditCardSecurityCode,
/**
- * Indicates that the associated component can be autofilled with a credit card expiration date.
+ * Indicates that the associated component can be auto-filled with a credit card expiration
+ * date.
*/
CreditCardExpirationDate,
/**
- * Indicates that the associated component can be autofilled with a credit card expiration
+ * Indicates that the associated component can be auto-filled with a credit card expiration
* month.
*/
CreditCardExpirationMonth,
/**
- * Indicates that the associated component can be autofilled with a credit card expiration year.
+ * Indicates that the associated component can be auto-filled with a credit card expiration
+ * year.
*/
CreditCardExpirationYear,
/**
- * Indicates that the associated component can be autofilled with a credit card expiration day.
+ * Indicates that the associated component can be auto-filled with a credit card expiration day.
*/
CreditCardExpirationDay,
- /** Indicates that the associated component can be autofilled with a country name/code. */
+ /** Indicates that the associated component can be auto-filled with a country name/code. */
AddressCountry,
- /** Indicates that the associated component can be autofilled with a region/state. */
+ /** Indicates that the associated component can be auto-filled with a region/state. */
AddressRegion,
/**
- * Indicates that the associated component can be autofilled with an address locality
+ * Indicates that the associated component can be auto-filled with an address locality
* (city/town).
*/
AddressLocality,
- /** Indicates that the associated component can be autofilled with a street address. */
+ /** Indicates that the associated component can be auto-filled with a street address. */
AddressStreet,
- /** Indicates that the associated component can be autofilled with auxiliary address details. */
+ /**
+ * Indicates that the associated component can be auto-filled with auxiliary address details.
+ */
AddressAuxiliaryDetails,
/**
- * Indicates that the associated component can be autofilled with an extended ZIP/POSTAL code.
+ * Indicates that the associated component can be auto-filled with an extended ZIP/POSTAL code.
*
* Example: In forms that split the U.S. ZIP+4 Code with nine digits 99999-9999 into two fields
* annotate the delivery route code with this hint.
*/
PostalCodeExtended,
- /** Indicates that the associated component can be autofilled with a person's full name. */
+ /** Indicates that the associated component can be auto-filled with a person's full name. */
PersonFullName,
/**
- * Indicates that the associated component can be autofilled with a person's first/given name.
+ * Indicates that the associated component can be auto-filled with a person's first/given name.
*/
PersonFirstName,
/**
- * Indicates that the associated component can be autofilled with a person's last/family name.
+ * Indicates that the associated component can be auto-filled with a person's last/family name.
*/
PersonLastName,
- /** Indicates that the associated component can be autofilled with a person's middle name. */
+ /** Indicates that the associated component can be auto-filled with a person's middle name. */
PersonMiddleName,
- /** Indicates that the associated component can be autofilled with a person's middle initial. */
+ /**
+ * Indicates that the associated component can be auto-filled with a person's middle initial.
+ */
PersonMiddleInitial,
- /** Indicates that the associated component can be autofilled with a person's name prefix. */
+ /** Indicates that the associated component can be auto-filled with a person's name prefix. */
PersonNamePrefix,
- /** Indicates that the associated component can be autofilled with a person's name suffix. */
+ /** Indicates that the associated component can be auto-filled with a person's name suffix. */
PersonNameSuffix,
/**
- * Indicates that the associated component can be autofilled with a phone number with country
+ * Indicates that the associated component can be auto-filled with a phone number with country
* code.
*
* Example: +1 123-456-7890
@@ -142,39 +148,43 @@
PhoneNumber,
/**
- * Indicates that the associated component can be autofilled with the current device's phone
+ * Indicates that the associated component can be auto-filled with the current device's phone
* number usually for Sign Up / OTP flows.
*/
PhoneNumberDevice,
/**
- * Indicates that the associated component can be autofilled with a phone number's country code.
+ * Indicates that the associated component can be auto-filled with a phone number's country
+ * code.
*/
PhoneCountryCode,
/**
- * Indicates that the associated component can be autofilled with a phone number without country
- * code.
+ * Indicates that the associated component can be auto-filled with a phone number without
+ * country code.
*/
PhoneNumberNational,
- /** Indicates that the associated component can be autofilled with a gender. */
+ /** Indicates that the associated component can be auto-filled with a gender. */
Gender,
- /** Indicates that the associated component can be autofilled with a full birth date. */
+ /** Indicates that the associated component can be auto-filled with a full birth date. */
BirthDateFull,
- /** Indicates that the associated component can be autofilled with a birth day(of the month). */
+ /**
+ * Indicates that the associated component can be auto-filled with a birth day(of the month).
+ */
BirthDateDay,
- /** Indicates that the associated component can be autofilled with a birth month. */
+ /** Indicates that the associated component can be auto-filled with a birth month. */
BirthDateMonth,
- /** Indicates that the associated component can be autofilled with a birth year. */
+ /** Indicates that the associated component can be auto-filled with a birth year. */
BirthDateYear,
/**
- * Indicates that the associated component can be autofilled with a SMS One Time Password (OTP).
+ * Indicates that the associated component can be auto-filled with a SMS One Time Password
+ * (OTP).
*
* TODO(b/153386346): Support use-case where you specify the start and end index of the OTP.
*/
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt
index d358fbb..f4fa22c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt
@@ -67,8 +67,7 @@
staticCompositionLocalOf<AutofillTree> { noLocalProvidedFor("LocalAutofillTree") }
/**
- * The CompositionLocal that can be used to trigger autofill actions. Eg.
- * [LocalAutofillManager.commit].
+ * The CompositionLocal that can be used to trigger autofill actions. Eg. [AutofillManager.commit].
*/
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
val LocalAutofillManager =
@@ -92,8 +91,9 @@
* Consumers that access this Local directly and call [GraphicsContext.createGraphicsLayer] are
* responsible for calling [GraphicsContext.releaseGraphicsLayer].
*
- * It is recommended that consumers invoke [rememberGraphicsLayer] instead to ensure that a
- * [GraphicsLayer] is released when the corresponding composable is disposed.
+ * It is recommended that consumers invoke [rememberGraphicsLayer][import
+ * androidx.compose.ui.graphics.rememberGraphicsLayer] instead to ensure that a [GraphicsLayer] is
+ * released when the corresponding composable is disposed.
*/
val LocalGraphicsContext =
staticCompositionLocalOf<GraphicsContext> { noLocalProvidedFor("LocalGraphicsContext") }
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/build.gradle b/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/build.gradle
index 9e1b39c..30ed10b 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/build.gradle
+++ b/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import androidx.build.LibraryType
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -38,4 +40,8 @@
android {
compileSdk = 35
namespace = "androidx.constraintlayout.compose.benchmark"
+}
+
+androidx {
+ type = LibraryType.BENCHMARK
}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-core/build.gradle b/constraintlayout/constraintlayout-core/build.gradle
index 4f6f7c0..a2570c0 100644
--- a/constraintlayout/constraintlayout-core/build.gradle
+++ b/constraintlayout/constraintlayout-core/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
diff --git a/constraintlayout/constraintlayout/build.gradle b/constraintlayout/constraintlayout/build.gradle
index fa426e9..c29662e 100644
--- a/constraintlayout/constraintlayout/build.gradle
+++ b/constraintlayout/constraintlayout/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
diff --git a/core/core-google-shortcuts/build.gradle b/core/core-google-shortcuts/build.gradle
index c73b56f..7e8f7d0 100644
--- a/core/core-google-shortcuts/build.gradle
+++ b/core/core-google-shortcuts/build.gradle
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
diff --git a/core/core-splashscreen/samples/build.gradle b/core/core-splashscreen/samples/build.gradle
index 2417028..2ddb138 100644
--- a/core/core-splashscreen/samples/build.gradle
+++ b/core/core-splashscreen/samples/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
diff --git a/datastore/datastore-benchmark/build.gradle b/datastore/datastore-benchmark/build.gradle
index f58443f..2d43429 100644
--- a/datastore/datastore-benchmark/build.gradle
+++ b/datastore/datastore-benchmark/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -48,3 +48,7 @@
android {
namespace = "androidx.datastore.benchmark"
}
+
+androidx {
+ type = LibraryType.BENCHMARK
+}
diff --git a/datastore/datastore-preferences-core/build.gradle b/datastore/datastore-preferences-core/build.gradle
index 9dfab0a..9a16953 100644
--- a/datastore/datastore-preferences-core/build.gradle
+++ b/datastore/datastore-preferences-core/build.gradle
@@ -33,15 +33,22 @@
}
androidXMultiplatform {
- jvm() {
- withJava()
- }
+ jvm()
mac()
linux()
ios()
watchos()
tvos()
- // NOTE, if you add android target here, make sure to add the proguard file as well.
+ androidLibrary {
+ namespace = "androidx.datastore.preferences.core"
+ optimization {
+ it.consumerKeepRules.publish = true
+ it.consumerKeepRules.files.add(
+ new File("src/jvmMain/resources/META-INF/proguard/androidx.datastore_datastore-preferences-core.pro")
+ )
+ }
+ experimentalProperties["android.lint.useK2Uast"] = false
+ }
defaultPlatform(PlatformIdentifier.JVM)
@@ -61,6 +68,7 @@
commonTest {
dependencies {
implementation(libs.kotlinTestCommon)
+ implementation(libs.kotlinTest)
implementation(libs.kotlinTestAnnotationsCommon)
implementation(libs.kotlinCoroutinesTest)
implementation(project(":datastore:datastore-core"))
@@ -68,19 +76,22 @@
implementation(project(":internal-testutils-datastore"))
}
}
- jvmMain {
+ jvmAndroidMain {
dependsOn(commonMain)
dependencies {
implementation(project(":datastore:datastore-preferences-proto"))
}
}
- jvmTest {
+ jvmMain {
+ dependsOn(jvmAndroidMain)
+ }
+ androidMain {
+ dependsOn(jvmAndroidMain)
+ }
+ jvmAndroidTest {
dependsOn(commonTest)
dependencies {
implementation(libs.junit)
- implementation(libs.kotlinTest)
- implementation(project(":internal-testutils-datastore"))
- implementation(project(":kruth:kruth"))
}
}
nativeMain {
@@ -90,7 +101,9 @@
implementation(libs.kotlinSerializationProtobuf)
}
}
-
+ jvmTest {
+ dependsOn(jvmAndroidTest)
+ }
nativeTest {
dependsOn(commonTest)
dependencies {
diff --git a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/Actual.jvm.kt b/datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/Actual.jvmAndroid.kt
similarity index 100%
rename from datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/Actual.jvm.kt
rename to datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/Actual.jvmAndroid.kt
diff --git a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.jvm.kt b/datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.jvmAndroid.kt
similarity index 98%
rename from datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.jvm.kt
rename to datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.jvmAndroid.kt
index 5436d55..365276b 100644
--- a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.jvm.kt
+++ b/datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.jvmAndroid.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializer.jvm.kt b/datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializer.jvmAndroid.kt
similarity index 100%
rename from datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializer.jvm.kt
rename to datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializer.jvmAndroid.kt
diff --git a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.jvm.kt b/datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.jvmAndroid.kt
similarity index 100%
rename from datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.jvm.kt
rename to datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.jvmAndroid.kt
diff --git a/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactoryTest.kt b/datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactoryTest.kt
similarity index 100%
rename from datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactoryTest.kt
rename to datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactoryTest.kt
diff --git a/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializerTest.kt b/datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializerTest.kt
similarity index 100%
rename from datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializerTest.kt
rename to datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializerTest.kt
diff --git a/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesFromJavaTest.java b/datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferencesFromJavaTest.java
similarity index 100%
rename from datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesFromJavaTest.java
rename to datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferencesFromJavaTest.java
diff --git a/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt b/datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt
similarity index 100%
rename from datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt
rename to datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt
diff --git a/datastore/datastore-preferences-proto/build.gradle b/datastore/datastore-preferences-proto/build.gradle
index 28e315f..52653b5 100644
--- a/datastore/datastore-preferences-proto/build.gradle
+++ b/datastore/datastore-preferences-proto/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.RunApiTasks
plugins {
id("AndroidXPlugin")
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 18bda36..2900002 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -240,32 +240,32 @@
docs("androidx.media2:media2-widget:1.3.0")
docs("androidx.media:media:1.7.0")
// androidx.media3 is not hosted in androidx
- docsWithoutApiSince("androidx.media3:media3-cast:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-common:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-common-ktx:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-container:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-database:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-datasource:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-datasource-cronet:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-datasource-okhttp:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-datasource-rtmp:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-decoder:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-effect:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-exoplayer:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-dash:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-hls:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-ima:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-rtsp:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-smoothstreaming:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-exoplayer-workmanager:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-extractor:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-muxer:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-session:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-test-utils:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-test-utils-robolectric:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-transformer:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-ui:1.5.0")
- docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.5.0")
+ docsWithoutApiSince("androidx.media3:media3-cast:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-common:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-common-ktx:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-container:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-database:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-datasource:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-datasource-cronet:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-datasource-okhttp:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-datasource-rtmp:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-decoder:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-effect:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-dash:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-hls:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-ima:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-rtsp:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-smoothstreaming:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-exoplayer-workmanager:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-extractor:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-muxer:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-session:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-test-utils:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-test-utils-robolectric:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-transformer:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-ui:1.6.0-alpha01")
+ docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.6.0-alpha01")
docs("androidx.mediarouter:mediarouter:1.8.0-alpha01")
docs("androidx.mediarouter:mediarouter-testing:1.8.0-alpha01")
docs("androidx.metrics:metrics-performance:1.0.0-beta01")
diff --git a/documentfile/documentfile/src/main/java/androidx/documentfile/provider/DocumentFile.java b/documentfile/documentfile/src/main/java/androidx/documentfile/provider/DocumentFile.java
index 9692bad2..d73c2bf 100644
--- a/documentfile/documentfile/src/main/java/androidx/documentfile/provider/DocumentFile.java
+++ b/documentfile/documentfile/src/main/java/androidx/documentfile/provider/DocumentFile.java
@@ -107,6 +107,7 @@
* {@link android.os.Build.VERSION_CODES#KITKAT} or later, and will return
* {@code null} when called on earlier platform versions.
*
+ * @param context {@link Context} used to resolve resources
* @param singleUri the {@link Intent#getData()} from a successful
* {@link Intent#ACTION_OPEN_DOCUMENT} or
* {@link Intent#ACTION_CREATE_DOCUMENT} request.
@@ -122,6 +123,7 @@
* {@link android.os.Build.VERSION_CODES#LOLLIPOP} or later, and will return
* {@code null} when called on earlier platform versions.
*
+ * @param context {@link Context} used to resolve resources
* @param treeUri the {@link Intent#getData()} from a successful
* {@link Intent#ACTION_OPEN_DOCUMENT_TREE} request.
*/
diff --git a/emoji2/emoji2-benchmark/build.gradle b/emoji2/emoji2-benchmark/build.gradle
index ed2ec2f..5eab826 100644
--- a/emoji2/emoji2-benchmark/build.gradle
+++ b/emoji2/emoji2-benchmark/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -64,7 +64,7 @@
androidx {
name = "Emoji2 Benchmarks"
- publish = Publish.NONE
+ type = LibraryType.BENCHMARK
inceptionYear = "2021"
description = "Emoji2 Benchmarks"
}
diff --git a/external/libyuv/build.gradle b/external/libyuv/build.gradle
index 45640f2..9f91b2a 100644
--- a/external/libyuv/build.gradle
+++ b/external/libyuv/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
apply plugin: 'AndroidXPlugin'
apply plugin: 'com.android.library'
@@ -69,7 +68,7 @@
androidx {
name = "libyuv"
// Only intended to be used as snapshots, do not change to PUBLISHED.
- publish = Publish.SNAPSHOT_ONLY
+ type = LibraryType.SNAPSHOT_ONLY_LIBRARY
inceptionYear = "2021"
description = "libyuv is an open source project that includes YUV scaling and conversion functionality."
}
diff --git a/fragment/fragment-truth/build.gradle b/fragment/fragment-truth/build.gradle
index 6b158d9..a1b437d 100644
--- a/fragment/fragment-truth/build.gradle
+++ b/fragment/fragment-truth/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -43,7 +43,7 @@
androidx {
name = "Fragment Truth Extensions"
- publish = Publish.NONE
+ type = LibraryType.UNSET
inceptionYear = "2019"
description = "Truth extensions for Fragments"
}
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index d52b52e..35daeff 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -7,7 +7,6 @@
*/
import androidx.build.LibraryType
-import androidx.build.Publish
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
diff --git a/glance/glance-appwidget/glance-layout-generator/build.gradle b/glance/glance-appwidget/glance-layout-generator/build.gradle
index 6d3102d..1146591 100644
--- a/glance/glance-appwidget/glance-layout-generator/build.gradle
+++ b/glance/glance-appwidget/glance-layout-generator/build.gradle
@@ -23,8 +23,6 @@
*/
import androidx.build.KotlinTarget
import androidx.build.LibraryType
-import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
@@ -42,8 +40,7 @@
androidx {
name = "Glance AppWidget Layout Generator"
- type = LibraryType.OTHER_CODE_PROCESSOR
- publish = Publish.NONE
+ type = LibraryType.INTERNAL_OTHER_CODE_PROCESSOR
inceptionYear = "2021"
description = "Generator module that generates the layouts Glance AppWidget needs."
kotlinTarget = KotlinTarget.KOTLIN_1_9
diff --git a/glance/glance-template/build.gradle b/glance/glance-template/build.gradle
index a0f7619..b358f48 100644
--- a/glance/glance-template/build.gradle
+++ b/glance/glance-template/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
import androidx.build.AndroidXComposePlugin
plugins {
diff --git a/graphics/graphics-core/build.gradle b/graphics/graphics-core/build.gradle
index efeb042..34580b6 100644
--- a/graphics/graphics-core/build.gradle
+++ b/graphics/graphics-core/build.gradle
@@ -24,7 +24,6 @@
import androidx.build.KotlinTarget
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
diff --git a/graphics/graphics-core/samples/build.gradle b/graphics/graphics-core/samples/build.gradle
index 655493e..0386700 100644
--- a/graphics/graphics-core/samples/build.gradle
+++ b/graphics/graphics-core/samples/build.gradle
@@ -15,7 +15,6 @@
*/
import androidx.build.KotlinTarget
-import androidx.build.Publish
import androidx.build.LibraryType
plugins {
diff --git a/health/connect/connect-client/samples/build.gradle b/health/connect/connect-client/samples/build.gradle
index 0c314fa..62b1f75 100644
--- a/health/connect/connect-client/samples/build.gradle
+++ b/health/connect/connect-client/samples/build.gradle
@@ -21,7 +21,6 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
import androidx.build.LibraryType
plugins {
diff --git a/health/health-services-client-proto/build.gradle b/health/health-services-client-proto/build.gradle
index 95300ca..60cfefe 100644
--- a/health/health-services-client-proto/build.gradle
+++ b/health/health-services-client-proto/build.gradle
@@ -23,7 +23,6 @@
*/
import androidx.build.LibraryType
-import androidx.build.RunApiTasks
plugins {
id("AndroidXPlugin")
diff --git a/hilt/hilt-navigation-compose/samples/build.gradle b/hilt/hilt-navigation-compose/samples/build.gradle
index 0c455f0..4e90f1c 100644
--- a/hilt/hilt-navigation-compose/samples/build.gradle
+++ b/hilt/hilt-navigation-compose/samples/build.gradle
@@ -24,7 +24,6 @@
import androidx.build.KotlinTarget
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
diff --git a/inspection/inspection-gradle-plugin/build.gradle b/inspection/inspection-gradle-plugin/build.gradle
index 19bab5d..c459a13 100644
--- a/inspection/inspection-gradle-plugin/build.gradle
+++ b/inspection/inspection-gradle-plugin/build.gradle
@@ -22,8 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
-import androidx.build.SdkResourceGenerator
plugins {
id("AndroidXPlugin")
@@ -59,8 +57,7 @@
androidx {
name = "Inspection Gradle Plugin"
- type = LibraryType.GRADLE_PLUGIN
- publish = Publish.NONE
+ type = LibraryType.INTERNAL_GRADLE_PLUGIN
inceptionYear = "2019"
description = "Android Inspection Gradle Plugin"
}
diff --git a/inspection/inspection-testing/build.gradle b/inspection/inspection-testing/build.gradle
index 8067271..cee9da2 100644
--- a/inspection/inspection-testing/build.gradle
+++ b/inspection/inspection-testing/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
diff --git a/inspection/inspection/build.gradle b/inspection/inspection/build.gradle
index d14ab58..af4ba30 100644
--- a/inspection/inspection/build.gradle
+++ b/inspection/inspection/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.RunApiTasks
plugins {
id("AndroidXPlugin")
@@ -45,9 +44,6 @@
type = LibraryType.PUBLISHED_LIBRARY
inceptionYear = "2019"
description = "Experimental AndroidX Inspection Project"
- runApiTasks = new RunApiTasks.Yes(
- "Interfaces provided in this artifact should be binary compatible to guarantee " +
- "that old inspectors are compatible with newer Android Studio versions")
legacyDisableKotlinStrictApiMode = true
doNotDocumentReason = "Not shipped externally"
// TODO: b/326456246
diff --git a/kruth/kruth/build.gradle b/kruth/kruth/build.gradle
index e56160f..cd7daef 100644
--- a/kruth/kruth/build.gradle
+++ b/kruth/kruth/build.gradle
@@ -26,8 +26,6 @@
import androidx.build.KotlinTarget
import androidx.build.LibraryType
import androidx.build.PlatformIdentifier
-import androidx.build.Publish
-import androidx.build.RunApiTasks
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
plugins {
@@ -158,9 +156,7 @@
androidx {
legacyDisableKotlinStrictApiMode = true // Temporarily enabled to allow API tracking
- publish = Publish.SNAPSHOT_ONLY
- runApiTasks = new RunApiTasks.Yes() // Used to diff against Google Truth
- type = LibraryType.INTERNAL_TEST_LIBRARY
+ type = LibraryType.SNAPSHOT_ONLY_TEST_LIBRARY_WITH_API_TASKS // Used to diff against Google Truth
doNotDocumentReason = "Not shipped externally"
metalavaK2UastEnabled = false
kotlinTarget = KotlinTarget.KOTLIN_1_9
diff --git a/lifecycle/lifecycle-extensions/build.gradle b/lifecycle/lifecycle-extensions/build.gradle
index 802867a..5a2b6b6 100644
--- a/lifecycle/lifecycle-extensions/build.gradle
+++ b/lifecycle/lifecycle-extensions/build.gradle
@@ -21,8 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
-import androidx.build.RunApiTasks
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -59,8 +58,7 @@
androidx {
name = "Lifecycle Extensions"
- publish = Publish.NONE
- runApiTasks = new RunApiTasks.Yes("Need to track API surface before moving to publish")
+ type = LibraryType.INTERNAL_LIBRARY_WITH_API_TASKS
inceptionYear = "2017"
description = "Android Lifecycle Extensions"
failOnDeprecationWarnings = false
diff --git a/lifecycle/lifecycle-livedata-core-truth/build.gradle b/lifecycle/lifecycle-livedata-core-truth/build.gradle
index d17fe02..68ff222 100644
--- a/lifecycle/lifecycle-livedata-core-truth/build.gradle
+++ b/lifecycle/lifecycle-livedata-core-truth/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -41,7 +41,7 @@
androidx {
name = "LiveData Core Truth Extensions"
- publish = Publish.NONE
+ type = LibraryType.UNSET
inceptionYear = "2019"
description = "Truth extensions for 'livedata-core' artifact"
}
diff --git a/lifecycle/lifecycle-runtime-compose/integration-tests/lifecycle-runtime-compose-demos/build.gradle b/lifecycle/lifecycle-runtime-compose/integration-tests/lifecycle-runtime-compose-demos/build.gradle
index ddfa56f..caa6056 100644
--- a/lifecycle/lifecycle-runtime-compose/integration-tests/lifecycle-runtime-compose-demos/build.gradle
+++ b/lifecycle/lifecycle-runtime-compose/integration-tests/lifecycle-runtime-compose-demos/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -36,7 +36,7 @@
androidx {
name = "Lifecycle Runtime Compose Demos"
- publish = Publish.NONE
+ type = LibraryType.TEST_APPLICATION
inceptionYear = "2022"
description = "This is a project for Lifecycle Runtime Compose demos."
}
diff --git a/lifecycle/lifecycle-viewmodel-compose/integration-tests/lifecycle-viewmodel-demos/build.gradle b/lifecycle/lifecycle-viewmodel-compose/integration-tests/lifecycle-viewmodel-demos/build.gradle
index 224f7bc..3047354 100644
--- a/lifecycle/lifecycle-viewmodel-compose/integration-tests/lifecycle-viewmodel-demos/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/integration-tests/lifecycle-viewmodel-demos/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -36,7 +36,7 @@
androidx {
name = "Compose Lifecycle ViewModel Demos"
- publish = Publish.NONE
+ type = LibraryType.TEST_APPLICATION
inceptionYear = "2021"
description = "This is a project for Lifecycle ViewModel demos."
}
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/build.gradle b/lifecycle/lifecycle-viewmodel-navigation3/build.gradle
index 85d6c9e..e85f6a2 100644
--- a/lifecycle/lifecycle-viewmodel-navigation3/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-navigation3/build.gradle
@@ -24,7 +24,6 @@
import androidx.build.KotlinTarget
import androidx.build.LibraryType
-import androidx.build.Publish
import androidx.build.PlatformIdentifier
plugins {
@@ -103,8 +102,7 @@
androidx {
name = "Androidx Lifecycle Navigation3 ViewModel"
- publish = Publish.SNAPSHOT_ONLY
- type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+ type = LibraryType.SNAPSHOT_ONLY_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
inceptionYear = "2024"
description = "Provides the ViewModel wrapper for nav3."
doNotDocumentReason = "Not published to maven"
diff --git a/lint/lint-gradle/build.gradle b/lint/lint-gradle/build.gradle
index 9605875..65c3f7d 100644
--- a/lint/lint-gradle/build.gradle
+++ b/lint/lint-gradle/build.gradle
@@ -24,7 +24,6 @@
import androidx.build.KotlinTarget
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
diff --git a/mediarouter/mediarouter/api/current.txt b/mediarouter/mediarouter/api/current.txt
index 0c0847b..8b9f3d9 100644
--- a/mediarouter/mediarouter/api/current.txt
+++ b/mediarouter/mediarouter/api/current.txt
@@ -274,6 +274,7 @@
method public boolean isGroupable();
method public boolean isTransferable();
method public boolean isUnselectable();
+ field public static final int NOT_IN_GROUP = 4; // 0x4
field public static final int SELECTED = 3; // 0x3
field public static final int SELECTING = 2; // 0x2
field public static final int UNSELECTED = 1; // 0x1
@@ -379,7 +380,7 @@
method @MainThread public void addProvider(androidx.mediarouter.media.MediaRouteProvider);
method @Deprecated @MainThread public void addRemoteControlClient(Object);
method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo? getBluetoothRoute();
- method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getConnectedRoutes();
+ method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.GroupRouteInfo!> getConnectedGroupRoutes();
method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo getDefaultRoute();
method @MainThread public static androidx.mediarouter.media.MediaRouter getInstance(android.content.Context);
method public android.support.v4.media.session.MediaSessionCompat.Token? getMediaSessionToken();
@@ -443,6 +444,33 @@
method public void onResult(android.os.Bundle?);
}
+ public static class MediaRouter.GroupRouteInfo extends androidx.mediarouter.media.MediaRouter.RouteInfo {
+ method @MainThread public int addRoute(androidx.mediarouter.media.MediaRouter.RouteInfo);
+ method public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getRoutesInGroup();
+ method public int getSelectionState(androidx.mediarouter.media.MediaRouter.RouteInfo);
+ method @MainThread public boolean isConnected();
+ method public boolean isGroupable(androidx.mediarouter.media.MediaRouter.RouteInfo);
+ method public boolean isTransferable(androidx.mediarouter.media.MediaRouter.RouteInfo);
+ method public boolean isUnselectable(androidx.mediarouter.media.MediaRouter.RouteInfo);
+ method @MainThread public int removeRoute(androidx.mediarouter.media.MediaRouter.RouteInfo);
+ method @MainThread public int updateRoutes(java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!>);
+ field public static final int ADD_ROUTE_FAILED_REASON_ALREADY_IN_GROUP = 3; // 0x3
+ field public static final int ADD_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 5; // 0x5
+ field public static final int ADD_ROUTE_FAILED_REASON_NOT_GROUPABLE = 2; // 0x2
+ field public static final int ADD_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 4; // 0x4
+ field public static final int ADD_ROUTE_SUCCESSFUL = 1; // 0x1
+ field public static final int REMOVE_ROUTE_FAILED_REASON_LAST_ROUTE_IN_GROUP = 4; // 0x4
+ field public static final int REMOVE_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 6; // 0x6
+ field public static final int REMOVE_ROUTE_FAILED_REASON_NOT_IN_GROUP = 3; // 0x3
+ field public static final int REMOVE_ROUTE_FAILED_REASON_NOT_UNSELECTABLE = 2; // 0x2
+ field public static final int REMOVE_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 5; // 0x5
+ field public static final int REMOVE_ROUTE_SUCCESSFUL = 1; // 0x1
+ field public static final int UPDATE_ROUTES_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 4; // 0x4
+ field public static final int UPDATE_ROUTES_FAILED_REASON_NOT_TRANSFERABLE = 2; // 0x2
+ field public static final int UPDATE_ROUTES_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 3; // 0x3
+ field public static final int UPDATE_ROUTES_SUCCESSFUL = 1; // 0x1
+ }
+
public static interface MediaRouter.OnPrepareTransferListener {
method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!>? onPrepareTransfer(androidx.mediarouter.media.MediaRouter.RouteInfo, androidx.mediarouter.media.MediaRouter.RouteInfo);
}
@@ -471,13 +499,11 @@
method public int getPlaybackType();
method @MainThread public android.view.Display? getPresentationDisplay();
method public androidx.mediarouter.media.MediaRouter.ProviderInfo getProvider();
- method public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getRoutesInGroup();
method public android.content.IntentSender? getSettingsIntent();
method public int getVolume();
method public int getVolumeHandling();
method public int getVolumeMax();
method @MainThread public boolean isBluetooth();
- method @MainThread public boolean isConnected();
method @Deprecated public boolean isConnecting();
method @MainThread public boolean isDefault();
method public boolean isDeviceSpeaker();
diff --git a/mediarouter/mediarouter/api/restricted_current.txt b/mediarouter/mediarouter/api/restricted_current.txt
index 0c0847b..8b9f3d9 100644
--- a/mediarouter/mediarouter/api/restricted_current.txt
+++ b/mediarouter/mediarouter/api/restricted_current.txt
@@ -274,6 +274,7 @@
method public boolean isGroupable();
method public boolean isTransferable();
method public boolean isUnselectable();
+ field public static final int NOT_IN_GROUP = 4; // 0x4
field public static final int SELECTED = 3; // 0x3
field public static final int SELECTING = 2; // 0x2
field public static final int UNSELECTED = 1; // 0x1
@@ -379,7 +380,7 @@
method @MainThread public void addProvider(androidx.mediarouter.media.MediaRouteProvider);
method @Deprecated @MainThread public void addRemoteControlClient(Object);
method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo? getBluetoothRoute();
- method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getConnectedRoutes();
+ method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.GroupRouteInfo!> getConnectedGroupRoutes();
method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo getDefaultRoute();
method @MainThread public static androidx.mediarouter.media.MediaRouter getInstance(android.content.Context);
method public android.support.v4.media.session.MediaSessionCompat.Token? getMediaSessionToken();
@@ -443,6 +444,33 @@
method public void onResult(android.os.Bundle?);
}
+ public static class MediaRouter.GroupRouteInfo extends androidx.mediarouter.media.MediaRouter.RouteInfo {
+ method @MainThread public int addRoute(androidx.mediarouter.media.MediaRouter.RouteInfo);
+ method public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getRoutesInGroup();
+ method public int getSelectionState(androidx.mediarouter.media.MediaRouter.RouteInfo);
+ method @MainThread public boolean isConnected();
+ method public boolean isGroupable(androidx.mediarouter.media.MediaRouter.RouteInfo);
+ method public boolean isTransferable(androidx.mediarouter.media.MediaRouter.RouteInfo);
+ method public boolean isUnselectable(androidx.mediarouter.media.MediaRouter.RouteInfo);
+ method @MainThread public int removeRoute(androidx.mediarouter.media.MediaRouter.RouteInfo);
+ method @MainThread public int updateRoutes(java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!>);
+ field public static final int ADD_ROUTE_FAILED_REASON_ALREADY_IN_GROUP = 3; // 0x3
+ field public static final int ADD_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 5; // 0x5
+ field public static final int ADD_ROUTE_FAILED_REASON_NOT_GROUPABLE = 2; // 0x2
+ field public static final int ADD_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 4; // 0x4
+ field public static final int ADD_ROUTE_SUCCESSFUL = 1; // 0x1
+ field public static final int REMOVE_ROUTE_FAILED_REASON_LAST_ROUTE_IN_GROUP = 4; // 0x4
+ field public static final int REMOVE_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 6; // 0x6
+ field public static final int REMOVE_ROUTE_FAILED_REASON_NOT_IN_GROUP = 3; // 0x3
+ field public static final int REMOVE_ROUTE_FAILED_REASON_NOT_UNSELECTABLE = 2; // 0x2
+ field public static final int REMOVE_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 5; // 0x5
+ field public static final int REMOVE_ROUTE_SUCCESSFUL = 1; // 0x1
+ field public static final int UPDATE_ROUTES_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 4; // 0x4
+ field public static final int UPDATE_ROUTES_FAILED_REASON_NOT_TRANSFERABLE = 2; // 0x2
+ field public static final int UPDATE_ROUTES_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 3; // 0x3
+ field public static final int UPDATE_ROUTES_SUCCESSFUL = 1; // 0x1
+ }
+
public static interface MediaRouter.OnPrepareTransferListener {
method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!>? onPrepareTransfer(androidx.mediarouter.media.MediaRouter.RouteInfo, androidx.mediarouter.media.MediaRouter.RouteInfo);
}
@@ -471,13 +499,11 @@
method public int getPlaybackType();
method @MainThread public android.view.Display? getPresentationDisplay();
method public androidx.mediarouter.media.MediaRouter.ProviderInfo getProvider();
- method public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getRoutesInGroup();
method public android.content.IntentSender? getSettingsIntent();
method public int getVolume();
method public int getVolumeHandling();
method public int getVolumeMax();
method @MainThread public boolean isBluetooth();
- method @MainThread public boolean isConnected();
method @Deprecated public boolean isConnecting();
method @MainThread public boolean isDefault();
method public boolean isDeviceSpeaker();
diff --git a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterDynamicProviderTest.java b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterDynamicProviderTest.java
index 3de8cc9..5722db5 100644
--- a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterDynamicProviderTest.java
+++ b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterDynamicProviderTest.java
@@ -16,11 +16,24 @@
package androidx.mediarouter.media;
+import static androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.SELECTED;
+import static androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.UNSELECTED;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.ADD_ROUTE_SUCCESSFUL;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.REMOVE_ROUTE_SUCCESSFUL;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.UPDATE_ROUTES_SUCCESSFUL;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_GROUPABLE_1;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_GROUPABLE_2;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_GROUPABLE_3;
import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_ID_1;
import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_ID_2;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_ID_3;
import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_ID_GROUP;
import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_NAME_1;
import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_NAME_2;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_NAME_3;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_TRANSFERABLE_1;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_TRANSFERABLE_2;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_TRANSFERABLE_3;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -31,7 +44,9 @@
import static org.junit.Assert.assertTrue;
import android.content.Context;
+import android.content.Intent;
import android.os.Build;
+import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.Looper;
@@ -45,6 +60,7 @@
import org.junit.Before;
import org.junit.Test;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
@@ -52,6 +68,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* Test for {@link MediaRouter} functionality around routes from a provider that supports {@link
@@ -67,13 +84,19 @@
STATE_DISCONNECTED
}
+ private static final List<String> EXPECTED_ROUTE_IDS_AFTER_ROUTE_ADDED =
+ List.of(ROUTE_ID_1, ROUTE_ID_2);
+ private static final List<String> EXPECTED_ROUTE_IDS_AFTER_ROUTE_REMOVED = List.of(ROUTE_ID_1);
+ private static final List<String> EXPECTED_ROUTE_IDS_AFTER_ROUTE_UPDATED = List.of(ROUTE_ID_3);
private Context mContext;
private MediaRouter mRouter;
private MediaRouteSelector mSelector;
private MediaRouterCallbackImpl mCallback;
private MediaRouter.RouteInfo mRoute1;
private MediaRouter.RouteInfo mRoute2;
+ private MediaRouter.RouteInfo mRoute3;
private RouteConnectionState mRouteConnectionState;
+ private MediaRouter.RouteInfo mChangedRoute;
private MediaRouter.RouteInfo mConnectedRoute;
private MediaRouter.RouteInfo mDisconnectedRoute;
private MediaRouter.RouteInfo mRequestedRoute;
@@ -113,6 +136,13 @@
Objects.requireNonNull(mediaRouteDescriptor2);
assertEquals(ROUTE_ID_2, mediaRouteDescriptor2.getId());
assertEquals(ROUTE_NAME_2, mediaRouteDescriptor2.getName());
+
+ mRoute3 = routeSnapshot.get(ROUTE_ID_3);
+ Objects.requireNonNull(mRoute3);
+ MediaRouteDescriptor mediaRouteDescriptor3 = mRoute3.getMediaRouteDescriptor();
+ Objects.requireNonNull(mediaRouteDescriptor3);
+ assertEquals(ROUTE_ID_3, mediaRouteDescriptor3.getId());
+ assertEquals(ROUTE_NAME_3, mediaRouteDescriptor3.getName());
}
@After
@@ -134,41 +164,37 @@
@Test()
public void connectDynamicRoute_shouldNotifyRouteConnected() {
assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
- List<MediaRouter.RouteInfo> connectedRoutes =
+ List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
mCallback.connectAndWaitForOnConnected(mRoute2);
assertNotNull(mConnectedRoute);
assertEquals(ROUTE_ID_GROUP, mConnectedRoute.getDescriptorId());
- assertEquals(1, connectedRoutes.size());
- MediaRouter.RouteInfo connectedRoute = connectedRoutes.get(0);
- assertEquals(ROUTE_ID_GROUP, connectedRoute.getDescriptorId());
- assertTrue(runBlockingOnMainThreadWithResult(connectedRoute::isConnected));
+ assertEquals(1, connectedGroupRoutes.size());
+ MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+ assertEquals(ROUTE_ID_GROUP, connectedGroupRoute.getDescriptorId());
+ assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
assertNotNull(mRequestedRoute);
assertEquals(ROUTE_ID_2, mRequestedRoute.getDescriptorId());
- assertFalse(runBlockingOnMainThreadWithResult(mRequestedRoute::isConnected));
- assertFalse(runBlockingOnMainThreadWithResult(mRoute2::isConnected));
assertEquals(RouteConnectionState.STATE_CONNECTED, mRouteConnectionState);
}
@Test()
public void disconnectDynamicRoute_shouldNotifyRouteDisconnected() {
assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
- List<MediaRouter.RouteInfo> connectedRoutes =
+ List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
mCallback.connectAndWaitForOnConnected(mRoute2);
assertEquals(RouteConnectionState.STATE_CONNECTED, mRouteConnectionState);
- assertEquals(1, connectedRoutes.size());
+ assertEquals(1, connectedGroupRoutes.size());
- connectedRoutes = mCallback.disconnectAndWaitForOnDisconnected(mRoute2);
+ connectedGroupRoutes = mCallback.disconnectAndWaitForOnDisconnected(mRoute2);
assertNotNull(mConnectedRoute);
assertNotNull(mDisconnectedRoute);
- assertEquals(0, connectedRoutes.size());
+ assertEquals(0, connectedGroupRoutes.size());
assertNotNull(mRequestedRoute);
assertEquals(ROUTE_ID_2, mRequestedRoute.getDescriptorId());
- assertFalse(runBlockingOnMainThreadWithResult(mRequestedRoute::isConnected));
- assertFalse(runBlockingOnMainThreadWithResult(mRoute2::isConnected));
assertEquals(RouteConnectionState.STATE_DISCONNECTED, mRouteConnectionState);
assertEquals(MediaRouter.REASON_DISCONNECTED, mRouteDisconnectedReason);
}
@@ -181,20 +207,247 @@
assertFalse(runBlockingOnMainThreadWithResult(mRoute1::isSelected));
assertTrue(runBlockingOnMainThreadWithResult(selectedRoute::isSelected));
- List<MediaRouter.RouteInfo> connectedRoutes =
- mCallback.connectAndWaitForOnConnected(selectedRoute);
+ List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+ mCallback.connectAndWaitForOnDisconnected(selectedRoute);
assertNull(mConnectedRoute);
assertNull(mDisconnectedRoute);
- assertEquals(0, connectedRoutes.size());
+ assertEquals(0, connectedGroupRoutes.size());
assertNotNull(mRequestedRoute);
assertEquals(ROUTE_ID_GROUP, mRequestedRoute.getDescriptorId());
- assertFalse(runBlockingOnMainThreadWithResult(mRequestedRoute::isConnected));
- assertFalse(runBlockingOnMainThreadWithResult(selectedRoute::isConnected));
assertEquals(RouteConnectionState.STATE_DISCONNECTED, mRouteConnectionState);
}
+ @Test()
+ public void addRouteToGroupAndRemoveRouteFromGroup_shouldChangeGroup() {
+ assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
+ List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+ mCallback.connectAndWaitForOnConnected(mRoute2);
+
+ assertNotNull(mConnectedRoute);
+ MediaRouter.GroupRouteInfo groupRouteInfo = mConnectedRoute.asGroup();
+ assertNotNull(groupRouteInfo);
+ assertEquals(ROUTE_ID_GROUP, mConnectedRoute.getDescriptorId());
+ assertEquals(1, connectedGroupRoutes.size());
+ MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+ assertEquals(ROUTE_ID_GROUP, connectedGroupRoute.getDescriptorId());
+ assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
+
+ assertEquals(1, mConnectedRoute.getSelectedRoutesInGroup().size());
+ assertEquals(
+ ROUTE_ID_2, mConnectedRoute.getSelectedRoutesInGroup().get(0).getDescriptorId());
+ assertEquals(3, groupRouteInfo.getRoutesInGroup().size());
+ verifyMemberRouteState(groupRouteInfo, mRoute1, /* isSelected= */ false);
+ verifyMemberRouteState(groupRouteInfo, mRoute2, /* isSelected= */ true);
+ verifyMemberRouteState(groupRouteInfo, mRoute3, /* isSelected= */ false);
+
+ List<MediaRouter.RouteInfo> memberRoutes =
+ mCallback.addRouteToGroupAndWaitForOnChanged(groupRouteInfo, mRoute1);
+ assertEquals(2, mConnectedRoute.getSelectedRoutesInGroup().size());
+ assertEquals(2, memberRoutes.size());
+ assertNotNull(mChangedRoute);
+ assertEquals(ROUTE_ID_GROUP, mChangedRoute.getDescriptorId());
+ assertEquals(3, groupRouteInfo.getRoutesInGroup().size());
+ verifyMemberRouteState(groupRouteInfo, mRoute1, /* isSelected= */ true);
+ verifyMemberRouteState(groupRouteInfo, mRoute2, /* isSelected= */ true);
+ verifyMemberRouteState(groupRouteInfo, mRoute3, /* isSelected= */ false);
+
+ mChangedRoute = null;
+ memberRoutes = mCallback.removeRouteFromGroupAndWaitForOnChanged(groupRouteInfo, mRoute2);
+ assertEquals(1, mConnectedRoute.getSelectedRoutesInGroup().size());
+ assertEquals(1, memberRoutes.size());
+ assertEquals(ROUTE_ID_1, memberRoutes.get(0).getDescriptorId());
+ assertNotNull(mChangedRoute);
+ assertEquals(ROUTE_ID_GROUP, mChangedRoute.getDescriptorId());
+ assertEquals(3, groupRouteInfo.getRoutesInGroup().size());
+ verifyMemberRouteState(groupRouteInfo, mRoute1, /* isSelected= */ true);
+ verifyMemberRouteState(groupRouteInfo, mRoute2, /* isSelected= */ false);
+ verifyMemberRouteState(groupRouteInfo, mRoute3, /* isSelected= */ false);
+ }
+
+ @Test()
+ public void updateRoutesForGroup_shouldChangeGroup() {
+ assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
+ List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+ mCallback.connectAndWaitForOnConnected(mRoute1);
+
+ assertNotNull(mConnectedRoute);
+ MediaRouter.GroupRouteInfo groupRouteInfo = mConnectedRoute.asGroup();
+ assertNotNull(groupRouteInfo);
+ assertEquals(ROUTE_ID_GROUP, mConnectedRoute.getDescriptorId());
+ assertEquals(1, connectedGroupRoutes.size());
+ MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+ assertEquals(ROUTE_ID_GROUP, connectedGroupRoute.getDescriptorId());
+ assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
+
+ assertEquals(1, mConnectedRoute.getSelectedRoutesInGroup().size());
+ assertEquals(
+ ROUTE_ID_1, mConnectedRoute.getSelectedRoutesInGroup().get(0).getDescriptorId());
+ assertEquals(3, groupRouteInfo.getRoutesInGroup().size());
+ verifyMemberRouteState(groupRouteInfo, mRoute1, /* isSelected= */ true);
+ verifyMemberRouteState(groupRouteInfo, mRoute2, /* isSelected= */ false);
+ verifyMemberRouteState(groupRouteInfo, mRoute3, /* isSelected= */ false);
+
+ List<MediaRouter.RouteInfo> memberRoutes =
+ mCallback.updateRoutesForGroupAndWaitForOnChanged(groupRouteInfo, List.of(mRoute3));
+ assertEquals(1, mConnectedRoute.getSelectedRoutesInGroup().size());
+ assertEquals(1, memberRoutes.size());
+ assertNotNull(mChangedRoute);
+ assertEquals(ROUTE_ID_GROUP, mChangedRoute.getDescriptorId());
+ assertEquals(3, groupRouteInfo.getRoutesInGroup().size());
+ verifyMemberRouteState(groupRouteInfo, mRoute1, /* isSelected= */ false);
+ verifyMemberRouteState(groupRouteInfo, mRoute2, /* isSelected= */ false);
+ verifyMemberRouteState(groupRouteInfo, mRoute3, /* isSelected= */ true);
+ }
+
+ @Test()
+ public void setGroupVolume_shouldSetGroupVolume() {
+ assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
+ List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+ mCallback.connectAndWaitForOnConnected(mRoute1);
+
+ assertNotNull(mConnectedRoute);
+ MediaRouter.GroupRouteInfo groupRouteInfo = mConnectedRoute.asGroup();
+ assertNotNull(groupRouteInfo);
+ assertEquals(
+ StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+ groupRouteInfo.getVolume());
+ assertEquals(1, connectedGroupRoutes.size());
+ MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+ assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
+ assertEquals(
+ StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+ connectedGroupRoute.getVolume());
+
+ final int expectedVolume = 6;
+ int updatedVolume =
+ mCallback.setGroupVolumeAndWaitForOnVolumeSet(groupRouteInfo, expectedVolume);
+ assertEquals(expectedVolume, updatedVolume);
+ assertEquals(expectedVolume, mConnectedRoute.getVolume());
+ }
+
+ @Test()
+ public void updateGroupVolume_shouldUpdateGroupVolume() {
+ assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
+ List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+ mCallback.connectAndWaitForOnConnected(mRoute2);
+
+ assertNotNull(mConnectedRoute);
+ MediaRouter.GroupRouteInfo groupRouteInfo = mConnectedRoute.asGroup();
+ assertNotNull(groupRouteInfo);
+ assertEquals(
+ StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+ groupRouteInfo.getVolume());
+ assertEquals(1, connectedGroupRoutes.size());
+ MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+ assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
+ assertEquals(
+ StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+ connectedGroupRoute.getVolume());
+
+ final int volumeDelta = -3;
+ final int expectedVolume =
+ StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE + volumeDelta;
+ int updatedVolume =
+ mCallback.updateGroupVolumeAndWaitForOnVolumeUpdated(groupRouteInfo, volumeDelta);
+ assertEquals(expectedVolume, updatedVolume);
+ assertEquals(expectedVolume, mConnectedRoute.getVolume());
+ }
+
+ @Test()
+ public void setMemberVolume_shouldSetMemberVolume() {
+ assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
+ List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+ mCallback.connectAndWaitForOnConnected(mRoute2);
+
+ assertNotNull(mConnectedRoute);
+ MediaRouter.GroupRouteInfo groupRouteInfo = mConnectedRoute.asGroup();
+ assertNotNull(groupRouteInfo);
+ assertEquals(
+ StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+ groupRouteInfo.getVolume());
+ assertEquals(1, connectedGroupRoutes.size());
+ MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+ assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
+ assertEquals(
+ StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+ connectedGroupRoute.getVolume());
+
+ final int expectedVolume = 14;
+ getInstrumentation().runOnMainSync(() -> mRoute2.requestSetVolume(expectedVolume));
+ MediaRouter.RouteInfo memberRoute =
+ mCallback.waitForRouteVolume(ROUTE_ID_2, expectedVolume);
+ assertEquals(expectedVolume, memberRoute.getVolume());
+ }
+
+ @Test()
+ public void updateMemberVolume_shouldUpdateMemberVolume() {
+ assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
+ List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+ mCallback.connectAndWaitForOnConnected(mRoute1);
+
+ assertNotNull(mConnectedRoute);
+ MediaRouter.GroupRouteInfo groupRouteInfo = mConnectedRoute.asGroup();
+ assertNotNull(groupRouteInfo);
+ assertEquals(
+ StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+ groupRouteInfo.getVolume());
+ assertEquals(1, connectedGroupRoutes.size());
+ MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+ assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
+ assertEquals(
+ StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+ connectedGroupRoute.getVolume());
+
+ final int volumeDelta = 4;
+ final int expectedVolume =
+ StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE + volumeDelta;
+ getInstrumentation().runOnMainSync(() -> mRoute1.requestUpdateVolume(volumeDelta));
+ MediaRouter.RouteInfo memberRoute =
+ mCallback.waitForRouteVolume(ROUTE_ID_1, expectedVolume);
+ assertEquals(expectedVolume, memberRoute.getVolume());
+ }
+
+ @Test()
+ public void sendControlRequest_shouldSendControlRequestToGroupRouteController() {
+ final ConditionVariable sendControlRequestConditionVariable =
+ new ConditionVariable(/* state= */ true);
+ assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
+ List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+ mCallback.connectAndWaitForOnConnected(mRoute2);
+
+ assertNotNull(mConnectedRoute);
+ MediaRouter.GroupRouteInfo groupRouteInfo = mConnectedRoute.asGroup();
+ assertNotNull(groupRouteInfo);
+ assertEquals(1, connectedGroupRoutes.size());
+ MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+ assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
+
+ final Bundle[] sendControlRequestResults = new Bundle[1];
+ MediaRouter.ControlRequestCallback callback =
+ new MediaRouter.ControlRequestCallback() {
+ @Override
+ public void onResult(@Nullable Bundle data) {
+ sendControlRequestResults[0] = data;
+ sendControlRequestConditionVariable.open();
+ }
+ };
+ sendControlRequestConditionVariable.close();
+ getInstrumentation()
+ .runOnMainSync(() -> groupRouteInfo.sendControlRequest(new Intent(), callback));
+ sendControlRequestConditionVariable.block();
+
+ Bundle sendControlRequestResult = sendControlRequestResults[0];
+ assertEquals(
+ StubDynamicMediaRouteProviderService.SEND_CONTROL_REQUEST_RESULT,
+ sendControlRequestResult);
+ assertEquals(
+ StubDynamicMediaRouteProviderService.SEND_CONTROL_REQUEST_VALUE,
+ sendControlRequestResult.getString(
+ StubDynamicMediaRouteProviderService.SEND_CONTROL_REQUEST_KEY));
+ }
+
// Internal methods.
private Map<String, MediaRouter.RouteInfo> getCurrentRoutesAsMap() {
@@ -220,6 +473,30 @@
}
}
+ private void verifyMemberRouteState(
+ MediaRouter.GroupRouteInfo groupRoute,
+ MediaRouter.RouteInfo route,
+ boolean isSelected) {
+ assertEquals(isSelected ? SELECTED : UNSELECTED, groupRoute.getSelectionState(route));
+ assertEquals(isSelected, groupRoute.isUnselectable(route));
+ switch (route.getDescriptorId()) {
+ case ROUTE_ID_1:
+ assertEquals(ROUTE_GROUPABLE_1, groupRoute.isGroupable(route));
+ assertEquals(ROUTE_TRANSFERABLE_1, groupRoute.isTransferable(route));
+ break;
+ case ROUTE_ID_2:
+ assertEquals(ROUTE_GROUPABLE_2, groupRoute.isGroupable(route));
+ assertEquals(ROUTE_TRANSFERABLE_2, groupRoute.isTransferable(route));
+ break;
+ case ROUTE_ID_3:
+ assertEquals(ROUTE_GROUPABLE_3, groupRoute.isGroupable(route));
+ assertEquals(ROUTE_TRANSFERABLE_3, groupRoute.isTransferable(route));
+ break;
+ default:
+ // Ignore.
+ }
+ }
+
// Internal classes and interfaces.
// Equivalent to java.util.function.Supplier, except it's available before API 24.
@@ -230,11 +507,24 @@
private class MediaRouterCallbackImpl extends MediaRouter.Callback {
- private final ConditionVariable mPendingRoutesConditionVariable = new ConditionVariable();
private final Set<String> mRouteIdsPending = new HashSet<>();
- private final ConditionVariable mSelectedRouteChangeConditionVariable =
+ private final Map<String, Integer> mRouteVolumePending = new HashMap<>();
+ private final ConditionVariable mPendingRoutesConditionVariable = new ConditionVariable();
+ private final ConditionVariable mPendingRouteVolumeConditionVariable =
+ new ConditionVariable();
+ private final ConditionVariable mRouteSelectedConditionVariable =
new ConditionVariable(/* state= */ true);
- private final ConditionVariable mRouteConnectionConditionVariable =
+ private final ConditionVariable mRouteConnectedConditionVariable =
+ new ConditionVariable(/* state= */ true);
+ private final ConditionVariable mRouteDisconnectedConditionVariable =
+ new ConditionVariable(/* state= */ true);
+ private final ConditionVariable mMemberRouteAddedConditionVariable =
+ new ConditionVariable(/* state= */ true);
+ private final ConditionVariable mMemberRouteRemovedConditionVariable =
+ new ConditionVariable(/* state= */ true);
+ private final ConditionVariable mMemberRoutesUpdatedConditionVariable =
+ new ConditionVariable(/* state= */ true);
+ private final ConditionVariable mGroupVolumeChangedConditionVariable =
new ConditionVariable(/* state= */ true);
private Map<String, MediaRouter.RouteInfo> waitForRoutes(String... routeIds) {
@@ -257,26 +547,103 @@
public MediaRouter.RouteInfo selectAndWaitForOnSelected(
MediaRouter.RouteInfo routeToSelect) {
- mSelectedRouteChangeConditionVariable.close();
+ mRouteSelectedConditionVariable.close();
getInstrumentation().runOnMainSync(routeToSelect::select);
- mSelectedRouteChangeConditionVariable.block();
+ mRouteSelectedConditionVariable.block();
return runBlockingOnMainThreadWithResult(() -> mRouter.getSelectedRoute());
}
- public List<MediaRouter.RouteInfo> connectAndWaitForOnConnected(
+ public List<MediaRouter.GroupRouteInfo> connectAndWaitForOnConnected(
MediaRouter.RouteInfo routeToConnect) {
- mRouteConnectionConditionVariable.close();
+ mRouteConnectedConditionVariable.close();
getInstrumentation().runOnMainSync(routeToConnect::connect);
- mRouteConnectionConditionVariable.block();
- return runBlockingOnMainThreadWithResult(() -> mRouter.getConnectedRoutes());
+ mRouteConnectedConditionVariable.block();
+ return runBlockingOnMainThreadWithResult(() -> mRouter.getConnectedGroupRoutes());
}
- public List<MediaRouter.RouteInfo> disconnectAndWaitForOnDisconnected(
+ public List<MediaRouter.GroupRouteInfo> connectAndWaitForOnDisconnected(
+ MediaRouter.RouteInfo routeToConnect) {
+ mRouteDisconnectedConditionVariable.close();
+ getInstrumentation().runOnMainSync(routeToConnect::connect);
+ mRouteDisconnectedConditionVariable.block();
+ return runBlockingOnMainThreadWithResult(() -> mRouter.getConnectedGroupRoutes());
+ }
+
+ public List<MediaRouter.GroupRouteInfo> disconnectAndWaitForOnDisconnected(
MediaRouter.RouteInfo routeToDisconnect) {
- mRouteConnectionConditionVariable.close();
+ mRouteDisconnectedConditionVariable.close();
getInstrumentation().runOnMainSync(routeToDisconnect::disconnect);
- mRouteConnectionConditionVariable.block();
- return runBlockingOnMainThreadWithResult(() -> mRouter.getConnectedRoutes());
+ mRouteDisconnectedConditionVariable.block();
+ return runBlockingOnMainThreadWithResult(() -> mRouter.getConnectedGroupRoutes());
+ }
+
+ public List<MediaRouter.RouteInfo> addRouteToGroupAndWaitForOnChanged(
+ MediaRouter.GroupRouteInfo groupRoute, MediaRouter.RouteInfo memberRoute) {
+ mMemberRouteAddedConditionVariable.close();
+ AtomicInteger addMemberStatus = new AtomicInteger();
+ getInstrumentation()
+ .runOnMainSync(() -> addMemberStatus.set(groupRoute.addRoute(memberRoute)));
+ assertEquals(ADD_ROUTE_SUCCESSFUL, addMemberStatus.get());
+ mMemberRouteAddedConditionVariable.block();
+ return runBlockingOnMainThreadWithResult(groupRoute::getSelectedRoutesInGroup);
+ }
+
+ public List<MediaRouter.RouteInfo> removeRouteFromGroupAndWaitForOnChanged(
+ MediaRouter.GroupRouteInfo groupRoute, MediaRouter.RouteInfo memberRoute) {
+ mMemberRouteRemovedConditionVariable.close();
+ AtomicInteger removeMemberStatus = new AtomicInteger();
+ getInstrumentation()
+ .runOnMainSync(
+ () -> removeMemberStatus.set(groupRoute.removeRoute(memberRoute)));
+ assertEquals(REMOVE_ROUTE_SUCCESSFUL, removeMemberStatus.get());
+ mMemberRouteRemovedConditionVariable.block();
+ return runBlockingOnMainThreadWithResult(groupRoute::getSelectedRoutesInGroup);
+ }
+
+ public List<MediaRouter.RouteInfo> updateRoutesForGroupAndWaitForOnChanged(
+ MediaRouter.GroupRouteInfo groupRoute, List<MediaRouter.RouteInfo> memberRoutes) {
+ mMemberRoutesUpdatedConditionVariable.close();
+ AtomicInteger updateMembersStatus = new AtomicInteger();
+ getInstrumentation()
+ .runOnMainSync(
+ () -> updateMembersStatus.set(groupRoute.updateRoutes(memberRoutes)));
+ assertEquals(UPDATE_ROUTES_SUCCESSFUL, updateMembersStatus.get());
+ mMemberRoutesUpdatedConditionVariable.block();
+ return runBlockingOnMainThreadWithResult(groupRoute::getSelectedRoutesInGroup);
+ }
+
+ public int setGroupVolumeAndWaitForOnVolumeSet(
+ MediaRouter.GroupRouteInfo groupRoute, int volume) {
+ mGroupVolumeChangedConditionVariable.close();
+ getInstrumentation().runOnMainSync(() -> groupRoute.requestSetVolume(volume));
+ mGroupVolumeChangedConditionVariable.block();
+ return runBlockingOnMainThreadWithResult(groupRoute::getVolume);
+ }
+
+ public int updateGroupVolumeAndWaitForOnVolumeUpdated(
+ MediaRouter.GroupRouteInfo groupRoute, int delta) {
+ mGroupVolumeChangedConditionVariable.close();
+ getInstrumentation().runOnMainSync(() -> groupRoute.requestUpdateVolume(delta));
+ mGroupVolumeChangedConditionVariable.block();
+ return runBlockingOnMainThreadWithResult(groupRoute::getVolume);
+ }
+
+ private MediaRouter.RouteInfo waitForRouteVolume(String routeId, int expectedVolume) {
+ getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ Map<String, MediaRouter.RouteInfo> routes = getCurrentRoutesAsMap();
+ if (!routes.containsKey(routeId)
+ || routes.get(routeId).getVolume() != expectedVolume) {
+ mPendingRouteVolumeConditionVariable.close();
+ mRouteVolumePending.clear();
+ mRouteVolumePending.put(routeId, expectedVolume);
+ } else {
+ mPendingRouteVolumeConditionVariable.open();
+ }
+ });
+ mPendingRouteVolumeConditionVariable.block();
+ return getCurrentRoutesAsMap().get(routeId);
}
@Override
@@ -285,7 +652,7 @@
@NonNull MediaRouter.RouteInfo selectedRoute,
int reason,
@NonNull MediaRouter.RouteInfo requestedRoute) {
- mSelectedRouteChangeConditionVariable.open();
+ mRouteSelectedConditionVariable.open();
}
@Override
@@ -294,6 +661,43 @@
if (getCurrentRoutesAsMap().keySet().containsAll(mRouteIdsPending)) {
mPendingRoutesConditionVariable.open();
}
+ checkPendingRouteVolume(route);
+ }
+
+ @Override
+ public void onRouteChanged(
+ @NonNull MediaRouter router, @NonNull MediaRouter.RouteInfo route) {
+ mChangedRoute = route;
+ MediaRouter.GroupRouteInfo groupRoute = route.asGroup();
+ if (groupRoute != null) {
+ List<String> selectedRouteIds = new ArrayList<>();
+ for (MediaRouter.RouteInfo selectedRoute : route.getSelectedRoutesInGroup()) {
+ selectedRouteIds.add(selectedRoute.getDescriptorId());
+ }
+ if (selectedRouteIds.containsAll(EXPECTED_ROUTE_IDS_AFTER_ROUTE_ADDED)) {
+ mMemberRouteAddedConditionVariable.open();
+ } else if (selectedRouteIds.containsAll(EXPECTED_ROUTE_IDS_AFTER_ROUTE_REMOVED)) {
+ mMemberRouteRemovedConditionVariable.open();
+ } else if (selectedRouteIds.containsAll(EXPECTED_ROUTE_IDS_AFTER_ROUTE_UPDATED)) {
+ mMemberRoutesUpdatedConditionVariable.open();
+ }
+ boolean isVolumeChanged = false;
+ if (route.getVolume()
+ != StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE) {
+ isVolumeChanged = true;
+ } else {
+ for (MediaRouter.RouteInfo memberRoute : route.getSelectedRoutesInGroup()) {
+ if (memberRoute.getVolume()
+ != StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE) {
+ isVolumeChanged = true;
+ }
+ }
+ }
+ if (isVolumeChanged) {
+ mGroupVolumeChangedConditionVariable.open();
+ }
+ }
+ checkPendingRouteVolume(route);
}
@Override
@@ -304,7 +708,7 @@
mRouteConnectionState = RouteConnectionState.STATE_CONNECTED;
mConnectedRoute = connectedRoute;
mRequestedRoute = requestedRoute;
- mRouteConnectionConditionVariable.open();
+ mRouteConnectedConditionVariable.open();
}
@Override
@@ -317,7 +721,17 @@
mDisconnectedRoute = disconnectedRoute;
mRequestedRoute = requestedRoute;
mRouteDisconnectedReason = reason;
- mRouteConnectionConditionVariable.open();
+ mRouteDisconnectedConditionVariable.open();
+ }
+
+ private void checkPendingRouteVolume(MediaRouter.RouteInfo route) {
+ String routeDescriptorId = route.getDescriptorId();
+ if (routeDescriptorId != null) {
+ Integer expectedVolume = mRouteVolumePending.get(routeDescriptorId);
+ if (expectedVolume != null && route.getVolume() == expectedVolume) {
+ mPendingRouteVolumeConditionVariable.open();
+ }
+ }
}
}
}
diff --git a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/StubDynamicMediaRouteProviderService.java b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/StubDynamicMediaRouteProviderService.java
index 1cebcb0..7a6c809 100644
--- a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/StubDynamicMediaRouteProviderService.java
+++ b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/StubDynamicMediaRouteProviderService.java
@@ -16,7 +16,9 @@
package androidx.mediarouter.media;
import android.content.Context;
+import android.content.Intent;
import android.content.IntentFilter;
+import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -37,18 +39,33 @@
public static final String CATEGORY_DYNAMIC_PROVIDER_TEST =
"androidx.mediarouter.media.CATEGORY_DYNAMIC_PROVIDER_TEST";
+ public static final int VOLUME_INITIAL_VALUE = 8;
+ public static final int VOLUME_MAX = 20;
public static final String ROUTE_ID_GROUP = "route_id_group";
public static final String ROUTE_NAME_GROUP = "Group route name";
public static final String ROUTE_ID_1 = "route_id1";
public static final String ROUTE_NAME_1 = "Sample Route 1";
+ public static final boolean ROUTE_GROUPABLE_1 = true;
+ public static final boolean ROUTE_TRANSFERABLE_1 = false;
public static final String ROUTE_ID_2 = "route_id2";
public static final String ROUTE_NAME_2 = "Sample Route 2";
+ public static final boolean ROUTE_GROUPABLE_2 = true;
+ public static final boolean ROUTE_TRANSFERABLE_2 = false;
+ public static final String ROUTE_ID_3 = "route_id3";
+ public static final String ROUTE_NAME_3 = "Sample Route 3";
+ public static final boolean ROUTE_GROUPABLE_3 = false;
+ public static final boolean ROUTE_TRANSFERABLE_3 = true;
public static final List<IntentFilter> CONTROL_FILTERS_TEST = new ArrayList<>();
+ public static final String SEND_CONTROL_REQUEST_KEY = "send_control_request_key";
+ public static final String SEND_CONTROL_REQUEST_VALUE = "send_control_request_value";
+ public static final Bundle SEND_CONTROL_REQUEST_RESULT = new Bundle();
static {
IntentFilter filter = new IntentFilter();
filter.addCategory(CATEGORY_DYNAMIC_PROVIDER_TEST);
CONTROL_FILTERS_TEST.add(filter);
+
+ SEND_CONTROL_REQUEST_RESULT.putString(SEND_CONTROL_REQUEST_KEY, SEND_CONTROL_REQUEST_VALUE);
}
@Override
@@ -59,7 +76,7 @@
private static final class Provider extends MediaRouteProvider {
private final Map<String, MediaRouteDescriptor> mRoutes = new ArrayMap<>();
private final Map<String, StubRouteController> mControllers = new ArrayMap<>();
- private final MediaRouteDescriptor mGroupDescriptor;
+ private MediaRouteDescriptor mGroupDescriptor;
private final Set<String> mCurrentSelectedRouteIds = new HashSet<>();
private boolean mCurrentlyScanning = false;
@Nullable private DynamicGroupRouteController mGroupController;
@@ -69,17 +86,30 @@
mGroupDescriptor =
new MediaRouteDescriptor.Builder(ROUTE_ID_GROUP, ROUTE_NAME_GROUP)
.addControlFilters(CONTROL_FILTERS_TEST)
+ .setVolumeMax(VOLUME_MAX)
+ .setVolume(VOLUME_INITIAL_VALUE)
.build();
MediaRouteDescriptor route1 =
new MediaRouteDescriptor.Builder(ROUTE_ID_1, ROUTE_NAME_1)
.addControlFilters(CONTROL_FILTERS_TEST)
+ .setVolumeMax(VOLUME_MAX)
+ .setVolume(VOLUME_INITIAL_VALUE)
.build();
MediaRouteDescriptor route2 =
new MediaRouteDescriptor.Builder(ROUTE_ID_2, ROUTE_NAME_2)
.addControlFilters(CONTROL_FILTERS_TEST)
+ .setVolumeMax(VOLUME_MAX)
+ .setVolume(VOLUME_INITIAL_VALUE)
+ .build();
+ MediaRouteDescriptor route3 =
+ new MediaRouteDescriptor.Builder(ROUTE_ID_3, ROUTE_NAME_3)
+ .addControlFilters(CONTROL_FILTERS_TEST)
+ .setVolumeMax(VOLUME_MAX)
+ .setVolume(VOLUME_INITIAL_VALUE)
.build();
mRoutes.put(route1.getId(), route1);
mRoutes.put(route2.getId(), route2);
+ mRoutes.put(route3.getId(), route3);
}
// MediaRouteProvider implementation.
@@ -127,6 +157,7 @@
"onCreateDynamicGroupRouteController with initialMemberRouteId = "
+ initialMemberRouteId);
mGroupController = new StubDynamicRouteController();
+ mCurrentSelectedRouteIds.add(initialMemberRouteId);
return mGroupController;
}
@@ -150,12 +181,41 @@
mCurrentSelectedRouteIds.contains(route.getId())
? DynamicRouteDescriptor.SELECTED
: DynamicRouteDescriptor.UNSELECTED)
+ .setIsGroupable(getIsGroupable(route.getId()))
+ .setIsTransferable(getIsTransferable(route.getId()))
+ .setIsUnselectable(mCurrentSelectedRouteIds.contains(route.getId()))
.build();
result.add(dynamicDescriptor);
}
return result;
}
+ private boolean getIsGroupable(String routeId) {
+ switch (routeId) {
+ case ROUTE_ID_1:
+ return ROUTE_GROUPABLE_1;
+ case ROUTE_ID_2:
+ return ROUTE_GROUPABLE_2;
+ case ROUTE_ID_3:
+ return ROUTE_GROUPABLE_3;
+ default:
+ return false;
+ }
+ }
+
+ private boolean getIsTransferable(String routeId) {
+ switch (routeId) {
+ case ROUTE_ID_1:
+ return ROUTE_TRANSFERABLE_1;
+ case ROUTE_ID_2:
+ return ROUTE_TRANSFERABLE_2;
+ case ROUTE_ID_3:
+ return ROUTE_TRANSFERABLE_3;
+ default:
+ return false;
+ }
+ }
+
// Internal classes.
private class StubRouteController extends RouteController {
@@ -188,6 +248,33 @@
.build());
publishProviderState();
}
+
+ @Override
+ public void onSetVolume(int volume) {
+ MediaRouteDescriptor route = mRoutes.get(mRouteId);
+ if (route == null) {
+ return;
+ }
+ mRoutes.put(
+ mRouteId,
+ new MediaRouteDescriptor.Builder(route).setVolume(volume).build());
+ publishProviderState();
+ }
+
+ @Override
+ public void onUpdateVolume(int delta) {
+ MediaRouteDescriptor route = mRoutes.get(mRouteId);
+ if (route == null) {
+ return;
+ }
+ int currentVolume = route.getVolume();
+ mRoutes.put(
+ mRouteId,
+ new MediaRouteDescriptor.Builder(route)
+ .setVolume(currentVolume + delta)
+ .build());
+ publishProviderState();
+ }
}
private class StubDynamicRouteController extends DynamicGroupRouteController {
@@ -200,6 +287,9 @@
@Override
public void onUpdateMemberRoutes(@Nullable List<String> routeIds) {
+ if (routeIds == null) {
+ return;
+ }
Log.i(TAG, "StubDynamicRouteController.onUpdateMemberRoutes()");
mCurrentSelectedRouteIds.clear();
mCurrentSelectedRouteIds.addAll(routeIds);
@@ -222,9 +312,41 @@
publishState();
}
+ @Override
+ public boolean onControlRequest(
+ @NonNull Intent intent, @Nullable MediaRouter.ControlRequestCallback callback) {
+ if (callback != null) {
+ callback.onResult(SEND_CONTROL_REQUEST_RESULT);
+ }
+ return true;
+ }
+
+ @Override
+ public void onSetVolume(int volume) {
+ mGroupDescriptor =
+ new MediaRouteDescriptor.Builder(mGroupDescriptor)
+ .setVolume(volume)
+ .build();
+ publishState();
+ }
+
+ @Override
+ public void onUpdateVolume(int delta) {
+ int currentVolume = mGroupDescriptor.getVolume();
+ mGroupDescriptor =
+ new MediaRouteDescriptor.Builder(mGroupDescriptor)
+ .setVolume(currentVolume + delta)
+ .build();
+ publishState();
+ }
+
private void publishState() {
- Log.i(TAG, "StubDynamicRouteController.publishState()");
- notifyDynamicRoutesChanged(mGroupDescriptor, buildDynamicRouteDescriptors());
+ Collection<DynamicRouteDescriptor> dynamicRoutes = buildDynamicRouteDescriptors();
+ Log.i(
+ TAG,
+ "StubDynamicRouteController.publishState() with dynamicRoutes.size() = "
+ + dynamicRoutes.size());
+ notifyDynamicRoutesChanged(mGroupDescriptor, dynamicRoutes);
}
}
}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteControllerDialog.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteControllerDialog.java
index aa1bf82..30172fb 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteControllerDialog.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteControllerDialog.java
@@ -232,7 +232,7 @@
}
private boolean isGroup() {
- return mRoute.isGroup() && mRoute.getRoutesInGroup().size() > 1;
+ return mRoute.isGroup() && mRoute.getSelectedRoutesInGroup().size() > 1;
}
/**
@@ -625,7 +625,9 @@
int volumeGroupListCount = mGroupMemberRoutes.size();
// Scale down volume group list items in landscape mode.
int expandedGroupListHeight =
- isGroup() ? mVolumeGroupListItemHeight * mRoute.getRoutesInGroup().size() : 0;
+ isGroup()
+ ? mVolumeGroupListItemHeight * mRoute.getSelectedRoutesInGroup().size()
+ : 0;
if (volumeGroupListCount > 0) {
expandedGroupListHeight += mVolumeGroupListPaddingTop;
}
@@ -745,7 +747,7 @@
}
private void rebuildVolumeGroupList(boolean animate) {
- List<MediaRouter.RouteInfo> routes = mRoute.getRoutesInGroup();
+ List<MediaRouter.RouteInfo> routes = mRoute.getSelectedRoutesInGroup();
if (routes.isEmpty()) {
mGroupMemberRoutes.clear();
mVolumeGroupAdapter.notifyDataSetChanged();
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteDynamicControllerDialog.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteDynamicControllerDialog.java
index 46260d7..697f70f 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteDynamicControllerDialog.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteDynamicControllerDialog.java
@@ -574,11 +574,12 @@
@SuppressWarnings("WeakerAccess") /* synthetic access */
List<MediaRouter.RouteInfo> getCurrentGroupableRoutes() {
List<MediaRouter.RouteInfo> groupableRoutes = new ArrayList<>();
- for (MediaRouter.RouteInfo route : mSelectedRoute.getProvider().getRoutes()) {
- MediaRouter.RouteInfo.DynamicGroupState state =
- mSelectedRoute.getDynamicGroupState(route);
- if (state != null && state.isGroupable()) {
- groupableRoutes.add(route);
+ MediaRouter.GroupRouteInfo groupRouteInfo = mSelectedRoute.asGroup();
+ if (groupRouteInfo != null) {
+ for (MediaRouter.RouteInfo route : mSelectedRoute.getProvider().getRoutes()) {
+ if (groupRouteInfo.isGroupable(route)) {
+ groupableRoutes.add(route);
+ }
}
}
return groupableRoutes;
@@ -622,17 +623,16 @@
mGroupableRoutes.clear();
mTransferableRoutes.clear();
- mMemberRoutes.addAll(mSelectedRoute.getRoutesInGroup());
- for (MediaRouter.RouteInfo route : mSelectedRoute.getProvider().getRoutes()) {
- MediaRouter.RouteInfo.DynamicGroupState state =
- mSelectedRoute.getDynamicGroupState(route);
- if (state == null) continue;
-
- if (state.isGroupable()) {
- mGroupableRoutes.add(route);
- }
- if (state.isTransferable()) {
- mTransferableRoutes.add(route);
+ mMemberRoutes.addAll(mSelectedRoute.getSelectedRoutesInGroup());
+ MediaRouter.GroupRouteInfo groupRouteInfo = mSelectedRoute.asGroup();
+ if (groupRouteInfo != null) {
+ for (MediaRouter.RouteInfo route : mSelectedRoute.getProvider().getRoutes()) {
+ if (groupRouteInfo.isGroupable(route)) {
+ mGroupableRoutes.add(route);
+ }
+ if (groupRouteInfo.isTransferable(route)) {
+ mTransferableRoutes.add(route);
+ }
}
}
@@ -784,7 +784,7 @@
}
boolean isGroupVolumeNeeded() {
- return mEnableGroupVolumeUX && mSelectedRoute.getRoutesInGroup().size() > 1;
+ return mEnableGroupVolumeUX && mSelectedRoute.getSelectedRoutesInGroup().size() > 1;
}
void animateLayoutHeight(final View view, int targetHeight) {
@@ -821,12 +821,12 @@
}
void mayUpdateGroupVolume(MediaRouter.RouteInfo route, boolean selected) {
- List<MediaRouter.RouteInfo> members = mSelectedRoute.getRoutesInGroup();
+ List<MediaRouter.RouteInfo> members = mSelectedRoute.getSelectedRoutesInGroup();
// Assume we have at least one member route(itself)
int memberCount = Math.max(1, members.size());
if (route.isGroup()) {
- for (MediaRouter.RouteInfo changedRoute : route.getRoutesInGroup()) {
+ for (MediaRouter.RouteInfo changedRoute : route.getSelectedRoutesInGroup()) {
if (members.contains(changedRoute) != selected) {
memberCount += selected ? 1 : -1;
}
@@ -1126,15 +1126,16 @@
boolean isGroup = mRoute.isGroup();
if (selected) {
- mRouter.addMemberToDynamicGroup(mRoute);
+ mRouter.addRouteToSelectedGroup(mRoute);
} else {
- mRouter.removeMemberFromDynamicGroup(mRoute);
+ mRouter.removeRouteFromSelectedGroup(mRoute);
}
showSelectingProgress(selected, !isGroup);
if (isGroup) {
List<MediaRouter.RouteInfo> selectedRoutes =
- mSelectedRoute.getRoutesInGroup();
- for (MediaRouter.RouteInfo route : mRoute.getRoutesInGroup()) {
+ mSelectedRoute.getSelectedRoutesInGroup();
+ for (MediaRouter.RouteInfo route :
+ mRoute.getSelectedRoutesInGroup()) {
if (selectedRoutes.contains(route) != selected) {
MediaRouteVolumeSliderHolder volumeSliderHolder =
mVolumeSliderHolderMap.get(route.getId());
@@ -1177,11 +1178,11 @@
if (route.isSelected()) {
return true;
}
- MediaRouter.RouteInfo.DynamicGroupState state =
- mSelectedRoute.getDynamicGroupState(route);
- return state != null && state.getSelectionState()
- == MediaRouteProvider.DynamicGroupRouteController
- .DynamicRouteDescriptor.SELECTED;
+ MediaRouter.GroupRouteInfo groupRouteInfo = mSelectedRoute.asGroup();
+ return groupRouteInfo != null
+ && groupRouteInfo.getSelectionState(route)
+ == MediaRouteProvider.DynamicGroupRouteController
+ .DynamicRouteDescriptor.SELECTED;
}
private boolean isEnabled(MediaRouter.RouteInfo route) {
@@ -1190,14 +1191,13 @@
return false;
}
// The last member route can not be removed.
- if (isSelected(route) && mSelectedRoute.getRoutesInGroup().size() < 2) {
+ if (isSelected(route) && mSelectedRoute.getSelectedRoutesInGroup().size() < 2) {
return false;
}
// Selected route that can't be unselected has to be disabled.
if (isSelected(route)) {
- MediaRouter.RouteInfo.DynamicGroupState state =
- mSelectedRoute.getDynamicGroupState(route);
- return state != null && state.isUnselectable();
+ MediaRouter.GroupRouteInfo groupRouteInfo = mSelectedRoute.asGroup();
+ return groupRouteInfo != null && groupRouteInfo.isUnselectable(route);
}
return true;
}
@@ -1206,8 +1206,8 @@
MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) item.getData();
// This is required to sync volume and the name of the route
- if (route == mSelectedRoute && route.getRoutesInGroup().size() > 0) {
- for (MediaRouter.RouteInfo memberRoute : route.getRoutesInGroup()) {
+ if (route == mSelectedRoute && route.getSelectedRoutesInGroup().size() > 0) {
+ for (MediaRouter.RouteInfo memberRoute : route.getSelectedRoutesInGroup()) {
if (!mGroupableRoutes.contains(memberRoute)) {
route = memberRoute;
break;
@@ -1283,7 +1283,8 @@
}
private boolean isEnabled(MediaRouter.RouteInfo route) {
- List<MediaRouter.RouteInfo> currentMemberRoutes = mSelectedRoute.getRoutesInGroup();
+ List<MediaRouter.RouteInfo> currentMemberRoutes =
+ mSelectedRoute.getSelectedRoutesInGroup();
// Disable individual route if the only member of dynamic group is that route.
if (currentMemberRoutes.size() == 1 && currentMemberRoutes.get(0) == route) {
return false;
@@ -1357,14 +1358,15 @@
boolean shouldRefreshRoute = false;
if (route == mSelectedRoute && route.getDynamicGroupController() != null) {
for (MediaRouter.RouteInfo memberRoute : route.getProvider().getRoutes()) {
- if (mSelectedRoute.getRoutesInGroup().contains(memberRoute)) {
+ if (mSelectedRoute.getSelectedRoutesInGroup().contains(memberRoute)) {
continue;
}
- MediaRouter.RouteInfo.DynamicGroupState state =
- mSelectedRoute.getDynamicGroupState(memberRoute);
-
+ MediaRouter.GroupRouteInfo groupRouteInfo = mSelectedRoute.asGroup();
+ if (groupRouteInfo == null) {
+ continue;
+ }
// Refresh items only when a new groupable route is found.
- if (state != null && state.isGroupable()
+ if (groupRouteInfo.isGroupable(memberRoute)
&& !mGroupableRoutes.contains(memberRoute)) {
shouldRefreshRoute = true;
break;
@@ -1381,8 +1383,8 @@
}
@Override
- public void onRouteVolumeChanged(@NonNull MediaRouter router,
- @NonNull MediaRouter.RouteInfo route) {
+ public void onRouteVolumeChanged(
+ @NonNull MediaRouter router, @NonNull MediaRouter.RouteInfo route) {
int volume = route.getVolume();
if (DEBUG) {
Log.d(TAG, "onRouteVolumeChanged(), route.getVolume:" + volume);
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
index 9463a7a..c7ea94e 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
@@ -21,6 +21,21 @@
import static androidx.mediarouter.media.MediaRouter.CALLBACK_FLAG_FORCE_DISCOVERY;
import static androidx.mediarouter.media.MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN;
import static androidx.mediarouter.media.MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.ADD_ROUTE_FAILED_REASON_ALREADY_IN_GROUP;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.ADD_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.ADD_ROUTE_FAILED_REASON_NOT_GROUPABLE;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.ADD_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.ADD_ROUTE_SUCCESSFUL;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.REMOVE_ROUTE_FAILED_REASON_LAST_ROUTE_IN_GROUP;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.REMOVE_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.REMOVE_ROUTE_FAILED_REASON_NOT_IN_GROUP;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.REMOVE_ROUTE_FAILED_REASON_NOT_UNSELECTABLE;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.REMOVE_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.REMOVE_ROUTE_SUCCESSFUL;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.UPDATE_ROUTES_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.UPDATE_ROUTES_FAILED_REASON_NOT_TRANSFERABLE;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.UPDATE_ROUTES_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.UPDATE_ROUTES_SUCCESSFUL;
import static androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_DISCONNECTED;
import static androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_ROUTE_CHANGED;
import static androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_STOPPED;
@@ -218,10 +233,9 @@
MediaRouter.RouteInfo route,
Intent intent,
MediaRouter.ControlRequestCallback callback) {
- if (route == mSelectedRoute && mSelectedRouteController != null) {
- if (mSelectedRouteController.onControlRequest(intent, callback)) {
- return;
- }
+ MediaRouteProvider.RouteController controller = getRouteController(route);
+ if (controller != null && controller.onControlRequest(intent, callback)) {
+ return;
}
if (mTransferNotifier != null
&& route == mTransferNotifier.mToRoute
@@ -236,27 +250,42 @@
}
/* package */ void requestSetVolume(MediaRouter.RouteInfo route, int volume) {
- if (route == mSelectedRoute && mSelectedRouteController != null) {
- mSelectedRouteController.onSetVolume(volume);
- } else {
- MediaRouteProvider.RouteController controller =
- mRouteControllerMap.get(route.mUniqueId);
- if (controller != null) {
- controller.onSetVolume(volume);
- }
+ MediaRouteProvider.RouteController controller = getRouteController(route);
+ if (controller != null) {
+ controller.onSetVolume(volume);
}
}
/* package */ void requestUpdateVolume(MediaRouter.RouteInfo route, int delta) {
+ MediaRouteProvider.RouteController controller = getRouteController(route);
+ if (controller != null) {
+ controller.onUpdateVolume(delta);
+ }
+ }
+
+ @Nullable
+ private MediaRouteProvider.RouteController getRouteController(MediaRouter.RouteInfo route) {
if (route == mSelectedRoute && mSelectedRouteController != null) {
- mSelectedRouteController.onUpdateVolume(delta);
- } else {
- MediaRouteProvider.RouteController controller =
- mRouteControllerMap.get(route.mUniqueId);
- if (controller != null) {
- controller.onUpdateVolume(delta);
+ return mSelectedRouteController;
+ }
+ if (route instanceof MediaRouter.GroupRouteInfo) {
+ MediaRouter.GroupRouteInfo groupRoute = (MediaRouter.GroupRouteInfo) route;
+ if (groupRoute.isConnected()) {
+ RouteConnection routeConnection = getRouteConnection(groupRoute);
+ return (routeConnection != null) ? routeConnection.mController : null;
}
}
+ MediaRouteProvider.RouteController controller = mRouteControllerMap.get(route.mUniqueId);
+ if (controller != null) {
+ return controller;
+ }
+ for (RouteConnection routeConnection : mRouteIdToRouteConnectionMap.values()) {
+ controller = routeConnection.mRouteIdToMemberControllerMap.get(route.mUniqueId);
+ if (controller != null) {
+ break;
+ }
+ }
+ return controller;
}
/* package */ MediaRouter.RouteInfo getRoute(String uniqueId) {
@@ -360,67 +389,183 @@
}
@NonNull
- /* package */ List<MediaRouter.RouteInfo> getConnectedRoutes() {
- List<MediaRouter.RouteInfo> connectedRoutes = new ArrayList<>();
+ /* package */ List<MediaRouter.GroupRouteInfo> getConnectedGroupRoutes() {
+ List<MediaRouter.GroupRouteInfo> connectedGroupRoutes = new ArrayList<>();
for (RouteConnection routeConnection : mRouteIdToRouteConnectionMap.values()) {
if (routeConnection.mGroupRoute != null) {
- connectedRoutes.add(routeConnection.mGroupRoute);
+ connectedGroupRoutes.add(routeConnection.mGroupRoute);
}
}
- return connectedRoutes;
+ return connectedGroupRoutes;
}
@Nullable
- /* package */ MediaRouter.RouteInfo.DynamicGroupState getDynamicGroupState(
- MediaRouter.RouteInfo route) {
- return mSelectedRoute.getDynamicGroupState(route);
+ private RouteConnection getRouteConnection(@NonNull MediaRouter.GroupRouteInfo groupRoute) {
+ for (RouteConnection routeConnection : mRouteIdToRouteConnectionMap.values()) {
+ if (routeConnection.mGroupRoute == groupRoute) {
+ return routeConnection;
+ }
+ }
+ return null;
}
- /* package */ void addMemberToDynamicGroup(@NonNull MediaRouter.RouteInfo route) {
- if (!(mSelectedRouteController instanceof MediaRouteProvider.DynamicGroupRouteController)) {
- throw new IllegalStateException("There is no currently selected dynamic group route.");
- }
- MediaRouter.RouteInfo.DynamicGroupState state = getDynamicGroupState(route);
- if (mSelectedRoute.getRoutesInGroup().contains(route)
- || state == null
- || !state.isGroupable()) {
- Log.w(TAG, "Ignoring attempt to add a non-groupable route to dynamic group : " + route);
+ /* package */ void addRouteToSelectedGroup(@NonNull MediaRouter.RouteInfo route) {
+ MediaRouter.GroupRouteInfo selectedGroupRoute = mSelectedRoute.asGroup();
+ if (selectedGroupRoute == null) {
+ Log.w(TAG, "Ignoring attempt to add a member route to a selected non-group route");
return;
}
- ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController)
- .onAddMemberRoute(route.getDescriptorId());
+ addRouteToGroup(selectedGroupRoute, route);
}
- /* package */ void removeMemberFromDynamicGroup(@NonNull MediaRouter.RouteInfo route) {
- if (!(mSelectedRouteController instanceof MediaRouteProvider.DynamicGroupRouteController)) {
- throw new IllegalStateException("There is no currently selected dynamic group route.");
- }
- MediaRouter.RouteInfo.DynamicGroupState state = getDynamicGroupState(route);
- if (!mSelectedRoute.getRoutesInGroup().contains(route)
- || state == null
- || !state.isUnselectable()) {
- Log.w(TAG, "Ignoring attempt to remove a non-unselectable member route : " + route);
+ /* package */ void removeRouteFromSelectedGroup(@NonNull MediaRouter.RouteInfo route) {
+ MediaRouter.GroupRouteInfo selectedGroupRoute = mSelectedRoute.asGroup();
+ if (selectedGroupRoute == null) {
+ Log.w(TAG, "Ignoring attempt to remove a member route from a selected non-group route");
return;
}
- if (mSelectedRoute.getRoutesInGroup().size() <= 1) {
- Log.w(TAG, "Ignoring attempt to remove the last member route.");
- return;
- }
- ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController)
- .onRemoveMemberRoute(route.getDescriptorId());
+ removeRouteFromGroup(selectedGroupRoute, route);
}
/* package */ void transferToRoute(@NonNull MediaRouter.RouteInfo route) {
- if (!(mSelectedRouteController instanceof MediaRouteProvider.DynamicGroupRouteController)) {
- throw new IllegalStateException("There is no currently selected dynamic group route.");
- }
- MediaRouter.RouteInfo.DynamicGroupState state = getDynamicGroupState(route);
- if (state == null || !state.isTransferable()) {
- Log.w(TAG, "Ignoring attempt to transfer to a non-transferable route.");
+ MediaRouter.GroupRouteInfo selectedGroupRoute = mSelectedRoute.asGroup();
+ if (selectedGroupRoute == null) {
+ Log.w(TAG, "Ignoring attempt to transfer for a selected non-group route");
return;
}
- ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController)
- .onUpdateMemberRoutes(Collections.singletonList(route.getDescriptorId()));
+ updateRoutesForGroup(selectedGroupRoute, Collections.singletonList(route));
+ }
+
+ @MediaRouter.GroupRouteInfo.AddRouteReason
+ /* package */ int addRouteToGroup(
+ @NonNull MediaRouter.GroupRouteInfo groupRoute,
+ @NonNull MediaRouter.RouteInfo memberRoute) {
+ if (!groupRoute.isGroupable(memberRoute)) {
+ Log.w(TAG, "Ignoring attempt to add a non-groupable member route: " + memberRoute);
+ return ADD_ROUTE_FAILED_REASON_NOT_GROUPABLE;
+ }
+ if (groupRoute.getSelectedRoutesInGroup().contains(memberRoute)) {
+ Log.w(TAG, "Ignoring attempt to add an existing member route: " + memberRoute);
+ return ADD_ROUTE_FAILED_REASON_ALREADY_IN_GROUP;
+ }
+ if (groupRoute.isSelected()) {
+ if (!(mSelectedRouteController
+ instanceof MediaRouteProvider.DynamicGroupRouteController)) {
+ throw new IllegalStateException(
+ "There is no currently selected dynamic group route.");
+ }
+ ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController)
+ .onAddMemberRoute(memberRoute.getDescriptorId());
+ } else if (groupRoute.isConnected()) {
+ RouteConnection routeConnection = getRouteConnection(groupRoute);
+ if (routeConnection == null) {
+ Log.w(
+ TAG,
+ "Ignoring attempt to add a route to a non-available connected route: "
+ + groupRoute);
+ return ADD_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION;
+ }
+ routeConnection.mController.onAddMemberRoute(memberRoute.getDescriptorId());
+ } else {
+ Log.w(
+ TAG,
+ "Ignoring attempt to add a route to an unsupported group route:" + groupRoute);
+ return ADD_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE;
+ }
+ return ADD_ROUTE_SUCCESSFUL;
+ }
+
+ @MediaRouter.GroupRouteInfo.RemoveRouteReason
+ /* package */ int removeRouteFromGroup(
+ @NonNull MediaRouter.GroupRouteInfo groupRoute,
+ @NonNull MediaRouter.RouteInfo memberRoute) {
+ if (!groupRoute.isUnselectable(memberRoute)) {
+ Log.w(
+ TAG,
+ "Ignoring attempt to remove a non-unselectable member route: " + memberRoute);
+ return REMOVE_ROUTE_FAILED_REASON_NOT_UNSELECTABLE;
+ }
+ if (!groupRoute.getSelectedRoutesInGroup().contains(memberRoute)) {
+ Log.w(TAG, "Ignoring attempt to remove a non-in-group member route: " + memberRoute);
+ return REMOVE_ROUTE_FAILED_REASON_NOT_IN_GROUP;
+ }
+ if (groupRoute.getSelectedRoutesInGroup().size() <= 1) {
+ Log.w(TAG, "Ignoring attempt to remove the last member route.");
+ return REMOVE_ROUTE_FAILED_REASON_LAST_ROUTE_IN_GROUP;
+ }
+ if (groupRoute.isSelected()) {
+ if (!(mSelectedRouteController
+ instanceof MediaRouteProvider.DynamicGroupRouteController)) {
+ throw new IllegalStateException(
+ "There is no currently selected dynamic group route.");
+ }
+ ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController)
+ .onRemoveMemberRoute(memberRoute.getDescriptorId());
+ } else if (groupRoute.isConnected()) {
+ RouteConnection routeConnection = getRouteConnection(groupRoute);
+ if (routeConnection == null) {
+ Log.w(
+ TAG,
+ "Ignoring attempt to update routes for a non-available connected route: "
+ + groupRoute);
+ return REMOVE_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION;
+ }
+ routeConnection.mController.onRemoveMemberRoute(memberRoute.getDescriptorId());
+ } else {
+ Log.w(
+ TAG,
+ "Ignoring attempt to remove a route from an unsupported group route:"
+ + groupRoute);
+ return REMOVE_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE;
+ }
+ return REMOVE_ROUTE_SUCCESSFUL;
+ }
+
+ @MediaRouter.GroupRouteInfo.UpdateRoutesReason
+ /* package */ int updateRoutesForGroup(
+ @NonNull MediaRouter.GroupRouteInfo groupRoute,
+ @NonNull List<MediaRouter.RouteInfo> memberRoutes) {
+ List<String> memberRouteDescriptorIds = new ArrayList<>();
+ for (MediaRouter.RouteInfo route : memberRoutes) {
+ if (!groupRoute.isTransferable(route)) {
+ Log.w(
+ TAG,
+ "Ignoring attempt to update the group with a non-transferable route: "
+ + route);
+ continue;
+ }
+ memberRouteDescriptorIds.add(route.getDescriptorId());
+ }
+ if (memberRouteDescriptorIds.isEmpty()) {
+ Log.w(TAG, "Ignoring attempt to update the group with non-transferable routes");
+ return UPDATE_ROUTES_FAILED_REASON_NOT_TRANSFERABLE;
+ }
+ if (groupRoute.isSelected()) {
+ if (!(mSelectedRouteController
+ instanceof MediaRouteProvider.DynamicGroupRouteController)) {
+ throw new IllegalStateException(
+ "There is no currently selected dynamic group route.");
+ }
+ ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController)
+ .onUpdateMemberRoutes(memberRouteDescriptorIds);
+ } else if (groupRoute.isConnected()) {
+ RouteConnection routeConnection = getRouteConnection(groupRoute);
+ if (routeConnection == null) {
+ Log.w(
+ TAG,
+ "Ignoring attempt to update routes for a non-available connected route: "
+ + groupRoute);
+ return UPDATE_ROUTES_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION;
+ }
+ routeConnection.mController.onUpdateMemberRoutes(memberRouteDescriptorIds);
+ } else {
+ Log.w(
+ TAG,
+ "Ignoring attempt to update routes for an unsupported group route:"
+ + groupRoute);
+ return UPDATE_ROUTES_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE;
+ }
+ return UPDATE_ROUTES_SUCCESSFUL;
}
/**
@@ -1189,7 +1334,7 @@
if (!mSelectedRoute.isGroup()) {
return;
}
- List<MediaRouter.RouteInfo> routes = mSelectedRoute.getRoutesInGroup();
+ List<MediaRouter.RouteInfo> routes = mSelectedRoute.getSelectedRoutesInGroup();
// Build a set of descriptor IDs for the new route group.
Set<String> idSet = new HashSet<>();
for (MediaRouter.RouteInfo route : routes) {
@@ -1283,8 +1428,8 @@
String groupId = groupRouteDescriptor.getId();
String uniqueId = assignRouteUniqueId(provider, groupId);
- MediaRouter.RouteInfo route =
- new MediaRouter.RouteInfo(provider, groupId, uniqueId);
+ MediaRouter.GroupRouteInfo route =
+ new MediaRouter.GroupRouteInfo(provider, groupId, uniqueId);
route.maybeUpdateDescriptor(groupRouteDescriptor);
if (mSelectedRoute == route) {
@@ -1307,7 +1452,11 @@
updateRouteDescriptorAndNotify(
mSelectedRoute, groupRouteDescriptor);
}
- mSelectedRoute.updateDynamicDescriptors(routes);
+ MediaRouter.GroupRouteInfo groupRouteInfo =
+ mSelectedRoute.asGroup();
+ if (groupRouteInfo != null) {
+ groupRouteInfo.updateDynamicDescriptors(routes);
+ }
}
}
};
@@ -1509,18 +1658,20 @@
private final MediaRouter.RouteInfo mRequestedRoute;
private final MediaRouteProvider.DynamicGroupRouteController mController;
+ private final Map<String, MediaRouteProvider.RouteController> mRouteIdToMemberControllerMap;
private final Handler mHandler;
private final Runnable mRouteConnectionTimeoutRunnable;
// Holds the {@link MediaRouter.RouteInfo} of the route that corresponds to the dynamic
// group created as the result of connecting to {@link mRequestedRoute}. or null if the
// dynamic group hasn't been created by the provider yet.
- @Nullable private MediaRouter.RouteInfo mGroupRoute;
+ @Nullable private MediaRouter.GroupRouteInfo mGroupRoute;
/* package */ RouteConnection(
MediaRouter.RouteInfo requestedRoute,
MediaRouteProvider.DynamicGroupRouteController controller) {
mRequestedRoute = requestedRoute;
mController = controller;
+ mRouteIdToMemberControllerMap = new HashMap<>();
mHandler = new Handler(Looper.getMainLooper());
mRouteConnectionTimeoutRunnable = this::routeConnectionTimeout;
}
@@ -1550,7 +1701,9 @@
if (mController != controller) {
return;
}
- if (groupRouteDescriptor == null && mGroupRoute == null) {
+ MediaRouteDescriptor updatedGroupRouteDescriptor =
+ updateGroupMemberIdsIfNeeded(groupRouteDescriptor, routes);
+ if (updatedGroupRouteDescriptor == null && mGroupRoute == null) {
// The provider has not yet set a group route descriptor, which is needed to
// establish the connection. We need to wait for a group route descriptor.
Log.e(
@@ -1559,24 +1712,107 @@
+ mRequestedRoute);
return;
}
+
if (mGroupRoute == null) {
- // groupRouteDescriptor cannot be null.
- mGroupRoute = convertFromRouteDescriptorToRouteInfo(groupRouteDescriptor);
+ // updatedGroupRouteDescriptor cannot be null.
+ mGroupRoute = convertFromRouteDescriptorToRouteInfo(updatedGroupRouteDescriptor);
+ mGroupRoute.updateDynamicDescriptors(routes);
notifyRouteConnected();
} else {
// mGroupRoute cannot be null.
+ updateRouteDescriptorAndNotify(mGroupRoute, updatedGroupRouteDescriptor);
mGroupRoute.updateDynamicDescriptors(routes);
- updateRouteDescriptorAndNotify(mGroupRoute, groupRouteDescriptor);
+ }
+ updateMemberRouteControllers();
+ }
+
+ @Nullable
+ private MediaRouteDescriptor updateGroupMemberIdsIfNeeded(
+ @Nullable MediaRouteDescriptor groupRouteDescriptor,
+ @NonNull
+ Collection<
+ MediaRouteProvider.DynamicGroupRouteController
+ .DynamicRouteDescriptor>
+ routes) {
+ if (groupRouteDescriptor == null) {
+ return groupRouteDescriptor;
+ }
+ boolean memberRoutesMatched =
+ groupRouteDescriptor.getGroupMemberIds().size() == routes.size();
+ if (memberRoutesMatched) {
+ for (MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor route :
+ routes) {
+ String routeDescriptorId = route.getRouteDescriptor().getId();
+ if (!groupRouteDescriptor.getGroupMemberIds().contains(routeDescriptorId)) {
+ memberRoutesMatched = false;
+ break;
+ }
+ }
+ }
+ if (memberRoutesMatched) {
+ return groupRouteDescriptor;
+ }
+ List<String> groupMemberIds = new ArrayList<>();
+ for (MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor route :
+ routes) {
+ groupMemberIds.add(route.getRouteDescriptor().getId());
+ }
+ return new MediaRouteDescriptor.Builder(groupRouteDescriptor)
+ .clearGroupMemberIds()
+ .addGroupMemberIds(groupMemberIds)
+ .build();
+ }
+
+ private void updateMemberRouteControllers() {
+ if (mGroupRoute == null) {
+ return;
+ }
+ Set<String> routeIdsToRemove = new HashSet<>(mRouteIdToMemberControllerMap.keySet());
+ for (MediaRouter.RouteInfo route : mGroupRoute.mSelectedRoutesInGroup) {
+ routeIdsToRemove.remove(route.mUniqueId);
+ if (!mRouteIdToMemberControllerMap.containsKey(route.mUniqueId)) {
+ createAndConnectMemberRouteController(route);
+ }
+ }
+
+ for (String routeId : routeIdsToRemove) {
+ disconnectAndRemoveMemberRouteController(routeId);
}
}
- private MediaRouter.RouteInfo convertFromRouteDescriptorToRouteInfo(
+ private void createAndConnectMemberRouteController(MediaRouter.RouteInfo route) {
+ if (mRouteIdToMemberControllerMap.containsKey(route.mUniqueId) || mGroupRoute == null) {
+ return;
+ }
+ MediaRouteProvider.RouteController routeController =
+ mRequestedRoute
+ .getProviderInstance()
+ .onCreateRouteController(
+ route.getDescriptorId(), mGroupRoute.getDescriptorId());
+ if (routeController != null) {
+ mRouteIdToMemberControllerMap.put(route.mUniqueId, routeController);
+ routeController.onSelect();
+ }
+ }
+
+ private void disconnectAndRemoveMemberRouteController(String routeId) {
+ MediaRouteProvider.RouteController routeController =
+ mRouteIdToMemberControllerMap.get(routeId);
+ if (routeController == null) {
+ return;
+ }
+ routeController.onUnselect(UNSELECT_REASON_DISCONNECTED);
+ routeController.onRelease();
+ mRouteIdToMemberControllerMap.remove(routeId);
+ }
+
+ private MediaRouter.GroupRouteInfo convertFromRouteDescriptorToRouteInfo(
MediaRouteDescriptor routeDescriptor) {
MediaRouter.ProviderInfo provider = mRequestedRoute.getProvider();
String descriptorId = routeDescriptor.getId();
String uniqueId = assignRouteUniqueId(provider, descriptorId);
- MediaRouter.RouteInfo routeInfo =
- new MediaRouter.RouteInfo(provider, descriptorId, uniqueId);
+ MediaRouter.GroupRouteInfo routeInfo =
+ new MediaRouter.GroupRouteInfo(provider, descriptorId, uniqueId);
routeInfo.maybeUpdateDescriptor(routeDescriptor);
return routeInfo;
}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java
index 174169b..931c855 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java
@@ -682,21 +682,16 @@
static final String KEY_IS_GROUPABLE = "isGroupable";
static final String KEY_IS_TRANSFERABLE = "isTransferable";
- /**
- */
+ /** */
@RestrictTo(LIBRARY)
- @IntDef({
- UNSELECTING,
- UNSELECTED,
- SELECTING,
- SELECTED
- })
+ @IntDef({UNSELECTING, UNSELECTED, SELECTING, SELECTED, NOT_IN_GROUP})
@Retention(RetentionPolicy.SOURCE)
public @interface SelectionState {}
+
/**
* After a user unselects a route, it might take some time for a provider to complete
- * the operation. This state is used in this between time. MediaRouter can either
- * block the UI or show the route as unchecked.
+ * the operation. This state is used in this between time. MediaRouter can either block
+ * the UI or show the route as unchecked.
*/
public static final int UNSELECTING = 0;
@@ -723,6 +718,17 @@
*/
public static final int SELECTED = 3;
+ /**
+ * The route is not in a dynamic group.
+ *
+ * <p>The NOT_IN_GROUP selection state is different from the UNSELECTED state. The
+ * former represents a route that is not in a dynamic group. The latter represents an
+ * unselected route which could be selected to be part of the dynamic group.
+ *
+ * @see MediaRouter.GroupRouteInfo#getSelectionState(MediaRouter.RouteInfo)
+ */
+ public static final int NOT_IN_GROUP = 4;
+
//TODO: mMediaRouteDescriptor could have an old info. We should provide a way to
// update it or use only the route ID.
final MediaRouteDescriptor mMediaRouteDescriptor;
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
index 2bdace0..af4bd6d 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
@@ -144,28 +144,28 @@
/**
* The route connection is disconnected by {@link RouteInfo#disconnect()}.
*
- * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, int)
+ * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
*/
public static final int REASON_DISCONNECTED = 1;
/**
* The route connection has failed because the requested route is no longer available.
*
- * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, int)
+ * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
*/
public static final int REASON_ROUTE_NOT_AVAILABLE = 2;
/**
* The route connection has failed because the requested route is not enabled.
*
- * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, int)
+ * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
*/
public static final int REASON_ROUTE_NOT_ENABLED = 3;
/**
* The route connection has failed because the requested route is a selected route.
*
- * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, int)
+ * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
*/
public static final int REASON_REJECTED_FOR_SELECTED_ROUTE = 4;
@@ -173,7 +173,7 @@
* The route connection has failed because the provider for the requested route doesn't support
* dynamic groups.
*
- * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, int)
+ * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
*/
public static final int REASON_UNSUPPORTED_FOR_NON_DYNAMIC_CONTROLLER = 5;
@@ -181,14 +181,14 @@
* The route connection has failed because the provider for the requested route failed to create
* a dynamic group route controller.
*
- * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, int)
+ * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
*/
public static final int REASON_FAILED_TO_CREATE_DYNAMIC_GROUP_ROUTE_CONTROLLER = 6;
/**
* The route connection has failed due to a timeout.
*
- * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, int)
+ * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
*/
public static final int REASON_ROUTE_CONNECTION_TIMEOUT = 7;
@@ -501,9 +501,9 @@
* create dynamic group route connections.
*/
@MainThread
- public @NonNull List<RouteInfo> getConnectedRoutes() {
+ public @NonNull List<GroupRouteInfo> getConnectedGroupRoutes() {
checkCallingThread();
- return getGlobalRouter().getConnectedRoutes();
+ return getGlobalRouter().getConnectedGroupRoutes();
}
/**
@@ -587,35 +587,29 @@
}
}
- /**
- * Adds the specified route as a member to the current dynamic group.
- */
+ /** Adds the specified route as a member to the current selected dynamic group. */
@RestrictTo(LIBRARY)
@MainThread
- public void addMemberToDynamicGroup(@NonNull RouteInfo route) {
+ public void addRouteToSelectedGroup(@NonNull RouteInfo route) {
if (route == null) {
throw new NullPointerException("route must not be null");
}
checkCallingThread();
- getGlobalRouter().addMemberToDynamicGroup(route);
+ getGlobalRouter().addRouteToSelectedGroup(route);
}
- /**
- * Removes the specified route from the current dynamic group.
- */
+ /** Removes the specified route from the current selected dynamic group. */
@RestrictTo(LIBRARY)
@MainThread
- public void removeMemberFromDynamicGroup(@NonNull RouteInfo route) {
+ public void removeRouteFromSelectedGroup(@NonNull RouteInfo route) {
if (route == null) {
throw new NullPointerException("route must not be null");
}
checkCallingThread();
- getGlobalRouter().removeMemberFromDynamicGroup(route);
+ getGlobalRouter().removeRouteFromSelectedGroup(route);
}
- /**
- * Transfers the current dynamic group to the specified route.
- */
+ /** Transfers the current dynamic group to the specified route. */
@RestrictTo(LIBRARY)
@MainThread
public void transferToRoute(@NonNull RouteInfo route) {
@@ -1166,8 +1160,9 @@
private IntentSender mSettingsIntent;
MediaRouteDescriptor mDescriptor;
- private List<RouteInfo> mRoutesInGroup = new ArrayList<>();
- private Map<String, DynamicRouteDescriptor> mDynamicGroupDescriptors;
+ @RestrictTo(LIBRARY)
+ @NonNull
+ protected List<RouteInfo> mSelectedRoutesInGroup = new ArrayList<>();
@IntDef({
CONNECTION_STATE_DISCONNECTED,
@@ -1636,20 +1631,6 @@
}
/**
- * Returns {@code true} if this route is currently connected.
- *
- * <p>Must be called on the main thread.
- *
- * @return True if this route is currently connected
- * @see MediaRouter#getConnectedRoutes()
- */
- @MainThread
- public boolean isConnected() {
- checkCallingThread();
- return getGlobalRouter().getConnectedRoutes().contains(this);
- }
-
- /**
* Returns true if this route is the default route.
*
* <p>Must be called on the main thread.
@@ -2109,37 +2090,29 @@
}
/**
- * Returns true if the route has one or more members
- */
- @RestrictTo(LIBRARY)
- public boolean isGroup() {
- return !mRoutesInGroup.isEmpty();
- }
-
- /**
- * Gets the dynamic group state of the given route.
+ * Returns a {@link GroupRouteInfo} if the route is a group route or {code null} otherwise.
*/
@RestrictTo(LIBRARY)
@Nullable
- public DynamicGroupState getDynamicGroupState(@NonNull RouteInfo route) {
- if (route == null) {
- throw new NullPointerException("route must not be null");
- }
- if (mDynamicGroupDescriptors != null
- && mDynamicGroupDescriptors.containsKey(route.mUniqueId)) {
- return new DynamicGroupState(mDynamicGroupDescriptors.get(route.mUniqueId));
- }
- return null;
+ public GroupRouteInfo asGroup() {
+ return (this instanceof GroupRouteInfo) ? (GroupRouteInfo) this : null;
+ }
+
+ /** Returns true if the route has one or more members */
+ @RestrictTo(LIBRARY)
+ public boolean isGroup() {
+ return !mSelectedRoutesInGroup.isEmpty();
}
/**
- * Returns the routes in this group
+ * Returns the selected routes in this group
*
- * @return The list of the routes in this group
+ * @return The list of the selected routes in this group
*/
+ @RestrictTo(LIBRARY)
@NonNull
- public List<RouteInfo> getRoutesInGroup() {
- return Collections.unmodifiableList(mRoutesInGroup);
+ public List<RouteInfo> getSelectedRoutesInGroup() {
+ return Collections.unmodifiableList(mSelectedRoutesInGroup);
}
/**
@@ -2151,9 +2124,7 @@
return mDescriptor;
}
- /**
- *
- */
+ /** */
@MainThread
@RestrictTo(LIBRARY)
@Nullable
@@ -2192,11 +2163,11 @@
.append(", providerPackageName=").append(mProvider.getPackageName());
if (isGroup()) {
sb.append(", members=[");
- final int count = mRoutesInGroup.size();
+ final int count = mSelectedRoutesInGroup.size();
for (int i = 0; i < count; i++) {
if (i > 0) sb.append(", ");
- if (mRoutesInGroup.get(i) != this) {
- sb.append(mRoutesInGroup.get(i).getId());
+ if (mSelectedRoutesInGroup.get(i) != this) {
+ sb.append(mSelectedRoutesInGroup.get(i).getId());
}
}
sb.append(']');
@@ -2338,10 +2309,10 @@
List<String> groupMemberIds = descriptor.getGroupMemberIds();
List<RouteInfo> routes = new ArrayList<>();
- if (groupMemberIds.size() != mRoutesInGroup.size()) {
+ if (groupMemberIds.size() != mSelectedRoutesInGroup.size()) {
memberChanged = true;
}
- //TODO: Clean this up not to reference the global router
+ // TODO: Clean this up not to reference the global router
if (!groupMemberIds.isEmpty()) {
GlobalMediaRouter globalRouter = getGlobalRouter();
for (String groupMemberId : groupMemberIds) {
@@ -2349,14 +2320,14 @@
RouteInfo groupMember = globalRouter.getRoute(uniqueId);
if (groupMember != null) {
routes.add(groupMember);
- if (!memberChanged && !mRoutesInGroup.contains(groupMember)) {
+ if (!memberChanged && !mSelectedRoutesInGroup.contains(groupMember)) {
memberChanged = true;
}
}
}
}
if (memberChanged) {
- mRoutesInGroup = routes;
+ mSelectedRoutesInGroup = routes;
changes |= CHANGE_GENERAL;
}
}
@@ -2373,84 +2344,310 @@
return mProvider.getProviderInstance();
}
+ RouteInfo findRouteByDynamicRouteDescriptor(DynamicRouteDescriptor dynamicDescriptor) {
+ String descriptorId = dynamicDescriptor.getRouteDescriptor().getId();
+ return getProvider().findRouteByDescriptorId(descriptorId);
+ }
+ }
+
+ /** Provides information about a media route that represents a dynamic group. */
+ public static class GroupRouteInfo extends RouteInfo {
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ ADD_ROUTE_SUCCESSFUL,
+ ADD_ROUTE_FAILED_REASON_NOT_GROUPABLE,
+ ADD_ROUTE_FAILED_REASON_ALREADY_IN_GROUP,
+ ADD_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE,
+ ADD_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION
+ })
+ @interface AddRouteReason {}
+
+ /**
+ * The {@link #addRoute(RouteInfo)} has added a route to the dynamic group.
+ *
+ * @see #addRoute(RouteInfo)
+ */
+ public static final int ADD_ROUTE_SUCCESSFUL = 1;
+
+ /**
+ * Adding a route to a dynamic group has failed because the route is not groupable.
+ *
+ * @see #addRoute(RouteInfo)
+ */
+ public static final int ADD_ROUTE_FAILED_REASON_NOT_GROUPABLE = 2;
+
+ /**
+ * Adding a route to a dynamic group has failed because the route is already in the group.
+ *
+ * @see #addRoute(RouteInfo)
+ */
+ public static final int ADD_ROUTE_FAILED_REASON_ALREADY_IN_GROUP = 3;
+
+ /**
+ * Adding a route to a dynamic group has failed because the group route doesn't support
+ * adding a route.
+ *
+ * @see #addRoute(RouteInfo)
+ */
+ public static final int ADD_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 4;
+
+ /**
+ * Adding a route to a dynamic group has failed because the group route is a connected route
+ * but there is no available route connection for adding a route.
+ *
+ * @see #addRoute(RouteInfo)
+ */
+ public static final int ADD_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 5;
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ REMOVE_ROUTE_SUCCESSFUL,
+ REMOVE_ROUTE_FAILED_REASON_NOT_UNSELECTABLE,
+ REMOVE_ROUTE_FAILED_REASON_NOT_IN_GROUP,
+ REMOVE_ROUTE_FAILED_REASON_LAST_ROUTE_IN_GROUP,
+ REMOVE_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE,
+ REMOVE_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION
+ })
+ @interface RemoveRouteReason {}
+
+ /**
+ * The {@link #removeRoute(RouteInfo)} has removed a route from the dynamic group.
+ *
+ * @see #removeRoute(RouteInfo)
+ */
+ public static final int REMOVE_ROUTE_SUCCESSFUL = 1;
+
+ /**
+ * Removing a route from a dynamic group has failed because the route is not unselectable.
+ *
+ * @see #removeRoute(RouteInfo)
+ */
+ public static final int REMOVE_ROUTE_FAILED_REASON_NOT_UNSELECTABLE = 2;
+
+ /**
+ * Removing a route from a dynamic group has failed because the route isn't in the group.
+ *
+ * @see #removeRoute(RouteInfo)
+ */
+ public static final int REMOVE_ROUTE_FAILED_REASON_NOT_IN_GROUP = 3;
+
+ /**
+ * Removing a route from a dynamic group has failed because the route is the last route in
+ * the group.
+ *
+ * @see #removeRoute(RouteInfo)
+ */
+ public static final int REMOVE_ROUTE_FAILED_REASON_LAST_ROUTE_IN_GROUP = 4;
+
+ /**
+ * Removing a route from a dynamic group has failed because the group route doesn't support
+ * removing a route.
+ *
+ * @see #removeRoute(RouteInfo)
+ */
+ public static final int REMOVE_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 5;
+
+ /**
+ * Removing a route from a dynamic group has failed because the group route is a connected
+ * route but there is no available route connection for removing a route.
+ *
+ * @see #removeRoute(RouteInfo)
+ */
+ public static final int REMOVE_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 6;
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ UPDATE_ROUTES_SUCCESSFUL,
+ UPDATE_ROUTES_FAILED_REASON_NOT_TRANSFERABLE,
+ UPDATE_ROUTES_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE,
+ UPDATE_ROUTES_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION
+ })
+ @interface UpdateRoutesReason {}
+
+ /**
+ * The {@link #updateRoutes(List)} has updated routes for the dynamic group.
+ *
+ * @see #updateRoutes(List)
+ */
+ public static final int UPDATE_ROUTES_SUCCESSFUL = 1;
+
+ /**
+ * Updating routes for a dynamic group has failed because the updated routes don't contain
+ * any transferable route.
+ *
+ * @see #updateRoutes(List)
+ */
+ public static final int UPDATE_ROUTES_FAILED_REASON_NOT_TRANSFERABLE = 2;
+
+ /**
+ * Updating routes for a dynamic group has failed because the group route doesn't support
+ * updating routes.
+ *
+ * @see #updateRoutes(List)
+ */
+ public static final int UPDATE_ROUTES_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 3;
+
+ /**
+ * Updating routes for a dynamic group has failed because the group route is a connected
+ * route but there is no available route connection for updating routes.
+ *
+ * @see #updateRoutes(List)
+ */
+ public static final int UPDATE_ROUTES_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 4;
+
+ @NonNull private final List<RouteInfo> mRoutesInGroup = new ArrayList<>();
+
+ @NonNull
+ private final Map<String, DynamicRouteDescriptor> mRouteIdToDynamicRouteDescriptorMap =
+ new ArrayMap<>();
+
+ /* package */ GroupRouteInfo(ProviderInfo provider, String descriptorId, String uniqueId) {
+ super(provider, descriptorId, uniqueId);
+ }
+
+ /**
+ * Returns {@code true} if this route is currently connected.
+ *
+ * <p>Must be called on the main thread.
+ *
+ * @return True if this route is currently connected
+ * @see MediaRouter#getConnectedGroupRoutes()
+ */
+ @MainThread
+ public boolean isConnected() {
+ checkCallingThread();
+ return getGlobalRouter().getConnectedGroupRoutes().contains(this);
+ }
+
+ /**
+ * Adds the route as a member of the dynamic group if the route is groupable.
+ *
+ * <p>If the route is not groupable or is already in the dynamic group, then adding it to
+ * the dynamic group will do nothing.
+ *
+ * @return The state of adding a route to the dynamic group.
+ * @see #isGroupable(RouteInfo)
+ */
+ @MainThread
+ @AddRouteReason
+ public int addRoute(@NonNull RouteInfo route) {
+ checkCallingThread();
+ return getGlobalRouter().addRouteToGroup(this, route);
+ }
+
+ /**
+ * Removes the route from the dynamic group if the route is unselectable.
+ *
+ * <p>If the route is not unselectable, not in the dynamic group, or is the last route of
+ * the dynamic group, then removing it from the dynamic group will do nothing.
+ *
+ * @return The state of removing a route from the dynamic group.
+ * @see #isUnselectable(RouteInfo)
+ */
+ @MainThread
+ @RemoveRouteReason
+ public int removeRoute(@NonNull RouteInfo route) {
+ checkCallingThread();
+ return getGlobalRouter().removeRouteFromGroup(this, route);
+ }
+
+ /**
+ * Updates the routes to be members of the dynamic group if the routes are transferable.
+ * Non-transferable routes will not be included in the dynamic group.
+ *
+ * @return The state of updating routes for the dynamic group.
+ * @see #isTransferable(RouteInfo)
+ */
+ @UpdateRoutesReason
+ @MainThread
+ public int updateRoutes(@NonNull List<RouteInfo> routes) {
+ checkCallingThread();
+ return getGlobalRouter().updateRoutesForGroup(this, routes);
+ }
+
+ /** Returns the list of {@link RouteInfo}s of the given dynamic group route. */
+ @NonNull
+ public List<RouteInfo> getRoutesInGroup() {
+ return Collections.unmodifiableList(mRoutesInGroup);
+ }
+
+ /**
+ * Gets the selection state of the route when the route is in the dynamic group or {@link
+ * DynamicRouteDescriptor#NOT_IN_GROUP} when the route isn't in the dynamic group.
+ */
+ @DynamicRouteDescriptor.SelectionState
+ public int getSelectionState(@NonNull RouteInfo route) {
+ DynamicRouteDescriptor dynamicRouteDescriptor =
+ mRouteIdToDynamicRouteDescriptorMap.get(route.getId());
+ return (dynamicRouteDescriptor != null)
+ ? dynamicRouteDescriptor.getSelectionState()
+ : DynamicRouteDescriptor.NOT_IN_GROUP;
+ }
+
+ /**
+ * Returns {@code true} if the route is in the dynamic group and is unselectable from the
+ * dynamic group with the {@link #removeRoute(RouteInfo)} method.
+ */
+ public boolean isUnselectable(@NonNull RouteInfo route) {
+ DynamicRouteDescriptor dynamicRouteDescriptor =
+ mRouteIdToDynamicRouteDescriptorMap.get(route.getId());
+ return (dynamicRouteDescriptor != null) && dynamicRouteDescriptor.isUnselectable();
+ }
+
+ /**
+ * Returns {@code true} if the route is groupable and can be added to the dynamic group with
+ * the {@link #addRoute(RouteInfo)} method.
+ */
+ public boolean isGroupable(@NonNull RouteInfo route) {
+ DynamicRouteDescriptor dynamicRouteDescriptor =
+ mRouteIdToDynamicRouteDescriptorMap.get(route.getId());
+ return (dynamicRouteDescriptor != null) && dynamicRouteDescriptor.isGroupable();
+ }
+
+ /**
+ * Returns {@code true} if the route is transferable and can be updated for the dynamic
+ * group with the {@link #updateRoutes(List)} method.
+ */
+ public boolean isTransferable(@NonNull RouteInfo route) {
+ DynamicRouteDescriptor dynamicRouteDescriptor =
+ mRouteIdToDynamicRouteDescriptorMap.get(route.getId());
+ return (dynamicRouteDescriptor != null) && dynamicRouteDescriptor.isTransferable();
+ }
+
void updateDynamicDescriptors(Collection<DynamicRouteDescriptor> dynamicDescriptors) {
+ mSelectedRoutesInGroup.clear();
mRoutesInGroup.clear();
- if (mDynamicGroupDescriptors == null) {
- mDynamicGroupDescriptors = new ArrayMap<>();
- }
- mDynamicGroupDescriptors.clear();
+ mRouteIdToDynamicRouteDescriptorMap.clear();
for (DynamicRouteDescriptor dynamicDescriptor : dynamicDescriptors) {
RouteInfo route = findRouteByDynamicRouteDescriptor(dynamicDescriptor);
if (route == null) {
continue;
}
- mDynamicGroupDescriptors.put(route.mUniqueId, dynamicDescriptor);
+ mRoutesInGroup.add(route);
+ mRouteIdToDynamicRouteDescriptorMap.put(route.getId(), dynamicDescriptor);
if ((dynamicDescriptor.getSelectionState() == DynamicRouteDescriptor.SELECTING)
|| (dynamicDescriptor.getSelectionState()
== DynamicRouteDescriptor.SELECTED)) {
- mRoutesInGroup.add(route);
+ mSelectedRoutesInGroup.add(route);
}
}
getGlobalRouter()
.mCallbackHandler
.post(GlobalMediaRouter.CallbackHandler.MSG_ROUTE_CHANGED, this);
}
-
- RouteInfo findRouteByDynamicRouteDescriptor(DynamicRouteDescriptor dynamicDescriptor) {
- String descriptorId = dynamicDescriptor.getRouteDescriptor().getId();
- return getProvider().findRouteByDescriptorId(descriptorId);
- }
-
- /** Represents the dynamic group state of the {@link RouteInfo}. */
- @RestrictTo(LIBRARY)
- public static final class DynamicGroupState {
- final DynamicRouteDescriptor mDynamicDescriptor;
-
- DynamicGroupState(DynamicRouteDescriptor descriptor) {
- mDynamicDescriptor = descriptor;
- }
-
- /**
- * Gets the selection state of the route when the {@link MediaRouteProvider} of the
- * route supports {@link MediaRouteProviderDescriptor#supportsDynamicGroupRoute()
- * dynamic group}.
- *
- * @return The selection state of the route: {@link DynamicRouteDescriptor#UNSELECTED},
- * {@link DynamicRouteDescriptor#SELECTING}, or {@link
- * DynamicRouteDescriptor#SELECTED}.
- */
- @RestrictTo(LIBRARY)
- public int getSelectionState() {
- return (mDynamicDescriptor != null)
- ? mDynamicDescriptor.getSelectionState()
- : DynamicRouteDescriptor.UNSELECTED;
- }
-
- @RestrictTo(LIBRARY)
- public boolean isUnselectable() {
- return mDynamicDescriptor == null || mDynamicDescriptor.isUnselectable();
- }
-
- @RestrictTo(LIBRARY)
- public boolean isGroupable() {
- return mDynamicDescriptor != null && mDynamicDescriptor.isGroupable();
- }
-
- @RestrictTo(LIBRARY)
- public boolean isTransferable() {
- return mDynamicDescriptor != null && mDynamicDescriptor.isTransferable();
- }
- }
}
/**
* Provides information about a media route provider.
- * <p>
- * This object may be used to determine which media route provider has
- * published a particular route.
- * </p>
+ *
+ * <p>This object may be used to determine which media route provider has published a particular
+ * route.
*/
public static final class ProviderInfo {
// Package private fields to avoid use of a synthetic accessor.
@@ -2992,7 +3189,10 @@
router.maybeUpdateMemberRouteControllers();
router.updatePlaybackInfoFromSelectedRoute();
if (mMemberRoutes != null) {
- router.mSelectedRoute.updateDynamicDescriptors(mMemberRoutes);
+ GroupRouteInfo groupRouteInfo = router.mSelectedRoute.asGroup();
+ if (groupRouteInfo != null) {
+ groupRouteInfo.updateDynamicDescriptors(mMemberRoutes);
+ }
}
}
}
diff --git a/metrics/metrics-benchmark/build.gradle b/metrics/metrics-benchmark/build.gradle
index 87d74a1..784e4ae 100644
--- a/metrics/metrics-benchmark/build.gradle
+++ b/metrics/metrics-benchmark/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import androidx.build.LibraryType
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -44,3 +46,6 @@
}
}
+androidx {
+ type = LibraryType.BENCHMARK
+}
diff --git a/navigation/navigation-benchmark/build.gradle b/navigation/navigation-benchmark/build.gradle
index bc91b4d..e05fb56 100644
--- a/navigation/navigation-benchmark/build.gradle
+++ b/navigation/navigation-benchmark/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -51,7 +51,7 @@
androidx {
name = "Navigation Benchmarks"
- publish = Publish.NONE
+ type = LibraryType.BENCHMARK
inceptionYear = "2018"
description = "Navigation Benchmarks"
}
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle b/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
index 6950b97..e3e8319 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -55,7 +55,7 @@
androidx {
name = "Compose Navigation Demos"
- publish = Publish.NONE
+ type = LibraryType.TEST_APPLICATION
inceptionYear = "2020"
description = "This is a project for Navigation demos."
}
diff --git a/navigation3/navigation3/build.gradle b/navigation3/navigation3/build.gradle
index c34eafd..796ed34 100644
--- a/navigation3/navigation3/build.gradle
+++ b/navigation3/navigation3/build.gradle
@@ -23,7 +23,6 @@
*/
import androidx.build.KotlinTarget
import androidx.build.LibraryType
-import androidx.build.Publish
import androidx.build.PlatformIdentifier
plugins {
@@ -114,8 +113,7 @@
androidx {
name = "Androidx Navigation 3"
- publish = Publish.SNAPSHOT_ONLY
- type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+ type = LibraryType.SNAPSHOT_ONLY_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
inceptionYear = "2024"
description = "Provides the building blocks for a Compose first Navigation solution that " +
"easily supports extensions."
diff --git a/paging/paging-compose/integration-tests/paging-demos/build.gradle b/paging/paging-compose/integration-tests/paging-demos/build.gradle
index 9ef6e39..eb640ed 100644
--- a/paging/paging-compose/integration-tests/paging-demos/build.gradle
+++ b/paging/paging-compose/integration-tests/paging-demos/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -50,7 +50,7 @@
androidx {
name = "Compose Paging Demos"
- publish = Publish.NONE
+ type = LibraryType.TEST_APPLICATION
inceptionYear = "2020"
description = "This is a project for Paging demos."
}
diff --git a/paging/paging-testing/build.gradle b/paging/paging-testing/build.gradle
index f79d5be..f6a4b33 100644
--- a/paging/paging-testing/build.gradle
+++ b/paging/paging-testing/build.gradle
@@ -26,7 +26,6 @@
import androidx.build.LibraryType
import androidx.build.PlatformIdentifier
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
-import androidx.build.Publish
import org.jetbrains.kotlin.konan.target.Family
plugins {
diff --git a/performance/performance-annotation/build.gradle b/performance/performance-annotation/build.gradle
index 72993cd..36a6772 100644
--- a/performance/performance-annotation/build.gradle
+++ b/performance/performance-annotation/build.gradle
@@ -16,7 +16,7 @@
import androidx.build.KotlinTarget
import androidx.build.PlatformIdentifier
-import androidx.build.Publish
+import androidx.build.LibraryType
import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile
plugins {
@@ -61,7 +61,7 @@
androidx {
name = "Performance - Annotation"
- publish = Publish.NONE
+ type = LibraryType.UNSET
inceptionYear = "2024"
description = "Provides source annotations for performance optimizations."
kotlinTarget = KotlinTarget.KOTLIN_1_9
diff --git a/performance/performance-unsafe/build.gradle b/performance/performance-unsafe/build.gradle
index 9c1b2cc..332949a 100644
--- a/performance/performance-unsafe/build.gradle
+++ b/performance/performance-unsafe/build.gradle
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -28,7 +28,7 @@
androidx {
name = "Performance - Unsafe"
- publish = Publish.NONE
+ type = LibraryType.UNSET
inceptionYear = "2024"
description = "Compile-time support for sun.misc.Unsafe."
}
diff --git a/privacysandbox/tools/tools-core/build.gradle b/privacysandbox/tools/tools-core/build.gradle
index 69f7a93..2f69c1d 100644
--- a/privacysandbox/tools/tools-core/build.gradle
+++ b/privacysandbox/tools/tools-core/build.gradle
@@ -23,7 +23,6 @@
*/
import androidx.build.KotlinTarget
import androidx.build.LibraryType
-import androidx.build.RunApiTasks
import androidx.build.SdkHelperKt
import androidx.build.AndroidXConfig
diff --git a/profileinstaller/profileinstaller-benchmark/build.gradle b/profileinstaller/profileinstaller-benchmark/build.gradle
index 78d904e..ec8c425 100644
--- a/profileinstaller/profileinstaller-benchmark/build.gradle
+++ b/profileinstaller/profileinstaller-benchmark/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -46,7 +46,7 @@
androidx {
name = "Profileinstaller Benchmarks"
- publish = Publish.NONE
+ type = LibraryType.BENCHMARK
inceptionYear = "2021"
description = "Profileinstaller Benchmarks"
}
diff --git a/recyclerview/recyclerview-benchmark/build.gradle b/recyclerview/recyclerview-benchmark/build.gradle
index e627eaf..f7542c7 100644
--- a/recyclerview/recyclerview-benchmark/build.gradle
+++ b/recyclerview/recyclerview-benchmark/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import androidx.build.LibraryType
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -38,3 +40,6 @@
namespace = "androidx.recyclerview.benchmark"
}
+androidx {
+ type = LibraryType.BENCHMARK
+}
\ No newline at end of file
diff --git a/room/benchmark/build.gradle b/room/benchmark/build.gradle
index 8613005..19362ae 100644
--- a/room/benchmark/build.gradle
+++ b/room/benchmark/build.gradle
@@ -53,5 +53,5 @@
}
androidx {
- type = LibraryType.INTERNAL_TEST_LIBRARY
+ type = LibraryType.BENCHMARK
}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/InMemoryTrackingModeTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/InMemoryTrackingModeTest.kt
new file mode 100644
index 0000000..488b763
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/InMemoryTrackingModeTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.integration.kotlintestapp.test
+
+import androidx.kruth.assertThat
+import androidx.room.ExperimentalRoomApi
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.integration.kotlintestapp.TestDatabase
+import androidx.room.util.useCursor
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlin.test.Test
+import org.junit.Before
+
+class InMemoryTrackingModeTest {
+
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext
+
+ @Before
+ fun setup() {
+ context.deleteDatabase("test.db")
+ }
+
+ @Test
+ @OptIn(ExperimentalRoomApi::class)
+ fun persistedTrackingTable() {
+ val database =
+ Room.databaseBuilder<TestDatabase>(context, "test.db")
+ .setInMemoryTrackingMode(false)
+ .build()
+
+ assertThat(findCreateSql(database, "sqlite_master")).isNotEmpty()
+ assertThat(findCreateSql(database, "sqlite_temp_master")).isNull()
+ database.close()
+ }
+
+ @Test
+ @OptIn(ExperimentalRoomApi::class)
+ fun temporaryTrackingTable() {
+ val database =
+ Room.databaseBuilder<TestDatabase>(context, "test.db")
+ .setInMemoryTrackingMode(true)
+ .build()
+
+ assertThat(findCreateSql(database, "sqlite_master")).isNull()
+ assertThat(findCreateSql(database, "sqlite_temp_master")).isNotEmpty()
+ database.close()
+ }
+
+ private fun findCreateSql(database: RoomDatabase, masterTable: String) =
+ database.runInTransaction<String?> {
+ database.openHelper.writableDatabase
+ .query("SELECT name, sql FROM $masterTable")
+ .useCursor { c ->
+ while (c.moveToNext()) {
+ if (c.getString(0) == TRACKING_TABLE_NAME) {
+ return@runInTransaction c.getString(1)
+ }
+ }
+ }
+ null
+ }
+
+ private companion object {
+ private const val TRACKING_TABLE_NAME = "room_table_modification_log"
+ }
+}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
index 321f855..c72e4e6 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
@@ -218,6 +218,8 @@
@CallSuper
@OptIn(ExperimentalCoroutinesApi::class) // For limitedParallelism(1)
actual open fun init(configuration: DatabaseConfiguration) {
+ useTempTrackingTable = configuration.useTempTrackingTable
+
connectionManager = createConnectionManager(configuration)
internalTracker = createInvalidationTracker()
validateAutoMigrations(configuration)
@@ -279,8 +281,6 @@
configuration.multiInstanceInvalidationServiceIntent
)
}
-
- useTempTrackingTable = configuration.useTempTrackingTable
}
/**
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/WrapperMediaRouteProvider.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/WrapperMediaRouteProvider.java
index 188a645..5b9a8ea 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/WrapperMediaRouteProvider.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/WrapperMediaRouteProvider.java
@@ -251,9 +251,9 @@
}
void addOriginalRoute(MediaRouter.@NonNull RouteInfo originalRoute) {
- if (originalRoute.isSelected() || originalRoute.isConnected()) {
+ if (originalRoute.isSelected() || originalRoute instanceof MediaRouter.GroupRouteInfo) {
// The wrapper route provider only wraps discovered routes and it wouldn't wrap
- // selected routes or connected routes.
+ // selected routes or group routes.
return;
}
String originalDescriptorId = getDescriptorId(originalRoute.getId());
diff --git a/tracing/tracing/build.gradle b/tracing/tracing/build.gradle
index d62d1a3..39bd6a2 100644
--- a/tracing/tracing/build.gradle
+++ b/tracing/tracing/build.gradle
@@ -26,11 +26,17 @@
plugins {
id("AndroidXPlugin")
- id("com.android.library")
}
androidXMultiplatform {
- android()
+ androidLibrary {
+ namespace = "androidx.tracing"
+ withAndroidTestOnDeviceBuilder {
+ it.compilationName = "instrumentedTest"
+ it.defaultSourceSetName = "androidInstrumentedTest"
+ it.sourceSetTreeName = "test"
+ }
+ }
defaultPlatform(PlatformIdentifier.ANDROID)
@@ -69,7 +75,3 @@
enableAlsoRunningOnPhysicalDevices = true
}
}
-
-android {
- namespace = "androidx.tracing"
-}
diff --git a/versionedparcelable/versionedparcelable/build.gradle b/versionedparcelable/versionedparcelable/build.gradle
index 5ad222b..f0ec27d 100644
--- a/versionedparcelable/versionedparcelable/build.gradle
+++ b/versionedparcelable/versionedparcelable/build.gradle
@@ -34,6 +34,11 @@
api("androidx.annotation:annotation:1.8.1")
implementation("androidx.collection:collection:1.4.2")
+ testImplementation(libs.testCore)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.junit)
+ testImplementation(libs.robolectric)
+
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
diff --git a/versionedparcelable/versionedparcelable/src/main/java/androidx/versionedparcelable/VersionedParcel.java b/versionedparcelable/versionedparcelable/src/main/java/androidx/versionedparcelable/VersionedParcel.java
index 809c0ab..b56562e 100644
--- a/versionedparcelable/versionedparcelable/src/main/java/androidx/versionedparcelable/VersionedParcel.java
+++ b/versionedparcelable/versionedparcelable/src/main/java/androidx/versionedparcelable/VersionedParcel.java
@@ -1599,7 +1599,7 @@
NoSuchMethodException, ClassNotFoundException {
Method m = mReadCache.get(parcelCls);
if (m == null) {
- Class<?> cls = Class.forName(parcelCls, true, VersionedParcel.class.getClassLoader());
+ Class<?> cls = Class.forName(parcelCls, false, VersionedParcel.class.getClassLoader());
m = cls.getDeclaredMethod("read", VersionedParcel.class);
mReadCache.put(parcelCls, m);
}
diff --git a/versionedparcelable/versionedparcelable/src/test/java/androidx/versionedparcelable/ParcelImplTest.java b/versionedparcelable/versionedparcelable/src/test/java/androidx/versionedparcelable/ParcelImplTest.java
new file mode 100644
index 0000000..1ba07b1
--- /dev/null
+++ b/versionedparcelable/versionedparcelable/src/test/java/androidx/versionedparcelable/ParcelImplTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.versionedparcelable;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Parcel;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class ParcelImplTest {
+
+ @Test
+ public void testFakeParcelableInit_throwsInitializedException() {
+ ExceptionInInitializerError e = assertThrows(
+ ExceptionInInitializerError.class, FakeParcelable::new);
+
+ assertTrue(e.getCause() instanceof InitializedException);
+ }
+
+ @Test
+ public void testCreateFromParcel_withNonVersionedParcelableClass_throwsNoSuchMethodException() {
+ RuntimeException e = assertThrows(RuntimeException.class, () -> {
+ Parcel p = Parcel.obtain();
+ p.writeString("androidx.versionedparcelable.ParcelImplTest$FakeParcelable");
+ p.setDataPosition(0);
+ ParcelImpl.CREATOR.createFromParcel(p);
+ });
+
+ assertTrue(e.getCause() instanceof NoSuchMethodException);
+ }
+
+ @Test
+ public void testCreateFromParcel_withMissingClass_throwsClassNotFoundException() {
+ RuntimeException e = assertThrows(RuntimeException.class, () -> {
+ Parcel p = Parcel.obtain();
+ p.writeString("androidx.versionedparcelable.MissingParcelable");
+ p.setDataPosition(0);
+ ParcelImpl.CREATOR.createFromParcel(p);
+ });
+
+ assertTrue(e.getCause() instanceof ClassNotFoundException);
+ }
+
+ public static class FakeParcelable {
+ static {
+ //noinspection ConstantValue
+ if (true) {
+ throw new InitializedException();
+ }
+ }
+ }
+
+ private static class InitializedException extends RuntimeException { }
+}
diff --git a/wear/compose/compose-foundation/benchmark/build.gradle b/wear/compose/compose-foundation/benchmark/build.gradle
index b69e6d1..d4f9b34 100644
--- a/wear/compose/compose-foundation/benchmark/build.gradle
+++ b/wear/compose/compose-foundation/benchmark/build.gradle
@@ -60,5 +60,5 @@
androidTestImplementation(libs.truth)
}
androidx {
- type = LibraryType.INTERNAL_TEST_LIBRARY
+ type = LibraryType.BENCHMARK
}
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategyTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategyTest.kt
index ed96326..696f902 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategyTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategyTest.kt
@@ -22,7 +22,6 @@
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.layout.AlignmentLine
-import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
@@ -46,7 +45,7 @@
class TransformingLazyColumnContentPaddingMeasurementStrategyTest {
private val screenHeight = 100
private val screenWidth = 120
- private val density = 1f
+ private val density = Density(1f)
private val containerConstraints =
Constraints(
@@ -116,13 +115,10 @@
@Test
fun twoItemsWithFirstTopAlignedWithPadding_measuredWithCorrectOffsets() {
val topPadding = 5.dp
- val topPaddingPx = with(measureScope) { topPadding.roundToPx() }
+ val topPaddingPx = with(density) { topPadding.roundToPx() }
val strategyWithTopPadding =
- TransformingLazyColumnContentPaddingMeasurementStrategy(
+ measurementStrategy(
PaddingValues(top = topPadding),
- measureScope,
- mockGraphicContext,
- mockItemAnimator
)
val result = strategyWithTopPadding.measure(listOf(screenHeight / 2, screenHeight / 2))
@@ -137,13 +133,10 @@
@Test
fun twoItemsWithLastOneAlignedWithPadding_measuredWithCorrectOffsets() {
val bottomPadding = 5.dp
- val bottomPaddingPx = with(measureScope) { bottomPadding.roundToPx() }
+ val bottomPaddingPx = with(density) { bottomPadding.roundToPx() }
val strategyWithBottomPadding =
- TransformingLazyColumnContentPaddingMeasurementStrategy(
+ measurementStrategy(
PaddingValues(bottom = bottomPadding),
- measureScope,
- mockGraphicContext,
- mockItemAnimator
)
val result = strategyWithBottomPadding.measure(listOf(screenHeight / 2, screenHeight / 2))
@@ -389,12 +382,9 @@
@Test
fun fullSizeBottomContentPadding_doesNotCrash() {
val strategy =
- TransformingLazyColumnContentPaddingMeasurementStrategy(
+ measurementStrategy(
// Padding takes the full size.
- PaddingValues(bottom = with(Density(density)) { screenHeight.toDp() }),
- measureScope,
- mockGraphicContext,
- mockItemAnimator
+ PaddingValues(bottom = with(density) { screenHeight.toDp() }),
)
val itemSize = screenHeight / 4
@@ -409,12 +399,9 @@
@Test
fun fullSizeTopContentPadding_doesNotCrash() {
val strategy =
- TransformingLazyColumnContentPaddingMeasurementStrategy(
+ measurementStrategy(
// Padding takes the full size.
- PaddingValues(top = with(Density(density)) { screenHeight.toDp() }),
- measureScope,
- mockGraphicContext,
- mockItemAnimator
+ PaddingValues(top = with(density) { screenHeight.toDp() }),
)
val itemSize = screenHeight / 4
@@ -429,16 +416,6 @@
assertThat(result.visibleItems.size).isEqualTo(2)
}
- private val measureScope: IntrinsicMeasureScope =
- object : IntrinsicMeasureScope {
- override val fontScale: Float
- get() = this@TransformingLazyColumnContentPaddingMeasurementStrategyTest.density
-
- override val layoutDirection: LayoutDirection = LayoutDirection.Ltr
- override val density: Float
- get() = this@TransformingLazyColumnContentPaddingMeasurementStrategyTest.density
- }
-
private val mockGraphicContext =
object : GraphicsContext {
override fun createGraphicsLayer(): GraphicsLayer {
@@ -452,14 +429,17 @@
private val mockItemAnimator = LazyLayoutItemAnimator<TransformingLazyColumnMeasuredItem>()
- private val strategy =
+ private fun measurementStrategy(contentPadding: PaddingValues) =
TransformingLazyColumnContentPaddingMeasurementStrategy(
- PaddingValues(0.dp),
- measureScope,
+ contentPadding,
+ density = density,
+ layoutDirection = LayoutDirection.Ltr,
mockGraphicContext,
mockItemAnimator
)
+ private val strategy = measurementStrategy(PaddingValues())
+
private fun TransformingLazyColumnMeasurementStrategy.measure(
itemHeights: List<Int>,
transformedHeight: ((Int, TransformingLazyColumnItemScrollProgress) -> Int)? = null,
@@ -480,7 +460,7 @@
lastMeasuredAnchorItemHeight = lastMeasuredAnchorItemHeight,
scrollToBeConsumed = scrollToBeConsumed,
coroutineScope = CoroutineScope(EmptyCoroutineContext),
- density = Density(density),
+ density = density,
layout = { width, height, _ ->
object : MeasureResult {
override val width = width
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumn.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumn.kt
index 3a57009..bee8429 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumn.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumn.kt
@@ -43,7 +43,7 @@
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.IntrinsicMeasureScope
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalGraphicsContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp
@@ -92,20 +92,25 @@
content: TransformingLazyColumnScope.() -> Unit
) {
val graphicsContext = LocalGraphicsContext.current
+ val layoutDirection = LocalLayoutDirection.current
+ val density = LocalDensity.current
+ val measurementStrategy =
+ remember(contentPadding) {
+ TransformingLazyColumnContentPaddingMeasurementStrategy(
+ contentPadding = contentPadding,
+ layoutDirection = layoutDirection,
+ density = density,
+ graphicsContext = graphicsContext,
+ itemAnimator = state.animator,
+ )
+ }
TransformingLazyColumnImpl(
modifier = modifier,
state = state,
verticalArrangement = verticalArrangement,
horizontalAlignment = horizontalAlignment,
- measurementStrategyProvider = {
- TransformingLazyColumnContentPaddingMeasurementStrategy(
- contentPadding = contentPadding,
- intrinsicMeasureScope = this,
- graphicsContext = graphicsContext,
- itemAnimator = state.animator,
- )
- },
+ measurementStrategy = measurementStrategy,
flingBehavior = flingBehavior,
userScrollEnabled = userScrollEnabled,
rotaryScrollableBehavior = rotaryScrollableBehavior,
@@ -146,12 +151,13 @@
rotaryScrollableBehavior: RotaryScrollableBehavior? = RotaryScrollableDefaults.behavior(state),
content: TransformingLazyColumnScope.() -> Unit
) {
+ val measurementStrategy = remember { TransformingLazyColumnCenterBoundsMeasurementStrategy() }
TransformingLazyColumnImpl(
modifier = modifier,
state = state,
verticalArrangement = verticalArrangement,
horizontalAlignment = horizontalAlignment,
- measurementStrategyProvider = { TransformingLazyColumnCenterBoundsMeasurementStrategy() },
+ measurementStrategy = measurementStrategy,
flingBehavior = flingBehavior,
userScrollEnabled = userScrollEnabled,
rotaryScrollableBehavior = rotaryScrollableBehavior,
@@ -179,8 +185,7 @@
alignment = Alignment.Top
),
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
- measurementStrategyProvider:
- IntrinsicMeasureScope.() -> TransformingLazyColumnMeasurementStrategy,
+ measurementStrategy: TransformingLazyColumnMeasurementStrategy,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
rotaryScrollableBehavior: RotaryScrollableBehavior? = RotaryScrollableDefaults.behavior(state),
@@ -213,7 +218,7 @@
state = state,
horizontalAlignment = horizontalAlignment,
verticalArrangement = verticalArrangement,
- measurementStrategyProvider = measurementStrategyProvider,
+ measurementStrategy = measurementStrategy,
coroutineScope = coroutineScope,
)
val reverseDirection =
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategy.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategy.kt
index 2c9d827..db358e3 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategy.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategy.kt
@@ -19,11 +19,11 @@
import androidx.collection.mutableObjectIntMapOf
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.graphics.GraphicsContext
-import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
@@ -40,19 +40,16 @@
internal class TransformingLazyColumnContentPaddingMeasurementStrategy(
contentPadding: PaddingValues,
- intrinsicMeasureScope: IntrinsicMeasureScope,
+ density: Density,
+ layoutDirection: LayoutDirection,
private val graphicsContext: GraphicsContext,
private val itemAnimator: LazyLayoutItemAnimator<TransformingLazyColumnMeasuredItem>
) : TransformingLazyColumnMeasurementStrategy {
override val rightContentPadding: Int =
- with(intrinsicMeasureScope) {
- contentPadding.calculateRightPadding(layoutDirection).roundToPx()
- }
+ with(density) { contentPadding.calculateRightPadding(layoutDirection).roundToPx() }
override val leftContentPadding: Int =
- with(intrinsicMeasureScope) {
- contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
- }
+ with(density) { contentPadding.calculateLeftPadding(layoutDirection).roundToPx() }
override fun measure(
itemsCount: Int,
@@ -352,10 +349,10 @@
}
private val beforeContentPadding: Int =
- with(intrinsicMeasureScope) { contentPadding.calculateTopPadding().roundToPx() }
+ with(density) { contentPadding.calculateTopPadding().roundToPx() }
private val afterContentPadding: Int =
- with(intrinsicMeasureScope) { contentPadding.calculateBottomPadding().roundToPx() }
+ with(density) { contentPadding.calculateBottomPadding().roundToPx() }
private fun restoreLayoutTopToBottom(
visibleItems: ArrayDeque<TransformingLazyColumnMeasuredItem>,
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnMeasurement.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnMeasurement.kt
index b9a3318..32a1708 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnMeasurement.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnMeasurement.kt
@@ -23,7 +23,6 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Alignment
-import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.constrainHeight
@@ -50,19 +49,16 @@
coroutineScope: CoroutineScope,
horizontalAlignment: Alignment.Horizontal,
verticalArrangement: Arrangement.Vertical,
- measurementStrategyProvider:
- IntrinsicMeasureScope.() -> TransformingLazyColumnMeasurementStrategy,
+ measurementStrategy: TransformingLazyColumnMeasurementStrategy,
): LazyLayoutMeasureScope.(Constraints) -> MeasureResult =
remember(
itemProviderLambda,
state,
horizontalAlignment,
verticalArrangement,
- measurementStrategyProvider
+ measurementStrategy
) {
{ containerConstraints ->
- val measurementStrategy = measurementStrategyProvider(this)
-
val childConstraints =
Constraints(
maxHeight = Constraints.Infinity,
diff --git a/wear/compose/compose-material/benchmark/build.gradle b/wear/compose/compose-material/benchmark/build.gradle
index 7d71235..3adc459 100644
--- a/wear/compose/compose-material/benchmark/build.gradle
+++ b/wear/compose/compose-material/benchmark/build.gradle
@@ -62,5 +62,5 @@
androidTestImplementation(libs.truth)
}
androidx {
- type = LibraryType.INTERNAL_TEST_LIBRARY
+ type = LibraryType.BENCHMARK
}
diff --git a/wear/compose/compose-material3/benchmark/build.gradle b/wear/compose/compose-material3/benchmark/build.gradle
index 9e88dd9..836aa4e 100644
--- a/wear/compose/compose-material3/benchmark/build.gradle
+++ b/wear/compose/compose-material3/benchmark/build.gradle
@@ -61,5 +61,5 @@
androidTestImplementation(libs.truth)
}
androidx {
- type = LibraryType.INTERNAL_TEST_LIBRARY
+ type = LibraryType.BENCHMARK
}
diff --git a/wear/compose/compose-material3/integration-tests/build.gradle b/wear/compose/compose-material3/integration-tests/build.gradle
index 24d5fb1..956afb7 100644
--- a/wear/compose/compose-material3/integration-tests/build.gradle
+++ b/wear/compose/compose-material3/integration-tests/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -47,7 +47,7 @@
androidx {
name = "AndroidX Wear Compose Material3 Components Demos"
- publish = Publish.NONE
+ type = LibraryType.TEST_APPLICATION
inceptionYear = "2023"
description = "Contains the demo code for the AndroidX Wear Compose Material 3 components."
}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeButtonDemo.kt
deleted file mode 100644
index 8d25aff..0000000
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeButtonDemo.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.compose.material3.demos
-
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CutCornerShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Home
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import androidx.wear.compose.material3.FilledIconButton
-import androidx.wear.compose.material3.FilledTonalIconButton
-import androidx.wear.compose.material3.Icon
-import androidx.wear.compose.material3.IconButton
-import androidx.wear.compose.material3.IconButtonDefaults
-import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.OutlinedIconButton
-import androidx.wear.compose.material3.Text
-import androidx.wear.compose.material3.TextButton
-import androidx.wear.compose.material3.TextButtonDefaults
-
-@Composable
-fun AnimatedShapeButtonDemo() {
- ScalingLazyDemo {
- item { ListHeader { Text("Animated Text Button") } }
- item {
- Row {
- TextButton(
- onClick = {},
- shapes = TextButtonDefaults.animatedShapes(),
- ) {
- Text(text = "ABC")
- }
-
- Spacer(modifier = Modifier.width(5.dp))
-
- TextButton(
- onClick = {},
- shapes =
- TextButtonDefaults.animatedShapes(
- shape = CutCornerShape(15.dp),
- pressedShape = RoundedCornerShape(15.dp),
- ),
- ) {
- Text(text = "ABC")
- }
- }
- }
- item { ListHeader { Text("Animated Icon Button") } }
- item {
- Row {
- IconButton(
- onClick = {},
- shapes = IconButtonDefaults.animatedShapes(),
- ) {
- Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
- }
-
- Spacer(modifier = Modifier.width(5.dp))
-
- IconButton(
- onClick = {},
- shapes =
- IconButtonDefaults.animatedShapes(
- shape = CutCornerShape(15.dp),
- pressedShape = RoundedCornerShape(15.dp)
- ),
- ) {
- Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
- }
- }
- }
- item { ListHeader { Text("Animated Filled Icon Button") } }
- item {
- Row {
- FilledIconButton(
- onClick = {},
- shapes = IconButtonDefaults.animatedShapes(),
- ) {
- Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
- }
-
- Spacer(modifier = Modifier.width(5.dp))
-
- FilledIconButton(
- onClick = {},
- shapes =
- IconButtonDefaults.animatedShapes(
- shape = CutCornerShape(15.dp),
- pressedShape = RoundedCornerShape(15.dp)
- ),
- ) {
- Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
- }
- }
- }
- item { ListHeader { Text("Animated Filled Tonal Icon Button") } }
- item {
- Row {
- FilledTonalIconButton(
- onClick = {},
- shapes = IconButtonDefaults.animatedShapes(),
- ) {
- Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
- }
-
- Spacer(modifier = Modifier.width(5.dp))
-
- FilledTonalIconButton(
- onClick = {},
- shapes =
- IconButtonDefaults.animatedShapes(
- shape = CutCornerShape(15.dp),
- pressedShape = RoundedCornerShape(15.dp)
- ),
- ) {
- Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
- }
- }
- }
- item { ListHeader { Text("Animated Outlined Icon Button") } }
- item {
- Row {
- OutlinedIconButton(
- onClick = {},
- shapes = IconButtonDefaults.animatedShapes(),
- ) {
- Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
- }
-
- Spacer(modifier = Modifier.width(5.dp))
-
- OutlinedIconButton(
- onClick = {},
- shapes =
- IconButtonDefaults.animatedShapes(
- shape = CutCornerShape(15.dp),
- pressedShape = RoundedCornerShape(15.dp)
- ),
- ) {
- Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
- }
- }
- }
- }
-}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeToggleButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeToggleButtonDemo.kt
deleted file mode 100644
index ed2dbcb..0000000
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeToggleButtonDemo.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.compose.material3.demos
-
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.width
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Home
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import androidx.wear.compose.material3.Icon
-import androidx.wear.compose.material3.IconToggleButton
-import androidx.wear.compose.material3.IconToggleButtonDefaults
-import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.Text
-import androidx.wear.compose.material3.TextToggleButton
-import androidx.wear.compose.material3.TextToggleButtonDefaults
-
-@Composable
-fun AnimatedShapeToggleButtonDemo() {
- ScalingLazyDemo {
- item { ListHeader { Text("Default Toggle") } }
- item {
- Row {
- val checked = remember { mutableStateOf(false) }
-
- TextToggleButton(
- onCheckedChange = { checked.value = !checked.value },
- shapes = TextToggleButtonDefaults.animatedShapes(),
- checked = checked.value,
- ) {
- Text(text = "ABC")
- }
-
- Spacer(modifier = Modifier.width(5.dp))
-
- IconToggleButton(
- onCheckedChange = { checked.value = !checked.value },
- shapes = IconToggleButtonDefaults.animatedShapes(),
- checked = checked.value,
- ) {
- Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
- }
- }
- }
- item { ListHeader { Text("Toggle Variant") } }
- item {
- Row {
- val checked = remember { mutableStateOf(false) }
-
- TextToggleButton(
- onCheckedChange = { checked.value = !checked.value },
- shapes = TextToggleButtonDefaults.variantAnimatedShapes(),
- checked = checked.value,
- ) {
- Text(text = "ABC")
- }
-
- Spacer(modifier = Modifier.width(5.dp))
-
- IconToggleButton(
- onCheckedChange = { checked.value = !checked.value },
- shapes = IconToggleButtonDefaults.variantAnimatedShapes(),
- checked = checked.value,
- ) {
- Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
- }
- }
- }
- }
-}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
index ba9730e..15ac940 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
@@ -19,8 +19,6 @@
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CutCornerShape
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -105,7 +103,7 @@
}
item { ListHeader { Text("With onLongClick") } }
item { IconButtonWithOnLongClickSample { showOnLongClickToast(context) } }
- item { ListHeader { Text("Corner Animation") } }
+ item { ListHeader { Text("Animated") } }
item {
Row {
IconButtonWithCornerAnimationSample()
@@ -115,33 +113,6 @@
)
}
}
- item { ListHeader { Text("Morphed Animation") } }
- item {
- Row {
- FilledIconButton(
- onClick = {},
- shapes =
- IconButtonDefaults.animatedShapes(
- shape = CutCornerShape(5.dp),
- pressedShape = RoundedCornerShape(5.dp)
- ),
- ) {
- FavoriteIcon(ButtonDefaults.IconSize)
- }
- Spacer(modifier = Modifier.width(5.dp))
- FilledIconButton(
- onClick = {},
- colors = IconButtonDefaults.filledVariantIconButtonColors(),
- shapes =
- IconButtonDefaults.animatedShapes(
- shape = CutCornerShape(5.dp),
- pressedShape = RoundedCornerShape(5.dp)
- ),
- ) {
- FavoriteIcon(ButtonDefaults.IconSize)
- }
- }
- }
item { ListHeader { Text("Sizes") } }
item {
Row(verticalAlignment = Alignment.CenterVertically) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt
index 088d524..0648dcc 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt
@@ -19,8 +19,6 @@
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CutCornerShape
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -113,7 +111,7 @@
}
item { ListHeader { Text("With onLongClick") } }
item { TextButtonWithOnLongClickSample { showOnLongClickToast(context) } }
- item { ListHeader { Text("Corner Animation") } }
+ item { ListHeader { Text("Animated") } }
item {
Row(verticalAlignment = Alignment.CenterVertically) {
TextButton(
@@ -133,34 +131,6 @@
}
}
}
- item { ListHeader { Text("Morphed Animation") } }
- item {
- Row(verticalAlignment = Alignment.CenterVertically) {
- TextButton(
- onClick = {},
- colors = TextButtonDefaults.filledTextButtonColors(),
- shapes =
- TextButtonDefaults.animatedShapes(
- shape = CutCornerShape(5.dp),
- pressedShape = RoundedCornerShape(5.dp)
- ),
- ) {
- Text(text = "ABC")
- }
- Spacer(modifier = Modifier.width(5.dp))
- TextButton(
- onClick = {},
- colors = TextButtonDefaults.filledVariantTextButtonColors(),
- shapes =
- TextButtonDefaults.animatedShapes(
- shape = CutCornerShape(5.dp),
- pressedShape = RoundedCornerShape(5.dp)
- ),
- ) {
- Text(text = "ABC")
- }
- }
- }
item { ListHeader { Text("Sizes") } }
item {
Row(verticalAlignment = Alignment.CenterVertically) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt
index 6ae2091..58ca9b8 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt
@@ -57,9 +57,7 @@
TextToggleButtonsDemo(enabled = false, initialChecked = false)
}
}
- item {
- ListHeader { Text("Text Toggle Button Shape morphing", textAlign = TextAlign.Center) }
- }
+ item { ListHeader { Text("Shape morphing", textAlign = TextAlign.Center) } }
item {
Row {
AnimatedTextToggleButtonsDemo(enabled = true, initialChecked = true)
@@ -74,11 +72,7 @@
AnimatedTextToggleButtonsDemo(enabled = false, initialChecked = false)
}
}
- item {
- ListHeader {
- Text("Text Toggle Button Shape morphing variant", textAlign = TextAlign.Center)
- }
- }
+ item { ListHeader { Text("Shape morphing variant", textAlign = TextAlign.Center) } }
item {
Row {
VariantAnimatedTextToggleButtonsDemo(enabled = true, initialChecked = true)
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
index 03d28c9..b70eeef 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
@@ -116,8 +116,6 @@
ComposableDemo("List Header") { Centralize { ListHeaderSample() } },
Material3DemoCategory("Time Text", TimeTextDemos),
ComposableDemo("Card") { CardDemo() },
- ComposableDemo("Animated Shape Buttons") { AnimatedShapeButtonDemo() },
- ComposableDemo("Animated Shape Toggle Buttons") { AnimatedShapeToggleButtonDemo() },
ComposableDemo("Text Toggle Button") { TextToggleButtonDemo() },
ComposableDemo("Icon Toggle Button") { IconToggleButtonDemo() },
ComposableDemo("Checkbox Button") { CheckboxButtonDemo() },
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
index fb631f4..a6c2594 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
@@ -27,6 +27,7 @@
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.testutils.assertContainsColor
import androidx.compose.testutils.assertShape
import androidx.compose.ui.Modifier
@@ -34,6 +35,8 @@
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsProperties
@@ -63,6 +66,7 @@
import androidx.compose.ui.unit.height
import androidx.wear.compose.material3.samples.FilledTonalCompactButtonSample
import androidx.wear.compose.material3.samples.SimpleButtonSample
+import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@@ -1187,6 +1191,54 @@
assertEquals(TextAlign.Center, labelAlignment)
}
+ @Test
+ fun button_long_click_triggers_haptic() {
+ val results = mutableMapOf<HapticFeedbackType, Int>()
+ val haptics = hapticFeedback(collectResultsFromHapticFeedback(results))
+
+ rule.setContentWithTheme {
+ CompositionLocalProvider(LocalHapticFeedback provides haptics) {
+ Button(
+ onClick = { /* Do nothing */ },
+ onLongClick = {},
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ Text("Test")
+ }
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performTouchInput { longClick() }
+
+ assertThat(results).hasSize(1)
+ assertThat(results).containsKey(HapticFeedbackType.LongPress)
+ assertThat(results[HapticFeedbackType.LongPress]).isEqualTo(1)
+ }
+
+ @Test
+ fun compactbutton_long_click_triggers_haptic() {
+ val results = mutableMapOf<HapticFeedbackType, Int>()
+ val haptics = hapticFeedback(collectResultsFromHapticFeedback(results))
+
+ rule.setContentWithTheme {
+ CompositionLocalProvider(LocalHapticFeedback provides haptics) {
+ CompactButton(
+ onClick = { /* Do nothing */ },
+ onLongClick = {},
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ Text("Test")
+ }
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performTouchInput { longClick() }
+
+ assertThat(results).hasSize(1)
+ assertThat(results).containsKey(HapticFeedbackType.LongPress)
+ assertThat(results[HapticFeedbackType.LongPress]).isEqualTo(1)
+ }
+
private fun responds_to_long_click(
enabled: Boolean,
onLongClick: () -> Unit,
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CardTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CardTest.kt
index 8c48118..98b8213 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CardTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CardTest.kt
@@ -24,10 +24,13 @@
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.testutils.assertContainsColor
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsProperties
@@ -48,6 +51,7 @@
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@@ -167,6 +171,29 @@
}
@Test
+ fun card_long_click_triggers_haptic() {
+ val results = mutableMapOf<HapticFeedbackType, Int>()
+ val haptics = hapticFeedback(collectResultsFromHapticFeedback(results))
+
+ rule.setContentWithTheme {
+ CompositionLocalProvider(LocalHapticFeedback provides haptics) {
+ Card(
+ onClick = { /* Do nothing */ },
+ onLongClick = {},
+ enabled = true,
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {}
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performTouchInput { longClick() }
+
+ assertThat(results).hasSize(1)
+ assertThat(results).containsKey(HapticFeedbackType.LongPress)
+ assertThat(results[HapticFeedbackType.LongPress]).isEqualTo(1)
+ }
+
+ @Test
fun card_does_not_respond_to_long_click_when_disabled() {
var longClicked = false
@@ -209,6 +236,33 @@
}
@Test
+ fun appCard_triggers_haptic_when_long_clicked() {
+ val results = mutableMapOf<HapticFeedbackType, Int>()
+ val haptics = hapticFeedback(collectResultsFromHapticFeedback(results))
+
+ rule.setContentWithTheme {
+ CompositionLocalProvider(LocalHapticFeedback provides haptics) {
+ AppCard(
+ onClick = { /* Do nothing */ },
+ onLongClick = {},
+ appName = {},
+ title = {},
+ enabled = true,
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ TestImage()
+ }
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performTouchInput { longClick() }
+
+ assertThat(results).hasSize(1)
+ assertThat(results).containsKey(HapticFeedbackType.LongPress)
+ assertThat(results[HapticFeedbackType.LongPress]).isEqualTo(1)
+ }
+
+ @Test
fun appCard_does_not_respond_to_long_click_when_disabled() {
var longClicked = false
@@ -252,6 +306,32 @@
}
@Test
+ fun titleCard_triggers_haptic_when_long_clicked() {
+ val results = mutableMapOf<HapticFeedbackType, Int>()
+ val haptics = hapticFeedback(collectResultsFromHapticFeedback(results))
+
+ rule.setContentWithTheme {
+ CompositionLocalProvider(LocalHapticFeedback provides haptics) {
+ TitleCard(
+ onClick = { /* Do nothing */ },
+ onLongClick = {},
+ title = {},
+ enabled = true,
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {
+ TestImage()
+ }
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performTouchInput { longClick() }
+
+ assertThat(results).hasSize(1)
+ assertThat(results).containsKey(HapticFeedbackType.LongPress)
+ assertThat(results[HapticFeedbackType.LongPress]).isEqualTo(1)
+ }
+
+ @Test
fun titleCard_does_not_respond_to_long_click_when_disabled() {
var longClicked = false
@@ -433,7 +513,7 @@
}
@Test
- public fun title_card_with_time_and_subtitle_gives_default_colors() {
+ fun title_card_with_time_and_subtitle_gives_default_colors() {
var expectedTimeColor = Color.Transparent
var expectedSubtitleColor = Color.Transparent
var expectedTitleColor = Color.Transparent
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconButtonTest.kt
index e13753d..ae3a245 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconButtonTest.kt
@@ -25,10 +25,13 @@
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.testutils.assertShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsProperties
@@ -51,6 +54,7 @@
import androidx.wear.compose.material3.IconButtonDefaults.ExtraSmallButtonSize
import androidx.wear.compose.material3.IconButtonDefaults.LargeButtonSize
import androidx.wear.compose.material3.IconButtonDefaults.SmallButtonSize
+import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@@ -151,6 +155,29 @@
}
@Test
+ fun triggers_haptic_when_long_clicked() {
+ val results = mutableMapOf<HapticFeedbackType, Int>()
+ val haptics = hapticFeedback(collectResultsFromHapticFeedback(results))
+
+ rule.setContentWithTheme {
+ CompositionLocalProvider(LocalHapticFeedback provides haptics) {
+ IconButton(
+ onClick = { /* Do nothing */ },
+ onLongClick = {},
+ enabled = true,
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {}
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performTouchInput { longClick() }
+
+ assertThat(results).hasSize(1)
+ assertThat(results).containsKey(HapticFeedbackType.LongPress)
+ assertThat(results[HapticFeedbackType.LongPress]).isEqualTo(1)
+ }
+
+ @Test
fun onLongClickLabel_includedInSemantics() {
val testLabel = "Long click action"
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextButtonTest.kt
index 3a64620..884643a 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextButtonTest.kt
@@ -25,10 +25,13 @@
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.testutils.assertShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsProperties
@@ -53,6 +56,7 @@
import androidx.wear.compose.material3.TextButtonDefaults.DefaultButtonSize
import androidx.wear.compose.material3.TextButtonDefaults.LargeButtonSize
import androidx.wear.compose.material3.TextButtonDefaults.SmallButtonSize
+import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@@ -181,6 +185,29 @@
}
@Test
+ fun triggers_haptic_when_long_clicked() {
+ val results = mutableMapOf<HapticFeedbackType, Int>()
+ val haptics = hapticFeedback(collectResultsFromHapticFeedback(results))
+
+ rule.setContentWithTheme {
+ CompositionLocalProvider(LocalHapticFeedback provides haptics) {
+ TextButton(
+ onClick = { /* Do nothing */ },
+ onLongClick = {},
+ enabled = true,
+ modifier = Modifier.testTag(TEST_TAG)
+ ) {}
+ }
+ }
+
+ rule.onNodeWithTag(TEST_TAG).performTouchInput { longClick() }
+
+ assertThat(results).hasSize(1)
+ assertThat(results).containsKey(HapticFeedbackType.LongPress)
+ assertThat(results[HapticFeedbackType.LongPress]).isEqualTo(1)
+ }
+
+ @Test
fun onLongClickLabel_includedInSemantics() {
val testLabel = "Long click action"
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
index 0f94a7d..7ef944f2 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
@@ -51,8 +51,6 @@
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.takeOrElse
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
@@ -1853,7 +1851,6 @@
interactionSource: MutableInteractionSource?,
content: @Composable RowScope.() -> Unit
) {
- val hapticFeedback = LocalHapticFeedback.current
Row(
verticalAlignment = Alignment.CenterVertically,
// Fill the container height but not its width as buttons have fixed size height but we
@@ -1866,14 +1863,7 @@
.combinedClickable(
enabled = enabled,
onClick = onClick,
- onLongClick =
- onLongClick?.let {
- {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
-
- it()
- }
- },
+ onLongClick = onLongClick, // NB CombinedClickable calls LongPress haptic
onLongClickLabel = onLongClickLabel,
role = Role.Button,
indication = ripple(),
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Card.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Card.kt
index a11d41f..cfe3743 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Card.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Card.kt
@@ -49,8 +49,6 @@
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.takeOrElse
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.LocalTransformingLazyColumnItemScope
@@ -850,7 +848,6 @@
interactionSource: MutableInteractionSource?,
content: @Composable ColumnScope.() -> Unit,
) {
- val hapticFeedback = LocalHapticFeedback.current
Column(
modifier =
modifier
@@ -859,14 +856,7 @@
.combinedClickable(
enabled = enabled,
onClick = onClick,
- onLongClick =
- onLongClick?.let {
- {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
-
- it()
- }
- },
+ onLongClick = onLongClick, // NB combinedClickable calls LongPress haptic
onLongClickLabel = onLongClickLabel,
role = null,
indication = ripple(),
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RoundButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RoundButton.kt
index 3409695..92016f0 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RoundButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RoundButton.kt
@@ -40,8 +40,6 @@
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
@@ -67,7 +65,6 @@
content: @Composable BoxScope.() -> Unit,
) {
val borderStroke = border(enabled)
- val hapticFeedback = LocalHapticFeedback.current
Box(
contentAlignment = Alignment.Center,
@@ -77,13 +74,7 @@
.clip(shape) // Clip for the touch area (e.g. for Ripple).
.combinedClickable(
onClick = onClick,
- onLongClick =
- onLongClick?.let {
- {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
- it()
- }
- },
+ onLongClick = onLongClick, // NB combinedClickable calls LongPress haptic
onLongClickLabel = onLongClickLabel,
enabled = enabled,
interactionSource = interactionSource,
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index c3306bc..24b5e35 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -383,7 +383,7 @@
@SuppressCompatibility @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD}) public @interface ProtoLayoutExperimental {
}
- @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD}) @kotlin.annotation.MustBeDocumented public @interface RequiresSchemaVersion {
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PARAMETER}) @kotlin.annotation.MustBeDocumented public @interface RequiresSchemaVersion {
method public abstract int major();
method public abstract int minor();
}
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index c3306bc..24b5e35 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -383,7 +383,7 @@
@SuppressCompatibility @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD}) public @interface ProtoLayoutExperimental {
}
- @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD}) @kotlin.annotation.MustBeDocumented public @interface RequiresSchemaVersion {
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PARAMETER}) @kotlin.annotation.MustBeDocumented public @interface RequiresSchemaVersion {
method public abstract int major();
method public abstract int minor();
}
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/RequiresSchemaVersion.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/RequiresSchemaVersion.java
index 6041b9f..84e995d 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/RequiresSchemaVersion.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/RequiresSchemaVersion.java
@@ -19,6 +19,7 @@
import static java.lang.annotation.ElementType.CONSTRUCTOR;
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.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.CLASS;
@@ -36,7 +37,7 @@
*/
@MustBeDocumented
@Retention(CLASS)
-@Target({TYPE, METHOD, CONSTRUCTOR, FIELD})
+@Target({TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER})
public @interface RequiresSchemaVersion {
int major();
diff --git a/wear/protolayout/protolayout-material3/api/current.txt b/wear/protolayout/protolayout-material3/api/current.txt
index 179ee74..7e28703 100644
--- a/wear/protolayout/protolayout-material3/api/current.txt
+++ b/wear/protolayout/protolayout-material3/api/current.txt
@@ -67,9 +67,9 @@
}
public final class CardKt {
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement appCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? avatar, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? label, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.AppCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement card(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.types.LayoutColor? backgroundColor, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> content);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement titleCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.TitleCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, optional int horizontalAlignment);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement appCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? avatar, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? label, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.AppCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement card(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.types.LayoutColor? backgroundColor, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> content);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement titleCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.TitleCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, optional int horizontalAlignment);
}
public final class ColorScheme {
@@ -139,8 +139,8 @@
}
public final class EdgeButtonKt {
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> iconContent);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> iconContent);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent);
}
public final class EdgeButtonStyle {
@@ -206,7 +206,7 @@
}
public final class TextKt {
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement text(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.TypeBuilders.StringProp text, optional androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint stringLayoutConstraint, optional int typography, optional androidx.wear.protolayout.types.LayoutColor color, optional boolean italic, optional boolean underline, optional boolean scalable, optional int maxLines, optional int multilineAlignment, optional int overflow, optional androidx.wear.protolayout.ModifiersBuilders.Modifiers modifiers);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement text(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.types.LayoutString text, optional int typography, optional androidx.wear.protolayout.types.LayoutColor color, optional boolean italic, optional boolean underline, optional boolean scalable, optional int maxLines, optional int multilineAlignment, optional int overflow, optional androidx.wear.protolayout.modifiers.LayoutModifier modifiers);
}
public final class TitleCardDefaults {
diff --git a/wear/protolayout/protolayout-material3/api/restricted_current.txt b/wear/protolayout/protolayout-material3/api/restricted_current.txt
index 179ee74..7e28703 100644
--- a/wear/protolayout/protolayout-material3/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-material3/api/restricted_current.txt
@@ -67,9 +67,9 @@
}
public final class CardKt {
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement appCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? avatar, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? label, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.AppCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement card(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.types.LayoutColor? backgroundColor, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> content);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement titleCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.TitleCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, optional int horizontalAlignment);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement appCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? avatar, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? label, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.AppCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement card(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.types.LayoutColor? backgroundColor, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> content);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement titleCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.TitleCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, optional int horizontalAlignment);
}
public final class ColorScheme {
@@ -139,8 +139,8 @@
}
public final class EdgeButtonKt {
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> iconContent);
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> iconContent);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent);
}
public final class EdgeButtonStyle {
@@ -206,7 +206,7 @@
}
public final class TextKt {
- method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement text(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.TypeBuilders.StringProp text, optional androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint stringLayoutConstraint, optional int typography, optional androidx.wear.protolayout.types.LayoutColor color, optional boolean italic, optional boolean underline, optional boolean scalable, optional int maxLines, optional int multilineAlignment, optional int overflow, optional androidx.wear.protolayout.ModifiersBuilders.Modifiers modifiers);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement text(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.types.LayoutString text, optional int typography, optional androidx.wear.protolayout.types.LayoutColor color, optional boolean italic, optional boolean underline, optional boolean scalable, optional int maxLines, optional int multilineAlignment, optional int overflow, optional androidx.wear.protolayout.modifiers.LayoutModifier modifiers);
}
public final class TitleCardDefaults {
diff --git a/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt b/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt
index 970f2cc..f20c349 100644
--- a/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt
+++ b/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt
@@ -24,8 +24,6 @@
import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement
import androidx.wear.protolayout.ModifiersBuilders
import androidx.wear.protolayout.ModifiersBuilders.Clickable
-import androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint
-import androidx.wear.protolayout.TypeBuilders.StringProp
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
import androidx.wear.protolayout.material3.AppCardStyle
import androidx.wear.protolayout.material3.CardDefaults.filledTonalCardColors
@@ -40,16 +38,20 @@
import androidx.wear.protolayout.material3.iconEdgeButton
import androidx.wear.protolayout.material3.materialScope
import androidx.wear.protolayout.material3.primaryLayout
-import androidx.wear.protolayout.material3.prop
import androidx.wear.protolayout.material3.text
import androidx.wear.protolayout.material3.textEdgeButton
import androidx.wear.protolayout.material3.titleCard
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.contentDescription
+import androidx.wear.protolayout.types.LayoutString
+import androidx.wear.protolayout.types.asLayoutConstraint
+import androidx.wear.protolayout.types.layoutString
/** Builds Material3 text element with default options. */
@Sampled
fun helloWorldTextDefault(context: Context, deviceConfiguration: DeviceParameters): LayoutElement =
materialScope(context, deviceConfiguration) {
- text(text = "Hello Material3".prop(), typography = Typography.DISPLAY_LARGE)
+ text(text = "Hello Material3".layoutString, typography = Typography.DISPLAY_LARGE)
}
/** Builds Material3 text element with some of the overridden defaults. */
@@ -61,10 +63,11 @@
materialScope(context, deviceConfiguration) {
text(
text =
- StringProp.Builder("Static")
- .setDynamicValue(DynamicString.constant("Dynamic"))
- .build(),
- stringLayoutConstraint = StringLayoutConstraint.Builder("Constraint").build(),
+ LayoutString(
+ "Static",
+ DynamicString.constant("Dynamic"),
+ "LongestConstraint".asLayoutConstraint()
+ ),
typography = Typography.DISPLAY_LARGE,
color = colorScheme.tertiary,
underline = true,
@@ -79,7 +82,10 @@
clickable: Clickable
): LayoutElement =
materialScope(context, deviceConfiguration) {
- iconEdgeButton(onClick = clickable, contentDescription = "Description of a button".prop()) {
+ iconEdgeButton(
+ onClick = clickable,
+ modifier = LayoutModifier.contentDescription("Description of a button")
+ ) {
icon(protoLayoutResourceId = "id")
}
}
@@ -91,8 +97,11 @@
clickable: Clickable
): LayoutElement =
materialScope(context, deviceConfiguration) {
- textEdgeButton(onClick = clickable, contentDescription = "Description of a button".prop()) {
- text("Hello".prop())
+ textEdgeButton(
+ onClick = clickable,
+ modifier = LayoutModifier.contentDescription("Description of a button")
+ ) {
+ text("Hello".layoutString)
}
}
@@ -104,7 +113,7 @@
): LayoutElement =
materialScope(context, deviceConfiguration) {
primaryLayout(
- titleSlot = { text("App title".prop()) },
+ titleSlot = { text("App title".layoutString) },
mainSlot = {
buttonGroup {
// To be populated with proper components
@@ -123,7 +132,14 @@
}
}
},
- bottomSlot = { iconEdgeButton(clickable, "Description".prop()) { icon("id") } }
+ bottomSlot = {
+ iconEdgeButton(
+ clickable,
+ modifier = LayoutModifier.contentDescription("Description")
+ ) {
+ icon("id")
+ }
+ }
)
}
@@ -138,12 +154,12 @@
mainSlot = {
card(
onClick = clickable,
- contentDescription = "Card with image background".prop(),
+ modifier = LayoutModifier.contentDescription("Card with image background"),
width = expand(),
height = expand(),
background = { backgroundImage(protoLayoutResourceId = "id") }
) {
- text("Content of the Card!".prop())
+ text("Content of the Card!".layoutString)
}
}
)
@@ -160,13 +176,13 @@
mainSlot = {
titleCard(
onClick = clickable,
- contentDescription = "Title Card".prop(),
+ modifier = LayoutModifier.contentDescription("Title Card"),
height = expand(),
colors = filledVariantCardColors(),
style = largeTitleCardStyle(),
- title = { text("This is title of the title card".prop()) },
- time = { text("NOW".prop()) },
- content = { text("Content of the Card!".prop()) }
+ title = { text("This is title of the title card".layoutString) },
+ time = { text("NOW".layoutString) },
+ content = { text("Content of the Card!".layoutString) }
)
}
)
@@ -183,14 +199,14 @@
mainSlot = {
appCard(
onClick = clickable,
- contentDescription = "App Card".prop(),
+ modifier = LayoutModifier.contentDescription("App Card"),
height = expand(),
colors = filledTonalCardColors(),
style = AppCardStyle.largeAppCardStyle(),
- title = { text("This is title of the app card".prop()) },
- time = { text("NOW".prop()) },
- label = { text("Label".prop()) },
- content = { text("Content of the Card!".prop()) },
+ title = { text("This is title of the app card".layoutString) },
+ time = { text("NOW".layoutString) },
+ label = { text("Label".layoutString) },
+ content = { text("Content of the Card!".layoutString) },
)
}
)
diff --git a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
index ed7c966..ff49447 100644
--- a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
+++ b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
@@ -32,7 +32,10 @@
import androidx.wear.protolayout.material3.ButtonDefaults.filledVariantButtonColors
import androidx.wear.protolayout.material3.CardDefaults.filledVariantCardColors
import androidx.wear.protolayout.material3.MaterialGoldenTest.Companion.pxToDp
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.contentDescription
import androidx.wear.protolayout.types.LayoutColor
+import androidx.wear.protolayout.types.layoutString
import com.google.common.collect.ImmutableMap
private const val CONTENT_DESCRIPTION_PLACEHOLDER = "Description"
@@ -79,19 +82,20 @@
primaryLayoutWithOverrideIcon(
mainSlot = {
text(
- text = "Text in the main slot that overflows".prop(),
+ text = "Text in the main slot that overflows".layoutString,
color = colorScheme.secondary
)
},
bottomSlot = {
textEdgeButton(
onClick = clickable,
- labelContent = { text("Action".prop()) },
- contentDescription = CONTENT_DESCRIPTION_PLACEHOLDER.prop(),
+ labelContent = { text("Action".layoutString) },
+ modifier =
+ LayoutModifier.contentDescription(CONTENT_DESCRIPTION_PLACEHOLDER),
colors = filledButtonColors()
)
},
- titleSlot = { text("Title".prop()) },
+ titleSlot = { text("Title".layoutString) },
overrideIcon = true
)
}
@@ -121,8 +125,9 @@
bottomSlot = {
textEdgeButton(
onClick = clickable,
- labelContent = { text("Action that overflows".prop()) },
- contentDescription = CONTENT_DESCRIPTION_PLACEHOLDER.prop(),
+ labelContent = { text("Action that overflows".layoutString) },
+ modifier =
+ LayoutModifier.contentDescription(CONTENT_DESCRIPTION_PLACEHOLDER),
colors = filledVariantButtonColors()
)
},
@@ -139,13 +144,13 @@
mainSlot = {
card(
onClick = clickable,
- contentDescription = "Card".prop(),
+ modifier = LayoutModifier.contentDescription("Card"),
width = expand(),
height = expand(),
background = { backgroundImage(protoLayoutResourceId = IMAGE_ID) }
) {
text(
- "Card with image background".prop(),
+ "Card with image background".layoutString,
color = colorScheme.onBackground
)
}
@@ -154,11 +159,14 @@
iconEdgeButton(
onClick = clickable,
iconContent = { icon(ICON_ID) },
- contentDescription = CONTENT_DESCRIPTION_PLACEHOLDER.prop(),
+ modifier =
+ LayoutModifier.contentDescription(CONTENT_DESCRIPTION_PLACEHOLDER),
colors = filledTonalButtonColors()
)
},
- titleSlot = { text("Title that overflows".prop(), color = colorScheme.error) }
+ titleSlot = {
+ text("Title that overflows".layoutString, color = colorScheme.error)
+ }
)
}
testCases["primarylayout_titlecard_bottomslot_golden$goldenSuffix"] =
@@ -171,21 +179,21 @@
mainSlot = {
titleCard(
onClick = clickable,
- contentDescription = "Card".prop(),
+ modifier = LayoutModifier.contentDescription("Card"),
height = expand(),
title = {
text(
"Title Card text that will overflow after 2 max lines of text"
- .prop()
+ .layoutString
)
},
- time = { text("Now".prop()) },
- content = { text("Default title card".prop()) },
+ time = { text("Now".layoutString) },
+ content = { text("Default title card".layoutString) },
colors = filledVariantCardColors()
)
},
- bottomSlot = { text("Bottom Slot that overflows".prop()) },
- titleSlot = { text("TitleCard".prop(), color = colorScheme.secondaryDim) }
+ bottomSlot = { text("Bottom Slot that overflows".layoutString) },
+ titleSlot = { text("TitleCard".layoutString, color = colorScheme.secondaryDim) }
)
}
testCases["primarylayout_bottomslot_withlabel_golden$goldenSuffix"] =
@@ -198,9 +206,11 @@
mainSlot = {
coloredBox(color = colorScheme.errorContainer, shape = shapes.extraLarge)
},
- bottomSlot = { text("Bottom Slot".prop()) },
- labelForBottomSlot = { text("Label in bottom slot overflows".prop()) },
- titleSlot = { text("Title".prop(), color = colorScheme.secondaryContainer) }
+ bottomSlot = { text("Bottom Slot".layoutString) },
+ labelForBottomSlot = { text("Label in bottom slot overflows".layoutString) },
+ titleSlot = {
+ text("Title".layoutString, color = colorScheme.secondaryContainer)
+ }
)
}
testCases["primarylayout_nobottomslot_golden$goldenSuffix"] =
@@ -213,8 +223,10 @@
mainSlot = {
coloredBox(color = colorScheme.tertiaryContainer, shape = shapes.extraLarge)
},
- labelForBottomSlot = { text("Ignored Label in bottom slot".prop()) },
- titleSlot = { text("Title".prop(), color = colorScheme.secondaryContainer) }
+ labelForBottomSlot = { text("Ignored Label in bottom slot".layoutString) },
+ titleSlot = {
+ text("Title".layoutString, color = colorScheme.secondaryContainer)
+ }
)
}
testCases["primarylayout_nobottomslotnotitle_golden$NORMAL_SCALE_SUFFIX"] =
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
index 39391fe..84f52a6 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
@@ -29,9 +29,8 @@
import androidx.wear.protolayout.ModifiersBuilders.Background
import androidx.wear.protolayout.ModifiersBuilders.Clickable
import androidx.wear.protolayout.ModifiersBuilders.Corner
-import androidx.wear.protolayout.ModifiersBuilders.Modifiers
import androidx.wear.protolayout.ModifiersBuilders.Padding
-import androidx.wear.protolayout.TypeBuilders.StringProp
+import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
import androidx.wear.protolayout.material3.AppCardDefaults.buildContentForAppCard
import androidx.wear.protolayout.material3.AppCardStyle.Companion.defaultAppCardStyle
import androidx.wear.protolayout.material3.CardDefaults.DEFAULT_CONTENT_PADDING
@@ -39,6 +38,10 @@
import androidx.wear.protolayout.material3.CardDefaults.filledCardColors
import androidx.wear.protolayout.material3.TitleCardDefaults.buildContentForTitleCard
import androidx.wear.protolayout.material3.TitleCardStyle.Companion.defaultTitleCardStyle
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.contentDescription
+import androidx.wear.protolayout.modifiers.semanticsRole
+import androidx.wear.protolayout.modifiers.toProtoLayoutModifiersBuilder
import androidx.wear.protolayout.types.LayoutColor
/**
@@ -48,9 +51,10 @@
*
* @param onClick Associated [Clickable] for click events. When the card is clicked it will fire the
* associated action.
- * @param contentDescription The content description to be read by Talkback.
* @param title A slot for displaying the title of the card, expected to be one or two lines of
* text. Uses [CardColors.title] color by default.
+ * @param modifier Modifiers to set to this element. It's highly recommended to set a content
+ * description using [contentDescription].
* @param content The optional body content of the card. Uses [CardColors.content] color by default.
* @param time An optional slot for displaying the time relevant to the contents of the card,
* expected to be a short piece of text. Uses [CardColors.time] color by default.
@@ -83,8 +87,8 @@
// TODO: b/373578620 - Add how corners affects margins in the layout.
public fun MaterialScope.titleCard(
onClick: Clickable,
- contentDescription: StringProp,
title: (MaterialScope.() -> LayoutElement),
+ modifier: LayoutModifier = LayoutModifier,
content: (MaterialScope.() -> LayoutElement)? = null,
time: (MaterialScope.() -> LayoutElement)? = null,
height: ContainerDimension = wrapWithMinTapTargetDimension(),
@@ -99,7 +103,7 @@
): LayoutElement =
card(
onClick = onClick,
- contentDescription = contentDescription,
+ modifier = modifier,
width = expand(),
height = height,
shape = shape,
@@ -167,9 +171,10 @@
*
* @param onClick Associated [Clickable] for click events. When the card is clicked it will fire the
* associated action.
- * @param contentDescription The content description to be read by Talkback.
* @param title A slot for displaying the title of the card, expected to be one line of text. Uses
* [CardColors.title] color by default.
+ * @param modifier Modifiers to set to this element. It's highly recommended to set a content
+ * description using [contentDescription].
* @param content The optional body content of the card. Uses [CardColors.content] color by default.
* @param avatar An optional slot in header for displaying small image, such as [avatarImage].
* @param label An optional slot in header for displaying short, label text. Uses [CardColors.label]
@@ -203,8 +208,8 @@
// TODO: b/373578620 - Add how corners affects margins in the layout.
public fun MaterialScope.appCard(
onClick: Clickable,
- contentDescription: StringProp,
title: (MaterialScope.() -> LayoutElement),
+ modifier: LayoutModifier = LayoutModifier,
content: (MaterialScope.() -> LayoutElement)? = null,
avatar: (MaterialScope.() -> LayoutElement)? = null,
label: (MaterialScope.() -> LayoutElement)? = null,
@@ -219,7 +224,7 @@
): LayoutElement =
card(
onClick = onClick,
- contentDescription = contentDescription,
+ modifier = modifier,
width = expand(),
height = height,
shape = shape,
@@ -302,7 +307,8 @@
*
* @param onClick Associated [Clickable] for click events. When the card is clicked it will fire the
* associated action.
- * @param contentDescription The content description to be read by Talkback.
+ * @param modifier Modifiers to set to this element. It's highly recommended to set a content
+ * description using [contentDescription].
* @param shape Defines the card's shape, in other words the corner radius for this card.
* @param backgroundColor The color to be used as a background of this card. If the background image
* is also specified, the image will be laid out on top of this color. In case of the fully opaque
@@ -323,7 +329,7 @@
// TODO: b/373578620 - Add how corners affects margins in the layout.
public fun MaterialScope.card(
onClick: Clickable,
- contentDescription: StringProp,
+ modifier: LayoutModifier = LayoutModifier,
width: ContainerDimension = wrapWithMinTapTargetDimension(),
height: ContainerDimension = wrapWithMinTapTargetDimension(),
shape: Corner = shapes.large,
@@ -336,10 +342,11 @@
backgroundColor?.let { backgroundBuilder.setColor(it.prop) }
+ val defaultModifier = LayoutModifier.semanticsRole(SEMANTICS_ROLE_BUTTON) then modifier
val modifiers =
- Modifiers.Builder()
+ defaultModifier
+ .toProtoLayoutModifiersBuilder()
.setClickable(onClick)
- .setSemantics(contentDescription.buttonRoleSemantics())
.setMetadata(METADATA_TAG.toElementMetadata())
.setBackground(backgroundBuilder.build())
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
index 65142f6..227f92c 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
@@ -28,7 +28,7 @@
import androidx.wear.protolayout.ModifiersBuilders.Corner
import androidx.wear.protolayout.ModifiersBuilders.Modifiers
import androidx.wear.protolayout.ModifiersBuilders.Padding
-import androidx.wear.protolayout.TypeBuilders.StringProp
+import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
import androidx.wear.protolayout.material3.ButtonDefaults.filledButtonColors
import androidx.wear.protolayout.material3.EdgeButtonDefaults.BOTTOM_MARGIN_DP
import androidx.wear.protolayout.material3.EdgeButtonDefaults.EDGE_BUTTON_HEIGHT_DP
@@ -41,6 +41,10 @@
import androidx.wear.protolayout.material3.EdgeButtonDefaults.TOP_CORNER_RADIUS
import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.DEFAULT
import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.TOP_ALIGN
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.contentDescription
+import androidx.wear.protolayout.modifiers.semanticsRole
+import androidx.wear.protolayout.modifiers.toProtoLayoutModifiersBuilder
/**
* ProtoLayout Material3 component edge button that offers a single slot to take an icon or similar
@@ -56,7 +60,8 @@
*
* @param onClick Associated [Clickable] for click events. When the button is clicked it will fire
* the associated action.
- * @param contentDescription The content description to be read by Talkback.
+ * @param modifier Modifiers to set to this element. It's highly recommended to set a content
+ * description using [contentDescription].
* @param colors The colors used for this button. If not set, [ButtonDefaults.filledButtonColors]
* will be used as high emphasis button. Other recommended colors are
* [ButtonDefaults.filledTonalButtonColors] and [ButtonDefaults.filledVariantButtonColors]. If
@@ -69,16 +74,11 @@
// TODO: b/346958146 - link EdgeButton visuals in DAC
public fun MaterialScope.iconEdgeButton(
onClick: Clickable,
- contentDescription: StringProp,
+ modifier: LayoutModifier = LayoutModifier,
colors: ButtonColors = filledButtonColors(),
iconContent: (MaterialScope.() -> LayoutElement)
): LayoutElement =
- edgeButton(
- onClick = onClick,
- contentDescription = contentDescription,
- colors = colors,
- style = DEFAULT
- ) {
+ edgeButton(onClick = onClick, modifier = modifier, colors = colors, style = DEFAULT) {
withStyle(defaultIconStyle = IconStyle(size = ICON_SIZE_DP.toDp(), tintColor = colors.icon))
.iconContent()
}
@@ -97,7 +97,8 @@
*
* @param onClick Associated [Clickable] for click events. When the button is clicked it will fire
* the associated action.
- * @param contentDescription The content description to be read by Talkback.
+ * @param modifier Modifiers to set to this element. It's highly recommended to set a content
+ * description using [contentDescription].
* @param colors The colors used for this button. If not set, [ButtonDefaults.filledButtonColors]
* will be used as high emphasis button. Other recommended colors are
* [ButtonDefaults.filledTonalButtonColors] and [ButtonDefaults.filledVariantButtonColors]. If
@@ -110,16 +111,11 @@
// TODO(b/346958146): link EdgeButton visuals in DAC
public fun MaterialScope.textEdgeButton(
onClick: Clickable,
- contentDescription: StringProp,
+ modifier: LayoutModifier = LayoutModifier,
colors: ButtonColors = filledButtonColors(),
labelContent: (MaterialScope.() -> LayoutElement)
): LayoutElement =
- edgeButton(
- onClick = onClick,
- contentDescription = contentDescription,
- colors = colors,
- style = TOP_ALIGN
- ) {
+ edgeButton(onClick = onClick, modifier = modifier, colors = colors, style = TOP_ALIGN) {
withStyle(
defaultTextElementStyle =
TextElementStyle(
@@ -144,12 +140,13 @@
*
* @param onClick Associated [Clickable] for click events. When the button is clicked it will fire
* the associated action.
- * @param contentDescription The content description to be read by Talkback.
* @param colors The colors used for this button. If not set, [ButtonDefaults.filledButtonColors]
* will be used as high emphasis button. Other recommended colors are
* [ButtonDefaults.filledTonalButtonColors] and [ButtonDefaults.filledVariantButtonColors]. If
* using custom colors, it is important to choose a color pair from same role to ensure
* accessibility with sufficient color contrast.
+ * @param modifier Modifiers to set to this element. It's highly recommended to set a content
+ * description using [contentDescription].
* @param style The style used for the inner content, specifying how the content should be aligned.
* It is recommended to use [EdgeButtonStyle.TOP_ALIGN] for long, wide content. If not set,
* defaults to [EdgeButtonStyle.DEFAULT] which center-aligns the content.
@@ -159,8 +156,8 @@
// TODO(b/346958146): link EdgeButton visuals in DAC
private fun MaterialScope.edgeButton(
onClick: Clickable,
- contentDescription: StringProp,
colors: ButtonColors,
+ modifier: LayoutModifier = LayoutModifier,
style: EdgeButtonStyle = DEFAULT,
content: MaterialScope.() -> LayoutElement
): LayoutElement {
@@ -173,10 +170,11 @@
val bottomCornerRadiusX = dp(edgeButtonWidth / 2f)
val bottomCornerRadiusY = dp(EDGE_BUTTON_HEIGHT_DP - TOP_CORNER_RADIUS.value)
- val modifiers: Modifiers.Builder =
- Modifiers.Builder()
+ val defaultModifier = LayoutModifier.semanticsRole(SEMANTICS_ROLE_BUTTON) then modifier
+ val modifiers =
+ defaultModifier
+ .toProtoLayoutModifiersBuilder()
.setClickable(onClick)
- .setSemantics(contentDescription.buttonRoleSemantics())
.setBackground(
Background.Builder()
.setColor(colors.container.prop)
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
index 36c3f6f..4cf5b75 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
@@ -22,8 +22,6 @@
import androidx.annotation.Dimension.Companion.DP
import androidx.annotation.Dimension.Companion.SP
import androidx.annotation.FloatRange
-import androidx.annotation.RestrictTo
-import androidx.wear.protolayout.ColorBuilders.argb
import androidx.wear.protolayout.DimensionBuilders.DpProp
import androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp
import androidx.wear.protolayout.DimensionBuilders.dp
@@ -43,9 +41,6 @@
import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata
import androidx.wear.protolayout.ModifiersBuilders.Modifiers
import androidx.wear.protolayout.ModifiersBuilders.Padding
-import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
-import androidx.wear.protolayout.ModifiersBuilders.Semantics
-import androidx.wear.protolayout.TypeBuilders.StringProp
import androidx.wear.protolayout.materialcore.fontscaling.FontScaleConverterFactory
import androidx.wear.protolayout.types.LayoutColor
import androidx.wear.protolayout.types.argb
@@ -85,9 +80,6 @@
@Dimension(unit = SP) private fun Float.dpToSpLinear(fontScale: Float): Float = this / fontScale
-internal fun StringProp.buttonRoleSemantics() =
- Semantics.Builder().setContentDescription(this).setRole(SEMANTICS_ROLE_BUTTON).build()
-
internal fun Int.toDp() = dp(this.toFloat())
internal fun String.toElementMetadata() = ElementMetadata.Builder().setTagData(toTagBytes()).build()
@@ -100,9 +92,6 @@
internal fun verticalSpacer(@Dimension(unit = DP) widthDp: Int): Spacer =
Spacer.Builder().setWidth(widthDp.toDp()).setHeight(expand()).build()
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun String.prop(): StringProp = StringProp.Builder(this).build()
-
/**
* Returns [wrap] but with minimum dimension of [MINIMUM_TAP_TARGET_SIZE] for accessibility
* requirements of tap targets.
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Text.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Text.kt
index 5faf4b5..074afc8 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Text.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Text.kt
@@ -17,14 +17,13 @@
package androidx.wear.protolayout.material3
import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement
-import androidx.wear.protolayout.LayoutElementBuilders.Text
import androidx.wear.protolayout.LayoutElementBuilders.TextAlignment
import androidx.wear.protolayout.LayoutElementBuilders.TextOverflow
-import androidx.wear.protolayout.ModifiersBuilders.Modifiers
-import androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint
-import androidx.wear.protolayout.TypeBuilders.StringProp
+import androidx.wear.protolayout.layout.basicText
import androidx.wear.protolayout.material3.Typography.TypographyToken
+import androidx.wear.protolayout.modifiers.LayoutModifier
import androidx.wear.protolayout.types.LayoutColor
+import androidx.wear.protolayout.types.LayoutString
/**
* ProtoLayout component that represents text object holding any information.
@@ -33,8 +32,6 @@
* [Typography].
*
* @param text The text content for this component.
- * @param stringLayoutConstraint The layout constraints used to correctly measure Text view size and
- * align text when `text` has dynamic value.
* @param typography The typography from [Typography] to be applied to this text. This will have
* predefined default value specified by each components that uses this text, to achieve the
* recommended look.
@@ -46,14 +43,12 @@
* @param maxLines The maximum number of lines that text can occupy.
* @param multilineAlignment The horizontal alignment of the multiple lines of text.
* @param overflow The overflow strategy when text doesn't have enough space to be shown.
- * @param modifiers The additional [Modifiers] for this text.
+ * @param modifiers Modifiers to set to this element.
* @sample androidx.wear.protolayout.material3.samples.helloWorldTextDefault
* @sample androidx.wear.protolayout.material3.samples.helloWorldTextDynamicCustom
*/
public fun MaterialScope.text(
- text: StringProp,
- stringLayoutConstraint: StringLayoutConstraint =
- StringLayoutConstraint.Builder(text.value).build(),
+ text: LayoutString,
@TypographyToken typography: Int = defaultTextElementStyle.typography,
color: LayoutColor = defaultTextElementStyle.color,
italic: Boolean = defaultTextElementStyle.italic,
@@ -62,20 +57,18 @@
maxLines: Int = defaultTextElementStyle.maxLines,
@TextAlignment multilineAlignment: Int = defaultTextElementStyle.multilineAlignment,
@TextOverflow overflow: Int = defaultTextElementStyle.overflow,
- modifiers: Modifiers = Modifiers.Builder().build()
+ modifiers: LayoutModifier = LayoutModifier
): LayoutElement =
- Text.Builder()
- .setText(text)
- .setLayoutConstraintsForDynamicText(stringLayoutConstraint)
- .setFontStyle(
+ basicText(
+ text = text,
+ fontStyle =
createFontStyleBuilder(typographyToken = typography, deviceConfiguration, scalable)
.setColor(color.prop)
.setItalic(italic)
.setUnderline(underline)
- .build()
- )
- .setMaxLines(maxLines)
- .setMultilineAlignment(multilineAlignment)
- .setOverflow(overflow)
- .setModifiers(modifiers)
- .build()
+ .build(),
+ maxLines = maxLines,
+ multilineAlignment = multilineAlignment,
+ overflow = overflow,
+ modifier = modifiers
+ )
diff --git a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CardTest.kt b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CardTest.kt
index f96a668..9d2b8aa 100644
--- a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CardTest.kt
+++ b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CardTest.kt
@@ -22,6 +22,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.wear.protolayout.DeviceParametersBuilders
import androidx.wear.protolayout.DimensionBuilders.expand
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.contentDescription
import androidx.wear.protolayout.testing.LayoutElementAssertionsProvider
import androidx.wear.protolayout.testing.hasClickable
import androidx.wear.protolayout.testing.hasColor
@@ -32,6 +34,7 @@
import androidx.wear.protolayout.testing.hasText
import androidx.wear.protolayout.testing.hasWidth
import androidx.wear.protolayout.types.argb
+import androidx.wear.protolayout.types.layoutString
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.internal.DoNotInstrument
@@ -151,10 +154,10 @@
materialScope(CONTEXT, DEVICE_CONFIGURATION) {
card(
onClick = CLICKABLE,
- contentDescription = CONTENT_DESCRIPTION.prop(),
+ modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
background = { backgroundImage(IMAGE_ID) }
) {
- text(TEXT.prop())
+ text(TEXT.layoutString)
}
}
@@ -169,10 +172,10 @@
materialScope(CONTEXT, DEVICE_CONFIGURATION) {
card(
onClick = CLICKABLE,
- contentDescription = CONTENT_DESCRIPTION.prop(),
+ modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
backgroundColor = color.argb
) {
- text(TEXT.prop())
+ text(TEXT.layoutString)
}
}
@@ -190,7 +193,7 @@
materialScope(CONTEXT, DEVICE_CONFIGURATION) {
titleCard(
onClick = CLICKABLE,
- contentDescription = CONTENT_DESCRIPTION.prop(),
+ modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
colors =
CardColors(
background = backgroundColor.argb,
@@ -198,9 +201,9 @@
content = contentColor.argb,
time = timeColor.argb
),
- title = { text(TEXT.prop()) },
- content = { text(TEXT2.prop()) },
- time = { text(TEXT3.prop()) },
+ title = { text(TEXT.layoutString) },
+ content = { text(TEXT2.layoutString) },
+ time = { text(TEXT3.layoutString) },
)
}
@@ -224,7 +227,7 @@
materialScope(CONTEXT, DEVICE_CONFIGURATION) {
appCard(
onClick = CLICKABLE,
- contentDescription = CONTENT_DESCRIPTION.prop(),
+ modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
colors =
CardColors(
background = backgroundColor.argb,
@@ -233,10 +236,10 @@
time = timeColor.argb,
label = labelColor.argb
),
- title = { text(TEXT.prop()) },
- content = { text(TEXT2.prop()) },
- time = { text(TEXT3.prop()) },
- label = { text(TEXT4.prop()) },
+ title = { text(TEXT.layoutString) },
+ content = { text(TEXT2.layoutString) },
+ time = { text(TEXT3.layoutString) },
+ label = { text(TEXT4.layoutString) },
)
}
@@ -259,11 +262,11 @@
materialScope(CONTEXT, DEVICE_CONFIGURATION) {
card(
onClick = CLICKABLE,
- contentDescription = CONTENT_DESCRIPTION.prop(),
+ modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
width = expand(),
height = height.toDp()
) {
- text(TEXT.prop())
+ text(TEXT.layoutString)
}
}
@@ -297,8 +300,11 @@
private val DEFAULT_CONTAINER_CARD_WITH_TEXT =
materialScope(CONTEXT, DEVICE_CONFIGURATION) {
- card(onClick = CLICKABLE, contentDescription = CONTENT_DESCRIPTION.prop()) {
- text(TEXT.prop())
+ card(
+ onClick = CLICKABLE,
+ modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION)
+ ) {
+ text(TEXT.layoutString)
}
}
@@ -306,10 +312,10 @@
materialScope(CONTEXT, DEVICE_CONFIGURATION) {
titleCard(
onClick = CLICKABLE,
- contentDescription = CONTENT_DESCRIPTION.prop(),
- title = { text(TEXT.prop()) },
- content = { text(TEXT2.prop()) },
- time = { text(TEXT3.prop()) },
+ modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
+ title = { text(TEXT.layoutString) },
+ content = { text(TEXT2.layoutString) },
+ time = { text(TEXT3.layoutString) },
)
}
@@ -317,12 +323,12 @@
materialScope(CONTEXT, DEVICE_CONFIGURATION) {
appCard(
onClick = CLICKABLE,
- contentDescription = CONTENT_DESCRIPTION.prop(),
- title = { text(TEXT.prop()) },
- content = { text(TEXT2.prop()) },
- time = { text(TEXT3.prop()) },
+ modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
+ title = { text(TEXT.layoutString) },
+ content = { text(TEXT2.layoutString) },
+ time = { text(TEXT3.layoutString) },
avatar = { avatarImage(AVATAR_ID) },
- label = { text(TEXT4.prop()) }
+ label = { text(TEXT4.layoutString) }
)
}
}
diff --git a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
index 0e0f7b7..7a85fae 100644
--- a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
+++ b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
@@ -23,11 +23,12 @@
import androidx.wear.protolayout.DeviceParametersBuilders
import androidx.wear.protolayout.LayoutElementBuilders.Image
import androidx.wear.protolayout.ModifiersBuilders.Clickable
-import androidx.wear.protolayout.TypeBuilders.StringProp
import androidx.wear.protolayout.expression.AppDataKey
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32
import androidx.wear.protolayout.material3.EdgeButtonDefaults.BOTTOM_MARGIN_DP
import androidx.wear.protolayout.material3.EdgeButtonDefaults.EDGE_BUTTON_HEIGHT_DP
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.contentDescription
import androidx.wear.protolayout.testing.LayoutElementAssertionsProvider
import androidx.wear.protolayout.testing.LayoutElementMatcher
import androidx.wear.protolayout.testing.hasColor
@@ -37,6 +38,9 @@
import androidx.wear.protolayout.testing.hasText
import androidx.wear.protolayout.testing.hasWidth
import androidx.wear.protolayout.testing.isClickable
+import androidx.wear.protolayout.types.LayoutString
+import androidx.wear.protolayout.types.asLayoutConstraint
+import androidx.wear.protolayout.types.layoutString
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.internal.DoNotInstrument
@@ -63,7 +67,7 @@
fun contentDescription() {
LayoutElementAssertionsProvider(ICON_EDGE_BUTTON)
.onElement(isClickable())
- .assert(hasContentDescription(CONTENT_DESCRIPTION.value))
+ .assert(hasContentDescription(CONTENT_DESCRIPTION))
}
@Test
@@ -92,7 +96,7 @@
materialScope(CONTEXT, DEVICE_CONFIGURATION, allowDynamicTheme = false) {
iconEdgeButton(
onClick = CLICKABLE,
- contentDescription = CONTENT_DESCRIPTION,
+ modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
colors =
ButtonColors(
container = COLOR_SCHEME.tertiaryContainer,
@@ -117,8 +121,11 @@
val label = "static text"
val textEdgeButton =
materialScope(CONTEXT, DEVICE_CONFIGURATION, allowDynamicTheme = false) {
- textEdgeButton(onClick = CLICKABLE, contentDescription = CONTENT_DESCRIPTION) {
- text(label.prop())
+ textEdgeButton(
+ onClick = CLICKABLE,
+ modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION)
+ ) {
+ text(label.layoutString)
}
}
@@ -132,14 +139,19 @@
val label = "test text"
val stateKey = AppDataKey<DynamicInt32>("testKey")
val dynamicLabel =
- StringProp.Builder(label)
- .setDynamicValue(DynamicInt32.from(stateKey).times(2).format())
- .build()
+ LayoutString(
+ label,
+ DynamicInt32.from(stateKey).times(2).format(),
+ label.asLayoutConstraint()
+ )
val queryProvider =
LayoutElementAssertionsProvider(
materialScope(CONTEXT, DEVICE_CONFIGURATION, allowDynamicTheme = false) {
- textEdgeButton(onClick = CLICKABLE, contentDescription = CONTENT_DESCRIPTION) {
+ textEdgeButton(
+ onClick = CLICKABLE,
+ modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION)
+ ) {
text(dynamicLabel)
}
}
@@ -166,12 +178,15 @@
.setId("action_id")
.build()
- private val CONTENT_DESCRIPTION = "it is an edge button".prop()
+ private const val CONTENT_DESCRIPTION = "it is an edge button"
- private val RES_ID = "resId"
+ private const val RES_ID = "resId"
private val ICON_EDGE_BUTTON =
materialScope(CONTEXT, DEVICE_CONFIGURATION, allowDynamicTheme = false) {
- iconEdgeButton(onClick = CLICKABLE, contentDescription = CONTENT_DESCRIPTION) {
+ iconEdgeButton(
+ onClick = CLICKABLE,
+ modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION)
+ ) {
icon(RES_ID)
}
}
diff --git a/wear/protolayout/protolayout-testing/api/current.txt b/wear/protolayout/protolayout-testing/api/current.txt
index 47ceaab..1503d9d1 100644
--- a/wear/protolayout/protolayout-testing/api/current.txt
+++ b/wear/protolayout/protolayout-testing/api/current.txt
@@ -12,7 +12,7 @@
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasHeight(androidx.wear.protolayout.DimensionBuilders.ContainerDimension height);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasHeight(androidx.wear.protolayout.DimensionBuilders.ProportionalDimensionProp height);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasImage(String protolayoutResId);
- method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(androidx.wear.protolayout.TypeBuilders.StringProp value);
+ method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(androidx.wear.protolayout.types.LayoutString value);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(String value);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(String value, optional boolean subString);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(String value, optional boolean subString, optional boolean ignoreCase);
diff --git a/wear/protolayout/protolayout-testing/api/restricted_current.txt b/wear/protolayout/protolayout-testing/api/restricted_current.txt
index 47ceaab..1503d9d1 100644
--- a/wear/protolayout/protolayout-testing/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-testing/api/restricted_current.txt
@@ -12,7 +12,7 @@
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasHeight(androidx.wear.protolayout.DimensionBuilders.ContainerDimension height);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasHeight(androidx.wear.protolayout.DimensionBuilders.ProportionalDimensionProp height);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasImage(String protolayoutResId);
- method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(androidx.wear.protolayout.TypeBuilders.StringProp value);
+ method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(androidx.wear.protolayout.types.LayoutString value);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(String value);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(String value, optional boolean subString);
method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(String value, optional boolean subString, optional boolean ignoreCase);
diff --git a/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt b/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt
index 2e024cc..8a8a141 100644
--- a/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt
+++ b/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt
@@ -33,8 +33,8 @@
import androidx.wear.protolayout.LayoutElementBuilders.Spannable
import androidx.wear.protolayout.LayoutElementBuilders.Text
import androidx.wear.protolayout.ModifiersBuilders.Clickable
-import androidx.wear.protolayout.TypeBuilders.StringProp
import androidx.wear.protolayout.proto.DimensionProto
+import androidx.wear.protolayout.types.LayoutString
/** Returns a [LayoutElementMatcher] which checks whether the element is clickable. */
public fun isClickable(): LayoutElementMatcher =
@@ -89,12 +89,12 @@
/**
* Returns a [LayoutElementMatcher] which checks whether the element's text equals the given value.
*/
-public fun hasText(value: StringProp): LayoutElementMatcher =
+public fun hasText(value: LayoutString): LayoutElementMatcher =
LayoutElementMatcher("Element text = '$value'") {
it is Text &&
// TODO: b/375448507 - Add dynamic data evaluation and compare the current string value
- it.text?.toProto()?.value == value.toProto().value &&
- it.text?.toProto()?.dynamicValue == value.toProto().dynamicValue
+ it.text?.toProto()?.value == value.staticValue &&
+ it.text?.toProto()?.dynamicValue == value.dynamicValue?.toDynamicStringProto()
}
/**
diff --git a/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt b/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt
index 6ca1a48..ba732a8 100644
--- a/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt
+++ b/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt
@@ -31,16 +31,18 @@
import androidx.wear.protolayout.LayoutElementBuilders.Image
import androidx.wear.protolayout.LayoutElementBuilders.Row
import androidx.wear.protolayout.LayoutElementBuilders.Spacer
-import androidx.wear.protolayout.LayoutElementBuilders.Text
import androidx.wear.protolayout.ModifiersBuilders.Background
import androidx.wear.protolayout.ModifiersBuilders.Clickable
import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata
import androidx.wear.protolayout.ModifiersBuilders.Modifiers
import androidx.wear.protolayout.ModifiersBuilders.Semantics
import androidx.wear.protolayout.StateBuilders
-import androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint
import androidx.wear.protolayout.TypeBuilders.StringProp
import androidx.wear.protolayout.expression.DynamicBuilders
+import androidx.wear.protolayout.layout.basicText
+import androidx.wear.protolayout.types.LayoutString
+import androidx.wear.protolayout.types.asLayoutConstraint
+import androidx.wear.protolayout.types.layoutString
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -142,7 +144,7 @@
@Test
fun hasText() {
val textContent = "random test content"
- val testElement = Text.Builder().setText(textContent).build()
+ val testElement = basicText(textContent.layoutString)
assertThat(hasText(textContent).matches(testElement)).isTrue()
assertThat(hasText("blabla").matches(testElement)).isFalse()
@@ -151,16 +153,12 @@
@Test
fun hasDynamicText() {
val textContent =
- StringProp.Builder("static content")
- .setDynamicValue(DynamicBuilders.DynamicString.constant("dynamic content"))
- .build()
- val testElement =
- Text.Builder()
- .setText(textContent)
- .setLayoutConstraintsForDynamicText(
- StringLayoutConstraint.Builder("static content").build()
- )
- .build()
+ LayoutString(
+ "static content",
+ DynamicBuilders.DynamicString.constant("dynamic content"),
+ "static content".asLayoutConstraint()
+ )
+ val testElement = basicText(textContent)
assertThat(hasText(textContent).matches(testElement)).isTrue()
assertThat(hasText("blabla").matches(testElement)).isFalse()
@@ -197,12 +195,11 @@
@Test
fun hasColor_onTextStyle() {
val testText =
- Text.Builder()
- .setText("text")
- .setFontStyle(
+ basicText(
+ "text".layoutString,
+ fontStyle =
FontStyle.Builder().setColor(ColorProp.Builder(Color.CYAN).build()).build()
- )
- .build()
+ )
assertThat(hasColor(Color.CYAN).matches(testText)).isTrue()
assertThat(hasColor(Color.GREEN).matches(testText)).isFalse()
@@ -340,7 +337,7 @@
.addContent(
Column.Builder()
.setWidth(width)
- .addContent(Text.Builder().setText("text").build())
+ .addContent(basicText("text".layoutString))
.build()
)
.build()
@@ -370,7 +367,7 @@
.addContent(
Column.Builder()
.setWidth(width)
- .addContent(Text.Builder().setText("text").build())
+ .addContent(basicText("text".layoutString))
.build()
)
.build()
diff --git a/wear/protolayout/protolayout/api/current.txt b/wear/protolayout/protolayout/api/current.txt
index 8ca5dc7..73620fa 100644
--- a/wear/protolayout/protolayout/api/current.txt
+++ b/wear/protolayout/protolayout/api/current.txt
@@ -1481,6 +1481,42 @@
}
+package androidx.wear.protolayout.layout {
+
+ public final class TextKt {
+ method public static androidx.wear.protolayout.LayoutElementBuilders.Text basicText(androidx.wear.protolayout.types.LayoutString text, optional androidx.wear.protolayout.LayoutElementBuilders.FontStyle? fontStyle, optional androidx.wear.protolayout.modifiers.LayoutModifier? modifier, optional int maxLines, optional int multilineAlignment, optional int overflow, optional @Dimension(unit=androidx.annotation.Dimension.Companion.SP) float lineHeight);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle fontStyle(optional @Dimension(unit=androidx.annotation.Dimension.Companion.SP) float size, optional boolean italic, optional boolean underline, optional androidx.wear.protolayout.types.LayoutColor? color, optional int weight, optional float letterSpacingEm, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) java.util.List<java.lang.Float> additionalSizesSp, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) java.util.List<? extends androidx.wear.protolayout.LayoutElementBuilders.FontSetting> settings, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) java.util.List<java.lang.String> preferredFontFamilies);
+ }
+
+}
+
+package androidx.wear.protolayout.modifiers {
+
+ public interface LayoutModifier {
+ method public <R> R foldIn(R initial, kotlin.jvm.functions.Function2<? super R,? super androidx.wear.protolayout.modifiers.LayoutModifier.Element,? extends R> operation);
+ method public default infix androidx.wear.protolayout.modifiers.LayoutModifier then(androidx.wear.protolayout.modifiers.LayoutModifier other);
+ field public static final androidx.wear.protolayout.modifiers.LayoutModifier.Companion Companion;
+ }
+
+ public static final class LayoutModifier.Companion implements androidx.wear.protolayout.modifiers.LayoutModifier {
+ method public <R> R foldIn(R initial, kotlin.jvm.functions.Function2<? super R,? super androidx.wear.protolayout.modifiers.LayoutModifier.Element,? extends R> operation);
+ }
+
+ public static interface LayoutModifier.Element extends androidx.wear.protolayout.modifiers.LayoutModifier {
+ method public default <R> R foldIn(R initial, kotlin.jvm.functions.Function2<? super R,? super androidx.wear.protolayout.modifiers.LayoutModifier.Element,? extends R> operation);
+ }
+
+ public final class ModifierAppliersKt {
+ method public static androidx.wear.protolayout.ModifiersBuilders.Modifiers toProtoLayoutModifiers(androidx.wear.protolayout.modifiers.LayoutModifier);
+ }
+
+ public final class SemanticsKt {
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier contentDescription(androidx.wear.protolayout.modifiers.LayoutModifier, String staticValue, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) androidx.wear.protolayout.expression.DynamicBuilders.DynamicString? dynamicValue);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier semanticsRole(androidx.wear.protolayout.modifiers.LayoutModifier, int semanticsRole);
+ }
+
+}
+
package androidx.wear.protolayout.types {
public final class LayoutColor {
diff --git a/wear/protolayout/protolayout/api/restricted_current.txt b/wear/protolayout/protolayout/api/restricted_current.txt
index 8ca5dc7..73620fa 100644
--- a/wear/protolayout/protolayout/api/restricted_current.txt
+++ b/wear/protolayout/protolayout/api/restricted_current.txt
@@ -1481,6 +1481,42 @@
}
+package androidx.wear.protolayout.layout {
+
+ public final class TextKt {
+ method public static androidx.wear.protolayout.LayoutElementBuilders.Text basicText(androidx.wear.protolayout.types.LayoutString text, optional androidx.wear.protolayout.LayoutElementBuilders.FontStyle? fontStyle, optional androidx.wear.protolayout.modifiers.LayoutModifier? modifier, optional int maxLines, optional int multilineAlignment, optional int overflow, optional @Dimension(unit=androidx.annotation.Dimension.Companion.SP) float lineHeight);
+ method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle fontStyle(optional @Dimension(unit=androidx.annotation.Dimension.Companion.SP) float size, optional boolean italic, optional boolean underline, optional androidx.wear.protolayout.types.LayoutColor? color, optional int weight, optional float letterSpacingEm, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) java.util.List<java.lang.Float> additionalSizesSp, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) java.util.List<? extends androidx.wear.protolayout.LayoutElementBuilders.FontSetting> settings, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) java.util.List<java.lang.String> preferredFontFamilies);
+ }
+
+}
+
+package androidx.wear.protolayout.modifiers {
+
+ public interface LayoutModifier {
+ method public <R> R foldIn(R initial, kotlin.jvm.functions.Function2<? super R,? super androidx.wear.protolayout.modifiers.LayoutModifier.Element,? extends R> operation);
+ method public default infix androidx.wear.protolayout.modifiers.LayoutModifier then(androidx.wear.protolayout.modifiers.LayoutModifier other);
+ field public static final androidx.wear.protolayout.modifiers.LayoutModifier.Companion Companion;
+ }
+
+ public static final class LayoutModifier.Companion implements androidx.wear.protolayout.modifiers.LayoutModifier {
+ method public <R> R foldIn(R initial, kotlin.jvm.functions.Function2<? super R,? super androidx.wear.protolayout.modifiers.LayoutModifier.Element,? extends R> operation);
+ }
+
+ public static interface LayoutModifier.Element extends androidx.wear.protolayout.modifiers.LayoutModifier {
+ method public default <R> R foldIn(R initial, kotlin.jvm.functions.Function2<? super R,? super androidx.wear.protolayout.modifiers.LayoutModifier.Element,? extends R> operation);
+ }
+
+ public final class ModifierAppliersKt {
+ method public static androidx.wear.protolayout.ModifiersBuilders.Modifiers toProtoLayoutModifiers(androidx.wear.protolayout.modifiers.LayoutModifier);
+ }
+
+ public final class SemanticsKt {
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier contentDescription(androidx.wear.protolayout.modifiers.LayoutModifier, String staticValue, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) androidx.wear.protolayout.expression.DynamicBuilders.DynamicString? dynamicValue);
+ method public static androidx.wear.protolayout.modifiers.LayoutModifier semanticsRole(androidx.wear.protolayout.modifiers.LayoutModifier, int semanticsRole);
+ }
+
+}
+
package androidx.wear.protolayout.types {
public final class LayoutColor {
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/layout/Text.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/layout/Text.kt
new file mode 100644
index 0000000..45b96f7
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/layout/Text.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.layout
+
+import android.annotation.SuppressLint
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.SP
+import androidx.annotation.OptIn
+import androidx.wear.protolayout.LayoutElementBuilders.FONT_WEIGHT_UNDEFINED
+import androidx.wear.protolayout.LayoutElementBuilders.FontSetting
+import androidx.wear.protolayout.LayoutElementBuilders.FontStyle
+import androidx.wear.protolayout.LayoutElementBuilders.FontWeight
+import androidx.wear.protolayout.LayoutElementBuilders.TEXT_ALIGN_UNDEFINED
+import androidx.wear.protolayout.LayoutElementBuilders.TEXT_OVERFLOW_UNDEFINED
+import androidx.wear.protolayout.LayoutElementBuilders.Text
+import androidx.wear.protolayout.LayoutElementBuilders.TextAlignment
+import androidx.wear.protolayout.LayoutElementBuilders.TextOverflow
+import androidx.wear.protolayout.expression.ProtoLayoutExperimental
+import androidx.wear.protolayout.expression.RequiresSchemaVersion
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.toProtoLayoutModifiers
+import androidx.wear.protolayout.types.LayoutColor
+import androidx.wear.protolayout.types.LayoutString
+import androidx.wear.protolayout.types.em
+import androidx.wear.protolayout.types.sp
+import java.util.stream.Collectors.toList
+import java.util.stream.Stream
+
+/**
+ * Builds a text string.
+ *
+ * @param text The text to render.
+ * @param fontStyle The style of font to use (size, bold etc). If not specified, defaults to the
+ * platform's default body font.
+ * @param modifier Modifiers to set to this element..
+ * @param maxLines The maximum number of lines that can be represented by the [Text] element. If not
+ * defined, the [Text] element will be treated as a single-line element.
+ * @param multilineAlignment Alignment of the text within its bounds. Note that a [Text] element
+ * will size itself to wrap its contents, so this option is meaningless for single-line text (for
+ * that, use alignment of the outer container). For multi-line text, however, this will set the
+ * alignment of lines relative to the [Text] element bounds. If not defined, defaults to
+ * TEXT_ALIGN_CENTER.
+ * @param overflow How to handle text which overflows the bound of the [Text] element. A [Text]
+ * element will grow as large as possible inside its parent container (while still respecting
+ * max_lines); if it cannot grow large enough to render all of its text, the text which cannot fit
+ * inside its container will be truncated. If not defined, defaults to TEXT_OVERFLOW_TRUNCATE.
+ * @param lineHeight The explicit height between lines of text. This is equivalent to the vertical
+ * distance between subsequent baselines. If not specified, defaults the font's recommended
+ * interline spacing.
+ */
+@SuppressLint("ProtoLayoutMinSchema")
+@Suppress("MissingJvmstatic") // Kotlin-friendly version of already available Java Apis
+fun basicText(
+ text: LayoutString,
+ fontStyle: FontStyle? = null,
+ modifier: LayoutModifier? = null,
+ maxLines: Int = 0,
+ @TextAlignment multilineAlignment: Int = TEXT_ALIGN_UNDEFINED,
+ @TextOverflow overflow: Int = TEXT_OVERFLOW_UNDEFINED,
+ @Dimension(SP) lineHeight: Float = Float.NaN,
+) =
+ Text.Builder()
+ .setText(text.prop)
+ .apply {
+ text.layoutConstraint?.let { setLayoutConstraintsForDynamicText(it) }
+ fontStyle?.let { setFontStyle(it) }
+ modifier?.let { setModifiers(it.toProtoLayoutModifiers()) }
+ if (maxLines != 0) {
+ setMaxLines(maxLines)
+ }
+ if (multilineAlignment != TEXT_ALIGN_UNDEFINED) {
+ setMultilineAlignment(multilineAlignment)
+ }
+ if (overflow != TEXT_OVERFLOW_UNDEFINED) {
+ setOverflow(overflow)
+ }
+ if (!lineHeight.isNaN()) {
+ setLineHeight(lineHeight.sp)
+ }
+ }
+ .build()
+
+/**
+ * Builds the styling of a font (e.g. font size, and metrics).
+ *
+ * @param size The size of the font, in scaled pixels (sp). If not specified, defaults to the size
+ * of the system's "body" font.
+ * @param italic Whether the text should be rendered in a italic typeface.
+ * @param underline Whether the text should be rendered with an underline.
+ * @param color The text color. If not defined, defaults to white.
+ * @param weight The weight of the font. If the provided value is not supported on a platform, the
+ * nearest supported value will be used. If not defined, or when set to an invalid value, defaults
+ * to "normal".
+ * @param letterSpacingEm The text letter-spacing. Positive numbers increase the space between
+ * letters while negative numbers tighten the space. If not specified, defaults to 0.
+ * @param additionalSizesSp when this [FontStyle] is applied to a [Text] element with static text,
+ * the text size will be automatically picked from the provided sizes to try to perfectly fit
+ * within its parent bounds. In other words, the largest size from the specified preset sizes that
+ * can fit the most text within the parent bounds will be used.
+ * @param settings The collection of font settings to be applied. If more than one Setting with the
+ * same axis tag is specified, the first one will be used. Supported settings depend on the font
+ * used and renderer version.
+ * @param preferredFontFamilies is the ordered list of font families to pick from for this
+ * [FontStyle]. If the given font family is not available on a device, the fallback values will be
+ * attempted to use, in order in which they are given. Note that support for font family
+ * customization is dependent on the target platform.
+ */
+@OptIn(ProtoLayoutExperimental::class)
+@SuppressLint("ProtoLayoutMinSchema")
+@Suppress("MissingJvmstatic") // Kotlin-friendly version of already available Java Apis
+fun fontStyle(
+ @Dimension(SP) size: Float = 0f,
+ italic: Boolean = false,
+ underline: Boolean = false,
+ color: LayoutColor? = null,
+ @FontWeight weight: Int = FONT_WEIGHT_UNDEFINED,
+ letterSpacingEm: Float = Float.NaN,
+ @RequiresSchemaVersion(major = 1, minor = 300) additionalSizesSp: List<Float> = listOf(),
+ @RequiresSchemaVersion(major = 1, minor = 400) settings: List<FontSetting> = listOf(),
+ @RequiresSchemaVersion(major = 1, minor = 400) preferredFontFamilies: List<String> = listOf()
+): FontStyle =
+ FontStyle.Builder()
+ .apply {
+ if (size != 0f) {
+ setSize(size.sp)
+ }
+ setItalic(italic)
+ setUnderline(underline)
+ color?.let { setColor(it.prop) }
+ if (weight != FONT_WEIGHT_UNDEFINED) {
+ setWeight(weight)
+ }
+ if (settings.isNotEmpty()) {
+ setSettings(*settings.toTypedArray())
+ }
+ if (!letterSpacingEm.isNaN()) {
+ setLetterSpacing(letterSpacingEm.em)
+ }
+ if (preferredFontFamilies.isNotEmpty()) {
+ setPreferredFontFamilies(
+ preferredFontFamilies.first(),
+ *preferredFontFamilies.subList(1, preferredFontFamilies.size).toTypedArray()
+ )
+ }
+ if (additionalSizesSp.isNotEmpty()) {
+ setSizes(
+ *Stream.concat(
+ if (size != 0f) Stream.of(size.toInt()) else Stream.empty(),
+ additionalSizesSp.stream().map { it.toInt() }
+ )
+ .collect(toList())
+ .toIntArray()
+ )
+ }
+ }
+ .build()
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/LayoutModifier.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/LayoutModifier.kt
new file mode 100644
index 0000000..74657a0
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/LayoutModifier.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.modifiers
+
+import java.util.Objects
+
+/**
+ * An ordered, immutable collection of [modifier elements][LayoutModifier.Element] that decorate or
+ * add behavior to ProtoLayout layout elements. For example, backgrounds, padding and click actions.
+ * When a single modifier is applied multiple times, the last one wins.
+ *
+ * @sample androidx.wear.protolayout.material3.samples.edgeButtonSampleIcon
+ */
+interface LayoutModifier {
+ /**
+ * Accumulates a value starting with [initial] and applying [operation] to the current value and
+ * each element from outside in.
+ *
+ * Elements wrap one another in a chain from left to right; an [Element] that appears to the
+ * left of another in a `+` expression or in [operation]'s parameter order affects all of the
+ * elements that appear after it. [foldIn] may be used to accumulate a value starting from the
+ * parent or head of the modifier chain to the final wrapped child.
+ */
+ fun <R> foldIn(initial: R, operation: (R, Element) -> R): R
+
+ /**
+ * Concatenates this modifier with another.
+ *
+ * Returns a [LayoutModifier] representing this modifier followed by [other] in sequence.
+ */
+ infix fun then(other: LayoutModifier): LayoutModifier =
+ if (other === LayoutModifier) this else CombinedLayoutModifier(this, other)
+
+ /** A single element contained within a [LayoutModifier] chain. */
+ interface Element : LayoutModifier {
+ override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R =
+ operation(initial, this)
+ }
+
+ /**
+ * The companion object `LayoutModifier` is the empty, default, or starter [LayoutModifier] that
+ * contains no [elements][Element]. Use it to create a new [LayoutModifier] using modifier
+ * extension factory functions.
+ */
+ companion object : LayoutModifier {
+ @Suppress("MissingJvmstatic")
+ override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = initial
+
+ @Suppress("MissingJvmstatic")
+ override infix fun then(other: LayoutModifier): LayoutModifier = other
+
+ @Suppress("MissingJvmstatic") override fun toString(): String = "LayoutModifier"
+ }
+}
+
+/**
+ * A node in a [LayoutModifier] chain. A CombinedModifier always contains at least two elements; a
+ * * Modifier [outer] that wraps around the Modifier [inner].
+ */
+internal class CombinedLayoutModifier(
+ private val outer: LayoutModifier,
+ private val inner: LayoutModifier
+) : LayoutModifier {
+ override fun <R> foldIn(initial: R, operation: (R, LayoutModifier.Element) -> R): R =
+ inner.foldIn(outer.foldIn(initial, operation), operation)
+
+ override fun equals(other: Any?): Boolean =
+ other is CombinedLayoutModifier && outer == other.outer && inner == other.inner
+
+ override fun hashCode(): Int = Objects.hash(outer, inner)
+
+ override fun toString(): String =
+ "[" +
+ foldIn("") { acc, element ->
+ if (acc.isEmpty()) element.toString() else "$acc, $element"
+ } +
+ "]"
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
new file mode 100644
index 0000000..194809c
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.modifiers
+
+import androidx.annotation.RestrictTo
+import androidx.wear.protolayout.ModifiersBuilders
+import androidx.wear.protolayout.ModifiersBuilders.Semantics
+
+/** Creates a [ModifiersBuilders.Modifiers] from a [LayoutModifier]. */
+fun LayoutModifier.toProtoLayoutModifiers(): ModifiersBuilders.Modifiers =
+ toProtoLayoutModifiersBuilder().build()
+
+// TODO: b/384921198 - Remove when M3 elements can use LayoutModifier chain for everything.
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+/** Creates a [ModifiersBuilders.Modifiers.Builder] from a [LayoutModifier]. */
+fun LayoutModifier.toProtoLayoutModifiersBuilder(): ModifiersBuilders.Modifiers.Builder {
+ data class AccumulatingModifier(val semantics: Semantics.Builder? = null)
+
+ val accumulatingModifier =
+ this.foldIn(AccumulatingModifier()) { acc, e ->
+ when (e) {
+ is BaseSemanticElement -> AccumulatingModifier(semantics = e.foldIn(acc.semantics))
+ else -> acc
+ }
+ }
+
+ return ModifiersBuilders.Modifiers.Builder().apply {
+ accumulatingModifier.semantics?.let { setSemantics(it.build()) }
+ }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt
new file mode 100644
index 0000000..3bf0caf
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.modifiers
+
+import android.annotation.SuppressLint
+import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_NONE
+import androidx.wear.protolayout.ModifiersBuilders.Semantics
+import androidx.wear.protolayout.ModifiersBuilders.SemanticsRole
+import androidx.wear.protolayout.TypeBuilders.StringProp
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
+import androidx.wear.protolayout.expression.RequiresSchemaVersion
+import java.util.Objects
+
+internal class BaseSemanticElement(
+ val contentDescription: StringProp? = null,
+ @SemanticsRole val semanticsRole: Int = SEMANTICS_ROLE_NONE
+) : LayoutModifier.Element {
+ @SuppressLint("ProtoLayoutMinSchema")
+ fun foldIn(initial: Semantics.Builder?): Semantics.Builder =
+ (initial ?: Semantics.Builder()).apply {
+ contentDescription?.let { setContentDescription(it) }
+ if (semanticsRole != SEMANTICS_ROLE_NONE) {
+ setRole(semanticsRole)
+ }
+ }
+
+ override fun equals(other: Any?): Boolean =
+ other is BaseSemanticElement &&
+ contentDescription == other.contentDescription &&
+ semanticsRole == other.semanticsRole
+
+ override fun hashCode(): Int = Objects.hash(contentDescription, semanticsRole)
+
+ override fun toString(): String =
+ "BaseSemanticElement[contentDescription=$contentDescription, semanticRole=$semanticsRole"
+}
+
+/**
+ * Adds content description to be read by Talkback.
+ *
+ * @param staticValue The static content description. This value will be used if [dynamicValue] is
+ * null, or if can't be resolved.
+ * @param dynamicValue The dynamic content description. This is useful when content of the element
+ * itself is dynamic.
+ */
+@SuppressLint("ProtoLayoutMinSchema") // 1.2 schema only used when dynamicValue is non-null
+fun LayoutModifier.contentDescription(
+ staticValue: String,
+ @RequiresSchemaVersion(major = 1, minor = 200) dynamicValue: DynamicString? = null
+): LayoutModifier =
+ this then
+ BaseSemanticElement(
+ contentDescription =
+ StringProp.Builder(staticValue)
+ .apply { dynamicValue?.let { setDynamicValue(it) } }
+ .build()
+ )
+
+/**
+ * Adds the semantic role of user interface element. Accessibility services might use this to
+ * describe the element or do customizations.
+ */
+fun LayoutModifier.semanticsRole(@SemanticsRole semanticsRole: Int): LayoutModifier =
+ this then BaseSemanticElement(semanticsRole = semanticsRole)
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt
new file mode 100644
index 0000000..2f4295e
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.types
+
+import androidx.wear.protolayout.DimensionBuilders.EmProp
+import androidx.wear.protolayout.DimensionBuilders.SpProp
+import androidx.wear.protolayout.TypeBuilders.BoolProp
+
+internal val Float.sp: SpProp
+ get() = SpProp.Builder().setValue(this).build()
+
+internal val Float.em: EmProp
+ get() = EmProp.Builder().setValue(this).build()
+
+internal val Boolean.prop: BoolProp
+ get() = BoolProp.Builder(this).build()
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
new file mode 100644
index 0000000..f91621c
--- /dev/null
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.modifiers
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
+import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_NONE
+import androidx.wear.protolayout.expression.DynamicBuilders
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ModifiersTest {
+
+ @Test
+ fun contentDescription_toModifier() {
+ val modifiers =
+ LayoutModifier.contentDescription(
+ STATIC_CONTENT_DESCRIPTION,
+ DYNAMIC_CONTENT_DESCRIPTION
+ )
+ .toProtoLayoutModifiers()
+
+ assertThat(modifiers.semantics?.contentDescription?.value)
+ .isEqualTo(STATIC_CONTENT_DESCRIPTION)
+ assertThat(modifiers.semantics?.contentDescription?.dynamicValue?.toDynamicStringProto())
+ .isEqualTo(DYNAMIC_CONTENT_DESCRIPTION.toDynamicStringProto())
+ assertThat(modifiers.semantics?.role).isEqualTo(SEMANTICS_ROLE_NONE)
+ }
+
+ @Test
+ fun semanticsRole_toModifier() {
+ val modifiers = LayoutModifier.semanticsRole(SEMANTICS_ROLE_BUTTON).toProtoLayoutModifiers()
+
+ assertThat(modifiers.semantics?.role).isEqualTo(SEMANTICS_ROLE_BUTTON)
+ assertThat(modifiers.semantics?.contentDescription).isNull()
+ }
+
+ @Test
+ fun contentDescription_semanticRole_toModifier() {
+ val modifiers =
+ LayoutModifier.contentDescription(
+ STATIC_CONTENT_DESCRIPTION,
+ DYNAMIC_CONTENT_DESCRIPTION
+ )
+ .semanticsRole(SEMANTICS_ROLE_BUTTON)
+ .toProtoLayoutModifiers()
+
+ assertThat(modifiers.semantics?.contentDescription?.value)
+ .isEqualTo(STATIC_CONTENT_DESCRIPTION)
+ assertThat(modifiers.semantics?.contentDescription?.dynamicValue?.toDynamicStringProto())
+ .isEqualTo(DYNAMIC_CONTENT_DESCRIPTION.toDynamicStringProto())
+ assertThat(modifiers.semantics?.role).isEqualTo(SEMANTICS_ROLE_BUTTON)
+ }
+
+ companion object {
+ const val STATIC_CONTENT_DESCRIPTION = "content desc"
+ val DYNAMIC_CONTENT_DESCRIPTION = DynamicBuilders.DynamicString.constant("dynamic content")
+ }
+}
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/types/LayoutColorTest.kt b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/types/LayoutColorTest.kt
index b201000..968f80b 100644
--- a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/types/LayoutColorTest.kt
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/types/LayoutColorTest.kt
@@ -16,11 +16,14 @@
package androidx.wear.protolayout.types
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.wear.protolayout.ColorBuilders.ColorProp
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor
import com.google.common.truth.Truth.assertThat
import org.junit.Test
+import org.junit.runner.RunWith
+@RunWith(AndroidJUnit4::class)
class LayoutColorTest {
@Test
fun staticColor_asLayoutColor() {
diff --git a/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt b/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
index 3d1239a..b4eb6a6 100644
--- a/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
+++ b/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
@@ -27,9 +27,11 @@
import androidx.wear.protolayout.material3.avatarImage
import androidx.wear.protolayout.material3.materialScope
import androidx.wear.protolayout.material3.primaryLayout
-import androidx.wear.protolayout.material3.prop
import androidx.wear.protolayout.material3.text
import androidx.wear.protolayout.material3.textEdgeButton
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.contentDescription
+import androidx.wear.protolayout.types.layoutString
import androidx.wear.tiles.RequestBuilders
import androidx.wear.tiles.TileBuilders
import androidx.wear.tiles.TileService
@@ -89,7 +91,7 @@
mainSlot = {
appCard(
onClick = EMPTY_LOAD_CLICKABLE,
- contentDescription = "Sample Card".prop(),
+ modifier = LayoutModifier.contentDescription("Sample Card"),
colors =
CardColors(
background = colorScheme.tertiary,
@@ -99,25 +101,25 @@
),
title = {
text(
- "Title Card!".prop(),
+ "Title Card!".layoutString,
maxLines = 1,
)
},
content = {
text(
- "Content of this Card!".prop(),
+ "Content of this Card!".layoutString,
maxLines = 1,
)
},
label = {
text(
- "Hello and welcome Tiles in AndroidX!".prop(),
+ "Hello and welcome Tiles in AndroidX!".layoutString,
)
},
avatar = { avatarImage("id") },
time = {
text(
- "NOW".prop(),
+ "NOW".layoutString,
)
}
)
@@ -125,9 +127,9 @@
bottomSlot = {
textEdgeButton(
onClick = EMPTY_LOAD_CLICKABLE,
- contentDescription = "EdgeButton".prop(),
+ modifier = LayoutModifier.contentDescription("EdgeButton"),
) {
- text("Edge".prop())
+ text("Edge".layoutString)
}
}
)
diff --git a/wear/tiles/tiles-testing/build.gradle b/wear/tiles/tiles-testing/build.gradle
index b7c9811..3b3f44b 100644
--- a/wear/tiles/tiles-testing/build.gradle
+++ b/wear/tiles/tiles-testing/build.gradle
@@ -21,7 +21,6 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
import androidx.build.LibraryType
plugins {
diff --git a/wear/tiles/tiles-tooling-preview/build.gradle b/wear/tiles/tiles-tooling-preview/build.gradle
index dc02618..ea458da 100644
--- a/wear/tiles/tiles-tooling-preview/build.gradle
+++ b/wear/tiles/tiles-tooling-preview/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
diff --git a/wear/watchface/watchface-client-guava/build.gradle b/wear/watchface/watchface-client-guava/build.gradle
index eba4a21..012b9e03 100644
--- a/wear/watchface/watchface-client-guava/build.gradle
+++ b/wear/watchface/watchface-client-guava/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
diff --git a/wear/watchface/watchface-complications-data/build.gradle b/wear/watchface/watchface-complications-data/build.gradle
index 579d3a7..021c9e3 100644
--- a/wear/watchface/watchface-complications-data/build.gradle
+++ b/wear/watchface/watchface-complications-data/build.gradle
@@ -21,7 +21,6 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.RunApiTasks
import androidx.build.LibraryType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
diff --git a/wear/watchface/watchface-complications-rendering/build.gradle b/wear/watchface/watchface-complications-rendering/build.gradle
index 995d4ad..f398b88 100644
--- a/wear/watchface/watchface-complications-rendering/build.gradle
+++ b/wear/watchface/watchface-complications-rendering/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.RunApiTasks
+
import androidx.build.LibraryType
plugins {
diff --git a/wear/watchface/watchface-complications/build.gradle b/wear/watchface/watchface-complications/build.gradle
index 0ce442d..c780293 100644
--- a/wear/watchface/watchface-complications/build.gradle
+++ b/wear/watchface/watchface-complications/build.gradle
@@ -21,7 +21,6 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.RunApiTasks
import androidx.build.LibraryType
diff --git a/wear/watchface/watchface-editor-guava/build.gradle b/wear/watchface/watchface-editor-guava/build.gradle
index be06686..a05c3e8 100644
--- a/wear/watchface/watchface-editor-guava/build.gradle
+++ b/wear/watchface/watchface-editor-guava/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
diff --git a/wear/watchface/watchface-guava/build.gradle b/wear/watchface/watchface-guava/build.gradle
index d975a8c..8fdfbf2 100644
--- a/wear/watchface/watchface-guava/build.gradle
+++ b/wear/watchface/watchface-guava/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.Publish
plugins {
id("AndroidXPlugin")
diff --git a/wear/watchface/watchface/build.gradle b/wear/watchface/watchface/build.gradle
index 8efd472..910148f 100644
--- a/wear/watchface/watchface/build.gradle
+++ b/wear/watchface/watchface/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.RunApiTasks
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
diff --git a/webkit/integration-tests/testapp/build.gradle b/webkit/integration-tests/testapp/build.gradle
index 74df165..b6afc79 100644
--- a/webkit/integration-tests/testapp/build.gradle
+++ b/webkit/integration-tests/testapp/build.gradle
@@ -22,7 +22,7 @@
* modifying its settings.
*/
import androidx.build.ApkCopyHelperKt
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -71,7 +71,7 @@
androidx {
name = "WebKit Test App"
- publish = Publish.NONE
+ type = LibraryType.TEST_APPLICATION
inceptionYear = "2017"
description = "The WebKit Support Library test application is a demonstration of the APIs provided in the androidx.webkit library."
additionalDeviceTestApkKeys.add("chrome")
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java b/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java
index 82a2eb7..6b84532 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java
@@ -28,12 +28,9 @@
import androidx.webkit.SpeculativeLoadingParameters;
import org.chromium.support_lib_boundary.ProfileBoundaryInterface;
-import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
-import java.lang.reflect.InvocationHandler;
-
/**
* Internal implementation of Profile.
@@ -108,52 +105,17 @@
@Nullable CancellationSignal cancellationSignal,
@NonNull SpeculativeLoadingParameters params,
@NonNull OutcomeReceiverCompat<Void, PrefetchException> callback) {
- ApiFeature.NoFramework feature = WebViewFeatureInternal.PROFILE_URL_PREFETCH;
- if (feature.isSupportedByWebView()) {
- InvocationHandler paramsBoundaryInterface =
- BoundaryInterfaceReflectionUtil.createInvocationHandlerFor(
- new SpeculativeLoadingParametersAdapter(params));
-
- mProfileImpl.prefetchUrl(url, paramsBoundaryInterface,
- PrefetchOperationCallbackAdapter.buildInvocationHandler(callback));
-
- if (cancellationSignal != null) {
- cancellationSignal.setOnCancelListener(() -> mProfileImpl.cancelPrefetch(url,
- null));
- }
- } else {
- throw WebViewFeatureInternal.getUnsupportedOperationException();
- }
}
@Override
public void prefetchUrlAsync(@NonNull String url,
@Nullable CancellationSignal cancellationSignal,
@NonNull OutcomeReceiverCompat<Void, PrefetchException> callback) {
- ApiFeature.NoFramework feature = WebViewFeatureInternal.PROFILE_URL_PREFETCH;
- if (feature.isSupportedByWebView()) {
- mProfileImpl.prefetchUrl(url,
- PrefetchOperationCallbackAdapter.buildInvocationHandler(callback));
-
- if (cancellationSignal != null) {
- cancellationSignal.setOnCancelListener(() -> mProfileImpl.cancelPrefetch(url,
- null));
- }
- } else {
- throw WebViewFeatureInternal.getUnsupportedOperationException();
- }
}
@Override
public void clearPrefetchAsync(@NonNull String url,
@NonNull OutcomeReceiverCompat<Void, PrefetchException> callback) {
- ApiFeature.NoFramework feature = WebViewFeatureInternal.PROFILE_URL_PREFETCH;
- if (feature.isSupportedByWebView()) {
- mProfileImpl.clearPrefetch(url,
- PrefetchOperationCallbackAdapter.buildInvocationHandler(callback));
- } else {
- throw WebViewFeatureInternal.getUnsupportedOperationException();
- }
}
}
diff --git a/window/extensions/extensions/build.gradle b/window/extensions/extensions/build.gradle
index 7781812..26acee1 100644
--- a/window/extensions/extensions/build.gradle
+++ b/window/extensions/extensions/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.RunApiTasks
plugins {
id("AndroidXPlugin")
@@ -52,7 +51,6 @@
androidx {
name = "WindowManager Extensions"
type = LibraryType.PUBLISHED_LIBRARY // Only to generate per-project-zips
- runApiTasks = new RunApiTasks.Yes("Need to track API surface before moving to publish")
inceptionYear = "2020"
description = "OEM extension interface definition for the Jetpack WindowManager. " +
"This module declares the interface the the core component of this library " +
diff --git a/window/sidecar/sidecar/build.gradle b/window/sidecar/sidecar/build.gradle
index e902d344..c7d41342 100644
--- a/window/sidecar/sidecar/build.gradle
+++ b/window/sidecar/sidecar/build.gradle
@@ -22,7 +22,6 @@
* modifying its settings.
*/
import androidx.build.LibraryType
-import androidx.build.RunApiTasks
plugins {
id("AndroidXPlugin")
@@ -37,7 +36,6 @@
androidx {
name = "WindowManager Sidecar"
type = LibraryType.PUBLISHED_LIBRARY // Only to generate per-project-zips
- runApiTasks = new RunApiTasks.Yes("Need to track API surface but should never publish")
inceptionYear = "2020"
description = "This version of the OEM extension is deprecated. Please use window:extensions:extensions." +
"module instead."
diff --git a/work/work-benchmark/build.gradle b/work/work-benchmark/build.gradle
index 695cac9..04d2a36 100644
--- a/work/work-benchmark/build.gradle
+++ b/work/work-benchmark/build.gradle
@@ -21,7 +21,7 @@
* Please use that script when creating a new project, rather than copying an existing project and
* modifying its settings.
*/
-import androidx.build.Publish
+import androidx.build.LibraryType
plugins {
id("AndroidXPlugin")
@@ -47,7 +47,7 @@
androidx {
name = "WorkManager Benchmarks"
- publish = Publish.NONE
+ type = LibraryType.BENCHMARK
inceptionYear = "2019"
description = "Android WorkManager Benchmark Library"
}
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java
index 3d30602..958e1a6 100644
--- a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java
@@ -251,6 +251,7 @@
}
@Test
+ @Ignore
public void initRuntimePerceptionFailure() {
ListenableFuture<Session> sessionFuture =
immediateFailedFuture(
@@ -276,12 +277,14 @@
}
@Test
+ @Ignore
public void requestHomeSpaceMode_callsExtensions() {
realityCoreRuntime.requestHomeSpaceMode();
assertThat(fakeExtensions.getSpaceMode()).isEqualTo(SpaceMode.HOME_SPACE);
}
@Test
+ @Ignore
public void requestFullSpaceMode_callsExtensions() {
realityCoreRuntime.requestFullSpaceMode();
assertThat(fakeExtensions.getSpaceMode()).isEqualTo(SpaceMode.FULL_SPACE);
@@ -299,6 +302,7 @@
}
@Test
+ @Ignore
public void loggingEntitySetParent() {
Pose pose = new Pose();
LoggingEntity childEntity = realityCoreRuntime.createLoggingEntity(pose);
@@ -314,6 +318,7 @@
}
@Test
+ @Ignore
public void loggingEntityUpdateParent() {
Pose pose = new Pose();
LoggingEntity childEntity = realityCoreRuntime.createLoggingEntity(pose);
@@ -332,6 +337,7 @@
}
@Test
+ @Ignore
public void onSpatialStateChanged_setsSpatialCapabilities() {
realityCoreRuntime =
JxrPlatformAdapterAxr.create(
@@ -371,6 +377,7 @@
}
@Test
+ @Ignore
public void onSpatialStateChanged_setsEnvironmentVisibility() {
SpatialEnvironment environment = realityCoreRuntime.getSpatialEnvironment();
assertThat(environment.isSpatialEnvironmentPreferenceActive()).isFalse();
@@ -395,6 +402,7 @@
}
@Test
+ @Ignore
public void onSpatialStateChanged_callsEnvironmentListenerOnlyForChanges() {
SpatialEnvironment environment = realityCoreRuntime.getSpatialEnvironment();
@SuppressWarnings(value = "unchecked")
@@ -431,6 +439,7 @@
}
@Test
+ @Ignore
public void onSpatialStateChanged_setsPassthroughOpacity() {
SpatialEnvironment environment = realityCoreRuntime.getSpatialEnvironment();
assertThat(environment.getCurrentPassthroughOpacity()).isZero();
@@ -461,6 +470,7 @@
}
@Test
+ @Ignore
public void onSpatialStateChanged_callsPassthroughListenerOnlyForChanges() {
SpatialEnvironment environment = realityCoreRuntime.getSpatialEnvironment();
@SuppressWarnings(value = "unchecked")
@@ -506,6 +516,7 @@
}
@Test
+ @Ignore
public void currentPassthroughOpacity_isSetDuringRuntimeCreation() {
fakeExtensions.fakeSpatialState.setPassthroughVisibility(
new FakePassthroughVisibilityState(PassthroughVisibilityState.APP, 0.5f));
@@ -527,6 +538,7 @@
}
@Test
+ @Ignore
public void onSpatialStateChanged_firesSpatialCapabilitiesChangedListener() {
realityCoreRuntime =
JxrPlatformAdapterAxr.create(
@@ -577,6 +589,7 @@
}
@Test
+ @Ignore
public void getHeadPoseInOpenXrUnboundedSpace_returnsNullWhenPerceptionSessionUninitialized() {
when(perceptionLibrary.getSession()).thenReturn(null);
assertThat(((JxrPlatformAdapterAxr) realityCoreRuntime).getHeadPoseInOpenXrUnboundedSpace())
@@ -584,6 +597,7 @@
}
@Test
+ @Ignore
public void getHeadPoseInOpenXrUnboundedSpace_returnsPose() {
when(session.getHeadPose())
.thenReturn(
@@ -595,6 +609,7 @@
}
@Test
+ @Ignore
public void
getStereoViewsInOpenXrUnboundedSpace_returnsNullWhenPerceptionSessionUninitialized() {
when(perceptionLibrary.getSession()).thenReturn(null);
@@ -605,6 +620,7 @@
}
@Test
+ @Ignore
public void getStereoViewsInOpenXrUnboundedSpace_returnsViewProjections() {
ViewProjection leftViewProjection =
new ViewProjection(
@@ -626,6 +642,7 @@
}
@Test
+ @Ignore
public void loggingEntity_getActivitySpacePose_returnsIdentityPose() {
Pose identityPose = new Pose();
LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(identityPose);
@@ -633,6 +650,7 @@
}
@Test
+ @Ignore
public void loggingEntity_transformPoseTo_returnsIdentityPose() {
Pose identityPose = new Pose();
LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(identityPose);
@@ -640,6 +658,7 @@
}
@Test
+ @Ignore
public void getPose_returnsSetPose() throws Exception {
Pose pose = new Pose(new Vector3(1f, 2f, 3f), new Quaternion(1f, 2f, 3f, 4f));
Pose identityPose = new Pose();
@@ -665,6 +684,7 @@
}
@Test
+ @Ignore
public void getPose_returnsFactoryMethodPose() throws Exception {
Pose pose = new Pose(new Vector3(1f, 2f, 3f), new Quaternion(1f, 2f, 3f, 4f));
PanelEntity panelEntity = createPanelEntity(pose);
@@ -679,6 +699,7 @@
}
@Test
+ @Ignore
public void getPoseInActivitySpace_withParentChainTranslation_returnsOffsetPositionFromRoot()
throws Exception {
// Create a simple pose with only a small translation on all axes
@@ -703,6 +724,7 @@
}
@Test
+ @Ignore
public void getPoseInActivitySpace_withParentChainRotation_returnsOffsetRotationFromRoot()
throws Exception {
// Create a pose with a translation and one with 90 degree rotation around the y axis.
@@ -736,6 +758,7 @@
}
@Test
+ @Ignore
public void getPoseInActivitySpace_withParentChainPoseOffsets_returnsOffsetPoseFromRoot()
throws Exception {
// Create a pose with a 1D translation and a 90 degree rotation around the z axis.
@@ -775,6 +798,7 @@
}
@Test
+ @Ignore
public void getPoseInActivitySpace_withActivitySpaceParent_returnsScaledPose()
throws Exception {
Pose pose = new Pose(new Vector3(1f, 2f, 3f), new Quaternion(1f, 2f, 3f, 4f));
@@ -796,6 +820,7 @@
}
@Test
+ @Ignore
public void getPoseInActivitySpace_withScale_returnsPose() throws Exception {
Pose localPose = new Pose(new Vector3(1f, 2f, 1f), Quaternion.Identity);
@@ -844,6 +869,7 @@
}
@Test
+ @Ignore
public void getActivitySpacePose_withParentChainTranslation_returnsOffsetPositionFromRoot()
throws Exception {
// Create a simple pose with only a small translation on all axes
@@ -867,6 +893,7 @@
}
@Test
+ @Ignore
public void getActivitySpacePose_withParentChainRotation_returnsOffsetRotationFromRoot()
throws Exception {
// Create a pose with a translation and one with 90 degree rotation around the y axis.
@@ -893,6 +920,7 @@
}
@Test
+ @Ignore
public void getActivitySpacePose_withParentChainPoseOffsets_returnsOffsetPoseFromRoot()
throws Exception {
// Create a pose with a 1D translation and a 90 degree rotation around the z axis.
@@ -929,6 +957,7 @@
}
@Test
+ @Ignore
public void getActivitySpacePose_withDefaultParent_returnsPose() throws Exception {
Pose pose = new Pose(new Vector3(1f, 2f, 3f), new Quaternion(1f, 2f, 3f, 4f));
@@ -943,6 +972,7 @@
}
@Test
+ @Ignore
public void getPoseInActivitySpace_withScale_returnsScaledPose() throws Exception {
Pose localPose = new Pose(new Vector3(1f, 2f, 1f), Quaternion.Identity);
@@ -979,6 +1009,7 @@
}
@Test
+ @Ignore
public void transformPoseTo_sameDestAndSourceEntity_returnsUnchangedPose() throws Exception {
Pose pose =
new Pose(new Vector3(1f, 2f, 3f), new Quaternion(1f, 2f, 3f, 4f).toNormalized());
@@ -997,6 +1028,7 @@
}
@Test
+ @Ignore
public void transformPoseTo_withOnlyTranslationOffset_returnsTranslationDifference()
throws Exception {
PanelEntityImpl sourceEntity = (PanelEntityImpl) createPanelEntity();
@@ -1017,6 +1049,7 @@
}
@Test
+ @Ignore
public void transformPoseTo_withOnlyRotationOffset_returnsRotationDifference()
throws Exception {
PanelEntityImpl sourceEntity = (PanelEntityImpl) createPanelEntity();
@@ -1044,6 +1077,7 @@
}
@Test
+ @Ignore
public void transformPoseTo_withDifferentTranslationAndRotation_returnsTransformedPose() {
// Assume the source and destination entities are in the same coordinate space.
Vector3 sourceVector = new Vector3(1f, 2f, 3f);
@@ -1107,6 +1141,7 @@
}
@Test
+ @Ignore
public void getAlpha_returnsSetAlpha() throws Exception {
PanelEntity panelEntity = createPanelEntity();
GltfEntity gltfEntity = createGltfEntity();
@@ -1131,6 +1166,7 @@
}
@Test
+ @Ignore
public void getActivitySpaceAlpha_returnsTotalAncestorAlpha() throws Exception {
PanelEntity grandparent = createPanelEntity();
GltfEntity parent = createGltfEntity();
@@ -1157,6 +1193,7 @@
}
@Test
+ @Ignore
public void transformPoseTo_withScale_returnsPose() throws Exception {
PanelEntityImpl sourceEntity = (PanelEntityImpl) createPanelEntity();
GltfEntityImpl destinationEntity = (GltfEntityImpl) createGltfEntity();
@@ -1196,6 +1233,7 @@
}
@Test
+ @Ignore
public void isHidden_returnsSetHidden() throws Exception {
PanelEntity parentEntity = createPanelEntity();
assertThat(parentEntity.isHidden(true)).isFalse();
@@ -1235,6 +1273,7 @@
}
@Test
+ @Ignore
public void setHidden_modifiesReforms() throws Exception {
PanelEntity testEntity = createPanelEntity();
FakeNode testNode = (FakeNode) ((AndroidXrEntity) testEntity).getNode();
@@ -1255,6 +1294,7 @@
}
@Test
+ @Ignore
public void loggingEntityAddChildren() {
Pose pose = new Pose();
LoggingEntity childEntity1 = realityCoreRuntime.createLoggingEntity(pose);
@@ -1273,6 +1313,7 @@
}
@Test
+ @Ignore
public void getActivitySpace_returnsEntity() {
ActivitySpace activitySpace = realityCoreRuntime.getActivitySpace();
@@ -1283,6 +1324,7 @@
}
@Test
+ @Ignore
public void getActivitySpaceRootImpl_returnsEntity() {
Entity activitySpaceRoot = realityCoreRuntime.getActivitySpaceRootImpl();
assertThat(activitySpaceRoot).isNotNull();
@@ -1293,12 +1335,14 @@
}
@Test
+ @Ignore
public void getEnvironment_returnsEnvironment() {
SpatialEnvironment environment = realityCoreRuntime.getSpatialEnvironment();
assertThat(environment).isNotNull();
}
@Test
+ @Ignore
public void getHeadActivityPose_returnsNullIfNotReady() {
when(perceptionLibrary.getSession()).thenReturn(session);
when(session.getHeadPose()).thenReturn(null);
@@ -1308,6 +1352,7 @@
}
@Test
+ @Ignore
public void getHeadActivityPose_returnsActivityPose() {
when(perceptionLibrary.getSession()).thenReturn(session);
when(session.getHeadPose())
@@ -1318,6 +1363,7 @@
}
@Test
+ @Ignore
public void getCameraViewActivityPose_returnsNullIfNotReady() {
when(perceptionLibrary.getSession()).thenReturn(session);
when(session.getStereoViews()).thenReturn(new ViewProjections(null, null));
@@ -1334,6 +1380,7 @@
}
@Test
+ @Ignore
public void getLeftCameraViewActivityPose_returnsActivityPose() {
when(perceptionLibrary.getSession()).thenReturn(session);
ViewProjection viewProjection =
@@ -1349,6 +1396,7 @@
}
@Test
+ @Ignore
public void getRightCameraViewActivityPose_returnsActivityPose() {
when(perceptionLibrary.getSession()).thenReturn(session);
ViewProjection viewProjection =
@@ -1364,6 +1412,7 @@
}
@Test
+ @Ignore
public void getUnknownCameraViewActivityPose_returnsEmptyOptional() {
CameraViewActivityPose cameraViewActivityPose =
realityCoreRuntime.getCameraViewActivityPose(555);
@@ -1372,6 +1421,7 @@
}
@Test
+ @Ignore
public void loadExrImageByAssetName_returnsImage() throws Exception {
ListenableFuture<ExrImageResource> imageFuture =
realityCoreRuntime.loadExrImageByAssetName("FakeAsset.exr");
@@ -1388,6 +1438,7 @@
}
@Test
+ @Ignore
public void loadGltfByAssetName_returnsModel() throws Exception {
ListenableFuture<GltfModelResource> modelFuture =
realityCoreRuntime.loadGltfByAssetName("FakeAsset.glb");
@@ -1404,16 +1455,19 @@
}
@Test
+ @Ignore
public void createGltfEntity_returnsEntity() throws Exception {
assertThat(createGltfEntity()).isNotNull();
}
@Test
+ @Ignore
public void createGltfEntitySplitEngine_returnsEntity() throws Exception {
assertThat(createGltfEntitySplitEngine()).isNotNull();
}
@Test
+ @Ignore
public void animateGltfEntitySplitEngine_gltfEntityIsAnimating() throws Exception {
GltfEntity gltfEntitySplitEngine = createGltfEntitySplitEngine();
gltfEntitySplitEngine.startAnimation(false, "animation_name");
@@ -1429,6 +1483,7 @@
}
@Test
+ @Ignore
public void animateLoopGltfEntitySplitEngine_gltfEntityIsAnimatingInLoop() throws Exception {
GltfEntity gltfEntitySplitEngine = createGltfEntitySplitEngine();
gltfEntitySplitEngine.startAnimation(true, "animation_name");
@@ -1442,6 +1497,7 @@
}
@Test
+ @Ignore
public void stopAnimateGltfEntitySplitEngine_gltfEntityStopsAnimating() throws Exception {
GltfEntity gltfEntitySplitEngine = createGltfEntitySplitEngine();
gltfEntitySplitEngine.startAnimation(true, "animation_name");
@@ -1456,6 +1512,7 @@
}
@Test
+ @Ignore
public void gltfEntitySetParent() throws Exception {
GltfEntity childEntity = createGltfEntity();
GltfEntity parentEntity = createGltfEntity();
@@ -1474,6 +1531,7 @@
}
@Test
+ @Ignore
public void gltfEntityUpdateParent() throws Exception {
GltfEntity childEntity = createGltfEntity();
GltfEntity parentEntity1 = createGltfEntity();
@@ -1495,6 +1553,7 @@
}
@Test
+ @Ignore
public void gltfEntityAddChildren() throws Exception {
GltfEntity childEntity1 = createGltfEntity();
GltfEntity childEntity2 = createGltfEntity();
@@ -1517,11 +1576,13 @@
}
@Test
+ @Ignore
public void createPanelEntity_returnsEntity() throws Exception {
assertThat(createPanelEntity()).isNotNull();
}
@Test
+ @Ignore
public void allPanelEnities_haveActivitySpaceRootImplAsParentByDefault() throws Exception {
PanelEntity panelEntity = createPanelEntity();
@@ -1530,6 +1591,7 @@
}
@Test
+ @Ignore
public void panelEntitySetParent_setsParent() throws Exception {
PanelEntity childEntity = createPanelEntity();
PanelEntity parentEntity = createPanelEntity();
@@ -1546,6 +1608,7 @@
}
@Test
+ @Ignore
public void panelEntityUpdateParent_updatesParent() throws Exception {
PanelEntity childEntity = createPanelEntity();
PanelEntity parentEntity1 = createPanelEntity();
@@ -1567,6 +1630,7 @@
}
@Test
+ @Ignore
public void panelEntityAddChildren_addsChildren() throws Exception {
PanelEntity childEntity1 = createPanelEntity();
PanelEntity childEntity2 = createPanelEntity();
@@ -1589,6 +1653,7 @@
}
@Test
+ @Ignore
public void createAnchorEntity_returnsAndInitsAnchor() throws Exception {
Dimensions anchorDimensions = new Dimensions(2f, 5f, 0f);
androidx.xr.scenecore.impl.perception.Pose perceptionPose =
@@ -1615,11 +1680,13 @@
}
@Test
+ @Ignore
public void getMainPanelEntity_returnsPanelEntity() throws Exception {
assertThat(realityCoreRuntime.getMainPanelEntity()).isNotNull();
}
@Test
+ @Ignore
public void getMainPanelEntity_usesWindowLeashNode() throws Exception {
PanelEntity mainPanel = realityCoreRuntime.getMainPanelEntity();
@@ -1628,6 +1695,7 @@
}
@Test
+ @Ignore
public void addInputEventConsumerToEntity_setsUpNodeListener() {
InputEventListener mockConsumer = mock(InputEventListener.class);
PanelEntity panelEntity = createPanelEntity();
@@ -1649,6 +1717,7 @@
}
@Test
+ @Ignore
public void inputEvent_hasHitInfo() {
InputEventListener mockConsumer = mock(InputEventListener.class);
PanelEntity panelEntity = createPanelEntity();
@@ -1676,6 +1745,7 @@
}
@Test
+ @Ignore
public void passingNullExecutorWhenAddingConsumer_usesInternalExecutor() {
InputEventListener mockConsumer = mock(InputEventListener.class);
PanelEntity panelEntity = createPanelEntity();
@@ -1687,6 +1757,7 @@
}
@Test
+ @Ignore
public void addMultipleInputEventConsumerToEntity_setsUpInputCallbacksForAll() {
InputEventListener mockConsumer1 = mock(InputEventListener.class);
InputEventListener mockConsumer2 = mock(InputEventListener.class);
@@ -1707,6 +1778,7 @@
}
@Test
+ @Ignore
public void addMultipleInputEventConsumersToEntity_setsUpInputCallbacksOnGivenExecutors() {
InputEventListener mockConsumer1 = mock(InputEventListener.class);
InputEventListener mockConsumer2 = mock(InputEventListener.class);
@@ -1734,6 +1806,7 @@
}
@Test
+ @Ignore
public void removeInputEventConsumerToEntity_removesFromCallbacks() {
InputEventListener mockConsumer1 = mock(InputEventListener.class);
InputEventListener mockConsumer2 = mock(InputEventListener.class);
@@ -1759,6 +1832,7 @@
}
@Test
+ @Ignore
public void removeAllInputEventConsumers_stopsInputListening() {
InputEventListener mockConsumer1 = mock(InputEventListener.class);
InputEventListener mockConsumer2 = mock(InputEventListener.class);
@@ -1786,6 +1860,7 @@
}
@Test
+ @Ignore
public void dispose_stopsInputListening() {
InputEventListener mockConsumer1 = mock(InputEventListener.class);
InputEventListener mockConsumer2 = mock(InputEventListener.class);
@@ -1812,17 +1887,20 @@
}
@Test
+ @Ignore
public void createContentlessEntity_returnsEntity() throws Exception {
assertThat(createContentlessEntity()).isNotNull();
}
@Test
+ @Ignore
public void contentlessEntity_hasActivitySpaceRootImplAsParentByDefault() throws Exception {
Entity entity = createContentlessEntity();
assertThat(entity.getParent()).isEqualTo(realityCoreRuntime.getActivitySpaceRootImpl());
}
@Test
+ @Ignore
public void contentlessEntityAddChildren_addsChildren() throws Exception {
Entity childEntity1 = createContentlessEntity();
Entity childEntity2 = createContentlessEntity();
@@ -1845,6 +1923,7 @@
}
@Test
+ @Ignore
public void addComponent_callsOnAttach() throws Exception {
PanelEntity panelEntity = createPanelEntity();
GltfEntity gltfEntity = createGltfEntity();
@@ -1863,6 +1942,7 @@
}
@Test
+ @Ignore
public void addComponent_failsIfOnAttachFails() throws Exception {
PanelEntity panelEntity = createPanelEntity();
GltfEntity gltfEntity = createGltfEntity();
@@ -1881,6 +1961,7 @@
}
@Test
+ @Ignore
public void removeComponent_callsOnDetach() throws Exception {
PanelEntity panelEntity = createPanelEntity();
GltfEntity gltfEntity = createGltfEntity();
@@ -1908,6 +1989,7 @@
}
@Test
+ @Ignore
public void addingSameComponentTypeAgain_addsComponent() throws Exception {
PanelEntity panelEntity = createPanelEntity();
GltfEntity gltfEntity = createGltfEntity();
@@ -1934,6 +2016,7 @@
}
@Test
+ @Ignore
public void addingDifferentComponentType_addComponentSucceeds() throws Exception {
PanelEntity panelEntity = createPanelEntity();
GltfEntity gltfEntity = createGltfEntity();
@@ -1960,6 +2043,7 @@
}
@Test
+ @Ignore
public void removeAll_callsOnDetachOnAll() throws Exception {
PanelEntity panelEntity = createPanelEntity();
GltfEntity gltfEntity = createGltfEntity();
@@ -1998,6 +2082,7 @@
}
@Test
+ @Ignore
public void addSameComponentTwice_callsOnAttachTwice() throws Exception {
PanelEntity panelEntity = createPanelEntity();
GltfEntity gltfEntity = createGltfEntity();
@@ -2019,6 +2104,7 @@
}
@Test
+ @Ignore
public void removeSameComponentTwice_callsOnDetachOnce() throws Exception {
PanelEntity panelEntity = createPanelEntity();
GltfEntity gltfEntity = createGltfEntity();
@@ -2049,6 +2135,7 @@
}
@Test
+ @Ignore
public void createInteractableComponent_returnsComponent() {
InputEventListener mockConsumer = mock(InputEventListener.class);
InteractableComponent interactableComponent =
@@ -2057,6 +2144,7 @@
}
@Test
+ @Ignore
public void createPersistedAnchorEntity_returnsEntityInNominalCase() throws Exception {
when(perceptionLibrary.getSession()).thenReturn(session);
when(session.createAnchorFromUuid(any())).thenReturn(anchor);
@@ -2067,6 +2155,7 @@
}
@Test
+ @Ignore
public void createPersistedAnchorEntity_returnsEntityForNullSession() throws Exception {
when(perceptionLibrary.getSession()).thenReturn(null);
assertThat(
@@ -2076,6 +2165,7 @@
}
@Test
+ @Ignore
public void createPersistedAnchorEntity_returnsEntityForNullAnchor() throws Exception {
when(perceptionLibrary.getSession()).thenReturn(session);
when(session.createAnchorFromUuid(any())).thenReturn(null);
@@ -2086,6 +2176,7 @@
}
@Test
+ @Ignore
public void createPersistedAnchorEntity_returnsEntityForNullAnchorToken() throws Exception {
when(perceptionLibrary.getSession()).thenReturn(session);
when(session.createAnchorFromUuid(any())).thenReturn(anchor);
@@ -2101,12 +2192,14 @@
}
@Test
+ @Ignore
public void unpersistAnchor_failsWhenSessionIsNotInitialized() {
when(perceptionLibrary.getSession()).thenReturn(null);
assertThat(realityCoreRuntime.unpersistAnchor(UUID.randomUUID())).isFalse();
}
@Test
+ @Ignore
public void unpersistAnchor_sessionIsInitialized_operationSucceeds() {
when(perceptionLibrary.getSession()).thenReturn(session);
UUID uuid = UUID.randomUUID();
@@ -2115,6 +2208,7 @@
}
@Test
+ @Ignore
public void unpersistAnchor_sessionIsInitialized_operationFails() {
when(perceptionLibrary.getSession()).thenReturn(session);
UUID uuid = UUID.randomUUID();
@@ -2123,6 +2217,7 @@
}
@Test
+ @Ignore
public void createMovableComponent_returnsComponent() {
MovableComponent movableComponent =
realityCoreRuntime.createMovableComponent(
@@ -2131,6 +2226,7 @@
}
@Test
+ @Ignore
public void createAnchorPlacement_returnsAnchorPlacement() {
AnchorPlacement anchorPlacement =
realityCoreRuntime.createAnchorPlacementForPlanes(
@@ -2139,6 +2235,7 @@
}
@Test
+ @Ignore
public void createResizableComponent_returnsComponent() {
ResizableComponent resizableComponent =
realityCoreRuntime.createResizableComponent(
@@ -2147,6 +2244,7 @@
}
@Test
+ @Ignore
public void createPointerCaptureComponent_returnsComponent() {
PointerCaptureComponent pointerCaptureComponent =
realityCoreRuntime.createPointerCaptureComponent(
@@ -2155,6 +2253,7 @@
}
@Test
+ @Ignore
public void dispose_clearsReformOptions() {
AndroidXrEntity entity = (AndroidXrEntity) createContentlessEntity();
FakeNode node = (FakeNode) entity.getNode();
@@ -2168,6 +2267,7 @@
}
@Test
+ @Ignore
public void dispose_clearsParents() {
AndroidXrEntity entity = (AndroidXrEntity) createContentlessEntity();
entity.setParent(realityCoreRuntime.getActivitySpaceRootImpl());
@@ -2178,6 +2278,7 @@
}
@Test
+ @Ignore
public void setFullSpaceMode_callsExtensions() {
Bundle bundle = Bundle.EMPTY;
bundle = realityCoreRuntime.setFullSpaceMode(bundle);
@@ -2185,6 +2286,7 @@
}
@Test
+ @Ignore
public void setFullSpaceModeWithEnvironmentInherited_callsExtensions() {
Bundle bundle = Bundle.EMPTY;
bundle = realityCoreRuntime.setFullSpaceModeWithEnvironmentInherited(bundle);
@@ -2192,12 +2294,14 @@
}
@Test
+ @Ignore
public void setPreferredAspectRatio_callsExtensions() {
realityCoreRuntime.setPreferredAspectRatio(activity, 1.23f);
assertThat(fakeExtensions.getPreferredAspectRatio()).isEqualTo(1.23f);
}
@Test
+ @Ignore
public void createStereoSurface_returnsStereoSurface() {
// Not a great test, since it returns the (non-SplitEngine) StereoSurfaceEntityImpl
// and that throws this from its Ctor.
@@ -2213,6 +2317,7 @@
}
@Test
+ @Ignore
public void getSurfaceFromStereoSurface_returnsSurface() {
assertThrows(
IllegalArgumentException.class,
@@ -2220,6 +2325,7 @@
}
@Test
+ @Ignore
public void setStereoModeForStereoSurface_callsExtensions() {
assertThrows(
IllegalArgumentException.class,
@@ -2229,6 +2335,7 @@
}
@Test
+ @Ignore
public void injectRootNodeAndTaskWindowLeashNode_runtimeImplUsesThoseNodes() {
FakeNode rootNode = (FakeNode) fakeExtensions.createNode();
FakeNode taskWindowLeashNode = (FakeNode) fakeExtensions.createNode();
@@ -2252,6 +2359,7 @@
}
@Test
+ @Ignore
public void dispose_clearsResources() {
AndroidXrEntity entity = (AndroidXrEntity) createContentlessEntity();
FakeNode node = (FakeNode) entity.getNode();
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MovableComponentImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MovableComponentImplTest.java
index 2cef0e9..2f9e930 100644
--- a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MovableComponentImplTest.java
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MovableComponentImplTest.java
@@ -83,6 +83,7 @@
import com.google.common.truth.Expect;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -187,6 +188,7 @@
}
@Test
+ @Ignore
public void addMovableComponent_addsReformOptionsToNode() {
Entity entity = createTestEntity();
MovableComponent movableComponent =
@@ -215,6 +217,7 @@
}
@Test
+ @Ignore
public void addMovableComponent_addsSystemMovableFlagToNode() {
Entity entity = createTestEntity();
MovableComponent movableComponent =
@@ -243,6 +246,7 @@
}
@Test
+ @Ignore
public void addMovableComponent_addsScaleInZFlagToNode() {
Entity entity = createTestEntity();
MovableComponent movableComponent =
@@ -271,6 +275,7 @@
}
@Test
+ @Ignore
public void addMovableComponent_addsAllFlagsToNode() {
Entity entity = createTestEntity();
MovableComponent movableComponent =
@@ -300,6 +305,7 @@
}
@Test
+ @Ignore
public void setSystemMovableFlag_alsoUpdatesEntityPoseAndScale() {
AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
MovableComponentImpl movableComponent =
@@ -341,6 +347,7 @@
}
@Test
+ @Ignore
public void systemMovableFlagNotSet_doesNotUpdateEntityPoseAndScale() {
AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
MovableComponent movableComponent =
@@ -380,6 +387,7 @@
}
@Test
+ @Ignore
public void setSizeOnMovableComponent_setsSizeOnNodeReformOptions() {
Entity entity = createTestEntity();
MovableComponent movableComponent =
@@ -407,6 +415,7 @@
}
@Test
+ @Ignore
public void scaleWithDistanceOnMovableComponent_defaultsToDefaultMode() {
Entity entity = createTestEntity();
MovableComponent movableComponent =
@@ -434,6 +443,7 @@
}
@Test
+ @Ignore
public void setScaleWithDistanceOnMovableComponent_setsScaleWithDistanceOnNodeReformOptions() {
Entity entity = createTestEntity();
MovableComponent movableComponent =
@@ -462,6 +472,7 @@
}
@Test
+ @Ignore
public void setPropertiesOnMovableComponentAttachLater_setsPropertiesOnNodeReformOptions() {
AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
MovableComponentImpl movableComponent =
@@ -495,6 +506,7 @@
}
@Test
+ @Ignore
public void addMoveEventListener_onlyInvokedOnMoveEvent() {
AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
Vector3 initialTranslation = new Vector3(1f, 2f, 3f);
@@ -548,6 +560,7 @@
}
@Test
+ @Ignore
public void addMoveEventListenerWithExecutor_invokesListenerOnGivenExecutor() {
Entity entity = createTestEntity();
MovableComponent movableComponent =
@@ -587,6 +600,7 @@
}
@Test
+ @Ignore
public void addMoveEventListenerMultiple_invokesAllListeners() {
Entity entity = createTestEntity();
MovableComponentImpl movableComponent =
@@ -626,6 +640,7 @@
}
@Test
+ @Ignore
public void removeMoveEventListenerMultiple_removesGivenListener() {
Entity entity = createTestEntity();
MovableComponentImpl movableComponent =
@@ -677,6 +692,7 @@
}
@Test
+ @Ignore
public void removeMovableComponent_clearsMoveReformOptionsAndMoveEventListeners() {
Entity entity = createTestEntity();
MovableComponentImpl movableComponent =
@@ -715,6 +731,7 @@
}
@Test
+ @Ignore
public void movableComponent_canAttachAgainAfterDetach() {
Entity entity = createTestEntity();
assertThat(entity).isNotNull();
@@ -739,6 +756,7 @@
}
@Test
+ @Ignore
public void anchorable_updatesThePoseBasedOnPlanes() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -823,6 +841,7 @@
}
@Test
+ @Ignore
public void anchorable_nullParent_updatesThePoseBasedOnPlanes() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -903,6 +922,7 @@
}
@Test
+ @Ignore
public void anchorable_updatesPoseButDoesNotMove_ifNotSystemMovable() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -988,6 +1008,7 @@
}
@Test
+ @Ignore
public void anchorable_withNonActivityParent_updatesPoseBasedOnPlanesAndParent() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -1074,6 +1095,7 @@
}
@Test
+ @Ignore
public void anchorableAndScaledParent_updatesThePoseBasedOnPlanes() {
// Set the activity space pose to be 1 unit to the left of the origin. with a scale of 2.
setActivitySpacePose(
@@ -1151,6 +1173,7 @@
}
@Test
+ @Ignore
public void anchorable_withinAnchorDistance_setsAnchorEntity() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -1238,6 +1261,7 @@
}
@Test
+ @Ignore
public void anchorable_withinAnchorDistanceAboveAnchor_resetsPose() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -1327,6 +1351,7 @@
}
@Test
+ @Ignore
public void anchorable_withIncorrectPlaneType_doesNotCreateAnchor() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -1417,6 +1442,7 @@
}
@Test
+ @Ignore
public void anchorable_withinAnchorDistanceAndScale_setsAnchorEntityAndScales() {
// Set the activity space pose to be 1 unit to the left of the OpenXR origin and add a scale
// of
@@ -1510,6 +1536,7 @@
}
@Test
+ @Ignore
public void anchorable_noPlanes_keepsProposedPose() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -1567,6 +1594,7 @@
}
@Test
+ @Ignore
public void anchorable_noPlaneData_keepsProposedPose() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -1627,6 +1655,7 @@
}
@Test
+ @Ignore
public void anchorable_outsideExtents_keepsProposedPose() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -1698,6 +1727,7 @@
}
@Test
+ @Ignore
public void anchorable_resetsToActivityPoseAfterAnchoring() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -1810,6 +1840,7 @@
}
@Test
+ @Ignore
public void anchorable_resetsAndScaleToActivityPoseAfterAnchoring() {
// Set the activity space pose to be 1 unit to the left of the OpenXR origin and add a scale
// of
@@ -1932,6 +1963,7 @@
}
@Test
+ @Ignore
public void anchorableChildOfEntity_resetsToActivityPoseAfterAnchoring() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -2050,6 +2082,7 @@
}
@Test
+ @Ignore
public void anchorable_shouldDispose_disposesAnchorAfterUnparenting() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -2168,6 +2201,7 @@
}
@Test
+ @Ignore
public void anchorable_shouldDispose_doeNotDisposeIfAnchorHasChildren() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -2289,6 +2323,7 @@
}
@Test
+ @Ignore
public void anchorablePanelEntity_nearPlane_rendersShadow() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -2352,6 +2387,7 @@
}
@Test
+ @Ignore
public void anchorablePanelEntity_awayFromPlane_hidesShadow() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
@@ -2410,6 +2446,7 @@
}
@Test
+ @Ignore
public void anchorablePanelEntity_endMovement_callsDestroy() {
// Set the activity space pose to be 1 unit to the left of the origin.
setActivitySpacePose(
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/OpenXrActivityPoseTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/OpenXrActivityPoseTest.java
index dd24f63..4169854 100644
--- a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/OpenXrActivityPoseTest.java
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/OpenXrActivityPoseTest.java
@@ -38,6 +38,7 @@
import androidx.xr.scenecore.testing.FakeXrExtensions.FakeGltfModelToken;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.ParameterizedRobolectricTestRunner;
@@ -167,6 +168,7 @@
assertPose(testActivityPose.getPoseInActivitySpace(), new Pose());
}
+ @Ignore("b/384930655")
@Test
public void getPoseInActivitySpace_returnsDifferencePose() {
testActivityPose = createTestActivityPose();