Merge "Read property accessor meteadata in KAPT" into androidx-main
diff --git a/OWNERS b/OWNERS
index 8089367..5d06014 100644
--- a/OWNERS
+++ b/OWNERS
@@ -13,7 +13,6 @@
mount@google.com
nickanthony@google.com
owengray@google.com
-pavlis@google.com
romainguy@android.com
saff@google.com
sergeyv@google.com
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
index 6a11376..19cc93a 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
@@ -185,12 +185,9 @@
)
}
}
- // Kill Process
+ // Kill process before compiling
Log.d(TAG, "Killing process $packageName")
Shell.executeCommand("am force-stop $packageName")
- // Compile
- compilePackage(packageName)
- Log.d(TAG, "$packageName is compiled.")
} else if (this is SpeedProfile) {
repeat(this.warmupIterations) {
block()
@@ -204,8 +201,8 @@
Log.d(TAG, "Received dump profile response $response")
throw RuntimeException("Failed to dump profile for $packageName ($response)")
}
- compilePackage(packageName)
}
+ compilePackage(packageName)
}
/**
diff --git a/benchmark/integration-tests/macrobenchmark-target/build.gradle b/benchmark/integration-tests/macrobenchmark-target/build.gradle
index 2ef7963..20dd547 100644
--- a/benchmark/integration-tests/macrobenchmark-target/build.gradle
+++ b/benchmark/integration-tests/macrobenchmark-target/build.gradle
@@ -35,6 +35,7 @@
implementation(libs.constraintLayout)
implementation(project(":appcompat:appcompat"))
implementation(project(":activity:activity"))
+ implementation(project(":profileinstaller:profileinstaller"))
implementation(project(":recyclerview:recyclerview"))
implementation(project(":tracing:tracing-ktx"))
implementation(libs.material)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
index a06cdbd..737f8f0 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
@@ -178,16 +178,13 @@
}
}
- // TODO(148540713): remove this exclusion when Lint can support using multiple lint jars
- configurations.getByName("lintChecks").exclude(
- mapOf("module" to "lint-checks")
- )
// TODO: figure out how to apply this to multiplatform modules
dependencies.add(
"lintChecks",
project.dependencies.project(
mapOf(
"path" to ":compose:lint:internal-lint-checks",
+ // TODO(b/206617878) remove this shadow configuration
"configuration" to "shadow"
)
)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index cb6f0f2..47ac289 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -169,10 +169,13 @@
Sync::class.java
) { task ->
val sources = docsConfiguration.incoming.artifactView { }.files
+ // Store archiveOperations into a local variable to prevent access to the plugin
+ // during the task execution, as that breaks configuration caching.
+ val localVar = archiveOperations
task.from(
sources.elements.map { jars ->
jars.map {
- archiveOperations.zipTree(it).matching {
+ localVar.zipTree(it).matching {
// Filter out files that documentation tools cannot process.
it.exclude("**/*.MF")
it.exclude("**/*.aidl")
@@ -206,13 +209,15 @@
docsConfiguration: Configuration
): TaskProvider<Sync> {
return project.tasks.register("unzipSourcesForDackka", Sync::class.java) { task ->
-
val sources = docsConfiguration.incoming.artifactView { }.files
+ // Store archiveOperations into a local variable to prevent access to the plugin
+ // during the task execution, as that breaks configuration caching.
+ val localVar = archiveOperations
task.from(
sources.elements.map { jars ->
jars.map {
- archiveOperations.zipTree(it)
+ localVar.zipTree(it)
}
}
)
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/public/src/main/kotlin/androidx/build/LibraryVersions.kt
index 9522e20..d31a45c 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -23,7 +23,7 @@
val ACTIVITY = Version("1.5.0-alpha01")
val ADS_IDENTIFIER = Version("1.0.0-alpha05")
val ANNOTATION = Version("1.4.0-alpha01")
- val ANNOTATION_EXPERIMENTAL = Version("1.2.0-beta02")
+ val ANNOTATION_EXPERIMENTAL = Version("1.2.0-rc01")
val APPCOMPAT = Version("1.5.0-alpha01")
val APPSEARCH = Version("1.0.0-alpha05")
val ARCH_CORE = Version("2.2.0-alpha01")
@@ -104,7 +104,7 @@
val RECYCLERVIEW_SELECTION = Version("1.2.0-alpha02")
val REMOTECALLBACK = Version("1.0.0-alpha02")
val RESOURCEINSPECTION = Version("1.1.0-alpha01")
- val ROOM = Version("2.4.0-beta03")
+ val ROOM = Version("2.4.0-rc01")
val SAVEDSTATE = Version("1.2.0-alpha01")
val SECURITY = Version("1.1.0-alpha04")
val SECURITY_APP_AUTHENTICATOR = Version("1.0.0-alpha03")
@@ -118,7 +118,7 @@
val SLICE_REMOTECALLBACK = Version("1.0.0-alpha01")
val SLIDINGPANELAYOUT = Version("1.2.0-rc01")
val STARTUP = Version("1.2.0-alpha01")
- val SQLITE = Version("2.2.0-beta03")
+ val SQLITE = Version("2.2.0-rc01")
val SQLITE_INSPECTOR = Version("2.1.0-alpha01")
val SWIPEREFRESHLAYOUT = Version("1.2.0-alpha01")
val TESTSCREENSHOT = Version("1.0.0-alpha01")
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt b/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
index 931fa0a..48197b5 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
@@ -27,13 +27,7 @@
const val DEFAULT_MIN_SDK_VERSION = 14
const val INSTRUMENTATION_RUNNER = "androidx.test.runner.AndroidJUnitRunner"
const val BUILD_TOOLS_VERSION = "30.0.3"
- val NDK_VERSION by lazy {
- // TODO(aurimas) b/173737578 remove when we no longer have divergent versions
- when (getOperatingSystem()) {
- OperatingSystem.LINUX -> "23.0.7243079"
- else -> "23.0.7599858"
- }
- }
+ const val NDK_VERSION = "23.1.7779620"
/**
* The Android SDK version to use for compilation.
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index be5dfad..a08ceb5 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -53,6 +53,7 @@
import androidx.camera.core.impl.SurfaceSizeDefinition;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.utils.CameraOrientationUtil;
+import androidx.camera.core.impl.utils.CompareSizesByArea;
import androidx.core.util.Preconditions;
import java.util.ArrayList;
@@ -1334,34 +1335,6 @@
return excludedSizes;
}
- /** Comparator based on area of the given {@link Size} objects. */
- @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
- static final class CompareSizesByArea implements Comparator<Size> {
- private boolean mReverse = false;
-
- CompareSizesByArea() {
- }
-
- CompareSizesByArea(boolean reverse) {
- mReverse = reverse;
- }
-
- @Override
- public int compare(Size lhs, Size rhs) {
- // We cast here to ensure the multiplications won't overflow
- int result =
- Long.signum(
- (long) lhs.getWidth() * lhs.getHeight()
- - (long) rhs.getWidth() * rhs.getHeight());
-
- if (mReverse) {
- result *= -1;
- }
-
- return result;
- }
- }
-
/** Comparator based on how close they are to the target aspect ratio. */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
static final class CompareAspectRatiosByDistanceToTargetRatio implements Comparator<Rational> {
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
index 3561b59..c3117bc 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
@@ -61,6 +61,7 @@
import androidx.camera.core.impl.SurfaceConfig.ConfigType;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.impl.utils.CompareSizesByArea;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.internal.CameraUseCaseAdapter;
import androidx.camera.testing.CameraUtil;
@@ -1204,7 +1205,7 @@
}
// The testing sizes array will be equal to mSupportedSizes after sorting.
- Arrays.sort(sizes, new SupportedSurfaceCombination.CompareSizesByArea(true));
+ Arrays.sort(sizes, new CompareSizesByArea(true));
assertThat(Arrays.asList(sizes)).isEqualTo(Arrays.asList(mSupportedSizes));
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/CompareSizesByArea.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/CompareSizesByArea.java
new file mode 100644
index 0000000..a5ce6a8
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/CompareSizesByArea.java
@@ -0,0 +1,55 @@
+/*
+ * 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.core.impl.utils;
+
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.util.Comparator;
+
+/** Comparator based on area of the given {@link Size} objects. */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public final class CompareSizesByArea implements Comparator<Size> {
+ private boolean mReverse = false;
+
+ /** Creates a comparator with standard total ordering. */
+ public CompareSizesByArea() {
+ this(false);
+ }
+
+ /** Creates a comparator which can reverse the total ordering. */
+ public CompareSizesByArea(boolean reverse) {
+ mReverse = reverse;
+ }
+
+ @Override
+ public int compare(@NonNull Size lhs, @NonNull Size rhs) {
+ // We cast here to ensure the multiplications won't overflow
+ int result =
+ Long.signum(
+ (long) lhs.getWidth() * lhs.getHeight()
+ - (long) rhs.getWidth() * rhs.getHeight());
+
+ if (mReverse) {
+ result *= -1;
+ }
+
+ return result;
+ }
+}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/AudioSourceTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/AudioSourceTest.kt
index cdb46b4..a870135 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/AudioSourceTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/AudioSourceTest.kt
@@ -68,14 +68,16 @@
}
fakeBufferProvider.setActive(true)
- audioSource = AudioSource.Builder()
- .setExecutor(CameraXExecutors.ioExecutor())
- .setAudioSource(AUDIO_SOURCE)
- .setSampleRate(SAMPLE_RATE)
- .setChannelCount(CHANNEL_COUNT)
- .setAudioFormat(AUDIO_FORMAT)
- .setBufferProvider(fakeBufferProvider)
- .build()
+ audioSource = AudioSource(
+ AudioSource.Settings.builder()
+ .setAudioSource(AUDIO_SOURCE)
+ .setSampleRate(SAMPLE_RATE)
+ .setChannelCount(CHANNEL_COUNT)
+ .setAudioFormat(AUDIO_FORMAT)
+ .build(),
+ CameraXExecutors.ioExecutor()
+ )
+ audioSource.setBufferProvider(fakeBufferProvider)
}
@After
@@ -123,4 +125,44 @@
verify(bufferFactoryInvocations, noInvocation(3000L, 6000L)).call()
}
}
+
+ @Test
+ fun canResetBufferProvider_beforeStarting() {
+ // Arrange
+ val localBufferFactoryInvocations = mock(Callable::class.java)
+ val localFakeBufferProvider = FakeBufferProvider {
+ localBufferFactoryInvocations.call()
+ FakeInputBuffer()
+ }
+
+ // Act
+ audioSource.setBufferProvider(localFakeBufferProvider)
+ audioSource.start()
+ localFakeBufferProvider.setActive(true)
+
+ // Assert.
+ // It should continuously send audio data by invoking BufferProvider#acquireBuffer
+ verify(localBufferFactoryInvocations, timeout(10000L).atLeast(3)).call()
+ }
+
+ @Test
+ fun canResetBufferProvider_afterStarting() {
+ // Arrange
+ val localBufferFactoryInvocations = mock(Callable::class.java)
+ val localFakeBufferProvider = FakeBufferProvider {
+ localBufferFactoryInvocations.call()
+ FakeInputBuffer()
+ }
+ audioSource.start()
+ fakeBufferProvider.setActive(true)
+ verify(bufferFactoryInvocations, timeout(10000L).atLeast(3)).call()
+
+ // Act
+ audioSource.setBufferProvider(localFakeBufferProvider)
+ localFakeBufferProvider.setActive(true)
+
+ // Assert.
+ // It should continuously send audio data by invoking BufferProvider#acquireBuffer
+ verify(localBufferFactoryInvocations, timeout(10000L).atLeast(3)).call()
+ }
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
index 99a635a9..c4acf18 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
@@ -63,7 +63,6 @@
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.camera.video.internal.AudioSource;
import androidx.camera.video.internal.AudioSourceAccessException;
-import androidx.camera.video.internal.BufferProvider;
import androidx.camera.video.internal.ResourceCreationException;
import androidx.camera.video.internal.compat.Api26Impl;
import androidx.camera.video.internal.compat.quirk.DeactivateEncoderSurfaceBeforeStopEncoderQuirk;
@@ -74,7 +73,6 @@
import androidx.camera.video.internal.encoder.Encoder;
import androidx.camera.video.internal.encoder.EncoderCallback;
import androidx.camera.video.internal.encoder.EncoderImpl;
-import androidx.camera.video.internal.encoder.InputBuffer;
import androidx.camera.video.internal.encoder.InvalidConfigException;
import androidx.camera.video.internal.encoder.OutputConfig;
import androidx.camera.video.internal.encoder.VideoEncoderConfig;
@@ -1103,38 +1101,44 @@
@ExecutedBy("mSequentialExecutor")
private void setupAudio() throws ResourceCreationException {
MediaSpec mediaSpec = getObservableData(mMediaSpec);
- AudioEncoderConfig config = composeAudioEncoderConfig(mediaSpec);
+ // Create the audio source
+ AudioSpec audioSpec = mediaSpec.getAudioSpec();
+ AudioSource.Settings audioSourceSettings = AudioSource.Settings.builder()
+ .setAudioSource(audioSpec.getSource())
+ .setSampleRate(selectSampleRate(audioSpec))
+ .setChannelCount(audioSpec.getChannelCount())
+ .setAudioFormat(audioSpec.getSourceFormat())
+ .build();
try {
+ mAudioSource = setupAudioSource(audioSourceSettings);
+ } catch (AudioSourceAccessException e) {
+ throw new ResourceCreationException(e);
+ }
+
+
+ // Create the audio encoder
+ AudioEncoderConfig config = composeAudioEncoderConfig(mediaSpec);
+ try {
mAudioEncoder = new EncoderImpl(mExecutor, config);
} catch (InvalidConfigException e) {
throw new ResourceCreationException(e);
}
+ // Connect the audio source to the audio encoder
Encoder.EncoderInput bufferProvider = mAudioEncoder.getInput();
if (!(bufferProvider instanceof Encoder.ByteBufferInput)) {
throw new AssertionError("The EncoderInput of audio isn't a ByteBufferInput.");
}
- try {
- mAudioSource = setupAudioSource((Encoder.ByteBufferInput) bufferProvider,
- mediaSpec.getAudioSpec());
- } catch (AudioSourceAccessException e) {
- throw new ResourceCreationException(e);
- }
+ mAudioSource.setBufferProvider((Encoder.ByteBufferInput) bufferProvider);
}
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
@NonNull
- private AudioSource setupAudioSource(@NonNull BufferProvider<InputBuffer> bufferProvider,
- @NonNull AudioSpec audioSpec) throws AudioSourceAccessException {
- AudioSource audioSource = new AudioSource.Builder()
- .setExecutor(CameraXExecutors.ioExecutor())
- .setBufferProvider(bufferProvider)
- .setAudioSource(audioSpec.getSource())
- .setSampleRate(selectSampleRate(audioSpec))
- .setChannelCount(audioSpec.getChannelCount())
- .setAudioFormat(audioSpec.getSourceFormat())
- .build();
+ private AudioSource setupAudioSource(@NonNull AudioSource.Settings audioSourceSettings)
+ throws AudioSourceAccessException {
+ AudioSource audioSource = new AudioSource(audioSourceSettings,
+ CameraXExecutors.ioExecutor());
audioSource.setAudioSourceCallback(mSequentialExecutor,
new AudioSource.AudioSourceCallback() {
@Override
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSource.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSource.java
index 6a6dd23..aad2189 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSource.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSource.java
@@ -21,6 +21,7 @@
import static androidx.camera.video.internal.AudioSource.InternalState.STARTED;
import android.Manifest;
+import android.annotation.SuppressLint;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
@@ -28,6 +29,7 @@
import android.media.AudioTimestamp;
import android.os.Build;
+import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@@ -43,6 +45,8 @@
import androidx.camera.video.internal.encoder.InputBuffer;
import androidx.core.util.Preconditions;
+import com.google.auto.value.AutoValue;
+
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
@@ -55,7 +59,7 @@
* AudioSource is used to obtain audio raw data and write to the buffer from {@link BufferProvider}.
*
* <p>The audio raw data could be one of sources from the device. The target source can be
- * specified with {@link Builder#setAudioSource(int)}.
+ * specified with {@link Settings.Builder#setAudioSource(int)}.
*
* <p>Calling {@link #start} will start reading audio data from the target source and then write
* the data into the buffer from {@link BufferProvider}. Calling {@link #stop} will stop sending
@@ -87,8 +91,6 @@
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Executor mExecutor;
- private final BufferProvider<InputBuffer> mBufferProvider;
-
private AudioManager.AudioRecordingCallback mAudioRecordingCallback;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@@ -115,28 +117,50 @@
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
AudioSourceCallback mAudioSourceCallback;
+ // The following should only be accessed by mExecutor
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ BufferProvider<InputBuffer> mBufferProvider;
+ private FutureCallback<InputBuffer> mAcquireBufferCallback;
+ private Observable.Observer<BufferProvider.State> mStateObserver;
+
+
+ /**
+ * Creates an AudioSource for the given settings.
+ *
+ * <p>It should be verified the combination of sample rate, channel count and audio format is
+ * supported with {@link #isSettingsSupported(int, int, int)} before passing the settings to
+ * this constructor, or an {@link UnsupportedOperationException} will be thrown.
+ *
+ * @throws UnsupportedOperationException if the combination of sample rate, channel count,
+ * and audio format in the provided settings is unsupported.
+ * @throws AudioSourceAccessException if the audio device is not available or cannot be
+ * initialized with the given settings.
+ */
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
- AudioSource(@NonNull Executor executor,
- @NonNull BufferProvider<InputBuffer> bufferProvider,
- int audioSource,
- int sampleRate,
- int channelCount,
- int audioFormat)
+ public AudioSource(@NonNull Settings settings, @NonNull Executor executor)
throws AudioSourceAccessException {
- int minBufferSize = getMinBufferSize(sampleRate, channelCount, audioFormat);
+ if (!isSettingsSupported(settings.getSampleRate(), settings.getChannelCount(),
+ settings.getAudioFormat())) {
+ throw new UnsupportedOperationException(String.format(
+ "The combination of sample rate %d, channel count %d and audio format"
+ + " %d is not supported.",
+ settings.getSampleRate(), settings.getChannelCount(),
+ settings.getAudioFormat()));
+ }
+
+ int minBufferSize = getMinBufferSize(settings.getSampleRate(), settings.getChannelCount(),
+ settings.getAudioFormat());
// The minBufferSize should be a positive value since the settings had already been checked
// by the isSettingsSupported().
Preconditions.checkState(minBufferSize > 0);
mExecutor = CameraXExecutors.newSequentialExecutor(executor);
- mBufferProvider = bufferProvider;
mBufferSize = minBufferSize * 2;
try {
- mAudioRecord = new AudioRecord(audioSource,
- sampleRate,
- channelCountToChannelConfig(channelCount),
- audioFormat,
+ mAudioRecord = new AudioRecord(settings.getAudioSource(),
+ settings.getSampleRate(),
+ channelCountToChannelConfig(settings.getChannelCount()),
+ settings.getAudioFormat(),
mBufferSize);
} catch (IllegalArgumentException e) {
throw new AudioSourceAccessException("Unable to create AudioRecord", e);
@@ -152,8 +176,6 @@
Api29Impl.registerAudioRecordingCallback(mAudioRecord, mExecutor,
mAudioRecordingCallback);
}
-
- mBufferProvider.addObserver(mExecutor, mStateObserver);
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@@ -178,9 +200,38 @@
}
}
+
+ /**
+ * Sets the {@link BufferProvider}.
+ *
+ * <p>A buffer provider is required to stream audio. If no buffer provider is provided, then
+ * audio will be dropped until one is provided and active.
+ *
+ * @param bufferProvider The new buffer provider to use.
+ */
+ public void setBufferProvider(@NonNull BufferProvider<InputBuffer> bufferProvider) {
+ mExecutor.execute(() -> {
+ switch (mState) {
+ case CONFIGURED:
+ // Fall-through
+ case STARTED:
+ if (mBufferProvider != bufferProvider) {
+ resetBufferProvider(bufferProvider);
+ }
+ break;
+ case RELEASED:
+ throw new IllegalStateException("AudioRecorder is released");
+ }
+ });
+ }
+
/**
* Starts the AudioSource.
*
+ * <p>Before starting, a {@link BufferProvider} should be set with
+ * {@link #setBufferProvider(BufferProvider)}. If a buffer provider is not set, audio data
+ * will be dropped.
+ *
* <p>Audio data will start being sent to the {@link BufferProvider} when
* {@link BufferProvider}'s state is {@link BufferProvider.State#ACTIVE}.
*
@@ -236,7 +287,7 @@
case STARTED:
// Fall-through
case CONFIGURED:
- mBufferProvider.removeObserver(mStateObserver);
+ resetBufferProvider(null);
if (Build.VERSION.SDK_INT >= 29) {
Api29Impl.unregisterAudioRecordingCallback(mAudioRecord,
mAudioRecordingCallback);
@@ -277,6 +328,75 @@
});
}
+ @ExecutedBy("mExecutor")
+ private void resetBufferProvider(@Nullable BufferProvider<InputBuffer> bufferProvider) {
+ if (mBufferProvider != null) {
+ mBufferProvider.removeObserver(mStateObserver);
+ mBufferProvider = null;
+ mStateObserver = null;
+ mAcquireBufferCallback = null;
+ }
+ mBufferProviderState = BufferProvider.State.INACTIVE;
+ updateSendingAudio();
+ if (bufferProvider != null) {
+ mBufferProvider = bufferProvider;
+ mStateObserver = new Observable.Observer<BufferProvider.State>() {
+ @ExecutedBy("mExecutor")
+ @Override
+ public void onNewData(@Nullable BufferProvider.State state) {
+ if (mBufferProvider == bufferProvider) {
+ Logger.d(TAG, "Receive BufferProvider state change: "
+ + mBufferProviderState + " to " + state);
+ mBufferProviderState = state;
+ updateSendingAudio();
+ }
+ }
+
+ @ExecutedBy("mExecutor")
+ @Override
+ public void onError(@NonNull Throwable throwable) {
+ if (mBufferProvider == bufferProvider) {
+ notifyError(throwable);
+ }
+ }
+ };
+
+ mAcquireBufferCallback = new FutureCallback<InputBuffer>() {
+ @ExecutedBy("mExecutor")
+ @Override
+ public void onSuccess(InputBuffer inputBuffer) {
+ if (!mIsSendingAudio || mBufferProvider != bufferProvider) {
+ inputBuffer.cancel();
+ return;
+ }
+ ByteBuffer byteBuffer = inputBuffer.getByteBuffer();
+
+ int length = mAudioRecord.read(byteBuffer, mBufferSize);
+ if (length > 0) {
+ byteBuffer.limit(length);
+ inputBuffer.setPresentationTimeUs(generatePresentationTimeUs());
+ inputBuffer.submit();
+ } else {
+ Logger.w(TAG, "Unable to read data from AudioRecord.");
+ inputBuffer.cancel();
+ }
+ sendNextAudio();
+ }
+
+ @ExecutedBy("mExecutor")
+ @Override
+ public void onFailure(Throwable throwable) {
+ if (mBufferProvider != bufferProvider) {
+ Logger.d(TAG, "Unable to get input buffer, the BufferProvider "
+ + "could be transitioning to INACTIVE state.");
+ notifyError(throwable);
+ }
+ }
+ };
+ mBufferProvider.addObserver(mExecutor, mStateObserver);
+ }
+ }
+
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
void notifyError(Throwable throwable) {
if (mCallbackExecutor != null && mAudioSourceCallback != null) {
@@ -368,56 +488,6 @@
return presentationTimeUs;
}
- private final FutureCallback<InputBuffer> mAcquireBufferCallback =
- new FutureCallback<InputBuffer>() {
- @ExecutedBy("mExecutor")
- @Override
- public void onSuccess(InputBuffer inputBuffer) {
- if (!mIsSendingAudio) {
- inputBuffer.cancel();
- return;
- }
- ByteBuffer byteBuffer = inputBuffer.getByteBuffer();
-
- int length = mAudioRecord.read(byteBuffer, mBufferSize);
- if (length > 0) {
- byteBuffer.limit(length);
- inputBuffer.setPresentationTimeUs(generatePresentationTimeUs());
- inputBuffer.submit();
- } else {
- Logger.w(TAG, "Unable to read data from AudioRecord.");
- inputBuffer.cancel();
- }
- sendNextAudio();
- }
-
- @ExecutedBy("mExecutor")
- @Override
- public void onFailure(Throwable throwable) {
- Logger.d(TAG, "Unable to get input buffer, the BufferProvider "
- + "could be transitioning to INACTIVE state.");
- notifyError(throwable);
- }
- };
-
- private final Observable.Observer<BufferProvider.State> mStateObserver =
- new Observable.Observer<BufferProvider.State>() {
- @ExecutedBy("mExecutor")
- @Override
- public void onNewData(@Nullable BufferProvider.State state) {
- Logger.d(TAG, "Receive BufferProvider state change: "
- + mBufferProviderState + " to " + state);
- mBufferProviderState = state;
- updateSendingAudio();
- }
-
- @ExecutedBy("mExecutor")
- @Override
- public void onError(@NonNull Throwable throwable) {
- notifyError(throwable);
- }
- };
-
/** Check if the combination of sample rate, channel count and audio format is supported. */
public static boolean isSettingsSupported(int sampleRate, int channelCount, int audioFormat) {
if (sampleRate <= 0 || channelCount <= 0) {
@@ -436,124 +506,129 @@
}
/**
- * The builder of the AudioSource.
+ * Settings required to configure the audio source.
*/
- public static class Builder {
- private Executor mExecutor;
- private int mAudioSource = -1;
- private int mSampleRate = -1;
- private int mChannelCount = -1;
- private int mAudioFormat = -1;
- private BufferProvider<InputBuffer> mBufferProvider;
+ @AutoValue
+ public abstract static class Settings {
- /** Sets the executor to run the background task. */
+ /** Creates a builder for these settings. */
+ @SuppressLint("Range") // Need to initialize as invalid values
@NonNull
- public Builder setExecutor(@NonNull Executor executor) {
- mExecutor = Preconditions.checkNotNull(executor);
- return this;
+ public static Settings.Builder builder() {
+ return new AutoValue_AudioSource_Settings.Builder()
+ .setAudioSource(-1)
+ .setSampleRate(-1)
+ .setChannelCount(-1)
+ .setAudioFormat(-1);
}
/**
- * Sets the device audio source.
+ * Gets the device audio source.
*
* @see android.media.MediaRecorder.AudioSource#MIC
* @see android.media.MediaRecorder.AudioSource#CAMCORDER
*/
- @NonNull
- public Builder setAudioSource(int audioSource) {
- mAudioSource = audioSource;
- return this;
- }
+ public abstract int getAudioSource();
/**
- * Sets the audio sample rate.
- *
- * <p>It has to ensure the combination of sample rate, channel count and audio format is
- * supported by {@link AudioSource#isSettingsSupported(int, int, int)}.
- *
- * @throws IllegalArgumentException if the sample rate is not positive.
+ * Gets the audio sample rate.
*/
- @NonNull
- public Builder setSampleRate(int sampleRate) {
- Preconditions.checkArgument(sampleRate > 0);
- mSampleRate = sampleRate;
- return this;
- }
+ @IntRange(from = 1)
+ public abstract int getSampleRate();
/**
- * Sets the channel count.
- *
- * <p>It has to ensure the combination of sample rate, channel count and audio format is
- * supported by {@link AudioSource#isSettingsSupported(int, int, int)}.
- *
- * @throws IllegalArgumentException if the channel count is not positive.
+ * Gets the channel count.
*/
- @NonNull
- public Builder setChannelCount(int channelCount) {
- Preconditions.checkArgument(channelCount > 0);
- mChannelCount = channelCount;
- return this;
- }
+ @IntRange(from = 1)
+ public abstract int getChannelCount();
/**
* Sets the audio format.
*
- * <p>It has to ensure the combination of sample rate, channel count and audio format is
- * supported by {@link AudioSource#isSettingsSupported(int, int, int)}.
- *
* @see AudioFormat#ENCODING_PCM_16BIT
*/
- @NonNull
- public Builder setAudioFormat(int audioFormat) {
- mAudioFormat = audioFormat;
- return this;
+ public abstract int getAudioFormat();
+
+ // Should not be instantiated directly
+ Settings() {
}
- /** Sets the {@link BufferProvider}. */
- @NonNull
- public Builder setBufferProvider(@NonNull BufferProvider<InputBuffer> bufferProvider) {
- mBufferProvider = Preconditions.checkNotNull(bufferProvider);
- return this;
- }
+ /**
+ * A Builder for {@link AudioSource.Settings}
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ /**
+ * Sets the device audio source.
+ *
+ * @see android.media.MediaRecorder.AudioSource#MIC
+ * @see android.media.MediaRecorder.AudioSource#CAMCORDER
+ */
+ @NonNull
+ public abstract Builder setAudioSource(int audioSource);
- /** Build the AudioSource. */
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
- @NonNull
- public AudioSource build() throws AudioSourceAccessException {
- String missing = "";
- if (mExecutor == null) {
- missing += " executor";
+ /**
+ * Sets the audio sample rate in Hertz.
+ */
+ @NonNull
+ public abstract Builder setSampleRate(@IntRange(from = 1) int sampleRate);
+
+ /**
+ * Sets the channel count.
+ */
+ @NonNull
+ public abstract Builder setChannelCount(@IntRange(from = 1) int channelCount);
+
+ /**
+ * Sets the audio format.
+ *
+ * @see AudioFormat#ENCODING_PCM_16BIT
+ */
+ @NonNull
+ public abstract Builder setAudioFormat(int audioFormat);
+
+ abstract Settings autoBuild(); // Actual build method. Not public.
+
+ /**
+ * Returns the built config after performing settings validation.
+ *
+ * <p>It should be verified that combination of sample rate, channel count and audio
+ * format is supported by {@link AudioSource#isSettingsSupported(int, int, int)} or
+ * an {@link UnsupportedOperationException} will be thrown when passing the settings
+ * to the
+ * {@linkplain AudioSource#AudioSource(Settings, Executor, BufferProvider) AudioSource
+ * constructor}.
+ *
+ * @throws IllegalArgumentException if a setting is missing or invalid.
+ */
+ @NonNull
+ public final Settings build() {
+ Settings settings = autoBuild();
+ String missingOrInvalid = "";
+ if (settings.getAudioSource() == -1) {
+ missingOrInvalid += " audioSource";
+ }
+ if (settings.getSampleRate() <= 0) {
+ missingOrInvalid += " sampleRate";
+ }
+ if (settings.getChannelCount() <= 0) {
+ missingOrInvalid += " channelCount";
+ }
+ if (settings.getAudioFormat() == -1) {
+ missingOrInvalid += " audioFormat";
+ }
+
+ if (!missingOrInvalid.isEmpty()) {
+ throw new IllegalArgumentException("Required settings missing or "
+ + "non-positive:" + missingOrInvalid);
+ }
+
+ return settings;
}
- if (mBufferProvider == null) {
- missing += " bufferProvider";
+
+ // Should not be instantiated directly
+ Builder() {
}
- if (mAudioSource == -1) {
- missing += " audioSource";
- }
- if (mSampleRate == -1) {
- missing += " sampleRate";
- }
- if (mChannelCount == -1) {
- missing += " channelCount";
- }
- if (mAudioFormat == -1) {
- missing += " audioFormat";
- }
- if (!missing.isEmpty()) {
- throw new IllegalStateException("Missing required properties:" + missing);
- }
- if (!isSettingsSupported(mSampleRate, mChannelCount, mAudioFormat)) {
- throw new IllegalStateException(String.format("The combination of sample rate %d "
- + ", channel count %d and audio format %d is not supported.",
- mSampleRate, mChannelCount, mAudioFormat));
- }
- return new AudioSource(mExecutor,
- mBufferProvider,
- mAudioSource,
- mSampleRate,
- mChannelCount,
- mAudioFormat
- );
}
}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt
index b3286f1..39bc237 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt
@@ -52,8 +52,8 @@
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
-import kotlinx.coroutines.withTimeout
import org.junit.After
+import org.junit.Assert.assertTrue
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
@@ -223,17 +223,15 @@
val waitingList = mutableListOf<CaptureContainer>()
- suspend fun waitFor(
+ fun waitFor(
timeout: Long = TimeUnit.SECONDS.toMillis(5),
numOfCaptures: Int = 1,
verifyResults: (captureRequests: List<CaptureRequest>) -> Unit
) {
val resultContainer = CaptureContainer(CountDownLatch(numOfCaptures))
waitingList.add(resultContainer)
- withTimeout(timeout) {
- resultContainer.countDownLatch.await()
- verifyResults(resultContainer.captureRequests)
- }
+ assertTrue(resultContainer.countDownLatch.await(timeout, TimeUnit.MILLISECONDS))
+ verifyResults(resultContainer.captureRequests)
waitingList.remove(resultContainer)
}
diff --git a/compose/benchmark-utils/OWNERS b/compose/benchmark-utils/OWNERS
index 305021a..b2c0d56 100644
--- a/compose/benchmark-utils/OWNERS
+++ b/compose/benchmark-utils/OWNERS
@@ -1,2 +1 @@
-pavlis@google.com
jellefresen@google.com
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyListScrollingBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyListScrollingBenchmark.kt
index 036d159..55964fa 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyListScrollingBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyListScrollingBenchmark.kt
@@ -48,6 +48,7 @@
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assume
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -83,6 +84,7 @@
}
}
+ @Ignore("b/206865677")
@Test
fun scrollViaPointerInput_noNewItems() {
benchmarkRule.toggleStateBenchmark {
@@ -95,6 +97,7 @@
}
}
+ @Ignore("b/206865677")
@Test
fun scrollViaPointerInput_newItemComposed() {
benchmarkRule.toggleStateBenchmark {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TransformableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TransformableTest.kt
index 073fad1..3fb977b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TransformableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TransformableTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.TransformableState
import androidx.compose.foundation.gestures.rememberTransformableState
import androidx.compose.foundation.gestures.animatePanBy
@@ -29,7 +30,9 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.InspectableValue
@@ -37,6 +40,7 @@
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
@@ -47,6 +51,9 @@
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
@@ -66,6 +73,8 @@
@get:Rule
val rule = createComposeRule()
+ private lateinit var scope: CoroutineScope
+
@Before
fun before() {
isDebugInspectorInfoEnabled = true
@@ -76,6 +85,14 @@
isDebugInspectorInfoEnabled = false
}
+ private fun ComposeContentTestRule.setContentAndGetScope(content: @Composable () -> Unit) {
+ setContent {
+ val actualScope = rememberCoroutineScope()
+ SideEffect { scope = actualScope }
+ content()
+ }
+ }
+
@Test
fun transformable_zoomIn() {
var cumulativeScale = 1.0f
@@ -564,6 +581,36 @@
}
@Test
+ fun transformable_animateCancelledUpdatesIsTransformInProgress() {
+ rule.mainClock.autoAdvance = false
+ val state = TransformableState { _, _, _ -> }
+ setTransformableContent {
+ Modifier.transformable(state)
+ }
+
+ lateinit var animateJob: Job
+
+ rule.runOnIdle {
+ assertThat(state.isTransformInProgress).isFalse()
+ animateJob = scope.launch {
+ state.animateZoomBy(4f, tween(1000))
+ }
+ }
+
+ rule.mainClock.advanceTimeBy(500)
+
+ rule.runOnIdle {
+ assertThat(state.isTransformInProgress).isTrue()
+ }
+
+ animateJob.cancel()
+
+ rule.runOnIdle {
+ assertThat(state.isTransformInProgress).isFalse()
+ }
+ }
+
+ @Test
fun testInspectorValue() {
rule.setContent {
val state = rememberTransformableState { _, _, _ -> }
@@ -579,7 +626,7 @@
}
private fun setTransformableContent(getModifier: @Composable () -> Modifier) {
- rule.setContent {
+ rule.setContentAndGetScope {
Box(Modifier.size(600.dp).testTag(TEST_TAG).then(getModifier()))
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/TempListUtils.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/TempListUtils.kt
index 0d55ea2..34d6bae 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/TempListUtils.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/TempListUtils.kt
@@ -25,6 +25,10 @@
/**
* Returns a list containing only elements matching the given [predicate].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
@@ -42,6 +46,10 @@
*
* Returns the specified [initial] value if the collection is empty.
*
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ *
* @param [operation] function that takes current accumulator value and an element, and calculates the next accumulator value.
*/
@OptIn(ExperimentalContracts::class)
@@ -57,6 +65,10 @@
/**
* Returns a list containing the results of applying the given [transform] function
* to each element in the original collection.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T, R> List<T>.fastMapIndexedNotNull(
@@ -73,6 +85,10 @@
/**
* Returns the largest value among all values produced by selector function applied to each element
* in the collection or null if there are no elements.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T, R : Comparable<R>> List<T>.fastMaxOfOrNull(selector: (T) -> R): R? {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TransformableState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TransformableState.kt
index 5298664..1e5d2b7 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TransformableState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TransformableState.kt
@@ -245,8 +245,11 @@
): Unit = coroutineScope {
transformMutex.mutateWith(transformScope, transformPriority) {
isTransformingState.value = true
- block()
- isTransformingState.value = false
+ try {
+ block()
+ } finally {
+ isTransformingState.value = false
+ }
}
}
diff --git a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
index 2a9e88f..3ecc5c7 100644
--- a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
+++ b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
@@ -33,13 +33,11 @@
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.input.mouse.MouseScrollOrientation
-import androidx.compose.ui.input.mouse.MouseScrollUnit
-import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.InternalTestApi
@@ -432,10 +430,10 @@
@OptIn(InternalTestApi::class, ExperimentalComposeUiApi::class)
private fun ComposeTestRule.performMouseScroll(x: Int, y: Int, delta: Float) {
- (this as DesktopComposeTestRule).scene.sendPointerScrollEvent(
+ (this as DesktopComposeTestRule).scene.sendPointerEvent(
+ PointerEventType.Move,
Offset(x.toFloat(), y.toFloat()),
- MouseScrollUnit.Line(delta),
- MouseScrollOrientation.Vertical
+ scrollDelta = Offset(x = 0f, y = delta)
)
}
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
index 844d010..c225449 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.gestures
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.pointer.consumeDownChange
import androidx.compose.ui.input.pointer.consumePositionChange
import kotlinx.coroutines.delay
@@ -108,10 +109,12 @@
/**
* Clicking in the region should result in the callback being invoked.
*/
+ @OptIn(ExperimentalComposeUiApi::class)
@Test
fun normalTap() = util.executeInComposition {
val down = down(5f, 5f)
assertTrue(down.consumed.downChange)
+ assertTrue(down.isConsumed)
assertTrue(pressed)
assertFalse(tapped)
@@ -119,6 +122,7 @@
val up = down.up(50)
assertTrue(up.consumed.downChange)
+ assertTrue(up.isConsumed)
assertTrue(tapped)
assertTrue(released)
@@ -224,6 +228,7 @@
* Pressing in the region, sliding out and then lifting should result in
* the callback not being invoked
*/
+ @OptIn(ExperimentalComposeUiApi::class)
@Test
fun tapMiss() = util.executeInComposition {
val up = down(5f, 5f)
@@ -235,6 +240,7 @@
assertFalse(released)
assertFalse(tapped)
assertFalse(up.consumed.downChange)
+ assertFalse(up.isConsumed)
}
/**
@@ -467,9 +473,46 @@
assertTrue(canceled)
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Test
+ fun consumedChange_MotionTap() = util.executeInComposition {
+ down(5f, 5f)
+ .moveTo(6f, 2f) {
+ consume()
+ }
+ .up(50)
+
+ assertFalse(tapped)
+ assertTrue(pressed)
+ assertFalse(released)
+ assertTrue(canceled)
+ }
+
+ /**
+ * Clicking in the region with the up already consumed should result in the callback not
+ * being invoked.
+ */
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Test
+ fun consumedChange_upTap() = util.executeInComposition {
+ val down = down(5f, 5f)
+
+ assertFalse(tapped)
+ assertTrue(pressed)
+
+ down.up {
+ consume()
+ }
+
+ assertFalse(tapped)
+ assertFalse(released)
+ assertTrue(canceled)
+ }
+
/**
* Ensure that two-finger taps work.
*/
+ @OptIn(ExperimentalComposeUiApi::class)
@Test
fun twoFingerTap() = util.executeInComposition {
val down = down(1f, 1f)
@@ -480,16 +523,19 @@
val down2 = down(9f, 5f)
assertFalse(down2.consumed.downChange)
+ assertFalse(down2.isConsumed)
assertFalse(pressed)
val up = down.up()
assertFalse(up.consumed.downChange)
+ assertFalse(up.isConsumed)
assertFalse(tapped)
assertFalse(released)
val up2 = down2.up()
assertTrue(up2.consumed.downChange)
+ assertTrue(up2.isConsumed)
assertTrue(tapped)
assertTrue(released)
diff --git a/compose/integration-tests/demos/OWNERS b/compose/integration-tests/demos/OWNERS
index 49be039..cb0413de 100644
--- a/compose/integration-tests/demos/OWNERS
+++ b/compose/integration-tests/demos/OWNERS
@@ -1,4 +1,3 @@
-pavlis@google.com
adamp@google.com
mount@google.com
popam@google.com
diff --git a/compose/integration-tests/demos/build.gradle b/compose/integration-tests/demos/build.gradle
index 00e53eb..cec8a8f 100644
--- a/compose/integration-tests/demos/build.gradle
+++ b/compose/integration-tests/demos/build.gradle
@@ -22,6 +22,7 @@
implementation(project(":compose:foundation:foundation-layout"))
implementation(project(":compose:integration-tests:demos:common"))
implementation(project(":compose:material:material"))
+ implementation(project(":compose:material3:material3"))
implementation(project(":compose:runtime:runtime"))
implementation(project(":compose:ui:ui"))
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
index b438f13..0fa2230 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoActivity.kt
@@ -17,22 +17,23 @@
package androidx.compose.integration.demos
import android.app.Activity
-import android.content.Context
import android.content.Intent
-import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import android.view.Window
import androidx.activity.OnBackPressedCallback
import androidx.activity.OnBackPressedDispatcher
+import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.integration.demos.common.ActivityDemo
import androidx.compose.integration.demos.common.Demo
import androidx.compose.integration.demos.common.DemoCategory
-import androidx.compose.material.Colors
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.darkColors
-import androidx.compose.material.lightColors
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -45,12 +46,12 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalView
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
-import androidx.preference.PreferenceManager
/**
* Main [Activity] containing all Compose related demos.
@@ -75,18 +76,19 @@
) {
Navigator(AllDemosCategory, onBackPressedDispatcher, activityStarter)
}
- val demoColors = remember {
- DemoColors().also {
- lifecycle.addObserver(
- LifecycleEventObserver { _, event ->
- if (event == Lifecycle.Event.ON_RESUME) {
- it.loadColorsFromSharedPreferences(this)
- }
- }
- )
+ val isDynamicThemeOn = remember { mutableStateOf(IsDynamicThemingAvailable) }
+ DisposableEffect(lifecycle) {
+ val obs = LifecycleEventObserver { _, event ->
+ if (event == Lifecycle.Event.ON_RESUME) {
+ isDynamicThemeOn.value = isDynamicThemeSettingOn(applicationContext)
+ }
+ }
+ lifecycle.addObserver(obs)
+ onDispose {
+ lifecycle.removeObserver(obs)
}
}
- DemoTheme(demoColors, window) {
+ DemoTheme(isDynamicThemeOn.value, window) {
val filteringMode = rememberSaveable(
saver = FilterMode.Saver(onBackPressedDispatcher)
) {
@@ -122,19 +124,26 @@
@Composable
private fun DemoTheme(
- demoColors: DemoColors,
+ isDynamicThemeOn: Boolean,
window: Window,
content: @Composable () -> Unit
) {
- MaterialTheme(demoColors.colors) {
- val statusBarColor = with(MaterialTheme.colors) {
- (if (isLight) primaryVariant else Color.Black).toArgb()
+ val isDarkMode = isSystemInDarkTheme()
+
+ @Suppress("NewApi")
+ val colorScheme =
+ if (isDynamicThemeOn) {
+ val context = LocalContext.current
+ if (isDarkMode) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ } else {
+ if (isDarkMode) darkColorScheme() else lightColorScheme()
}
- SideEffect {
- window.statusBarColor = statusBarColor
- }
- content()
+
+ SideEffect {
+ window.statusBarColor =
+ (if (isDarkMode) Color.Black else colorScheme.inversePrimary).toArgb()
}
+ MaterialTheme(colorScheme = colorScheme, content = content)
}
private class Navigator private constructor(
@@ -254,95 +263,3 @@
)
}
}
-
-/**
- * Returns a [DemoColors] from the values saved to [SharedPreferences]. If a given color is
- * not present in the [SharedPreferences], its default value as defined in [Colors]
- * will be returned.
- */
-fun DemoColors.loadColorsFromSharedPreferences(context: Context) {
- val sharedPreferences =
- PreferenceManager.getDefaultSharedPreferences(context)
-
- fun getColorsFromSharedPreferences(isLightTheme: Boolean): Colors {
- val function = if (isLightTheme) ::reflectLightColors else ::reflectDarkColors
- val parametersToSet = function.parameters.mapNotNull { parameter ->
- val savedValue = sharedPreferences.getString(parameter.name + isLightTheme, "")
- if (savedValue.isNullOrBlank()) {
- null
- } else {
- // TODO: should be a Color(savedValue.toLong(16)) when b/154329050 is fixed
- val parsedColor = savedValue.toLong(16)
- parameter to parsedColor
- }
- }.toMap()
- return function.callBy(parametersToSet)
- }
-
- light = getColorsFromSharedPreferences(true)
- dark = getColorsFromSharedPreferences(false)
-}
-
-/**
- * TODO: remove after b/154329050 is fixed
- * Inline classes don't play well with reflection, so we want boxed classes for our
- * call to [lightColors].
- */
-internal fun reflectLightColors(
- primary: Long = 0xFF6200EE,
- primaryVariant: Long = 0xFF3700B3,
- secondary: Long = 0xFF03DAC6,
- secondaryVariant: Long = 0xFF018786,
- background: Long = 0xFFFFFFFF,
- surface: Long = 0xFFFFFFFF,
- error: Long = 0xFFB00020,
- onPrimary: Long = 0xFFFFFFFF,
- onSecondary: Long = 0xFF000000,
- onBackground: Long = 0xFF000000,
- onSurface: Long = 0xFF000000,
- onError: Long = 0xFFFFFFFF
-) = lightColors(
- primary = Color(primary),
- primaryVariant = Color(primaryVariant),
- secondary = Color(secondary),
- secondaryVariant = Color(secondaryVariant),
- background = Color(background),
- surface = Color(surface),
- error = Color(error),
- onPrimary = Color(onPrimary),
- onSecondary = Color(onSecondary),
- onBackground = Color(onBackground),
- onSurface = Color(onSurface),
- onError = Color(onError)
-)
-
-/**
- * TODO: remove after b/154329050 is fixed
- * Inline classes don't play well with reflection, so we want boxed classes for our
- * call to [darkColors].
- */
-internal fun reflectDarkColors(
- primary: Long = 0xFFBB86FC,
- primaryVariant: Long = 0xFF3700B3,
- secondary: Long = 0xFF03DAC6,
- background: Long = 0xFF121212,
- surface: Long = 0xFF121212,
- error: Long = 0xFFCF6679,
- onPrimary: Long = 0xFF000000,
- onSecondary: Long = 0xFF000000,
- onBackground: Long = 0xFFFFFFFF,
- onSurface: Long = 0xFFFFFFFF,
- onError: Long = 0xFF000000
-) = darkColors(
- primary = Color(primary),
- primaryVariant = Color(primaryVariant),
- secondary = Color(secondary),
- background = Color(background),
- surface = Color(surface),
- error = Color(error),
- onPrimary = Color(onPrimary),
- onSecondary = Color(onSecondary),
- onBackground = Color(onBackground),
- onSurface = Color(onSurface),
- onError = Color(onError)
-)
\ No newline at end of file
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoApp.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoApp.kt
index 858fa8d..902fe64 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoApp.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoApp.kt
@@ -18,10 +18,15 @@
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -31,28 +36,31 @@
import androidx.compose.integration.demos.common.DemoCategory
import androidx.compose.integration.demos.common.FragmentDemo
import androidx.compose.integration.demos.common.allLaunchableDemos
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.Icon
-import androidx.compose.material.IconButton
-import androidx.compose.material.ListItem
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Scaffold
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SmallTopAppBar
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.LayoutDirection
@@ -61,6 +69,7 @@
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentContainerView
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DemoApp(
currentDemo: Demo,
@@ -77,11 +86,14 @@
var filterText by rememberSaveable { mutableStateOf("") }
+ val scrollBehavior = remember { TopAppBarDefaults.pinnedScrollBehavior() }
+
Scaffold(
topBar = {
DemoAppBar(
title = backStackTitle,
- navigationIcon = navigationIcon,
+ scrollBehavior = scrollBehavior,
+ navigationIcon = navigationIcon ?: {},
launchSettings = launchSettings,
isFiltering = isFiltering,
filterText = filterText,
@@ -89,7 +101,8 @@
onStartFiltering = onStartFiltering,
onEndFiltering = onEndFiltering
)
- }
+ },
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
) { innerPadding ->
val modifier = Modifier.padding(innerPadding)
DemoContent(modifier, currentDemo, isFiltering, filterText, onNavigateToDemo, onNavigateUp)
@@ -106,7 +119,7 @@
onNavigateUp: () -> Unit
) {
Crossfade(isFiltering to currentDemo) { (filtering, demo) ->
- Surface(modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
+ Surface(modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
if (filtering) {
DemoFilter(
launchableDemos = AllDemosCategory.allLaunchableDemos(),
@@ -121,12 +134,26 @@
}
@Composable
+fun Material2LegacyTheme(content: @Composable () -> Unit) {
+ val material2Colors =
+ if (isSystemInDarkTheme()) {
+ androidx.compose.material.darkColors()
+ } else {
+ androidx.compose.material.lightColors()
+ }
+ androidx.compose.material.MaterialTheme(colors = material2Colors, content = content)
+}
+
+@Composable
private fun DisplayDemo(demo: Demo, onNavigate: (Demo) -> Unit, onNavigateUp: () -> Unit) {
when (demo) {
is ActivityDemo<*> -> {
/* should never get here as activity demos are not added to the backstack*/
}
- is ComposableDemo -> demo.content(onNavigateUp)
+ is ComposableDemo ->
+ // provide material 2 as well for interop, find a way to
+ // remove it when all demos migrated to m3
+ Material2LegacyTheme { demo.content(onNavigateUp) }
is DemoCategory -> DisplayDemoCategory(demo, onNavigate)
is FragmentDemo<*> -> {
lateinit var view: FragmentContainerView
@@ -156,21 +183,16 @@
}
@Composable
-@OptIn(ExperimentalMaterialApi::class)
private fun DisplayDemoCategory(category: DemoCategory, onNavigate: (Demo) -> Unit) {
- // TODO: migrate to LazyColumn after b/175671850
+ // TODO: migrate to LazyColumn after DemoTests are rewritten to accommodate laziness
Column(Modifier.verticalScroll(rememberScrollState())) {
category.demos.forEach { demo ->
- ListItem(
- text = {
- Text(
- modifier = Modifier.height(56.dp)
- .wrapContentSize(Alignment.Center),
- text = demo.title
- )
- },
- modifier = Modifier.clickable { onNavigate(demo) }
- )
+ ListItem(onClick = { onNavigate(demo) }) {
+ Text(
+ modifier = Modifier.height(56.dp).wrapContentSize(Alignment.Center),
+ text = demo.title
+ )
+ }
}
}
}
@@ -179,7 +201,8 @@
@Composable
private fun DemoAppBar(
title: String,
- navigationIcon: @Composable (() -> Unit)?,
+ scrollBehavior: TopAppBarScrollBehavior,
+ navigationIcon: @Composable () -> Unit,
isFiltering: Boolean,
filterText: String,
onFilter: (String) -> Unit,
@@ -191,13 +214,15 @@
FilterAppBar(
filterText = filterText,
onFilter = onFilter,
- onClose = onEndFiltering
+ onClose = onEndFiltering,
+ scrollBehavior = scrollBehavior
)
} else {
- TopAppBar(
+ SmallTopAppBar(
title = {
Text(title, Modifier.testTag(Tags.AppBarTitle))
},
+ scrollBehavior = scrollBehavior,
navigationIcon = navigationIcon,
actions = {
AppBarIcons.Filter(onClick = onStartFiltering)
@@ -233,3 +258,20 @@
}
}
}
+
+@Composable
+internal fun ListItem(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ content: @Composable (() -> Unit)
+) {
+ Box(
+ modifier
+ .heightIn(min = 48.dp)
+ .fillMaxWidth()
+ .clickable(onClick = onClick)
+ .padding(horizontal = 16.dp)
+ .wrapContentHeight(Alignment.CenterVertically),
+ contentAlignment = Alignment.CenterStart
+ ) { content() }
+}
\ No newline at end of file
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoColors.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoColors.kt
deleted file mode 100644
index 5bb3529..0000000
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoColors.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.integration.demos
-
-import android.content.SharedPreferences
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material.Colors
-import androidx.compose.material.darkColors
-import androidx.compose.material.lightColors
-
-/**
- * Wrapper class that contains a light and dark [Colors], to allow saving and
- * restoring the entire light / dark theme to and from [SharedPreferences].
- */
-@Stable
-class DemoColors {
- var light: Colors by mutableStateOf(lightColors())
- var dark: Colors by mutableStateOf(darkColors())
-
- val colors
- @Composable get() = if (isSystemInDarkTheme()) dark else light
-}
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
index 957232c..62a05db 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
@@ -17,7 +17,6 @@
package androidx.compose.integration.demos
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -26,17 +25,16 @@
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.verticalScroll
import androidx.compose.integration.demos.common.Demo
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.Icon
-import androidx.compose.material.IconButton
-import androidx.compose.material.ListItem
-import androidx.compose.material.LocalContentColor
-import androidx.compose.material.LocalTextStyle
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
-import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SmallTopAppBar
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.key
@@ -46,7 +44,6 @@
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
-import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
@@ -73,33 +70,30 @@
}
/**
- * [TopAppBar] with a text field allowing filtering all the demos.
+ * [SmallTopAppBar] with a text field allowing filtering all the demos.
*/
@Composable
fun FilterAppBar(
filterText: String,
onFilter: (String) -> Unit,
- onClose: () -> Unit
+ onClose: () -> Unit,
+ scrollBehavior: TopAppBarScrollBehavior
) {
- with(MaterialTheme.colors) {
- val appBarColor = if (isLight) {
- surface
- } else {
- // Blending primary over surface according to Material design guidance for brand
- // surfaces in dark theme
- primary.copy(alpha = 0.08f).compositeOver(surface)
- }
- TopAppBar(backgroundColor = appBarColor, contentColor = onSurface) {
- IconButton(modifier = Modifier.align(Alignment.CenterVertically), onClick = onClose) {
+ SmallTopAppBar(
+ navigationIcon = {
+ IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, null)
}
+ },
+ title = {
FilterField(
filterText,
onFilter,
- Modifier.fillMaxWidth().align(Alignment.CenterVertically)
+ Modifier.fillMaxWidth()
)
- }
- }
+ },
+ scrollBehavior = scrollBehavior
+ )
}
/**
@@ -131,13 +125,12 @@
* [ListItem] that displays a [demo] and highlights any matches for [filterText] inside [Demo.title]
*/
@Composable
-@OptIn(ExperimentalMaterialApi::class)
private fun FilteredDemoListItem(
demo: Demo,
filterText: String,
onNavigate: (Demo) -> Unit
) {
- val primary = MaterialTheme.colors.primary
+ val primary = MaterialTheme.colorScheme.primary
val annotatedString = buildAnnotatedString {
val title = demo.title
var currentIndex = 0
@@ -159,13 +152,11 @@
}
key(demo.title) {
ListItem(
- text = {
- Text(
- modifier = Modifier.height(56.dp).wrapContentSize(Alignment.Center),
- text = annotatedString
- )
- },
- modifier = Modifier.clickable { onNavigate(demo) }
- )
+ onClick = { onNavigate(demo) }) {
+ Text(
+ modifier = Modifier.height(56.dp).wrapContentSize(Alignment.Center),
+ text = annotatedString
+ )
+ }
}
}
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoSettingsActivity.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoSettingsActivity.kt
index 3f0f540..e41e89c 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoSettingsActivity.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoSettingsActivity.kt
@@ -17,21 +17,15 @@
package androidx.compose.integration.demos
import android.content.Context
-import android.content.SharedPreferences
+import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
-import androidx.preference.EditTextPreference
-import androidx.preference.Preference
+import androidx.compose.integration.demos.DemoSettingsActivity.SettingsFragment
+import androidx.preference.CheckBoxPreference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.preference.plusAssign
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.material.Colors
-import androidx.compose.material.darkColors
-import androidx.compose.material.lightColors
-import kotlin.reflect.full.memberProperties
/**
* Shell [AppCompatActivity] around [SettingsFragment], as we need a FragmentActivity subclass
@@ -56,371 +50,22 @@
screen += this
}
- general += Preference(context).apply {
- title = "Shuffle all colors"
- onPreferenceClickListener = Preference.OnPreferenceClickListener {
- generateRandomPalette().saveColors(context)
- requireActivity().finish()
- true
- }
- }
-
- general += Preference(context).apply {
- title = "Reset colors to default"
- onPreferenceClickListener = Preference.OnPreferenceClickListener {
- val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
- sharedPreferences.edit().clear().apply()
- requireActivity().finish()
- true
- }
- }
-
- val light = PreferenceCategory(context).apply {
- title = "Light colors"
- screen += this
- }
- // Create new Colors to resolve defaults
- lightColors().forEachColorProperty { name, color ->
- light += EditTextPreference(context).apply {
- key = name + true
- title = name
- // set the default value to be the default for Colors
- setDefaultValue(Integer.toHexString(color.toArgb()))
- summaryProvider = EditTextPreference.SimpleSummaryProvider.getInstance()
- }
- }
-
- val dark = PreferenceCategory(context).apply {
- title = "Dark colors"
- screen += this
- }
-
- darkColors().forEachColorProperty { name, color ->
- dark += EditTextPreference(context).apply {
- key = name + false
- title = name
- // set the default value to be the default for Colors
- setDefaultValue(Integer.toHexString(color.toArgb()))
- summaryProvider = EditTextPreference.SimpleSummaryProvider.getInstance()
- }
+ general += CheckBoxPreference(context).apply {
+ title = "Dynamic theming (android S+)"
+ isEnabled = IsDynamicThemingAvailable
+ key = IsDynamicThemeOnKey
+ setDefaultValue(isDynamicThemeSettingOn(context))
}
preferenceScreen = screen
}
}
}
-/**
- * Iterates over each color present in a given [Colors].
- *
- * @param action the action to take on each property, where name is the name of the property,
- * such as 'primary' for [Colors.primary], and color is the resolved [Color] of the
- * property.
- */
-private fun Colors.forEachColorProperty(action: (name: String, color: Color) -> Unit) {
- Colors::class.memberProperties.forEach { property ->
- val name = property.name
- val color = property.get(this) as? Color ?: return@forEach
- action(name, color)
- }
+internal fun isDynamicThemeSettingOn(context: Context): Boolean {
+ return PreferenceManager
+ .getDefaultSharedPreferences(context)
+ .getBoolean(IsDynamicThemeOnKey, IsDynamicThemingAvailable)
}
-/**
- * Persists the current [DemoColors] to [SharedPreferences].
- */
-private fun DemoColors.saveColors(context: Context) {
- light.forEachColorProperty { name, color ->
- PreferenceManager.getDefaultSharedPreferences(context)
- .edit()
- .putString(name + true, Integer.toHexString(color.toArgb()))
- .apply()
- }
- dark.forEachColorProperty { name, color ->
- PreferenceManager.getDefaultSharedPreferences(context)
- .edit()
- .putString(name + false, Integer.toHexString(color.toArgb()))
- .apply()
- }
-}
-
-/**
- * Generates a [DemoColors] with random [Colors.primary], [Colors.onPrimary],
- * [Colors.secondary] and [Colors.onSecondary] as random dark-on-light or light-on-dark
- * pairs. Other colors are kept from the baseline.
- */
-private fun generateRandomPalette(): DemoColors {
- return DemoColors().apply {
- val (lightPrimary, lightOnPrimary) = generateColorPair(true)
- val (lightSecondary, lightOnSecondary) = generateColorPair(true)
- light = lightColors(
- primary = lightPrimary,
- secondary = lightSecondary,
- onPrimary = lightOnPrimary,
- onSecondary = lightOnSecondary
- )
- val (darkPrimary, darkOnPrimary) = generateColorPair(false)
- val (darkSecondary, darkOnSecondary) = generateColorPair(false)
- dark = darkColors(
- primary = darkPrimary,
- secondary = darkSecondary,
- onPrimary = darkOnPrimary,
- onSecondary = darkOnSecondary
- )
- }
-}
-
-/**
- * Generate a random dark and light color from the palette, and returns either a dark-on-light
- * or light-on-dark color pair.
- */
-private fun generateColorPair(isLightTheme: Boolean): Pair<Color, Color> {
- val darkColor = Color(DarkPaletteColors.random())
- val lightColor = Color(LightPaletteColors.random())
- return if (isLightTheme) {
- darkColor to lightColor
- } else {
- lightColor to darkColor
- }
-}
-
-// Colors taken from https://material.io/design/color -> 2014 Material Design color palettes
-val LightPaletteColors = listOf(
- 0xFFEF5350,
- 0xFFF44336,
- 0xFFE53935,
- 0xFFD32F2F,
- 0xFFC62828,
- 0xFFB71C1C,
- 0xFFFF5252,
- 0xFFFF1744,
- 0xFFD50000,
- 0xFFEC407A,
- 0xFFE91E63,
- 0xFFD81B60,
- 0xFFC2185B,
- 0xFFAD1457,
- 0xFF880E4F,
- 0xFFFF4081,
- 0xFFF50057,
- 0xFFC51162,
- 0xFFBA68C8,
- 0xFFAB47BC,
- 0xFF9C27B0,
- 0xFF8E24AA,
- 0xFF7B1FA2,
- 0xFF6A1B9A,
- 0xFF4A148C,
- 0xFFE040FB,
- 0xFFD500F9,
- 0xFFAA00FF,
- 0xFF9575CD,
- 0xFF7E57C2,
- 0xFF673AB7,
- 0xFF5E35B1,
- 0xFF512DA8,
- 0xFF4527A0,
- 0xFF311B92,
- 0xFF7C4DFF,
- 0xFF651FFF,
- 0xFF6200EA,
- 0xFF7986CB,
- 0xFF5C6BC0,
- 0xFF3F51B5,
- 0xFF3949AB,
- 0xFF303F9F,
- 0xFF283593,
- 0xFF1A237E,
- 0xFF536DFE,
- 0xFF3D5AFE,
- 0xFF304FFE,
- 0xFF1E88E5,
- 0xFF1976D2,
- 0xFF1565C0,
- 0xFF0D47A1,
- 0xFF448AFF,
- 0xFF2979FF,
- 0xFF2962FF,
- 0xFF0288D1,
- 0xFF0277BD,
- 0xFF01579B,
- 0xFF0091EA,
- 0xFF0097A7,
- 0xFF00838F,
- 0xFF006064,
- 0xFF009688,
- 0xFF00897B,
- 0xFF00796B,
- 0xFF00695C,
- 0xFF004D40,
- 0xFF43A047,
- 0xFF388E3C,
- 0xFF2E7D32,
- 0xFF1B5E20,
- 0xFF558B2F,
- 0xFF33691E,
- 0xFF827717,
- 0xFFE65100,
- 0xFFF4511E,
- 0xFFE64A19,
- 0xFFD84315,
- 0xFFBF360C,
- 0xFFFF3D00,
- 0xFFDD2C00,
- 0xFFA1887F,
- 0xFF8D6E63,
- 0xFF795548,
- 0xFF6D4C41,
- 0xFF5D4037,
- 0xFF4E342E,
- 0xFF3E2723,
- 0xFF757575,
- 0xFF616161,
- 0xFF424242,
- 0xFF212121,
- 0xFF78909C,
- 0xFF607D8B,
- 0xFF546E7A,
- 0xFF455A64,
- 0xFF37474F,
- 0xFF263238
-)
-
-val DarkPaletteColors = listOf(
- 0xFFFFCDD2,
- 0xFFEF9A9A,
- 0xFFE57373,
- 0xFFFF8A80,
- 0xFFF8BBD0,
- 0xFFF48FB1,
- 0xFFF06292,
- 0xFFFF80AB,
- 0xFFE1BEE7,
- 0xFFCE93D8,
- 0xFFEA80FC,
- 0xFFD1C4E9,
- 0xFFB39DDB,
- 0xFFB388FF,
- 0xFFC5CAE9,
- 0xFF9FA8DA,
- 0xFF8C9EFF,
- 0xFFBBDEFB,
- 0xFF90CAF9,
- 0xFF64B5F6,
- 0xFF42A5F5,
- 0xFF2196F3,
- 0xFF82B1FF,
- 0xFFB3E5FC,
- 0xFF81D4FA,
- 0xFF4FC3F7,
- 0xFF29B6F6,
- 0xFF03A9F4,
- 0xFF039BE5,
- 0xFF80D8FF,
- 0xFF40C4FF,
- 0xFF00B0FF,
- 0xFFB2EBF2,
- 0xFF80DEEA,
- 0xFF4DD0E1,
- 0xFF26C6DA,
- 0xFF00BCD4,
- 0xFF00ACC1,
- 0xFF84FFFF,
- 0xFF18FFFF,
- 0xFF00E5FF,
- 0xFF00B8D4,
- 0xFFB2DFDB,
- 0xFF80CBC4,
- 0xFF4DB6AC,
- 0xFF26A69A,
- 0xFFA7FFEB,
- 0xFF64FFDA,
- 0xFF1DE9B6,
- 0xFF00BFA5,
- 0xFFC8E6C9,
- 0xFFA5D6A7,
- 0xFF81C784,
- 0xFF66BB6A,
- 0xFF4CAF50,
- 0xFFB9F6CA,
- 0xFF69F0AE,
- 0xFF00E676,
- 0xFF00C853,
- 0xFFDCEDC8,
- 0xFFC5E1A5,
- 0xFFAED581,
- 0xFF9CCC65,
- 0xFF8BC34A,
- 0xFF7CB342,
- 0xFF689F38,
- 0xFFCCFF90,
- 0xFFB2FF59,
- 0xFF76FF03,
- 0xFF64DD17,
- 0xFFF0F4C3,
- 0xFFE6EE9C,
- 0xFFDCE775,
- 0xFFD4E157,
- 0xFFCDDC39,
- 0xFFC0CA33,
- 0xFFAFB42B,
- 0xFF9E9D24,
- 0xFFF4FF81,
- 0xFFEEFF41,
- 0xFFC6FF00,
- 0xFFAEEA00,
- 0xFFFFF9C4,
- 0xFFFFF59D,
- 0xFFFFF176,
- 0xFFFFEE58,
- 0xFFFFEB3B,
- 0xFFFDD835,
- 0xFFFBC02D,
- 0xFFF9A825,
- 0xFFF57F17,
- 0xFFFFFF8D,
- 0xFFFFFF00,
- 0xFFFFEA00,
- 0xFFFFD600,
- 0xFFFFECB3,
- 0xFFFFE082,
- 0xFFFFD54F,
- 0xFFFFCA28,
- 0xFFFFC107,
- 0xFFFFB300,
- 0xFFFFA000,
- 0xFFFF8F00,
- 0xFFFF6F00,
- 0xFFFFE57F,
- 0xFFFFD740,
- 0xFFFFC400,
- 0xFFFFAB00,
- 0xFFFFE0B2,
- 0xFFFFCC80,
- 0xFFFFB74D,
- 0xFFFFA726,
- 0xFFFF9800,
- 0xFFFB8C00,
- 0xFFF57C00,
- 0xFFEF6C00,
- 0xFFFFD180,
- 0xFFFFAB40,
- 0xFFFF9100,
- 0xFFFF6D00,
- 0xFFFFCCBC,
- 0xFFFFAB91,
- 0xFFFF8A65,
- 0xFFFF7043,
- 0xFFFF5722,
- 0xFFFF9E80,
- 0xFFFF6E40,
- 0xFFD7CCC8,
- 0xFFBCAAA4,
- 0xFFF5F5F5,
- 0xFFEEEEEE,
- 0xFFE0E0E0,
- 0xFFBDBDBD,
- 0xFF9E9E9E,
- 0xFFCFD8DC,
- 0xFFB0BEC5,
- 0xFF90A4AE,
- 0xFFFFFFFF
-)
\ No newline at end of file
+private const val IsDynamicThemeOnKey = "material3_isDynamicThemeOn"
+internal val IsDynamicThemingAvailable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
\ No newline at end of file
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/InteropUi.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/InteropUi.kt
index 960a3c3..1ef5eae 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/InteropUi.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/interoperability/InteropUi.kt
@@ -34,6 +34,7 @@
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
@@ -43,6 +44,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -181,7 +183,27 @@
}
}
-private object InteropUiSnippet6 {
+@Composable
+fun InteropUiSnippet6(showCautionIcon: Boolean) {
+ if (showCautionIcon) {
+ CautionIcon(/* ... */)
+ }
+}
+
+@Composable
+fun InteropUiSnippet7() {
+ var isEnabled by rememberSaveable { mutableStateOf(false) }
+
+ Column {
+ ImageWithEnabledOverlay(isEnabled)
+ ControlPanelWithToggle(
+ isEnabled = isEnabled,
+ onEnabledChanged = { isEnabled = it }
+ )
+ }
+}
+
+private object InteropUiSnippet8 {
@Composable
fun MyComposable() {
BoxWithConstraints {
@@ -196,7 +218,7 @@
}
}
-private object InteropUiSnippet7 {
+private object InteropUiSnippet9 {
// import androidx.compose.ui.platform.ComposeView
class MyComposeAdapter : RecyclerView.Adapter<MyComposeViewHolder>() {
@@ -233,7 +255,7 @@
}
}
-private object InteropUiSnippet8 {
+private object InteropUiSnippet10 {
// import androidx.compose.ui.platform.ViewCompositionStrategy
class MyComposeViewHolder(
@@ -295,6 +317,21 @@
private fun Icon() {
}
+@Composable
+private fun CautionIcon() {
+}
+
+@Composable
+private fun ImageWithEnabledOverlay(isEnabled: Boolean) {
+}
+
+@Composable
+private fun ControlPanelWithToggle(
+ isEnabled: Boolean,
+ onEnabledChanged: (Boolean) -> Unit
+) {
+}
+
private class WindowCompat {
companion object {
fun setDecorFitsSystemWindows(window: Any, bool: Boolean) {}
diff --git a/compose/lint/internal-lint-checks/build.gradle b/compose/lint/internal-lint-checks/build.gradle
index 412cfb9..3039214 100644
--- a/compose/lint/internal-lint-checks/build.gradle
+++ b/compose/lint/internal-lint-checks/build.gradle
@@ -25,7 +25,6 @@
dependencies {
compileOnly(libs.androidLintApi)
compileOnly(libs.kotlinStdlib)
- api(project(":lint-checks"))
implementation(project(":compose:lint:common"))
testImplementation(project(":compose:lint:common-test"))
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
index 89745e0..3bce32a 100644
--- a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
@@ -18,7 +18,6 @@
package androidx.compose.lint
-import androidx.build.lint.AndroidXIssueRegistry
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.client.api.Vendor
import com.android.tools.lint.detector.api.CURRENT_API
@@ -32,7 +31,7 @@
ListIteratorDetector.ISSUE,
ModifierInspectorInfoDetector.ISSUE,
UnnecessaryLambdaCreationDetector.ISSUE,
- ) + AndroidXIssueRegistry.Issues
+ )
}
override val vendor = Vendor(
vendorName = "Jetpack Compose",
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderScreenshotTest.kt
index dfbdacc..b282d58 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderScreenshotTest.kt
@@ -223,4 +223,27 @@
}
assertSliderAgainstGolden("rangeSlider_fullRange")
}
+
+ @Test
+ @ExperimentalMaterialApi
+ fun rangeSliderTest_steps_customColors() {
+ rule.setMaterialContent {
+ Box(wrap.testTag(wrapperTestTag)) {
+ var position by remember { mutableStateOf(30f..70f) }
+ RangeSlider(
+ values = position,
+ valueRange = 0f..100f,
+ onValueChange = { position = it }, steps = 9,
+ colors = SliderDefaults.colors(
+ thumbColor = Color.Blue,
+ activeTrackColor = Color.Red,
+ inactiveTrackColor = Color.Yellow,
+ activeTickColor = Color.Magenta,
+ inactiveTickColor = Color.Cyan
+ )
+ )
+ }
+ }
+ assertSliderAgainstGolden("rangeSlider_steps_customColors")
+ }
}
\ No newline at end of file
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
index d7ad38c..4b99271 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
@@ -678,17 +678,18 @@
trackStrokeWidth,
StrokeCap.Round
)
- tickFractions.groupBy { it > positionFractionEnd }.forEach { (afterFraction, list) ->
- drawPoints(
- list.map {
- Offset(lerp(sliderStart, sliderEnd, it).x, center.y)
- },
- PointMode.Points,
- (if (afterFraction) inactiveTickColor else activeTickColor).value,
- trackStrokeWidth,
- StrokeCap.Round
- )
- }
+ tickFractions.groupBy { it > positionFractionEnd || it < positionFractionStart }
+ .forEach { (outsideFraction, list) ->
+ drawPoints(
+ list.map {
+ Offset(lerp(sliderStart, sliderEnd, it).x, center.y)
+ },
+ PointMode.Points,
+ (if (outsideFraction) inactiveTickColor else activeTickColor).value,
+ trackStrokeWidth,
+ StrokeCap.Round
+ )
+ }
}
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/ListUtils.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/ListUtils.kt
index 2e6554c..cb0552e 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/ListUtils.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/ListUtils.kt
@@ -22,6 +22,10 @@
/**
* Iterates through a [List] using the index and calls [action] for each item.
* This does not allocate an iterator like [Iterable.forEach].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
@@ -36,6 +40,10 @@
* Returns a [Set] of all elements.
*
* The returned set preserves the element iteration order of the original collection.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
internal fun <T> List<T>.fastToSet(): Set<T> = HashSet<T>(size).also { set ->
fastForEach { item -> set.add(item) }
@@ -44,6 +52,10 @@
/**
* Iterates through a [List] using the index and calls [action] for each item.
* This does not allocate an iterator like [Iterable.forEachIndexed].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T> List<T>.fastForEachIndexed(action: (Int, T) -> Unit) {
@@ -57,6 +69,10 @@
/**
* Returns a list containing the results of applying the given [transform] function
* to each element in the original collection.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T, R> List<T>.fastMap(transform: (T) -> R): List<R> {
@@ -75,6 +91,10 @@
* If the collection could be huge, you can specify a non-negative value of [limit], in which case
* only the first [limit] elements will be appended, followed by the [truncated] string (which
* defaults to "...").
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
internal fun <T> List<T>.fastJoinToString(
separator: CharSequence = ", ",
@@ -95,6 +115,10 @@
* If the collection could be huge, you can specify a non-negative value of [limit], in which
* case only the first [limit] elements will be appended, followed by the [truncated] string
* (which defaults to "...").
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
private fun <T, A : Appendable> List<T>.fastJoinTo(
buffer: A,
@@ -134,6 +158,10 @@
/**
* Returns a list containing the results of applying the given [transform] function
* to each element in the original collection.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
@@ -149,6 +177,10 @@
* Returns a list containing only elements matching the given [predicate].
* @param [predicate] function that takes the index of an element and the element itself
* and returns the result of predicate evaluation on the element.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T> List<T>.fastFilterIndexed(predicate: (index: Int, T) -> Boolean): List<T> {
diff --git a/compose/test-utils/OWNERS b/compose/test-utils/OWNERS
index 305021a..b2c0d56 100644
--- a/compose/test-utils/OWNERS
+++ b/compose/test-utils/OWNERS
@@ -1,2 +1 @@
-pavlis@google.com
jellefresen@google.com
diff --git a/compose/ui/ui-inspection/src/androidTest/AndroidManifest.xml b/compose/ui/ui-inspection/src/androidTest/AndroidManifest.xml
index bc217cf..48da372 100644
--- a/compose/ui/ui-inspection/src/androidTest/AndroidManifest.xml
+++ b/compose/ui/ui-inspection/src/androidTest/AndroidManifest.xml
@@ -17,6 +17,7 @@
<application>
<activity android:name="androidx.compose.ui.inspection.testdata.AndroidViewTestActivity" />
+ <activity android:name="androidx.compose.ui.inspection.testdata.ComposeViewTestActivity" />
<activity android:name="androidx.compose.ui.inspection.testdata.DialogTestActivity" />
<activity android:name="androidx.compose.ui.inspection.testdata.ParametersTestActivity" />
<activity android:name="androidx.compose.ui.inspection.testdata.RippleTestActivity" />
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/ComposeViewTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/ComposeViewTest.kt
new file mode 100644
index 0000000..9f6366a
--- /dev/null
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/ComposeViewTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.compose.ui.inspection
+
+import androidx.compose.ui.inspection.rules.ComposeInspectionRule
+import androidx.compose.ui.inspection.rules.sendCommand
+import androidx.compose.ui.inspection.testdata.ComposeViewTestActivity
+import androidx.compose.ui.inspection.util.GetComposablesCommand
+import androidx.compose.ui.inspection.util.GetParametersCommand
+import androidx.compose.ui.inspection.util.flatten
+import androidx.compose.ui.inspection.util.toMap
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.ComposableNode
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+class ComposeViewTest {
+ @get:Rule
+ val rule = ComposeInspectionRule(ComposeViewTestActivity::class)
+
+ @Test
+ fun composeView(): Unit = runBlocking {
+ val response = rule.inspectorTester.sendCommand(
+ GetComposablesCommand(rule.rootId, skipSystemComposables = false)
+ ).getComposablesResponse
+ val strings = response.stringsList.toMap()
+ val roots = response.rootsList
+ assertThat(roots).hasSize(3)
+ val firstText = roots[0].nodesList.findNode("Text", strings)
+ val secondText = roots[1].nodesList.findNode("Text", strings)
+ val thirdText = roots[2].nodesList.findNode("Text", strings)
+ assertThat(firstText?.textParameter).isEqualTo("one")
+ assertThat(secondText?.textParameter).isEqualTo("two")
+ assertThat(thirdText?.textParameter).isEqualTo("three")
+ }
+
+ private fun Iterable<ComposableNode>.findNode(
+ name: String,
+ strings: Map<Int, String>
+ ): ComposableNode? = flatMap { it.flatten() }.singleOrNull { strings[it.name] == name }
+
+ private val ComposableNode.textParameter: String?
+ get() = runBlocking {
+ val params = rule.inspectorTester.sendCommand(
+ GetParametersCommand(
+ rule.rootId,
+ skipSystemComposables = false,
+ composableId = id
+ )
+ ).getParametersResponse
+ val strings = params.stringsList.toMap()
+ val param = params.parameterGroup.parameterList.single { strings[it.name] == "text" }
+ strings[param.int32Value]
+ }
+}
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/ComposeViewTestActivity.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/ComposeViewTestActivity.kt
new file mode 100644
index 0000000..3c36a5a
--- /dev/null
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/ComposeViewTestActivity.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.compose.ui.inspection.testdata
+
+import android.os.Bundle
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material.Text
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.viewinterop.AndroidView
+
+class ComposeViewTestActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ Column {
+ Text("one")
+ AndroidView({ context ->
+ LinearLayout(context).apply {
+ orientation = LinearLayout.VERTICAL
+ addView(TextView(context).apply { text = "AndroidView" })
+ addView(ComposeView(context).apply {
+ setContent {
+ Column {
+ Text("two")
+ AndroidView({ context ->
+ LinearLayout(context).apply {
+ orientation = LinearLayout.VERTICAL
+ addView(ComposeView(context).apply {
+ setContent {
+ Text("three")
+ }
+ })
+ }
+ })
+ }
+ }
+ })
+ }
+ })
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
index 6999d46..af7dab6 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
@@ -27,7 +27,7 @@
import androidx.compose.ui.inspection.inspector.NodeParameterReference
import androidx.compose.ui.inspection.proto.StringTable
import androidx.compose.ui.inspection.proto.convert
-import androidx.compose.ui.inspection.proto.toComposableNodes
+import androidx.compose.ui.inspection.proto.toComposableRoot
import androidx.compose.ui.inspection.util.ThreadUtils
import androidx.compose.ui.unit.IntOffset
import androidx.inspection.Connection
@@ -37,7 +37,6 @@
import com.google.protobuf.ByteString
import com.google.protobuf.InvalidProtocolBufferException
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Command
-import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.ComposableRoot
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetAllParametersCommand
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetAllParametersResponse
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetComposablesCommand
@@ -66,19 +65,31 @@
class ComposeLayoutInspector(
connection: Connection,
+ @Suppress("unused")
private val environment: InspectorEnvironment
) : Inspector(connection) {
/** Cache data which allows us to reuse previously queried inspector nodes */
private class CacheData(
val rootView: View,
+ val trees: List<CacheTree>
+ ) {
+ /** The cached nodes as a map from node id to InspectorNode */
+ val lookup: Map<Long, InspectorNode>
+ get() = _lookup ?: trees.flatMap { it.nodes }
+ .flatMap { it.flatten() }
+ .associateBy { it.id }
+ .also { _lookup = it }
+
+ private var _lookup: Map<Long, InspectorNode>? = null
+ }
+
+ /** Cache data for a tree of [InspectorNode]s under a [viewParent] */
+ internal class CacheTree(
val viewParent: View,
val nodes: List<InspectorNode>,
val viewsToSkip: List<Long>
- ) {
- /** The cached nodes as a map from node id to InspectorNode */
- val lookup = nodes.flatMap { it.flatten() }.associateBy { it.id }
- }
+ )
private val layoutInspectorTree = LayoutInspectorTree()
@@ -142,21 +153,13 @@
val windowPos = IntOffset(location[0], location[1])
val stringTable = StringTable()
- val nodes = data?.nodes ?: emptyList()
- val composeNodes = nodes.toComposableNodes(stringTable, windowPos)
+ val trees = data?.trees ?: emptyList()
+ val roots = trees.map { it.toComposableRoot(stringTable, windowPos) }
callback.reply {
getComposablesResponse = GetComposablesResponse.newBuilder().apply {
addAllStrings(stringTable.toStringEntries())
- addRoots(
- ComposableRoot.newBuilder().apply {
- if (data != null) {
- viewId = data.viewParent.uniqueDrawingId
- addAllNodes(composeNodes)
- addAllViewsToSkip(data.viewsToSkip)
- }
- }
- )
+ addAllRoots(roots)
}.build()
}
}
@@ -284,15 +287,20 @@
val data = ThreadUtils.runOnMainThread {
layoutInspectorTree.resetAccumulativeState()
- val data = getAndroidComposeViews(rootViewId, skipSystemComposables, generation).map {
- CacheData(it.rootView, it.viewParent, it.createNodes(), it.viewsToSkip)
+ val composeViews = getAndroidComposeViews(rootViewId, skipSystemComposables, generation)
+ val composeViewsByRoot = composeViews.groupBy { it.rootView.uniqueDrawingId }
+ val data = composeViewsByRoot.mapValues { (_, composeViews) ->
+ CacheData(
+ composeViews.first().rootView,
+ composeViews.map { CacheTree(it.viewParent, it.createNodes(), it.viewsToSkip) }
+ )
}
layoutInspectorTree.resetAccumulativeState()
data
}.get()
cachedNodes.clear()
- data.associateByTo(cachedNodes) { it.rootView.uniqueDrawingId }
+ cachedNodes.putAll(data)
cachedGeneration = generation
cachedSystemComposablesSkipped = skipSystemComposables
return cachedNodes[rootViewId]
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/proto/ComposeExtensions.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/proto/ComposeExtensions.kt
index fa17f3c..3526f27 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/proto/ComposeExtensions.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/proto/ComposeExtensions.kt
@@ -18,6 +18,7 @@
import android.view.inspector.WindowInspector
import androidx.annotation.VisibleForTesting
+import androidx.compose.ui.inspection.ComposeLayoutInspector.CacheTree
import androidx.compose.ui.inspection.LambdaLocation
import androidx.compose.ui.inspection.inspector.InspectorNode
import androidx.compose.ui.inspection.inspector.NodeParameter
@@ -28,6 +29,7 @@
import androidx.compose.ui.unit.IntOffset
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Bounds
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.ComposableNode
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.ComposableRoot
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.LambdaValue
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Parameter
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.ParameterReference
@@ -233,12 +235,14 @@
}.build()
}
-fun Iterable<InspectorNode>.toComposableNodes(
+internal fun CacheTree.toComposableRoot(
stringTable: StringTable,
windowPos: IntOffset
-): List<ComposableNode> {
- return this.map { it.toComposableNode(stringTable, windowPos) }
-}
+): ComposableRoot = ComposableRoot.newBuilder().also { root ->
+ root.viewId = viewParent.uniqueDrawingId
+ root.addAllNodes(nodes.map { it.toComposableNode(stringTable, windowPos) })
+ root.addAllViewsToSkip(viewsToSkip)
+}.build()
fun Iterable<NodeParameter>.convertAll(stringTable: StringTable): List<Parameter> {
return this.map { it.convert(stringTable) }
diff --git a/compose/ui/ui-test-junit4/OWNERS b/compose/ui/ui-test-junit4/OWNERS
index 305021a..b2c0d56 100644
--- a/compose/ui/ui-test-junit4/OWNERS
+++ b/compose/ui/ui-test-junit4/OWNERS
@@ -1,2 +1 @@
-pavlis@google.com
jellefresen@google.com
diff --git a/compose/ui/ui-test-junit4/api/1.1.0-beta04.txt b/compose/ui/ui-test-junit4/api/1.1.0-beta04.txt
index a71c843..56b11a6 100644
--- a/compose/ui/ui-test-junit4/api/1.1.0-beta04.txt
+++ b/compose/ui/ui-test-junit4/api/1.1.0-beta04.txt
@@ -76,7 +76,7 @@
package androidx.compose.ui.test.junit4.android {
- public final class ComposeNotIdleException extends java.lang.Throwable {
+ public final class ComposeNotIdleException extends java.lang.Exception {
ctor public ComposeNotIdleException(String? message, Throwable? cause);
}
diff --git a/compose/ui/ui-test-junit4/api/current.ignore b/compose/ui/ui-test-junit4/api/current.ignore
index 6e4d61f..a21b2a6 100644
--- a/compose/ui/ui-test-junit4/api/current.ignore
+++ b/compose/ui/ui-test-junit4/api/current.ignore
@@ -1,7 +1,3 @@
// Baseline format: 1.0
-RemovedClass: androidx.compose.ui.test.junit4.android.ComposeIdlingResource_androidKt:
- Removed class androidx.compose.ui.test.junit4.android.ComposeIdlingResource_androidKt
-RemovedClass: androidx.compose.ui.test.junit4.android.ComposeRootRegistry_androidKt:
- Removed class androidx.compose.ui.test.junit4.android.ComposeRootRegistry_androidKt
-RemovedClass: androidx.compose.ui.test.junit4.android.EspressoLink_androidKt:
- Removed class androidx.compose.ui.test.junit4.android.EspressoLink_androidKt
+ChangedSuperclass: androidx.compose.ui.test.junit4.android.ComposeNotIdleException:
+ Class androidx.compose.ui.test.junit4.android.ComposeNotIdleException superclass changed from java.lang.Throwable to java.lang.Exception
diff --git a/compose/ui/ui-test-junit4/api/current.txt b/compose/ui/ui-test-junit4/api/current.txt
index a71c843..56b11a6 100644
--- a/compose/ui/ui-test-junit4/api/current.txt
+++ b/compose/ui/ui-test-junit4/api/current.txt
@@ -76,7 +76,7 @@
package androidx.compose.ui.test.junit4.android {
- public final class ComposeNotIdleException extends java.lang.Throwable {
+ public final class ComposeNotIdleException extends java.lang.Exception {
ctor public ComposeNotIdleException(String? message, Throwable? cause);
}
diff --git a/compose/ui/ui-test-junit4/api/public_plus_experimental_1.1.0-beta04.txt b/compose/ui/ui-test-junit4/api/public_plus_experimental_1.1.0-beta04.txt
index a71c843..56b11a6 100644
--- a/compose/ui/ui-test-junit4/api/public_plus_experimental_1.1.0-beta04.txt
+++ b/compose/ui/ui-test-junit4/api/public_plus_experimental_1.1.0-beta04.txt
@@ -76,7 +76,7 @@
package androidx.compose.ui.test.junit4.android {
- public final class ComposeNotIdleException extends java.lang.Throwable {
+ public final class ComposeNotIdleException extends java.lang.Exception {
ctor public ComposeNotIdleException(String? message, Throwable? cause);
}
diff --git a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
index a71c843..56b11a6 100644
--- a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
@@ -76,7 +76,7 @@
package androidx.compose.ui.test.junit4.android {
- public final class ComposeNotIdleException extends java.lang.Throwable {
+ public final class ComposeNotIdleException extends java.lang.Exception {
ctor public ComposeNotIdleException(String? message, Throwable? cause);
}
diff --git a/compose/ui/ui-test-junit4/api/restricted_1.1.0-beta04.txt b/compose/ui/ui-test-junit4/api/restricted_1.1.0-beta04.txt
index a71c843..56b11a6 100644
--- a/compose/ui/ui-test-junit4/api/restricted_1.1.0-beta04.txt
+++ b/compose/ui/ui-test-junit4/api/restricted_1.1.0-beta04.txt
@@ -76,7 +76,7 @@
package androidx.compose.ui.test.junit4.android {
- public final class ComposeNotIdleException extends java.lang.Throwable {
+ public final class ComposeNotIdleException extends java.lang.Exception {
ctor public ComposeNotIdleException(String? message, Throwable? cause);
}
diff --git a/compose/ui/ui-test-junit4/api/restricted_current.ignore b/compose/ui/ui-test-junit4/api/restricted_current.ignore
index 6e4d61f..a21b2a6 100644
--- a/compose/ui/ui-test-junit4/api/restricted_current.ignore
+++ b/compose/ui/ui-test-junit4/api/restricted_current.ignore
@@ -1,7 +1,3 @@
// Baseline format: 1.0
-RemovedClass: androidx.compose.ui.test.junit4.android.ComposeIdlingResource_androidKt:
- Removed class androidx.compose.ui.test.junit4.android.ComposeIdlingResource_androidKt
-RemovedClass: androidx.compose.ui.test.junit4.android.ComposeRootRegistry_androidKt:
- Removed class androidx.compose.ui.test.junit4.android.ComposeRootRegistry_androidKt
-RemovedClass: androidx.compose.ui.test.junit4.android.EspressoLink_androidKt:
- Removed class androidx.compose.ui.test.junit4.android.EspressoLink_androidKt
+ChangedSuperclass: androidx.compose.ui.test.junit4.android.ComposeNotIdleException:
+ Class androidx.compose.ui.test.junit4.android.ComposeNotIdleException superclass changed from java.lang.Throwable to java.lang.Exception
diff --git a/compose/ui/ui-test-junit4/api/restricted_current.txt b/compose/ui/ui-test-junit4/api/restricted_current.txt
index a71c843..56b11a6 100644
--- a/compose/ui/ui-test-junit4/api/restricted_current.txt
+++ b/compose/ui/ui-test-junit4/api/restricted_current.txt
@@ -76,7 +76,7 @@
package androidx.compose.ui.test.junit4.android {
- public final class ComposeNotIdleException extends java.lang.Throwable {
+ public final class ComposeNotIdleException extends java.lang.Exception {
ctor public ComposeNotIdleException(String? message, Throwable? cause);
}
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeNotIdleException.android.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeNotIdleException.android.kt
index bd68df1..815c8d6 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeNotIdleException.android.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeNotIdleException.android.kt
@@ -19,4 +19,4 @@
/**
* Thrown in cases where Compose can't get idle in Espresso's defined time limit.
*/
-class ComposeNotIdleException(message: String?, cause: Throwable?) : Throwable(message, cause)
+class ComposeNotIdleException(message: String?, cause: Throwable?) : Exception(message, cause)
diff --git a/compose/ui/ui-test-manifest/OWNERS b/compose/ui/ui-test-manifest/OWNERS
index 42abc4e..b2c0d56 100644
--- a/compose/ui/ui-test-manifest/OWNERS
+++ b/compose/ui/ui-test-manifest/OWNERS
@@ -1,2 +1 @@
jellefresen@google.com
-pavlis@google.com
diff --git a/compose/ui/ui-test-manifest/integration-tests/testapp/OWNERS b/compose/ui/ui-test-manifest/integration-tests/testapp/OWNERS
index 42abc4e..b2c0d56 100644
--- a/compose/ui/ui-test-manifest/integration-tests/testapp/OWNERS
+++ b/compose/ui/ui-test-manifest/integration-tests/testapp/OWNERS
@@ -1,2 +1 @@
jellefresen@google.com
-pavlis@google.com
diff --git a/compose/ui/ui-test/OWNERS b/compose/ui/ui-test/OWNERS
index 305021a..b2c0d56 100644
--- a/compose/ui/ui-test/OWNERS
+++ b/compose/ui/ui-test/OWNERS
@@ -1,2 +1 @@
-pavlis@google.com
jellefresen@google.com
diff --git a/compose/ui/ui-text/OWNERS b/compose/ui/ui-text/OWNERS
index f00bb31..9a0b796 100644
--- a/compose/ui/ui-text/OWNERS
+++ b/compose/ui/ui-text/OWNERS
@@ -1,6 +1,5 @@
include /TEXT_OWNERS
-pavlis@google.com
adamp@google.com
mount@google.com
popam@google.com
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TempListUtils.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TempListUtils.kt
index 79b7c59..b86d941 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TempListUtils.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TempListUtils.kt
@@ -24,6 +24,10 @@
/**
* Returns a list containing only elements matching the given [predicate].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
@@ -40,6 +44,10 @@
* having distinct keys returned by the given [selector] function.
*
* The elements in the resulting list are in the same order as they were in the source collection.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> {
@@ -56,6 +64,10 @@
/**
* Returns the first element yielding the largest value of the given function or `null` if there
* are no elements.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T, R : Comparable<R>> List<T>.fastMinByOrNull(selector: (T) -> R): T? {
@@ -80,7 +92,12 @@
*
* Returns the specified [initial] value if the collection is empty.
*
- * @param [operation] function that takes current accumulator value and an element, and calculates the next accumulator value.
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ *
+ * @param [operation] function that takes current accumulator value and an element, and calculates
+ * the next accumulator value.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T, R> List<T>.fastFold(initial: R, operation: (acc: R, T) -> R): R {
@@ -95,6 +112,10 @@
/**
* Returns a single list of all elements yielded from results of [transform] function being invoked
* on each element of original collection.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T, R> List<T>.fastFlatMap(transform: (T) -> Iterable<R>): List<R> {
@@ -109,6 +130,10 @@
/**
* Returns a list containing all elements not matching the given [predicate].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
@@ -122,6 +147,10 @@
/**
* Returns a list containing the first elements satisfying the given [predicate].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T> List<T>.fastTakeWhile(predicate: (T) -> Boolean): List<T> {
@@ -139,6 +168,10 @@
/**
* Returns a list containing all elements except first [n] elements.
*
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ *
* @throws IllegalArgumentException if [n] is negative.
*/
internal fun <T> List<T>.fastDrop(n: Int): List<T> {
@@ -167,6 +200,10 @@
* If the collection could be huge, you can specify a non-negative value of [limit], in which case
* only the first [limit] elements will be appended, followed by the [truncated] string (which
* defaults to "...").
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
internal fun <T> List<T>.fastJoinToString(
separator: CharSequence = ", ",
@@ -187,6 +224,10 @@
* If the collection could be huge, you can specify a non-negative value of [limit], in which
* case only the first [limit] elements will be appended, followed by the [truncated] string
* (which defaults to "...").
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
private fun <T, A : Appendable> List<T>.fastJoinTo(
buffer: A,
diff --git a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
index bf2f53d..bfa9798 100644
--- a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
+++ b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
@@ -22,6 +22,10 @@
/**
* Iterates through a [List] using the index and calls [action] for each item.
* This does not allocate an iterator like [Iterable.forEach].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
@@ -35,6 +39,10 @@
/**
* Iterates through a [List] using the index and calls [action] for each item.
* This does not allocate an iterator like [Iterable.forEachIndexed].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastForEachIndexed(action: (Int, T) -> Unit) {
@@ -47,6 +55,10 @@
/**
* Returns `true` if all elements match the given [predicate].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastAll(predicate: (T) -> Boolean): Boolean {
@@ -57,6 +69,10 @@
/**
* Returns `true` if at least one element matches the given [predicate].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastAny(predicate: (T) -> Boolean): Boolean {
@@ -67,6 +83,10 @@
/**
* Returns the first value that [predicate] returns `true` for or `null` if nothing matches.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastFirstOrNull(predicate: (T) -> Boolean): T? {
@@ -78,6 +98,10 @@
/**
* Returns the sum of all values produced by [selector] function applied to each element in the
* list.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastSumBy(selector: (T) -> Int): Int {
@@ -92,6 +116,10 @@
/**
* Returns a list containing the results of applying the given [transform] function
* to each element in the original collection.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T, R> List<T>.fastMap(transform: (T) -> R): List<R> {
@@ -107,6 +135,10 @@
/**
* Returns the first element yielding the largest value of the given function or `null` if there
* are no elements.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T, R : Comparable<R>> List<T>.fastMaxBy(selector: (T) -> R): T? {
@@ -128,6 +160,10 @@
/**
* Applies the given [transform] function to each element of the original collection
* and appends the results to the given [destination].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T, R, C : MutableCollection<in R>> List<T>.fastMapTo(
diff --git a/compose/ui/ui/api/public_plus_experimental_1.1.0-beta04.txt b/compose/ui/ui/api/public_plus_experimental_1.1.0-beta04.txt
index b293eee..eced941 100644
--- a/compose/ui/ui/api/public_plus_experimental_1.1.0-beta04.txt
+++ b/compose/ui/ui/api/public_plus_experimental_1.1.0-beta04.txt
@@ -1607,12 +1607,14 @@
method public int getMove();
method public int getPress();
method public int getRelease();
+ method @androidx.compose.ui.ExperimentalComposeUiApi public int getScroll();
method public int getUnknown();
property public final int Enter;
property public final int Exit;
property public final int Move;
property public final int Press;
property public final int Release;
+ property @androidx.compose.ui.ExperimentalComposeUiApi public final int Scroll;
property public final int Unknown;
}
@@ -1670,7 +1672,7 @@
@androidx.compose.runtime.Immutable public final class PointerInputChange {
ctor public PointerInputChange(long id, long uptimeMillis, long position, boolean pressed, long previousUptimeMillis, long previousPosition, boolean previousPressed, androidx.compose.ui.input.pointer.ConsumedData consumed, optional int type);
- ctor @androidx.compose.ui.ExperimentalComposeUiApi public PointerInputChange(long id, long uptimeMillis, long position, boolean pressed, long previousUptimeMillis, long previousPosition, boolean previousPressed, androidx.compose.ui.input.pointer.ConsumedData consumed, int type, java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical);
+ method @androidx.compose.ui.ExperimentalComposeUiApi public void consume();
method public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional androidx.compose.ui.input.pointer.ConsumedData consumed, optional int type);
method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional androidx.compose.ui.input.pointer.ConsumedData consumed, optional int type, java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical);
method public androidx.compose.ui.input.pointer.ConsumedData getConsumed();
@@ -1681,16 +1683,20 @@
method public long getPreviousPosition();
method public boolean getPreviousPressed();
method public long getPreviousUptimeMillis();
+ method @androidx.compose.ui.ExperimentalComposeUiApi public long getScrollDelta();
method public int getType();
method public long getUptimeMillis();
+ method @androidx.compose.ui.ExperimentalComposeUiApi public boolean isConsumed();
property public final androidx.compose.ui.input.pointer.ConsumedData consumed;
property @androidx.compose.ui.ExperimentalComposeUiApi public final java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical;
property public final long id;
+ property @androidx.compose.ui.ExperimentalComposeUiApi public final boolean isConsumed;
property public final long position;
property public final boolean pressed;
property public final long previousPosition;
property public final boolean previousPressed;
property public final long previousUptimeMillis;
+ property @androidx.compose.ui.ExperimentalComposeUiApi public final long scrollDelta;
property public final int type;
property public final long uptimeMillis;
}
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index b293eee..eced941 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -1607,12 +1607,14 @@
method public int getMove();
method public int getPress();
method public int getRelease();
+ method @androidx.compose.ui.ExperimentalComposeUiApi public int getScroll();
method public int getUnknown();
property public final int Enter;
property public final int Exit;
property public final int Move;
property public final int Press;
property public final int Release;
+ property @androidx.compose.ui.ExperimentalComposeUiApi public final int Scroll;
property public final int Unknown;
}
@@ -1670,7 +1672,7 @@
@androidx.compose.runtime.Immutable public final class PointerInputChange {
ctor public PointerInputChange(long id, long uptimeMillis, long position, boolean pressed, long previousUptimeMillis, long previousPosition, boolean previousPressed, androidx.compose.ui.input.pointer.ConsumedData consumed, optional int type);
- ctor @androidx.compose.ui.ExperimentalComposeUiApi public PointerInputChange(long id, long uptimeMillis, long position, boolean pressed, long previousUptimeMillis, long previousPosition, boolean previousPressed, androidx.compose.ui.input.pointer.ConsumedData consumed, int type, java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical);
+ method @androidx.compose.ui.ExperimentalComposeUiApi public void consume();
method public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional androidx.compose.ui.input.pointer.ConsumedData consumed, optional int type);
method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional androidx.compose.ui.input.pointer.ConsumedData consumed, optional int type, java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical);
method public androidx.compose.ui.input.pointer.ConsumedData getConsumed();
@@ -1681,16 +1683,20 @@
method public long getPreviousPosition();
method public boolean getPreviousPressed();
method public long getPreviousUptimeMillis();
+ method @androidx.compose.ui.ExperimentalComposeUiApi public long getScrollDelta();
method public int getType();
method public long getUptimeMillis();
+ method @androidx.compose.ui.ExperimentalComposeUiApi public boolean isConsumed();
property public final androidx.compose.ui.input.pointer.ConsumedData consumed;
property @androidx.compose.ui.ExperimentalComposeUiApi public final java.util.List<androidx.compose.ui.input.pointer.HistoricalChange> historical;
property public final long id;
+ property @androidx.compose.ui.ExperimentalComposeUiApi public final boolean isConsumed;
property public final long position;
property public final boolean pressed;
property public final long previousPosition;
property public final boolean previousPressed;
property public final long previousUptimeMillis;
+ property @androidx.compose.ui.ExperimentalComposeUiApi public final long scrollDelta;
property public final int type;
property public final long uptimeMillis;
}
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LayoutNodeModifierBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LayoutNodeModifierBenchmark.kt
index e93fc61..f462daa 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LayoutNodeModifierBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LayoutNodeModifierBenchmark.kt
@@ -121,6 +121,41 @@
}
}
+ @Test
+ fun setDrawModifiersToSameValue() {
+ modifiers = mutableListOf<Modifier>().apply {
+ repeat(numberOfModifiers) {
+ this += Modifier.drawBehind { }
+ }
+ }
+ combinedModifier = modifiers.fold<Modifier, Modifier>(Modifier) { acc, modifier ->
+ acc.then(modifier)
+ }
+
+ val altModifier = mutableListOf<Modifier>().apply {
+ repeat(numberOfModifiers) {
+ this += Modifier.drawBehind { }
+ }
+ }.fold<Modifier, Modifier>(Modifier) { acc, modifier ->
+ acc.then(modifier)
+ }
+
+ rule.activityTestRule.runOnUiThread {
+ rule.activityTestRule.activity.setContent {
+ TestModifierUpdaterLayout {
+ testModifierUpdater = it
+ }
+ }
+ }
+
+ rule.activityTestRule.runOnUiThread {
+ rule.benchmarkRule.measureRepeated {
+ testModifierUpdater.updateModifier(combinedModifier)
+ testModifierUpdater.updateModifier(altModifier)
+ }
+ }
+ }
+
class SimpleAndroidBenchmarkRule() : TestRule {
@Suppress("DEPRECATION")
val activityTestRule =
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/EventTypesDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/EventTypesDemo.kt
index 36b1565..29478d5 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/EventTypesDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/EventTypesDemo.kt
@@ -22,7 +22,6 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Text
@@ -30,8 +29,10 @@
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType
@@ -50,10 +51,10 @@
}
@Composable
-private fun DrawEvents(events: List<PointerEventType>, counts: List<Int>) {
+@OptIn(ExperimentalComposeUiApi::class)
+private fun DrawEvents(events: List<Pair<PointerEventType, Any>>) {
for (i in events.lastIndex downTo 0) {
- val type = events[i]
- val count = counts[i]
+ val (type, value) = events[i]
val color = when (type) {
PointerEventType.Press -> Color.Red
@@ -61,9 +62,10 @@
PointerEventType.Release -> Color.Yellow
PointerEventType.Enter -> Color.Green
PointerEventType.Exit -> Color.Blue
- else -> Color(0xFF800080) // Purple
+ PointerEventType.Scroll -> Color(0xFF800080) // Purple
+ else -> Color.Black
}
- TextItem("$type $count", color)
+ TextItem("$type $value", color)
}
}
@@ -72,59 +74,62 @@
*/
@Composable
fun EventTypesDemo() {
- val innerPointerEventTypes = remember { mutableStateListOf<PointerEventType>() }
- val innerPointerEventCounts = remember { mutableStateListOf<Int>() }
- val outerPointerEventTypes = remember { mutableStateListOf<PointerEventType>() }
- val outerPointerEventCounts = remember { mutableStateListOf<Int>() }
+ val innerPointerEvents = remember { mutableStateListOf<Pair<PointerEventType, Any>>() }
+ val outerPointerEvents = remember { mutableStateListOf<Pair<PointerEventType, Any>>() }
Box(
Modifier.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
event.changes.forEach { it.consumeAllChanges() }
- addEvent(event, outerPointerEventTypes, outerPointerEventCounts)
+ addEvent(event, outerPointerEvents)
}
}
}
) {
Column {
- DrawEvents(outerPointerEventTypes, outerPointerEventCounts)
+ DrawEvents(outerPointerEvents)
}
Column(
- Modifier
- .align(Alignment.CenterEnd)
- .requiredSize(200.dp)
+ Modifier.size(200.dp)
.border(2.dp, Color.Black)
+ .align(Alignment.CenterEnd)
.clipToBounds()
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
- addEvent(event, innerPointerEventTypes, innerPointerEventCounts)
+ addEvent(event, innerPointerEvents)
}
}
}
) {
- DrawEvents(innerPointerEventTypes, innerPointerEventCounts)
+ DrawEvents(innerPointerEvents)
}
}
}
+@OptIn(ExperimentalComposeUiApi::class)
private fun addEvent(
event: PointerEvent,
- events: MutableList<PointerEventType>,
- counts: MutableList<Int>
+ events: MutableList<Pair<PointerEventType, Any>>,
) {
event.changes.forEach { it.consumeAllChanges() }
- if (events.lastOrNull() == event.type) {
- counts[counts.lastIndex]++
+ val scrollTotal = event.changes.foldRight(Offset.Zero) { c, acc -> acc + c.scrollDelta }
+ if (events.lastOrNull()?.first == event.type) {
+ val (type, value) = events.last()
+ if (type == PointerEventType.Scroll) {
+ events[events.lastIndex] = type to ((value as Offset) + scrollTotal)
+ } else {
+ events[events.lastIndex] = type to ((value as Int) + 1)
+ }
+ } else if (event.type == PointerEventType.Scroll) {
+ events += event.type to scrollTotal
} else {
- events += event.type
- counts += 1
+ events += event.type to 1
}
while (events.size > 100) {
events.removeAt(0)
- counts.removeAt(0)
}
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
index c1b7f74..2c6747b 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
@@ -38,6 +38,7 @@
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
@@ -49,6 +50,7 @@
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.testutils.assertPixels
import androidx.compose.ui.draw.DrawModifier
+import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
@@ -3575,6 +3577,76 @@
assertTrue(composeView.isTransitionGroup)
}
+ @Test
+ fun drawnInCorrectLayer() {
+ var innerDrawLatch = CountDownLatch(1)
+ var outerDrawLatch = CountDownLatch(1)
+ var outerColor by mutableStateOf(Color.Blue)
+ var innerColor by mutableStateOf(Color.White)
+ activityTestRule.runOnUiThread {
+ activity.setContent {
+ with(LocalDensity.current) {
+ Box(Modifier.size(30.toDp())
+ .drawBehind {
+ drawRect(outerColor)
+ outerDrawLatch.countDown()
+ }
+ .drawLatchModifier()
+ .padding(10.toDp())
+ .clipToBounds()
+ .drawBehind {
+ // clipped by the layer
+ drawRect(innerColor, Offset(-10f, -10f), Size(30f, 30f))
+ innerDrawLatch.countDown()
+ }
+ .drawLatchModifier()
+ .size(10.toDp())
+ )
+ }
+ }
+ }
+ assertTrue(innerDrawLatch.await(1, TimeUnit.SECONDS))
+ assertTrue(outerDrawLatch.await(1, TimeUnit.SECONDS))
+
+ validateSquareColors(
+ outerColor = Color.Blue,
+ innerColor = Color.White,
+ size = 10
+ )
+
+ innerDrawLatch = CountDownLatch(1)
+ outerDrawLatch = CountDownLatch(1)
+ drawLatch = CountDownLatch(1)
+
+ // changing the inner color should only affect the inner layer
+ innerColor = Color.Yellow
+
+ assertTrue(innerDrawLatch.await(1, TimeUnit.SECONDS))
+
+ validateSquareColors(
+ outerColor = Color.Blue,
+ innerColor = Color.Yellow,
+ size = 10
+ )
+
+ assertEquals(1, outerDrawLatch.count)
+ innerDrawLatch = CountDownLatch(1)
+ drawLatch = CountDownLatch(1)
+
+ // changing the outer color should only affect the outer layer
+ outerColor = Color.Red
+
+ assertTrue(outerDrawLatch.await(1, TimeUnit.SECONDS))
+
+ validateSquareColors(
+ outerColor = Color.Red,
+ innerColor = Color.Yellow,
+ size = 10
+ )
+
+ assertEquals(1, innerDrawLatch.count)
+ }
+
private fun Modifier.layout(onLayout: () -> Unit) = layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/gesture/Utils.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/gesture/Utils.kt
index 31dc186..fb57437 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/gesture/Utils.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/gesture/Utils.kt
@@ -17,6 +17,8 @@
package androidx.compose.ui.gesture
import android.view.MotionEvent
+import android.view.MotionEvent.AXIS_HSCROLL
+import android.view.MotionEvent.AXIS_VSCROLL
import android.view.View
// We only need this because IR compiler doesn't like converting lambdas to Runnables
@@ -87,8 +89,10 @@
MotionEvent.PointerProperties().apply { this.id = id }
@Suppress("RemoveRedundantQualifierName")
-internal fun PointerCoords(x: Float, y: Float) =
+internal fun PointerCoords(x: Float, y: Float, scrollX: Float = 0f, scrollY: Float = 0f) =
MotionEvent.PointerCoords().apply {
this.x = x
this.y = y
+ setAxisValue(AXIS_HSCROLL, scrollX)
+ setAxisValue(AXIS_VSCROLL, scrollY)
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
index 5f5158f..4db8990 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
@@ -28,6 +28,7 @@
import android.view.MotionEvent.ACTION_HOVER_MOVE
import android.view.MotionEvent.ACTION_MOVE
import android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT
+import android.view.MotionEvent.ACTION_SCROLL
import android.view.MotionEvent.ACTION_UP
import android.view.MotionEvent.TOOL_TYPE_FINGER
import android.view.MotionEvent.TOOL_TYPE_MOUSE
@@ -50,6 +51,7 @@
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.AbsoluteAlignment
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.OpenComposeView
import androidx.compose.ui.composed
@@ -70,6 +72,7 @@
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.viewinterop.AndroidView
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -740,10 +743,24 @@
assertThat(event.type).isEqualTo(expectedHoverType)
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ private fun assertScrollEvent(
+ event: PointerEvent,
+ scrollExpected: Offset
+ ) {
+ assertThat(event.changes).hasSize(1)
+ val change = event.changes[0]
+ assertThat(change.pressed).isFalse()
+ assertThat(event.type).isEqualTo(PointerEventType.Scroll)
+ // we agreed to reverse the delta in android to be in line with other platforms
+ assertThat(change.scrollDelta).isEqualTo(-scrollExpected)
+ }
+
private fun dispatchMouseEvent(
- action: Int = ACTION_HOVER_ENTER,
+ action: Int,
layoutCoordinates: LayoutCoordinates,
- offset: Offset = Offset.Zero
+ offset: Offset = Offset.Zero,
+ scrollDelta: Offset = Offset.Zero
) {
rule.runOnUiThread {
val root = layoutCoordinates.findRoot()
@@ -754,13 +771,14 @@
1,
0,
arrayOf(PointerProperties(0).also { it.toolType = MotionEvent.TOOL_TYPE_MOUSE }),
- arrayOf(PointerCoords(pos.x, pos.y))
+ arrayOf(PointerCoords(pos.x, pos.y, scrollDelta.x, scrollDelta.y))
)
val androidComposeView = findAndroidComposeView(container) as AndroidComposeView
when (action) {
ACTION_HOVER_ENTER, ACTION_HOVER_MOVE, ACTION_HOVER_EXIT ->
androidComposeView.dispatchHoverEvent(event)
+ ACTION_SCROLL -> androidComposeView.dispatchGenericMotionEvent(event)
else -> androidComposeView.dispatchTouchEvent(event)
}
}
@@ -981,6 +999,149 @@
}
@Test
+ fun dispatchScroll() {
+ var layoutCoordinates: LayoutCoordinates? = null
+ val latch = CountDownLatch(1)
+ val events = mutableListOf<PointerEvent>()
+ val scrollDelta = Offset(0.35f, 0.65f)
+ rule.runOnUiThread {
+ container.setContent {
+ Box(
+ Modifier.fillMaxSize().onGloballyPositioned {
+ layoutCoordinates = it
+ latch.countDown()
+ }.pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ event.changes[0].consumeAllChanges()
+ events += event
+ }
+ }
+ }
+ )
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ dispatchMouseEvent(ACTION_SCROLL, layoutCoordinates!!, scrollDelta = scrollDelta)
+ rule.runOnUiThread {
+ assertThat(events).hasSize(2) // synthetic enter and scroll
+ assertHoverEvent(events[0], isEnter = true)
+ assertScrollEvent(events[1], scrollExpected = scrollDelta)
+ }
+ }
+
+ @Test
+ fun dispatchScroll_whenButtonPressed() {
+ var layoutCoordinates: LayoutCoordinates? = null
+ val latch = CountDownLatch(1)
+ val events = mutableListOf<PointerEvent>()
+ val scrollDelta = Offset(0.35f, 0.65f)
+ rule.runOnUiThread {
+ container.setContent {
+ Box(
+ Modifier.fillMaxSize().onGloballyPositioned {
+ layoutCoordinates = it
+ latch.countDown()
+ }.pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ event.changes[0].consumeAllChanges()
+ events += event
+ }
+ }
+ }
+ )
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ // press the button first before scroll
+ dispatchMouseEvent(ACTION_DOWN, layoutCoordinates!!)
+ dispatchMouseEvent(ACTION_SCROLL, layoutCoordinates!!, scrollDelta = scrollDelta)
+ rule.runOnUiThread {
+ assertThat(events).hasSize(3) // synthetic enter, button down, scroll
+ assertHoverEvent(events[0], isEnter = true)
+ assert(events[1].changes.fastAll { it.changedToDownIgnoreConsumed() })
+ assertScrollEvent(events[2], scrollExpected = scrollDelta)
+ }
+ }
+
+ @Test
+ fun dispatchScroll_batch() {
+ var layoutCoordinates: LayoutCoordinates? = null
+ val latch = CountDownLatch(1)
+ val events = mutableListOf<PointerEvent>()
+ val scrollDelta1 = Offset(0.32f, -0.75f)
+ val scrollDelta2 = Offset(0.14f, 0.35f)
+ val scrollDelta3 = Offset(-0.30f, -0.12f)
+ val scrollDelta4 = Offset(-0.05f, 0.68f)
+ rule.runOnUiThread {
+ container.setContent {
+ Box(
+ Modifier.fillMaxSize().onGloballyPositioned {
+ layoutCoordinates = it
+ latch.countDown()
+ }.pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ event.changes[0].consumeAllChanges()
+ events += event
+ }
+ }
+ }
+ )
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ listOf(scrollDelta1, scrollDelta2, scrollDelta3, scrollDelta4).fastForEach {
+ dispatchMouseEvent(ACTION_SCROLL, layoutCoordinates!!, scrollDelta = it)
+ }
+ rule.runOnUiThread {
+ assertThat(events).hasSize(5) // 4 + synthetic enter
+ assertHoverEvent(events[0], isEnter = true)
+ assertScrollEvent(events[1], scrollExpected = scrollDelta1)
+ assertScrollEvent(events[2], scrollExpected = scrollDelta2)
+ assertScrollEvent(events[3], scrollExpected = scrollDelta3)
+ assertScrollEvent(events[4], scrollExpected = scrollDelta4)
+ }
+ }
+
+ @Test
+ fun mouseScroll_ignoredAsDownEvent() {
+ var layoutCoordinates: LayoutCoordinates? = null
+ val latch = CountDownLatch(1)
+ val events = mutableListOf<PointerEvent>()
+ val scrollDelta = Offset(0.35f, 0.65f)
+ rule.runOnUiThread {
+ container.setContent {
+ Box(
+ Modifier.fillMaxSize().onGloballyPositioned {
+ layoutCoordinates = it
+ latch.countDown()
+ }.pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ event.changes[0].consumeAllChanges()
+ events += event
+ }
+ }
+ }
+ )
+ }
+ }
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ dispatchMouseEvent(ACTION_SCROLL, layoutCoordinates!!, scrollDelta = scrollDelta)
+ rule.runOnUiThread {
+ assertThat(events).hasSize(2) // hover enter + scroll
+ assertThat(events[1].changes).isNotEmpty()
+ assertThat(events[1].changes[0].changedToDown()).isFalse()
+ }
+ }
+
+ @Test
fun hoverEnterPressExitEnterExitRelease() {
var outerCoordinates: LayoutCoordinates? = null
var innerCoordinates: LayoutCoordinates? = null
@@ -1404,7 +1565,8 @@
// Hit the bottom box, but clipped
dispatchMouseEvent(ACTION_HOVER_MOVE, coords)
- dispatchMouseEvent(ACTION_HOVER_MOVE, coords,
+ dispatchMouseEvent(
+ ACTION_HOVER_MOVE, coords,
Offset(0f, (coords.size.height / 2 - 1).toFloat())
)
@@ -1413,17 +1575,20 @@
}
// Now hit the box in the unclipped region
- dispatchMouseEvent(ACTION_HOVER_MOVE, coords,
+ dispatchMouseEvent(
+ ACTION_HOVER_MOVE, coords,
Offset(0f, (coords.size.height / 2 + 1).toFloat())
)
// Now hit the bottom of the clipped region
- dispatchMouseEvent(ACTION_HOVER_MOVE, coords,
+ dispatchMouseEvent(
+ ACTION_HOVER_MOVE, coords,
Offset(0f, (coords.size.height - 1).toFloat())
)
// Now leave
- dispatchMouseEvent(ACTION_HOVER_MOVE, coords,
+ dispatchMouseEvent(
+ ACTION_HOVER_MOVE, coords,
Offset(0f, coords.size.height.toFloat() + 1f)
)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt
index 51ee357..28bb807 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt
@@ -119,6 +119,34 @@
assertThat(remeasurements).isEqualTo(2)
}
}
+
+ @Test
+ fun remeasuringChildWithExtraLayer_notPlacedChild() {
+ val height = mutableStateOf(10)
+ var remeasurements = 0
+
+ rule.setContent {
+ WrapChild(onMeasured = { actualHeight ->
+ assertThat(actualHeight).isEqualTo(height.value)
+ remeasurements++
+ }) {
+ NotPlaceChild(height) {
+ WrapChild {
+ Child(height)
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasurements).isEqualTo(1)
+ height.value = 20
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasurements).isEqualTo(2)
+ }
+ }
}
@Composable
@@ -134,6 +162,16 @@
}
@Composable
+private fun NotPlaceChild(height: State<Int>, content: @Composable () -> Unit) {
+ Layout(content = content) { measurables, constraints ->
+ layout(constraints.maxWidth, height.value) {
+ measurables.first()
+ .measure(constraints.copy(minHeight = 0, maxHeight = Constraints.Infinity))
+ }
+ }
+}
+
+@Composable
private fun Child(height: State<Int>) {
Layout { _, constraints ->
layout(constraints.maxWidth, height.value) {}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt
index 6193daf..84cf92a 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt
@@ -27,6 +27,7 @@
import android.view.MotionEvent.ACTION_HOVER_MOVE
import android.view.MotionEvent.ACTION_POINTER_DOWN
import android.view.MotionEvent.ACTION_POINTER_UP
+import android.view.MotionEvent.ACTION_SCROLL
import android.view.MotionEvent.ACTION_UP
import android.view.MotionEvent.TOOL_TYPE_ERASER
import android.view.MotionEvent.TOOL_TYPE_FINGER
@@ -95,6 +96,7 @@
val isHover = action == ACTION_HOVER_EXIT || action == ACTION_HOVER_MOVE ||
action == ACTION_HOVER_ENTER
+ val isScroll = action == ACTION_SCROLL
if (isHover) {
val hoverId = motionEvent.getPointerId(motionEvent.actionIndex)
@@ -117,7 +119,11 @@
positionCalculator,
motionEvent,
i,
- !isHover && i != upIndex
+ // "pressed" means:
+ // 1. we're not hovered
+ // 2. we didn't get UP event for a pointer
+ // 3. button on the mouse is pressed BUT it's not a "scroll" simulated button
+ !isHover && i != upIndex && (!isScroll || motionEvent.buttonState != 0)
)
)
}
@@ -277,6 +283,17 @@
}
}
}
+ val scrollDelta = if (motionEvent.actionMasked == ACTION_SCROLL) {
+ val x = motionEvent.getAxisValue(MotionEvent.AXIS_HSCROLL)
+ val y = motionEvent.getAxisValue(MotionEvent.AXIS_VSCROLL)
+ // NOTE: we revert the scroll offset because android is special compared to other
+ // platforms and send the reversed sign for up and down mouse wheel scroll. In order to
+ // support better x-platform mouse scroll, we revert to be in line with desktop
+ // platforms.
+ Offset(x, y) * -1f
+ } else {
+ Offset.Zero
+ }
val issuesEnterExit = canHover.get(motionEvent.getPointerId(index), false)
return PointerInputEventData(
@@ -287,7 +304,8 @@
pressed,
toolType,
issuesEnterExit,
- historical
+ historical,
+ scrollDelta
)
}
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt
index fde4a0a..82ac8cb 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt
@@ -18,6 +18,8 @@
import android.view.KeyEvent
import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_SCROLL
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
@@ -49,6 +51,7 @@
actual var type: PointerEventType = calculatePointerEventType()
internal set
+ @OptIn(ExperimentalComposeUiApi::class)
private fun calculatePointerEventType(): PointerEventType {
val motionEvent = motionEvent
if (motionEvent != null) {
@@ -61,6 +64,7 @@
MotionEvent.ACTION_MOVE -> PointerEventType.Move
MotionEvent.ACTION_HOVER_ENTER -> PointerEventType.Enter
MotionEvent.ACTION_HOVER_EXIT -> PointerEventType.Exit
+ ACTION_SCROLL -> PointerEventType.Scroll
else -> PointerEventType.Unknown
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEvent.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEvent.android.kt
index c3ee76b..3862b31 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEvent.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEvent.android.kt
@@ -22,4 +22,4 @@
actual val uptime: Long,
actual val pointers: List<PointerInputEventData>,
val motionEvent: MotionEvent
-)
\ No newline at end of file
+)
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 9c3862d..c4c6fc1 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
@@ -32,6 +32,7 @@
import android.view.MotionEvent.ACTION_HOVER_ENTER
import android.view.MotionEvent.ACTION_HOVER_EXIT
import android.view.MotionEvent.ACTION_HOVER_MOVE
+import android.view.MotionEvent.ACTION_SCROLL
import android.view.MotionEvent.ACTION_MOVE
import android.view.MotionEvent.ACTION_POINTER_UP
import android.view.MotionEvent.ACTION_UP
@@ -1016,6 +1017,14 @@
if (autofillSupported()) _autofill?.performAutofill(values)
}
+ override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
+ return if (event.actionMasked == ACTION_SCROLL) {
+ handleMotionEvent(event).dispatchedToAPointerInputModifier
+ } else {
+ super.dispatchGenericMotionEvent(event)
+ }
+ }
+
// TODO(shepshapard): Test this method.
override fun dispatchTouchEvent(motionEvent: MotionEvent): Boolean {
if (hoverExitReceived) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/TempListUtils.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/TempListUtils.kt
index 940317c..d5c4d7c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/TempListUtils.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/TempListUtils.kt
@@ -27,6 +27,10 @@
* to each pair of two adjacent elements in this collection.
*
* The returned list is empty if this collection contains less than two elements.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T, R> List<T>.fastZipWithNext(transform: (T, T) -> R): List<R> {
@@ -50,6 +54,10 @@
* Throws an exception if this collection is empty. If the collection can be empty in an expected
* way, please use [reduceOrNull] instead. It returns `null` when its receiver is empty.
*
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ *
* @param [operation] function that takes current accumulator value and an element,
* and calculates the next accumulator value.
*/
@@ -71,6 +79,10 @@
* If any of two pairs would have the same key the last one gets added to the map.
*
* The returned map preserves the entry iteration order of the original collection.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T, K, V> List<T>.fastAssociate(transform: (T) -> Pair<K, V>): Map<K, V> {
@@ -86,6 +98,10 @@
* Returns a list of values built from the elements of `this` collection and the [other] collection with the same index
* using the provided [transform] function applied to each pair of elements.
* The returned list has length of the shortest collection.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T, R, V> List<T>.fastZip(
@@ -104,6 +120,10 @@
/**
* Returns a list containing the results of applying the given [transform] function
* to each element in the original collection.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
@@ -122,6 +142,10 @@
* If the collection could be huge, you can specify a non-negative value of [limit], in which case
* only the first [limit] elements will be appended, followed by the [truncated] string (which
* defaults to "...").
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
internal fun <T> List<T>.fastJoinToString(
separator: CharSequence = ", ",
@@ -142,6 +166,10 @@
* If the collection could be huge, you can specify a non-negative value of [limit], in which
* case only the first [limit] elements will be appended, followed by the [truncated] string
* (which defaults to "...").
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
private fun <T, A : Appendable> List<T>.fastJoinTo(
buffer: A,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt
index b0d20eb..4a64979 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt
@@ -47,7 +47,8 @@
val down: Boolean,
val type: PointerType,
val issuesEnterExit: Boolean = false,
- val historical: List<HistoricalChange> = mutableListOf()
+ val historical: List<HistoricalChange> = mutableListOf(),
+ val scrollDelta: Offset = Offset.Zero
)
/**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
index 53e6827..34632aa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
@@ -27,7 +27,6 @@
import androidx.compose.ui.input.pointer.PointerEventPass.Initial
import androidx.compose.ui.input.pointer.PointerEventPass.Main
import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.node.InternalCoreApi
import androidx.compose.ui.unit.IntSize
/**
@@ -108,7 +107,7 @@
/**
* Describes a pointer input change event that has occurred at a particular point in time.
*/
-expect class PointerEvent @OptIn(InternalCoreApi::class) internal constructor(
+expect class PointerEvent internal constructor(
changes: List<PointerInputChange>,
internalPointerEvent: InternalPointerEvent?
) {
@@ -263,7 +262,7 @@
/**
* The device type that produces a [PointerInputChange], such as a mouse or stylus.
*/
-inline class PointerType internal constructor(private val value: Int) {
+inline class PointerType private constructor(private val value: Int) {
override fun toString(): String = when (value) {
1 -> "Touch"
@@ -304,7 +303,7 @@
/**
* Indicates the primary reason that the [PointerEvent] was sent.
*/
-inline class PointerEventType(internal val value: Int) {
+inline class PointerEventType private constructor(internal val value: Int) {
companion object {
/**
* An unknown reason for the event.
@@ -344,14 +343,25 @@
* [Enter], [Exit], and [Enter] will be received.
*/
val Exit = PointerEventType(5)
+
+ /**
+ * A scroll event was sent. This can happen, for example, due to a mouse scroll wheel.
+ * This event indicates that the [PointerInputChange.scrollDelta]'s [Offset] is non-zero.
+ */
+ @Suppress("EXPERIMENTAL_ANNOTATION_ON_WRONG_TARGET")
+ @ExperimentalComposeUiApi
+ @get:ExperimentalComposeUiApi
+ val Scroll = PointerEventType(6)
}
+ @OptIn(ExperimentalComposeUiApi::class)
override fun toString(): String = when (this) {
Press -> "Press"
Release -> "Release"
Move -> "Move"
Enter -> "Enter"
Exit -> "Exit"
+ Scroll -> "Scroll"
else -> "Unknown"
}
}
@@ -419,8 +429,41 @@
get() = _historical ?: listOf()
private var _historical: List<HistoricalChange>? = null
+ /**
+ * The amount of scroll wheel movement in the horizontal and vertical directions.
+ */
+ @Suppress("EXPERIMENTAL_ANNOTATION_ON_WRONG_TARGET")
@ExperimentalComposeUiApi
- constructor(
+ @get:ExperimentalComposeUiApi
+ val scrollDelta: Offset
+ get() = _scrollDelta
+ private var _scrollDelta: Offset = Offset.Zero
+
+ /**
+ * Indicates whether the change was consumed or not. Note that the change must be consumed in
+ * full as there's no partial consumption system provided.
+ */
+ @Suppress("EXPERIMENTAL_ANNOTATION_ON_WRONG_TARGET")
+ @ExperimentalComposeUiApi
+ @get:ExperimentalComposeUiApi
+ val isConsumed: Boolean
+ get() = consumed.downChange || consumed.positionChange
+
+ /**
+ * Consume change event, claiming all the corresponding change info to the caller. This is
+ * usually needed when, button, when being clicked, consumed the "up" event so no other parents
+ * of this button could consume this "up" again.
+ *
+ * "Consumption" is just an indication of the claim and each pointer input handler
+ * implementation must manually check this flag to respect it.
+ */
+ @ExperimentalComposeUiApi
+ fun consume() {
+ consumed.downChange = true
+ consumed.positionChange = true
+ }
+
+ internal constructor(
id: PointerId,
uptimeMillis: Long,
position: Offset,
@@ -430,7 +473,8 @@
previousPressed: Boolean,
consumed: ConsumedData,
type: PointerType,
- historical: List<HistoricalChange>
+ historical: List<HistoricalChange>,
+ scrollDelta: Offset,
) : this(
id,
uptimeMillis,
@@ -443,6 +487,7 @@
type
) {
_historical = historical
+ _scrollDelta = scrollDelta
}
fun copy(
@@ -465,7 +510,8 @@
previousPressed,
consumed,
type,
- this.historical
+ this.historical,
+ this.scrollDelta
)
@ExperimentalComposeUiApi
@@ -490,7 +536,34 @@
previousPressed,
consumed,
type,
- historical
+ historical,
+ this.scrollDelta
+ )
+
+ internal fun copy(
+ id: PointerId = this.id,
+ currentTime: Long = this.uptimeMillis,
+ currentPosition: Offset = this.position,
+ currentPressed: Boolean = this.pressed,
+ previousTime: Long = this.previousUptimeMillis,
+ previousPosition: Offset = this.previousPosition,
+ previousPressed: Boolean = this.previousPressed,
+ consumed: ConsumedData = this.consumed,
+ type: PointerType = this.type,
+ historical: List<HistoricalChange> = this.historical,
+ scrollDelta: Offset
+ ): PointerInputChange = PointerInputChange(
+ id,
+ currentTime,
+ currentPosition,
+ currentPressed,
+ previousTime,
+ previousPosition,
+ previousPressed,
+ consumed,
+ type,
+ historical,
+ scrollDelta
)
override fun toString(): String {
@@ -503,7 +576,8 @@
"previousPressed=$previousPressed, " +
"consumed=$consumed, " +
"type=$type, " +
- "historical=$historical)"
+ "historical=$historical," +
+ "scrollDelta=$scrollDelta)"
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
index e5f1347..20cf960 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
@@ -147,7 +147,8 @@
previousDown,
ConsumedData(),
it.type,
- it.historical
+ it.historical,
+ it.scrollDelta
)
if (it.down) {
previousPointerInputData[it.id] = PointerInputData(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
index d3d6f92..3c08f7a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
@@ -60,11 +60,8 @@
}
}
- /**
- * An initialization function that is called when the [LayoutNodeWrapper] is initially created,
- * and also called when the [LayoutNodeWrapper] is re-used.
- */
- open fun onInitialize() {
+ override fun onInitialize() {
+ super.onInitialize()
wrapped.wrappedBy = this
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedDrawNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawEntity.kt
similarity index 74%
rename from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedDrawNode.kt
rename to compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawEntity.kt
index 3f8536f..aa8d3a2c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedDrawNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawEntity.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,13 +21,21 @@
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toSize
-internal class ModifiedDrawNode(
- wrapped: LayoutNodeWrapper,
- drawModifier: DrawModifier
-) : DelegatingLayoutNodeWrapper<DrawModifier>(wrapped, drawModifier), OwnerScope {
+internal class DrawEntity(
+ val layoutNodeWrapper: LayoutNodeWrapper,
+ val modifier: DrawModifier
+) : OwnerScope {
+ private val layoutNode: LayoutNode
+ get() = layoutNodeWrapper.layoutNode
+
+ private val size: IntSize
+ get() = layoutNodeWrapper.size
+
+ var next: DrawEntity? = null
private var cacheDrawModifier: DrawCacheModifier? = updateCacheDrawModifier()
@@ -38,7 +46,7 @@
override val layoutDirection: LayoutDirection get() = layoutNode.layoutDirection
- override val size: Size get() = measuredSize.toSize()
+ override val size: Size get() = layoutNodeWrapper.size.toSize()
}
// Flag to determine if the cache should be re-built
@@ -71,30 +79,30 @@
}
}
- override fun onInitialize() {
- super.onInitialize()
+ fun onInitialize() {
cacheDrawModifier = updateCacheDrawModifier()
invalidateCache = true
+ next?.onInitialize()
}
- override fun onMeasureResultChanged(width: Int, height: Int) {
- super.onMeasureResultChanged(width, height)
+ fun onMeasureResultChanged(width: Int, height: Int) {
invalidateCache = true
+ next?.onMeasureResultChanged(width, height)
}
// This is not thread safe
- override fun performDraw(canvas: Canvas) {
- val size = measuredSize.toSize()
+ fun draw(canvas: Canvas) {
+ val size = size.toSize()
if (cacheDrawModifier != null && invalidateCache) {
layoutNode.requireOwner().snapshotObserver.observeReads(
this,
- onCommitAffectingModifiedDrawNode,
+ onCommitAffectingDrawEntity,
updateCache
)
}
val drawScope = layoutNode.mDrawScope
- drawScope.draw(canvas, size, wrapped) {
+ drawScope.draw(canvas, size, layoutNodeWrapper, this) {
with(drawScope) {
with(modifier) {
draw()
@@ -107,15 +115,15 @@
// Callback invoked whenever a state parameter that is read within the cache
// execution callback is updated. This marks the cache flag as dirty and
// invalidates the current layer.
- private val onCommitAffectingModifiedDrawNode: (ModifiedDrawNode) -> Unit =
- { modifiedDrawNode ->
- if (modifiedDrawNode.isValid) {
- modifiedDrawNode.invalidateCache = true
- modifiedDrawNode.invalidateLayer()
+ private val onCommitAffectingDrawEntity: (DrawEntity) -> Unit =
+ { drawEntity ->
+ if (drawEntity.isValid) {
+ drawEntity.invalidateCache = true
+ drawEntity.layoutNodeWrapper.invalidateLayer()
}
}
}
override val isValid: Boolean
- get() = isAttached
-}
\ No newline at end of file
+ get() = layoutNodeWrapper.isAttached
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index ab6d3c9..71b3960 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -653,6 +653,8 @@
val addedCallback = hasNewPositioningCallback()
onPositionedCallbacks?.clear()
+ innerLayoutNodeWrapper.onInitialize()
+
// Create a new chain of LayoutNodeWrappers, reusing existing ones from wrappers
// when possible.
val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
@@ -660,6 +662,13 @@
mod.onRemeasurementAvailable(this)
}
+ if (mod is DrawModifier) {
+ val drawEntity = DrawEntity(toWrap, mod)
+ drawEntity.next = toWrap.drawEntityHead
+ toWrap.drawEntityHead = drawEntity
+ drawEntity.onInitialize()
+ }
+
// Re-use the layoutNodeWrapper if possible.
reuseLayoutNodeWrapper(mod, toWrap)?.let {
return@foldOut it
@@ -686,10 +695,6 @@
.initialize()
.assignChained(toWrap)
}
- if (mod is DrawModifier) {
- wrapper = ModifiedDrawNode(wrapper, mod)
- .initialize()
- }
if (mod is FocusModifier) {
wrapper = ModifiedFocusNode(wrapper, mod)
.initialize()
@@ -1176,8 +1181,23 @@
val infoList = mutableVectorOf<ModifierInfo>()
forEachDelegate { wrapper ->
wrapper as DelegatingLayoutNodeWrapper<*>
- val info = ModifierInfo(wrapper.modifier, wrapper, wrapper.layer)
+ val layer = wrapper.layer
+ val info = ModifierInfo(wrapper.modifier, wrapper, layer)
infoList += info
+ var node = wrapper.drawEntityHead // head
+ while (node != null) {
+ infoList += ModifierInfo(node.modifier, wrapper, layer)
+ node = node.next
+ }
+ }
+ var innerNode = innerLayoutNodeWrapper.drawEntityHead
+ while (innerNode != null) {
+ infoList += ModifierInfo(
+ innerNode.modifier,
+ innerLayoutNodeWrapper,
+ innerLayoutNodeWrapper.layer
+ )
+ innerNode = innerNode.next
}
return infoList.asMutableList()
}
@@ -1243,7 +1263,9 @@
private fun copyWrappersToCache() {
forEachDelegate {
wrapperCache += it as DelegatingLayoutNodeWrapper<*>
+ it.drawEntityHead = null
}
+ innerLayoutNodeWrapper.drawEntityHead = null
}
private fun markReusedModifiers(modifier: Modifier) {
@@ -1330,7 +1352,7 @@
forEachDelegateIncludingInner {
if (it.layer != null) {
return false
- } else if (it is ModifiedDrawNode) {
+ } else if (it.drawEntityHead != null) {
return true
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeDrawScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeDrawScope.kt
index 8493379..de1893f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeDrawScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeDrawScope.kt
@@ -36,27 +36,36 @@
// draw calls for all composables.
// As a result there could be thread safety concerns here for multi-threaded drawing
// scenarios, generally a single ComponentDrawScope should be shared for a particular thread
- private var wrapped: LayoutNodeWrapper? = null
+ private var drawEntity: DrawEntity? = null
override fun drawContent() {
- drawIntoCanvas { canvas -> wrapped?.draw(canvas) }
+ drawIntoCanvas { canvas ->
+ val drawEntity = drawEntity!!
+ val nextDrawEntity = drawEntity.next
+ if (nextDrawEntity != null) {
+ nextDrawEntity.draw(canvas)
+ } else {
+ drawEntity.layoutNodeWrapper.performDraw(canvas)
+ }
+ }
}
internal inline fun draw(
canvas: Canvas,
size: Size,
- LayoutNodeWrapper: LayoutNodeWrapper,
+ layoutNodeWrapper: LayoutNodeWrapper,
+ drawEntity: DrawEntity,
block: DrawScope.() -> Unit
) {
- val previousWrapper = wrapped
- wrapped = LayoutNodeWrapper
+ val previousDrawEntity = this.drawEntity
+ this.drawEntity = drawEntity
canvasDrawScope.draw(
- LayoutNodeWrapper.measureScope,
- LayoutNodeWrapper.measureScope.layoutDirection,
+ layoutNodeWrapper.measureScope,
+ layoutNodeWrapper.measureScope.layoutDirection,
canvas,
size,
block
)
- wrapped = previousWrapper
+ this.drawEntity = previousDrawEntity
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
index 7f82f7b..a240c7d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
@@ -154,6 +154,7 @@
}
layoutNode.owner?.onLayoutChange(layoutNode)
measuredSize = IntSize(width, height)
+ drawEntityHead?.onMeasureResultChanged(width, height)
}
var position: IntOffset = IntOffset.Zero
@@ -190,6 +191,11 @@
private val snapshotObserver get() = layoutNode.requireOwner().snapshotObserver
+ /**
+ * The head of the DrawEntity linked list
+ */
+ var drawEntityHead: DrawEntity? = null
+
protected inline fun performingMeasure(
constraints: Constraints,
block: () -> Placeable
@@ -214,6 +220,14 @@
}
/**
+ * An initialization function that is called when the [LayoutNodeWrapper] is initially created,
+ * and also called when the [LayoutNodeWrapper] is re-used.
+ */
+ open fun onInitialize() {
+ layer?.invalidate()
+ }
+
+ /**
* Places the modified child.
*/
/*@CallSuper*/
@@ -252,12 +266,23 @@
val x = position.x.toFloat()
val y = position.y.toFloat()
canvas.translate(x, y)
- performDraw(canvas)
+ drawContainedDrawModifiers(canvas)
canvas.translate(-x, -y)
}
}
- protected abstract fun performDraw(canvas: Canvas)
+ private fun drawContainedDrawModifiers(canvas: Canvas) {
+ val head = drawEntityHead
+ if (head == null) {
+ performDraw(canvas)
+ } else {
+ head.draw(canvas)
+ }
+ }
+
+ open fun performDraw(canvas: Canvas) {
+ wrapped?.draw(canvas)
+ }
open fun onPlaced() {}
@@ -266,7 +291,7 @@
override fun invoke(canvas: Canvas) {
if (layoutNode.isPlaced) {
snapshotObserver.observeReads(this, onCommitAffectingLayer) {
- performDraw(canvas)
+ drawContainedDrawModifiers(canvas)
}
lastLayerDrawingWasSkipped = false
} else {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
index 1bc1644..775bdd3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
@@ -256,6 +256,9 @@
/**
* Makes sure the passed [layoutNode] and its subtree is remeasured and has the final sizes.
+ *
+ * The node or some of the nodes in its subtree can still be kept unmeasured if they are
+ * not placed and don't affect the parent size. See [requestRemeasure] for details.
*/
fun forceMeasureTheSubtree(layoutNode: LayoutNode) {
// if there is nothing in `relayoutNodes` everything is remeasured.
@@ -273,8 +276,13 @@
remeasureAndRelayoutIfNeeded(child)
}
- // run recursively for the subtree.
- forceMeasureTheSubtree(child)
+ // if the child is still in NeedsRemeasure state then this child remeasure wasn't
+ // needed. it can happen for example when this child is not placed and can't affect
+ // the parent size. we can skip the whole subtree.
+ if (child.layoutState != NeedsRemeasure) {
+ // run recursively for the subtree.
+ forceMeasureTheSubtree(child)
+ }
}
// if the child was resized during the remeasurement it could request a remeasure on
@@ -283,8 +291,6 @@
if (layoutNode.layoutState == NeedsRemeasure && relayoutNodes.remove(layoutNode)) {
remeasureAndRelayoutIfNeeded(layoutNode)
}
-
- require(layoutNode.layoutState != NeedsRemeasure)
}
/**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierLocalConsumerNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierLocalConsumerNode.kt
index 8966754..6ab7e29 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierLocalConsumerNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierLocalConsumerNode.kt
@@ -27,7 +27,7 @@
override fun onModifierChanged() {
super.onModifierChanged()
- if (isAttached) notifyConsumerOfChanges()
+ notifyConsumerOfChanges()
}
override fun attach() {
@@ -39,6 +39,10 @@
get() = onModifierLocalRead(this)
private fun notifyConsumerOfChanges() {
+ // If the node is not attached, we don't notify the consumers.
+ // Ultimately when the node is attached, this function will be called again.
+ if (!isAttached) return
+
layoutNode.requireOwner().snapshotObserver.observeReads(this, onReadValuesChanged) {
modifier.onModifierLocalsUpdated(this)
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ComposeScene.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ComposeScene.desktop.kt
index 5ecc568..2a64b96 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ComposeScene.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ComposeScene.desktop.kt
@@ -47,7 +47,8 @@
nativeEvent: Any?,
type: PointerType,
isMousePressed: Boolean,
- pointerId: Long
+ pointerId: Long,
+ scrollDelta: Offset
): PointerInputEvent {
return PointerInputEvent(
eventType,
@@ -59,7 +60,8 @@
position,
position,
isMousePressed,
- type
+ type,
+ scrollDelta = scrollDelta
)
),
nativeEvent as MouseEvent?
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt
index cd9aaf9..08c470d 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt
@@ -19,8 +19,6 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.mouse.MouseScrollOrientation
-import androidx.compose.ui.input.mouse.MouseScrollUnit
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.platform.PlatformComponent
@@ -267,19 +265,14 @@
private fun ComposeScene.onMouseWheelEvent(
density: Float,
event: MouseWheelEvent
-) = with(event) {
- sendPointerScrollEvent(
+) {
+ sendPointerEvent(
+ eventType = PointerEventType.Scroll,
position = Offset(event.x.toFloat(), event.y.toFloat()) * density,
- delta = if (scrollType == MouseWheelEvent.WHEEL_BLOCK_SCROLL) {
- MouseScrollUnit.Page((scrollAmount * preciseWheelRotation).toFloat())
+ scrollDelta = if (event.isShiftDown) {
+ Offset(event.preciseWheelRotation.toFloat(), 0f)
} else {
- MouseScrollUnit.Line((scrollAmount * preciseWheelRotation).toFloat())
- },
- // There are no other way to detect horizontal scrolling in AWT
- orientation = if (isShiftDown) {
- MouseScrollOrientation.Horizontal
- } else {
- MouseScrollOrientation.Vertical
+ Offset(0f, event.preciseWheelRotation.toFloat())
},
timeMillis = event.`when`,
type = PointerType.Mouse,
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.desktop.kt
index 309be44..2c74cda 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.desktop.kt
@@ -46,7 +46,10 @@
internal actual constructor(
changes: List<PointerInputChange>,
internalPointerEvent: InternalPointerEvent?
- ) : this(changes, internalPointerEvent?.mouseEvent) {
+ ) : this(
+ changes,
+ internalPointerEvent?.mouseEvent
+ ) {
this.type = internalPointerEvent?.type ?: PointerEventType.Unknown
}
@@ -57,7 +60,10 @@
/**
* @param changes The changes.
*/
- actual constructor(changes: List<PointerInputChange>) : this(changes, mouseEvent = null)
+ actual constructor(changes: List<PointerInputChange>) : this(
+ changes,
+ mouseEvent = null
+ )
actual var type: PointerEventType = PointerEventType.Unknown
internal set
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/TestComposeWindow.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/TestComposeWindow.desktop.kt
index 4eb942b..fc04147 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/TestComposeWindow.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/TestComposeWindow.desktop.kt
@@ -24,6 +24,8 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.mouse.MouseScrollEvent
+import androidx.compose.ui.input.mouse.MouseScrollOrientation
+import androidx.compose.ui.input.mouse.MouseScrollUnit
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.unit.Constraints
@@ -34,7 +36,11 @@
import org.jetbrains.skia.Surface
import org.jetbrains.skiko.FrameDispatcher
import java.awt.Component
+import java.awt.event.MouseWheelEvent
import kotlin.coroutines.CoroutineContext
+import kotlin.math.abs
+import kotlin.math.roundToInt
+import kotlin.math.sign
/**
* A virtual window for testing purposes.
@@ -115,10 +121,36 @@
*/
@OptIn(ExperimentalComposeUiApi::class)
fun onMouseScroll(x: Int, y: Int, event: MouseScrollEvent) {
- scene.sendPointerScrollEvent(
+ val delta = when (event.delta) {
+ is MouseScrollUnit.Line -> event.delta.value
+ is MouseScrollUnit.Page -> event.delta.value
+ }
+ val wheelRotation = sign(delta)
+ scene.sendPointerEvent(
+ eventType = PointerEventType.Scroll,
position = Offset(x.toFloat(), y.toFloat()),
- delta = event.delta,
- orientation = event.orientation
+ scrollDelta = if (event.orientation == MouseScrollOrientation.Vertical) {
+ Offset(0f, wheelRotation)
+ } else {
+ Offset(wheelRotation, 0f)
+ },
+ nativeEvent = MouseWheelEvent(
+ EventComponent,
+ MouseWheelEvent.MOUSE_WHEEL,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ false,
+ if (event.delta is MouseScrollUnit.Line) {
+ MouseWheelEvent.WHEEL_UNIT_SCROLL
+ } else {
+ MouseWheelEvent.WHEEL_BLOCK_SCROLL
+ },
+ abs(delta.roundToInt()),
+ wheelRotation.roundToInt()
+ )
)
}
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt
index f3a83bd..c358016 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt
@@ -28,9 +28,6 @@
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.key.KeyEvent as ComposeKeyEvent
-import androidx.compose.ui.input.mouse.MouseScrollEvent
-import androidx.compose.ui.input.mouse.MouseScrollOrientation
-import androidx.compose.ui.input.mouse.MouseScrollUnit
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerInputEvent
import androidx.compose.ui.input.pointer.PointerType
@@ -346,6 +343,7 @@
*
* @param eventType Indicates the primary reason that the event was sent.
* @param position The [Offset] of the current pointer event, relative to the content.
+ * @param scrollDelta scroll delta for the PointerEventType.Scroll event
* @param timeMillis The time of the current pointer event, in milliseconds. The start (`0`) time
* is platform-dependent.
* @param type The device type that produced the event, such as [mouse][PointerType.Mouse],
@@ -356,6 +354,7 @@
fun sendPointerEvent(
eventType: PointerEventType,
position: Offset,
+ scrollDelta: Offset = Offset(0f, 0f),
timeMillis: Long = System.nanoTime() / 1_000_000L,
type: PointerType = PointerType.Mouse,
nativeEvent: Any? = null,
@@ -369,7 +368,14 @@
PointerEventType.Release -> isMousePressed = false
}
val event = pointerInputEvent(
- eventType, position, timeMillis, nativeEvent, type, isMousePressed, pointerId
+ eventType,
+ position,
+ timeMillis,
+ nativeEvent,
+ type,
+ isMousePressed,
+ pointerId,
+ scrollDelta
)
when (eventType) {
PointerEventType.Press -> onMousePressed(event)
@@ -380,56 +386,10 @@
}
PointerEventType.Enter -> hoveredOwner?.processPointerInput(event)
PointerEventType.Exit -> hoveredOwner?.processPointerInput(event)
+ PointerEventType.Scroll -> hoveredOwner?.processPointerInput(event)
}
}
- // TODO(demin): remove/change when we will have scroll event support in the common code
- // TODO(demin): return Boolean (when it is consumed).
- // see ComposeLayer todo about AWTDebounceEventQueue
- /**
- * Send pointer scroll event to the content.
- *
- * @param position The [Offset] of the current pointer event, relative to the content
- * @param delta Change of mouse scroll.
- * Positive if scrolling down, negative if scrolling up.
- * @param orientation Orientation in which scrolling event occurs.
- * Up/down wheel scrolling causes events in vertical orientation.
- * Left/right wheel scrolling causes events in horizontal orientation.
- * @param timeMillis The time of the current pointer event, in milliseconds. The start (`0`) time
- * is platform-dependent.
- * @param type The device type that produced the event, such as [mouse][PointerType.Mouse],
- * or [touch][PointerType.Touch].
- * @param nativeEvent The original native event
- */
- @OptIn(ExperimentalComposeUiApi::class)
- @Suppress("UNUSED_PARAMETER")
- @ExperimentalComposeUiApi // it is more experimental than ComposeScene itself
- fun sendPointerScrollEvent(
- position: Offset,
- delta: MouseScrollUnit,
- orientation: MouseScrollOrientation = MouseScrollOrientation.Vertical,
- timeMillis: Long = System.nanoTime() / 1_000_000L,
- type: PointerType = PointerType.Mouse,
- nativeEvent: Any? = null,
-// buttons: PointerButtons? = null,
-// keyboardModifiers: PointerKeyboardModifiers? = null,
- ): Unit = postponeInvalidation {
- check(!isDisposed) { "ComposeScene is disposed" }
- hoveredOwner?.onMouseScroll(
- position,
- MouseScrollEvent(delta, orientation),
- pointerInputEvent = pointerInputEvent(
- PointerEventType.Unknown,
- position,
- timeMillis,
- nativeEvent,
- type,
- isMousePressed,
- pointerId
- )
- )
- }
-
private fun onMousePressed(event: PointerInputEvent) {
val currentOwner = hoveredOwner
if (currentOwner != null) {
@@ -471,5 +431,6 @@
nativeEvent: Any?,
type: PointerType,
isMousePressed: Boolean,
- pointerId: Long
+ pointerId: Long,
+ scrollDelta: Offset
): PointerInputEvent
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/input/mouse/MouseScrollFilter.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/input/mouse/MouseScrollFilter.skiko.kt
index 4cce567..cf9d2a0 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/input/mouse/MouseScrollFilter.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/input/mouse/MouseScrollFilter.skiko.kt
@@ -18,15 +18,14 @@
package androidx.compose.ui.input.mouse
-import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.input.pointer.PointerInputModifier
-import androidx.compose.ui.input.pointer.PointerEvent
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerInputFilter
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntSize
+import java.awt.event.MouseWheelEvent
/**
* Indicates distance by which we should scroll some container.
@@ -78,6 +77,10 @@
val orientation: MouseScrollOrientation
)
+// TODO(demin): how easy-to-use scroll API should look like?
+// maybe something like Modifier.pointerScroll { delta: Offset -> } ?
+// or Modifier.pointerInput(Unit) { scroll { delta: Offset -> } }
+// ?
/**
* Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
* allow it to intercept scroll events from mouse wheel and touchpad.
@@ -100,31 +103,40 @@
*/
bounds: IntSize
) -> Boolean
-): Modifier = composed {
- val filter = remember(::MouseScrollEventFilter)
- filter.onMouseScroll = onMouseScroll
- MousePointerInputModifierImpl(filter)
-}
-
-internal class MouseScrollEventFilter : PointerInputFilter() {
- lateinit var onMouseScroll: (MouseScrollEvent, IntSize) -> Boolean
-
- override fun onPointerEvent(
- pointerEvent: PointerEvent,
- pass: PointerEventPass,
- bounds: IntSize
- ) = Unit
-
- override fun onCancel() = Unit
-
- fun onMouseScroll(event: MouseScrollEvent): Boolean {
- return isAttached && onMouseScroll(event, size)
+): Modifier = pointerInput(onMouseScroll) {
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ val mouseEvent = (event.mouseEvent as? MouseWheelEvent) ?: continue
+ val mouseChange = event.changes.find { it.type == PointerType.Mouse }
+ val isScroll = event.type == PointerEventType.Scroll
+ if (isScroll && mouseChange != null && !mouseChange.isConsumed) {
+ val legacyEvent = mouseEvent.toLegacyEvent(mouseChange.scrollDelta)
+ if (onMouseScroll(legacyEvent, size)) {
+ mouseChange.consume()
+ }
+ }
+ }
}
}
-private data class MousePointerInputModifierImpl(
- override val pointerInputFilter: PointerInputFilter
-) : PointerInputModifier
+private fun MouseWheelEvent.toLegacyEvent(scrollDelta: Offset): MouseScrollEvent {
+ val value = if (scrollDelta.x != 0f) scrollDelta.x else scrollDelta.y
+ return MouseScrollEvent(
+ delta = if (scrollType == MouseWheelEvent.WHEEL_BLOCK_SCROLL) {
+ MouseScrollUnit.Page(value * scrollAmount)
+ } else {
+ MouseScrollUnit.Line(value * scrollAmount)
+ },
+
+ // There are no other way to detect horizontal scrolling in AWT
+ orientation = if (isShiftDown || scrollDelta.x != 0f) {
+ MouseScrollOrientation.Horizontal
+ } else {
+ MouseScrollOrientation.Vertical
+ }
+ )
+}
@ExperimentalComposeUiApi
enum class MouseScrollOrientation {
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
index a3d80e6..9f68c30 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
@@ -47,20 +47,16 @@
import androidx.compose.ui.input.key.isShiftPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
-import androidx.compose.ui.input.mouse.MouseScrollEvent
-import androidx.compose.ui.input.mouse.MouseScrollEventFilter
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.PointerIconDefaults
import androidx.compose.ui.input.pointer.PointerIconService
import androidx.compose.ui.input.pointer.PointerInputEvent
import androidx.compose.ui.input.pointer.PointerInputEventProcessor
-import androidx.compose.ui.input.pointer.PointerInputFilter
import androidx.compose.ui.input.pointer.PositionCalculator
import androidx.compose.ui.input.pointer.ProcessResult
import androidx.compose.ui.input.pointer.TestPointerInputEventData
import androidx.compose.ui.layout.RootMeasurePolicy
-import androidx.compose.ui.node.HitTestResult
import androidx.compose.ui.node.InternalCoreApi
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.LayoutNodeDrawScope
@@ -405,31 +401,6 @@
)
}
- // TODO(demin): This is likely temporary. After PointerInputEvent can handle mouse events
- // (scroll in particular), we can replace it with processPointerInput. see b/166105940
- internal fun onMouseScroll(
- position: Offset,
- event: MouseScrollEvent,
- pointerInputEvent: PointerInputEvent
- ) {
- measureAndLayout()
- sendSyntheticEvents()
- lastPointerEvent = pointerInputEvent
-
- val inputFilters = HitTestResult<PointerInputFilter>()
- root.hitTest(position, inputFilters)
-
- for (
- filter in inputFilters
- .asReversed()
- .asSequence()
- .filterIsInstance<MouseScrollEventFilter>()
- ) {
- val isConsumed = filter.onMouseScroll(event)
- if (isConsumed) break
- }
- }
-
override val pointerIconService: PointerIconService =
object : PointerIconService {
override var current: PointerIcon
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/pointer/PointerInputTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/pointer/PointerInputTest.kt
index 353cc36..d2ca364 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/pointer/PointerInputTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/pointer/PointerInputTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.input.pointer
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Offset
import com.google.common.truth.FailureMetadata
import com.google.common.truth.Subject
@@ -347,6 +348,16 @@
assertThat(pointerInputChange.anyChangeConsumed()).isFalse()
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Test
+ fun consume_noChanges_returnsTrue() {
+ val pointerInputChange =
+ createPointerInputChange(8f, 16f, true, 8f, 16f, true, 0f, 0f, false)
+ pointerInputChange.consume()
+ assertThat(pointerInputChange.isConsumed).isTrue()
+ assertThat(pointerInputChange.anyChangeConsumed()).isTrue()
+ }
+
@Test
fun anyChangeConsumed_downConsumed_returnsTrue() {
val pointerInputChange =
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 7475387..6c5a986 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -681,8 +681,8 @@
@Test
fun coordinatesAttachedWhenLayoutNodeAttached() {
val layoutNode = LayoutNode()
- val drawModifier = Modifier.drawBehind { }
- layoutNode.modifier = drawModifier
+ val layoutModifier = Modifier.graphicsLayer { }
+ layoutNode.modifier = layoutModifier
assertFalse(layoutNode.coordinates.isAttached)
assertFalse(layoutNode.coordinates.isAttached)
layoutNode.attach(MockOwner())
@@ -697,16 +697,16 @@
@Test
fun layoutNodeWrapperSameWithReplacementModifier() {
val layoutNode = LayoutNode()
- val drawModifier = Modifier.drawBehind { }
+ val layoutModifier = Modifier.graphicsLayer { }
- layoutNode.modifier = drawModifier
+ layoutNode.modifier = layoutModifier
val oldLayoutNodeWrapper = layoutNode.outerLayoutNodeWrapper
assertFalse(oldLayoutNodeWrapper.isAttached)
layoutNode.attach(MockOwner())
assertTrue(oldLayoutNodeWrapper.isAttached)
- layoutNode.modifier = Modifier.drawBehind { }
+ layoutNode.modifier = Modifier.graphicsLayer { }
val newLayoutNodeWrapper = layoutNode.outerLayoutNodeWrapper
assertSame(newLayoutNodeWrapper, oldLayoutNodeWrapper)
}
@@ -741,9 +741,9 @@
@Test
fun layoutNodeWrapperAttachedWhenLayoutNodeAttached() {
val layoutNode = LayoutNode()
- val drawModifier = Modifier.drawBehind { }
+ val layoutModifier = Modifier.graphicsLayer { }
- layoutNode.modifier = drawModifier
+ layoutNode.modifier = layoutModifier
val oldLayoutNodeWrapper = layoutNode.outerLayoutNodeWrapper
assertFalse(oldLayoutNodeWrapper.isAttached)
@@ -760,8 +760,8 @@
fun layoutNodeWrapperParentLayoutCoordinates() {
val layoutNode = LayoutNode()
val layoutNode2 = LayoutNode()
- val drawModifier = Modifier.drawBehind { }
- layoutNode.modifier = drawModifier
+ val layoutModifier = Modifier.graphicsLayer { }
+ layoutNode.modifier = layoutModifier
layoutNode2.insertAt(0, layoutNode)
layoutNode2.attach(MockOwner())
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerNodeTest.kt
new file mode 100644
index 0000000..4cc4663
--- /dev/null
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerNodeTest.kt
@@ -0,0 +1,358 @@
+/*
+ * 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.compose.ui.node
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.autofill.Autofill
+import androidx.compose.ui.autofill.AutofillTree
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.hapticfeedback.HapticFeedback
+import androidx.compose.ui.input.InputModeManager
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.pointer.PointerIconService
+import androidx.compose.ui.modifier.modifierLocalConsumer
+import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.modifier.modifierLocalProvider
+import androidx.compose.ui.platform.AccessibilityManager
+import androidx.compose.ui.platform.ClipboardManager
+import androidx.compose.ui.platform.TextToolbar
+import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.input.TextInputService
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalComposeUiApi::class)
+@RunWith(JUnit4::class)
+class ModifierLocalConsumerNodeTest {
+
+ private val default = "Default"
+ private val ModifierLocalString = modifierLocalOf { "Default" }
+
+ private val owner = FakeOwner()
+ private val layoutNode = LayoutNode()
+
+ @Test
+ fun `unattached modifier local consumer does not invoke lambda`() {
+ // Arrange.
+ var receivedValue = ""
+ TestBox(Modifier.modifierLocalConsumer { receivedValue = ModifierLocalString.current })
+
+ // Assert.
+ assertThat(receivedValue).isEmpty()
+ }
+
+ @Test
+ fun `attached modifier local consumer with no provider reads default value`() {
+ // Arrange.
+ lateinit var receivedValue: String
+ TestBox(Modifier.modifierLocalConsumer { receivedValue = ModifierLocalString.current })
+
+ // Act.
+ attach()
+
+ // Assert.
+ assertThat(receivedValue).isEqualTo(default)
+ }
+
+ @Test
+ fun `changing the consumer modifier with no provider reads default value`() {
+ // Arrange.
+ lateinit var receivedValue: String
+ TestBox(Modifier.modifierLocalConsumer { receivedValue = ModifierLocalString.current })
+ attach()
+ receivedValue = ""
+
+ // Act.
+ changeModifier(Modifier.modifierLocalConsumer {
+ receivedValue = ModifierLocalString.current
+ })
+
+ // Assert.
+ assertThat(receivedValue).isEqualTo(default)
+ }
+
+ @Test
+ fun `detached modifier local consumer with no provider does not invoke lambda`() {
+ // Arrange.
+ lateinit var receivedValue: String
+ TestBox(Modifier.modifierLocalConsumer { receivedValue = ModifierLocalString.current })
+ attach()
+ receivedValue = ""
+
+ // Act.
+ detach()
+
+ // Assert.
+ assertThat(receivedValue).isEmpty()
+ }
+
+ @Test
+ fun `unattached modifier local consumer with provider does not invoke lambda`() {
+ // Arrange.
+ var receivedValue = ""
+ TestBox(
+ modifier = Modifier
+ .modifierLocalProvider(ModifierLocalString) { "Initial Value" }
+ .modifierLocalConsumer { receivedValue = ModifierLocalString.current }
+ )
+
+ // Assert.
+ assertThat(receivedValue).isEmpty()
+ }
+
+ @Test
+ fun `attached modifier local consumer with provider reads provided value`() {
+ // Arrange.
+ val providedValue = "Provided Value"
+ lateinit var receivedValue: String
+ TestBox(
+ modifier = Modifier
+ .modifierLocalProvider(ModifierLocalString) { providedValue }
+ .modifierLocalConsumer { receivedValue = ModifierLocalString.current }
+ )
+ // Act.
+ attach()
+
+ // Assert.
+ assertThat(receivedValue).isEqualTo(providedValue)
+ }
+
+ @Test
+ fun `changing provided value causes consumer to receive new provided value`() {
+ // Arrange.
+ val initialValue = "Initial Value"
+ val finalValue = "Final Value"
+ var providedValue by mutableStateOf(initialValue)
+ lateinit var receivedValue: String
+ TestBox(
+ modifier = Modifier
+ .modifierLocalProvider(ModifierLocalString) { providedValue }
+ .modifierLocalConsumer { receivedValue = ModifierLocalString.current }
+ )
+ attach()
+
+ // Act.
+ Snapshot.withMutableSnapshot {
+ providedValue = finalValue
+ }
+
+ // Assert.
+ assertThat(receivedValue).isEqualTo(finalValue)
+ }
+
+ @Test
+ fun `changing provided value after detaching modifier does not invoke consumer lambda`() {
+ // Arrange.
+ val initialValue = "Initial Value"
+ val finalValue = "Final Value"
+ var providedValue by mutableStateOf(initialValue)
+ lateinit var receivedValue: String
+ TestBox(
+ modifier = Modifier
+ .modifierLocalProvider(ModifierLocalString) { providedValue }
+ .modifierLocalConsumer { receivedValue = ModifierLocalString.current }
+ )
+ attach()
+ detach()
+ receivedValue = ""
+
+ // Act.
+ Snapshot.withMutableSnapshot {
+ providedValue = finalValue
+ }
+
+ // Assert.
+ assertThat(receivedValue).isEmpty()
+ }
+
+ @Test
+ fun `changing modifiers after detaching modifier does not invoke consumer lambda`() {
+ // Arrange.
+ lateinit var receivedValue: String
+ TestBox(
+ modifier = Modifier
+ .modifierLocalProvider(ModifierLocalString) { "Provided Value" }
+ .modifierLocalConsumer { receivedValue = ModifierLocalString.current }
+ )
+ attach()
+ detach()
+ receivedValue = ""
+
+ // Act.
+ changeModifier(Modifier.modifierLocalConsumer {
+ receivedValue = ModifierLocalString.current
+ })
+
+ // Assert.
+ assertThat(receivedValue).isEmpty()
+ }
+
+ @Test
+ fun `changing the consumer modifier with provider reads provided value`() {
+ // Arrange.
+ val providedValue = "Provided Value"
+ lateinit var receivedValue: String
+ TestBox(
+ modifier = Modifier
+ .modifierLocalProvider(ModifierLocalString) { providedValue }
+ .modifierLocalConsumer { receivedValue = ModifierLocalString.current }
+ )
+ attach()
+ receivedValue = ""
+
+ // Act.
+ changeModifier(
+ Modifier
+ .modifierLocalProvider(ModifierLocalString) { providedValue }
+ .modifierLocalConsumer { receivedValue = ModifierLocalString.current }
+ )
+
+ // Assert.
+ assertThat(receivedValue).isEqualTo(providedValue)
+ }
+
+ @Test
+ fun `detached modifier local consumer with provider does not invoke lambda`() {
+ // Arrange.
+ lateinit var receivedValue: String
+ TestBox(
+ modifier = Modifier
+ .modifierLocalProvider(ModifierLocalString) { "Provided Value" }
+ .modifierLocalConsumer { receivedValue = ModifierLocalString.current }
+ )
+ attach()
+ receivedValue = ""
+
+ // Act.
+ detach()
+
+ // Assert.
+ assertThat(receivedValue).isEmpty()
+ }
+
+ private fun TestBox(modifier: Modifier = Modifier) {
+ owner.snapshotObserver.startObserving()
+ layoutNode.modifier = modifier
+ }
+
+ private fun attach() {
+ layoutNode.attach(owner)
+ }
+
+ private fun detach() {
+ layoutNode.detach()
+ }
+
+ private fun changeModifier(modifier: Modifier) {
+ with(layoutNode) {
+ if (isAttached) { forEachLayoutNodeWrapper { it.detach() } }
+ this.modifier = modifier
+ if (isAttached) { forEachLayoutNodeWrapper { it.attach() } }
+ }
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ private class FakeOwner : Owner {
+ @OptIn(InternalCoreApi::class)
+ override var showLayoutBounds: Boolean = false
+ override val snapshotObserver: OwnerSnapshotObserver = OwnerSnapshotObserver { it.invoke() }
+ override fun onRequestMeasure(layoutNode: LayoutNode) {}
+ override fun onAttach(node: LayoutNode) = node.forEachLayoutNodeWrapper { it.attach() }
+ override fun onDetach(node: LayoutNode) = node.forEachLayoutNodeWrapper { it.detach() }
+
+ override val root: LayoutNode
+ get() = TODO("Not yet implemented")
+ override val sharedDrawScope: LayoutNodeDrawScope
+ get() = TODO("Not yet implemented")
+ override val rootForTest: RootForTest
+ get() = TODO("Not yet implemented")
+ override val hapticFeedBack: HapticFeedback
+ get() = TODO("Not yet implemented")
+ override val inputModeManager: InputModeManager
+ get() = TODO("Not yet implemented")
+ override val clipboardManager: ClipboardManager
+ get() = TODO("Not yet implemented")
+ override val accessibilityManager: AccessibilityManager
+ get() = TODO("Not yet implemented")
+ override val textToolbar: TextToolbar
+ get() = TODO("Not yet implemented")
+ override val density: Density
+ get() = TODO("Not yet implemented")
+ override val textInputService: TextInputService
+ get() = TODO("Not yet implemented")
+ override val pointerIconService: PointerIconService
+ get() = TODO("Not yet implemented")
+ override val focusManager: FocusManager
+ get() = TODO("Not yet implemented")
+ override val windowInfo: WindowInfo
+ get() = TODO("Not yet implemented")
+ override val fontLoader: Font.ResourceLoader
+ get() = TODO("Not yet implemented")
+ override val layoutDirection: LayoutDirection
+ get() = TODO("Not yet implemented")
+ override val measureIteration: Long
+ get() = TODO("Not yet implemented")
+ override val viewConfiguration: ViewConfiguration
+ get() = TODO("Not yet implemented")
+ override val autofillTree: AutofillTree
+ get() = TODO("Not yet implemented")
+ override val autofill: Autofill
+ get() = TODO("Not yet implemented")
+
+ override fun createLayer(drawBlock: (Canvas) -> Unit, invalidateParentLayer: () -> Unit) =
+ TODO("Not yet implemented")
+ override fun onRequestRelayout(layoutNode: LayoutNode) =
+ TODO("Not yet implemented")
+ override fun calculatePositionInWindow(localPosition: Offset) =
+ TODO("Not yet implemented")
+ override fun calculateLocalPosition(positionInWindow: Offset) =
+ TODO("Not yet implemented")
+ override fun requestFocus() =
+ TODO("Not yet implemented")
+ override fun measureAndLayout(sendPointerUpdate: Boolean) =
+ TODO("Not yet implemented")
+ override fun forceMeasureTheSubtree(layoutNode: LayoutNode) =
+ TODO("Not yet implemented")
+ override fun onSemanticsChange() =
+ TODO("Not yet implemented")
+ override fun onLayoutChange(layoutNode: LayoutNode) =
+ TODO("Not yet implemented")
+ override fun getFocusDirection(keyEvent: KeyEvent) =
+ TODO("Not yet implemented")
+ }
+}
+
+private fun LayoutNode.forEachLayoutNodeWrapper(action: (LayoutNodeWrapper) -> Unit) {
+ var layoutNodeWrapper: LayoutNodeWrapper? = outerLayoutNodeWrapper
+ while (layoutNodeWrapper != null) {
+ action.invoke(layoutNodeWrapper)
+ layoutNodeWrapper = layoutNodeWrapper.wrapped
+ }
+}
diff --git a/core/core-ktx/api/current.txt b/core/core-ktx/api/current.txt
index fee3502..86d91a7 100644
--- a/core/core-ktx/api/current.txt
+++ b/core/core-ktx/api/current.txt
@@ -451,8 +451,10 @@
public final class SizeKt {
method @RequiresApi(21) public static inline operator int component1(android.util.Size);
method @RequiresApi(21) public static inline operator float component1(android.util.SizeF);
+ method public static inline operator float component1(androidx.core.util.SizeFCompat);
method @RequiresApi(21) public static inline operator int component2(android.util.Size);
method @RequiresApi(21) public static inline operator float component2(android.util.SizeF);
+ method public static inline operator float component2(androidx.core.util.SizeFCompat);
}
public final class SparseArrayKt {
diff --git a/core/core-ktx/api/public_plus_experimental_current.txt b/core/core-ktx/api/public_plus_experimental_current.txt
index fee3502..86d91a7 100644
--- a/core/core-ktx/api/public_plus_experimental_current.txt
+++ b/core/core-ktx/api/public_plus_experimental_current.txt
@@ -451,8 +451,10 @@
public final class SizeKt {
method @RequiresApi(21) public static inline operator int component1(android.util.Size);
method @RequiresApi(21) public static inline operator float component1(android.util.SizeF);
+ method public static inline operator float component1(androidx.core.util.SizeFCompat);
method @RequiresApi(21) public static inline operator int component2(android.util.Size);
method @RequiresApi(21) public static inline operator float component2(android.util.SizeF);
+ method public static inline operator float component2(androidx.core.util.SizeFCompat);
}
public final class SparseArrayKt {
diff --git a/core/core-ktx/api/restricted_current.txt b/core/core-ktx/api/restricted_current.txt
index fee3502..86d91a7 100644
--- a/core/core-ktx/api/restricted_current.txt
+++ b/core/core-ktx/api/restricted_current.txt
@@ -451,8 +451,10 @@
public final class SizeKt {
method @RequiresApi(21) public static inline operator int component1(android.util.Size);
method @RequiresApi(21) public static inline operator float component1(android.util.SizeF);
+ method public static inline operator float component1(androidx.core.util.SizeFCompat);
method @RequiresApi(21) public static inline operator int component2(android.util.Size);
method @RequiresApi(21) public static inline operator float component2(android.util.SizeF);
+ method public static inline operator float component2(androidx.core.util.SizeFCompat);
}
public final class SparseArrayKt {
diff --git a/core/core-ktx/src/androidTest/java/androidx/core/util/SizeTest.kt b/core/core-ktx/src/androidTest/java/androidx/core/util/SizeTest.kt
index 5cd063b..07aa580 100644
--- a/core/core-ktx/src/androidTest/java/androidx/core/util/SizeTest.kt
+++ b/core/core-ktx/src/androidTest/java/androidx/core/util/SizeTest.kt
@@ -23,18 +23,25 @@
import org.junit.Assert.assertEquals
import org.junit.Test
-@SdkSuppress(minSdkVersion = 21)
@SmallTest
class SizeTest {
+ @SdkSuppress(minSdkVersion = 21)
@Test fun destructuringSize() {
val (w, h) = Size(320, 240)
assertEquals(320, w)
assertEquals(240, h)
}
+ @SdkSuppress(minSdkVersion = 21)
@Test fun destructuringSizeF() {
val (w, h) = SizeF(1920.0f, 1080.0f)
assertEquals(1920.0f, w)
assertEquals(1080.0f, h)
}
+
+ @Test fun destructuringSizeFCompat() {
+ val (w, h) = SizeFCompat(1920.0f, 1080.0f)
+ assertEquals(1920.0f, w)
+ assertEquals(1080.0f, h)
+ }
}
diff --git a/core/core-ktx/src/main/java/androidx/core/util/Size.kt b/core/core-ktx/src/main/java/androidx/core/util/Size.kt
index ab2d80a..5cb85db 100644
--- a/core/core-ktx/src/main/java/androidx/core/util/Size.kt
+++ b/core/core-ktx/src/main/java/androidx/core/util/Size.kt
@@ -71,3 +71,25 @@
*/
@RequiresApi(21)
public inline operator fun SizeF.component2(): Float = height
+
+/**
+ * Returns "width", the first component of this [SizeFCompat].
+ *
+ * This method allows to use destructuring declarations when working with
+ * sizes, for example:
+ * ```
+ * val (w, h) = mySize
+ * ```
+ */
+public inline operator fun SizeFCompat.component1(): Float = width
+
+/**
+ * Returns "height", the second component of this [SizeFCompat].
+ *
+ * This method allows to use destructuring declarations when working with
+ * sizes, for example:
+ * ```
+ * val (w, h) = mySize
+ * ```
+ */
+public inline operator fun SizeFCompat.component2(): Float = height
diff --git a/core/core-performance/build.gradle b/core/core-performance/build.gradle
index 576a8d8..add1f93 100644
--- a/core/core-performance/build.gradle
+++ b/core/core-performance/build.gradle
@@ -29,7 +29,7 @@
testImplementation(libs.kotlinStdlib)
testImplementation(libs.junit)
testImplementation(libs.truth)
- testImplementation(libs.robolectric)
+ testImplementation("org.robolectric:robolectric:4.7") // TODO(b/206673076): use libs.robolectric
}
androidx {
diff --git a/core/core-performance/src/main/kotlin/androidx/core/performance/PerformanceClass.kt b/core/core-performance/src/main/kotlin/androidx/core/performance/PerformanceClass.kt
index afe7ad1..246f23a 100644
--- a/core/core-performance/src/main/kotlin/androidx/core/performance/PerformanceClass.kt
+++ b/core/core-performance/src/main/kotlin/androidx/core/performance/PerformanceClass.kt
@@ -35,7 +35,7 @@
* <p>
* Defaults to {@link Build.MEDIA_PERFORMANCE_CLASS}
*/
- fun getPerformanceClass(): Int {
+ fun getMediaPerformanceClass(): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return Build.VERSION.MEDIA_PERFORMANCE_CLASS
}
diff --git a/core/core-performance/src/test/kotlin/androidx/core/performance/PerformanceClassTest.kt b/core/core-performance/src/test/kotlin/androidx/core/performance/PerformanceClassTest.kt
index 23bcaf6..895b376 100644
--- a/core/core-performance/src/test/kotlin/androidx/core/performance/PerformanceClassTest.kt
+++ b/core/core-performance/src/test/kotlin/androidx/core/performance/PerformanceClassTest.kt
@@ -19,11 +19,11 @@
import android.os.Build.VERSION_CODES.R
import android.os.Build.VERSION_CODES.S
import com.google.common.truth.Truth.assertThat
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowBuild
import org.robolectric.shadows.ShadowSystemProperties
/** Unit tests for [PerformanceClass]. */
@@ -34,17 +34,24 @@
@Test
@Config(maxSdk = R)
- fun getPerformanceClass_sdk30() {
- assertThat(pc.getPerformanceClass()).isEqualTo(0)
+ fun getMediaPerformanceClass_sdk30() {
+ assertThat(pc.getMediaPerformanceClass()).isEqualTo(0)
}
@Test
- // Note this test is not actually running because robolectric does not support sdk31 yet
- @Ignore("b/206673076")
@Config(minSdk = S)
- fun getPerformanceClass_sdk31() {
+ fun getMediaPerformanceClass_sdk31_declared30() {
// TODO(b/205732671): Use ShadowBuild.setMediaPerformanceClass when available
- ShadowSystemProperties.override("ro.odm.build.media_performance_class", "31")
- assertThat(pc.getPerformanceClass()).isEqualTo(31)
+ ShadowSystemProperties.override("ro.odm.build.media_performance_class", "30")
+ ShadowBuild.reset()
+ assertThat(pc.getMediaPerformanceClass()).isEqualTo(30)
+ }
+
+ @Test
+ @Config(minSdk = S)
+ fun getMediaPerformanceClass_sdk31_notDeclared() {
+ // TODO(b/205732671): Use ShadowBuild.setMediaPerformanceClass when available
+ ShadowBuild.reset()
+ assertThat(pc.getMediaPerformanceClass()).isEqualTo(0)
}
}
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
index 57fef19..46694db 100644
--- a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
+++ b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
@@ -65,7 +65,7 @@
* This needs to be called before [Activity.setContentView] or other view operation on
* the root view (e.g setting flags).
*
- * Alternatively, if a [SplashScreen] instance is not required, the them can manually be
+ * Alternatively, if a [SplashScreen] instance is not required, the theme can manually be
* set using [Activity.setTheme].
*/
@JvmStatic
@@ -343,4 +343,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index e2580ac..2d9d03a 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -2030,6 +2030,14 @@
method public boolean test(T!);
}
+ public final class SizeFCompat {
+ ctor public SizeFCompat(float, float);
+ method @RequiresApi(21) public static androidx.core.util.SizeFCompat createFromSizeF(android.util.SizeF);
+ method public float getHeight();
+ method public float getWidth();
+ method @RequiresApi(21) public android.util.SizeF toSizeF();
+ }
+
public interface Supplier<T> {
method public T! get();
}
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 98fa24a..c7d450fb 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -2034,6 +2034,14 @@
method public boolean test(T!);
}
+ public final class SizeFCompat {
+ ctor public SizeFCompat(float, float);
+ method @RequiresApi(21) public static androidx.core.util.SizeFCompat createFromSizeF(android.util.SizeF);
+ method public float getHeight();
+ method public float getWidth();
+ method @RequiresApi(21) public android.util.SizeF toSizeF();
+ }
+
public interface Supplier<T> {
method public T! get();
}
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 65f68dd..39514e4 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -2400,6 +2400,7 @@
method public static void checkArgument(boolean);
method public static void checkArgument(boolean, Object);
method public static void checkArgument(boolean, String, java.lang.Object!...);
+ method public static float checkArgumentFinite(float, String);
method public static int checkArgumentInRange(int, int, int, String);
method public static long checkArgumentInRange(long, long, long, String);
method public static float checkArgumentInRange(float, float, float, String);
@@ -2420,6 +2421,14 @@
method public boolean test(T!);
}
+ public final class SizeFCompat {
+ ctor public SizeFCompat(float, float);
+ method @RequiresApi(21) public static androidx.core.util.SizeFCompat createFromSizeF(android.util.SizeF);
+ method public float getHeight();
+ method public float getWidth();
+ method @RequiresApi(21) public android.util.SizeF toSizeF();
+ }
+
public interface Supplier<T> {
method public T! get();
}
diff --git a/core/core/src/main/java/androidx/core/util/Preconditions.java b/core/core/src/main/java/androidx/core/util/Preconditions.java
index ce979e0..3461d12 100644
--- a/core/core/src/main/java/androidx/core/util/Preconditions.java
+++ b/core/core/src/main/java/androidx/core/util/Preconditions.java
@@ -336,6 +336,29 @@
return value;
}
+ /**
+ * Ensures that the argument floating point value is a finite number.
+ *
+ * <p>A finite number is defined to be both representable (that is, not NaN) and
+ * not infinite (that is neither positive or negative infinity).</p>
+ *
+ * @param value a floating point value
+ * @param valueName the name of the argument to use if the check fails
+ *
+ * @return the validated floating point value
+ *
+ * @throws IllegalArgumentException if {@code value} was not finite
+ */
+ public static float checkArgumentFinite(float value, @NonNull String valueName) {
+ if (Float.isNaN(value)) {
+ throw new IllegalArgumentException(valueName + " must not be NaN");
+ } else if (Float.isInfinite(value)) {
+ throw new IllegalArgumentException(valueName + " must not be infinite");
+ }
+
+ return value;
+ }
+
private Preconditions() {
}
}
diff --git a/core/core/src/main/java/androidx/core/util/SizeFCompat.java b/core/core/src/main/java/androidx/core/util/SizeFCompat.java
new file mode 100644
index 0000000..124a1ec
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/util/SizeFCompat.java
@@ -0,0 +1,106 @@
+/*
+ * 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.core.util;
+
+import android.util.SizeF;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * Immutable class for describing width and height dimensions in some arbitrary unit. Width and
+ * height are finite values stored as a floating point representation.
+ * <p>
+ * This is a backward-compatible version of {@link SizeF}.
+ */
+public final class SizeFCompat {
+
+ private final float mWidth;
+ private final float mHeight;
+
+ public SizeFCompat(float width, float height) {
+ mWidth = Preconditions.checkArgumentFinite(width, "width");
+ mHeight = Preconditions.checkArgumentFinite(height, "height");
+ }
+
+ /**
+ * Get the width of the size (as an arbitrary unit).
+ * @return width
+ */
+ public float getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Get the height of the size (as an arbitrary unit).
+ * @return height
+ */
+ public float getHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SizeFCompat)) return false;
+ SizeFCompat that = (SizeFCompat) o;
+ return that.mWidth == mWidth && that.mHeight == mHeight;
+ }
+
+ @Override
+ public int hashCode() {
+ return Float.floatToIntBits(mWidth) ^ Float.floatToIntBits(mHeight);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return mWidth + "x" + mHeight;
+ }
+
+ /** Converts this {@link SizeFCompat} into a {@link SizeF}. */
+ @RequiresApi(21)
+ @NonNull
+ public SizeF toSizeF() {
+ return Api21Impl.toSizeF(this);
+ }
+
+ /** Creates a {@link SizeFCompat} from a {@link SizeF}. */
+ @RequiresApi(21)
+ @NonNull
+ public static SizeFCompat createFromSizeF(@NonNull SizeF size) {
+ return Api21Impl.toSizeFCompat(size);
+ }
+
+ @RequiresApi(21)
+ private static final class Api21Impl {
+ @DoNotInline
+ @NonNull
+ static SizeFCompat toSizeFCompat(@NonNull SizeF size) {
+ Preconditions.checkNotNull(size);
+ return new SizeFCompat(size.getWidth(), size.getHeight());
+ }
+
+ @DoNotInline
+ @NonNull
+ static SizeF toSizeF(@NonNull SizeFCompat size) {
+ Preconditions.checkNotNull(size);
+ return new SizeF(size.getWidth(), size.getHeight());
+ }
+ }
+}
diff --git a/core/core/src/test/java/androidx/core/util/SizeFCompatTest.java b/core/core/src/test/java/androidx/core/util/SizeFCompatTest.java
new file mode 100644
index 0000000..cde04f1
--- /dev/null
+++ b/core/core/src/test/java/androidx/core/util/SizeFCompatTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.core.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.annotation.TargetApi;
+import android.util.SizeF;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(sdk = {19, 21})
+public class SizeFCompatTest {
+
+ @Test
+ public void constructor_validSize() {
+ SizeFCompat size = new SizeFCompat(100.4f, 150.2f);
+
+ assertThat(size.getWidth()).isEqualTo(100.4f);
+ assertThat(size.getHeight()).isEqualTo(150.2f);
+ }
+
+ @Test
+ public void constructor_invalidSize() {
+ assertThrows(IllegalArgumentException.class, () -> new SizeFCompat(Float.NaN, 100f));
+ assertThrows(IllegalArgumentException.class, () -> new SizeFCompat(12.5f, Float.NaN));
+ }
+
+ @Test
+ public void equals() {
+ assertThat(new SizeFCompat(100.4f, 150.2f)).isEqualTo(new SizeFCompat(100.4f, 150.2f));
+ assertThat(new SizeFCompat(100.4f, 150.2f)).isNotEqualTo(new SizeFCompat(10.4f, 150.2f));
+ assertThat(new SizeFCompat(100.4f, 150.2f)).isNotEqualTo(new SizeFCompat(100.4f, 15.2f));
+ }
+
+ @Test
+ public void hashCode_sameForEqualSizes() {
+ assertThat(new SizeFCompat(100.4f, 150.2f).hashCode())
+ .isEqualTo(new SizeFCompat(100.4f, 150.2f).hashCode());
+ assertThat(new SizeFCompat(100.4f, 150.2f).hashCode())
+ .isNotEqualTo(new SizeFCompat(10.4f, 150.2f).hashCode());
+ assertThat(new SizeFCompat(100.4f, 150.2f).hashCode())
+ .isNotEqualTo(new SizeFCompat(100.4f, 15.2f).hashCode());
+ }
+
+ @Test
+ public void toString_readable() {
+ assertThat(new SizeFCompat(10.2f, 20.4f).toString()).isEqualTo("10.2x20.4");
+ }
+
+ @TargetApi(21)
+ @Config(sdk = 21)
+ @Test
+ public void toSizeF() {
+ assertThat(new SizeFCompat(10.2f, 20.4f).toSizeF()).isEqualTo(new SizeF(10.2f, 20.4f));
+ }
+
+ @TargetApi(21)
+ @Config(sdk = 21)
+ @Test
+ public void createFromSizeF() {
+ assertThat(SizeFCompat.createFromSizeF(new SizeF(11.2f, 21.4f)))
+ .isEqualTo(new SizeFCompat(11.2f, 21.4f));
+ }
+}
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index ecc237e6..36287014 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -133,7 +133,6 @@
\- Support for Scala 3
For more details see .*
Daemon will be stopped at the end of the build
-# > Task :buildSrc:build
# > Configure project :appsearch:appsearch\-local\-backend
Configuration on demand is an incubating feature\.
Configuration cache is an incubating feature\.
@@ -177,9 +176,6 @@
Note: Recompile with \-Xlint:removal for details\.
Note\: Some input files use unchecked or unsafe operations\.
Note\: Recompile with \-Xlint\:unchecked for details\.
-# > Task :benchmark:benchmark-junit4:processDebugAndroidTestManifest
-# > Task :benchmark:benchmark-common:processDebugAndroidTestManifest
-# > Task :tracing:tracing:compileDebugAndroidTestJavaWithJavac
# > Task :ui:ui-tooling:processDebugAndroidTestManifest
application@android:debuggable was tagged at .*\.xml:[0-9]+ to replace other declarations but no other declaration present
\$OUT_DIR/androidx/wear/compose/compose\-material\-benchmark/build/intermediates/tmp/manifest/androidTest/release/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
@@ -208,7 +204,6 @@
\$OUT_DIR/androidx/compose/ui/ui\-graphics/ui\-graphics\-benchmark/build/intermediates/tmp/manifest/androidTest/release/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Task :compose:foundation:foundation-layout:processDebugAndroidTestManifest
\$SUPPORT/compose/foundation/foundation\-layout/src/androidAndroidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-# > Task :compose:navigation:navigation:processDebugAndroidTestManifest
# > Task :compose:runtime:runtime-saveable:processDebugAndroidTestManifest
\$SUPPORT/compose/runtime/runtime\-saveable/src/androidAndroidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Task :compose:runtime:runtime:benchmark:processReleaseAndroidTestManifest
@@ -230,7 +225,6 @@
\$OUT_DIR\/androidx\/docs\-public\/build\/unzippedDocsSources\/androidx\/work\/impl\/utils\/ForceStopRunnable\.java\:[0-9]+\: error\: cannot find symbol
# > Task :buildOnServer
[0-9]+ problems were found reusing the configuration cache, [0-9]+ of which seem unique\.
-\- Task `:docs\-tip\-of\-tree:unzipSampleSources` of type `org\.gradle\.api\.tasks\.Sync`: cannot deserialize object of type 'org\.gradle\.api\.Project' as these are not supported with the configuration cache\.
[0-9]+ actionable tasks: [0-9]+ executed, [0-9]+ up\-to\-date
Configuration cache entry reused with [0-9]+ problems\.
See the profiling report at: file://\$GRADLE_USER_HOME/daemon/.*/reports/profile/profile\-[0-9]+\-[0-9]+\-[0-9]+\-[0-9]+\-[0-9]+\-[0-9]+\.html
@@ -245,19 +239,8 @@
# > Task :lifecycle:lifecycle-common:compileJava
Note: \$[^ ]+ uses or overrides a deprecated API\.
Note: Recompile with \-Xlint\:deprecation for details\.
-# > Task :concurrent:concurrent-futures:compileJava
-# > Task :contentpager:contentpager:compileReleaseJavaWithJavac
-# > Task :coordinatorlayout:coordinatorlayout:compileReleaseJavaWithJavac
-# > Task :customview:customview:compileReleaseJavaWithJavac
-# > Task :recommendation:recommendation:compileReleaseJavaWithJavac
-# > Task :textclassifier:textclassifier:compileReleaseJavaWithJavac
# > Task :publicDocsTask
[0-9]+ warnings
-# > Task :support-animation-demos:compileDebugJavaWithJavac
-# > Task :lint-demos:lint-demo-appcompat:compileDebugJavaWithJavac
-# > Task :support-transition-demos:compileDebugJavaWithJavac
-# > Task :support-content-demos:compileDebugJavaWithJavac
-# > Task :support-preference-demos:compileDebugJavaWithJavac
# > Task :startup:integration-tests:first-library:processDebugManifest
\$SUPPORT/startup/integration\-tests/first\-library/src/main/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
meta\-data\#androidx\.work\.WorkManagerInitializer was tagged at AndroidManifest\.xml\:[0-9]+ to remove other declarations but no other declaration present
@@ -265,23 +248,12 @@
Note: \$SUPPORT/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser\.java uses unchecked or unsafe operations\.
# > Task :emoji2:emoji2-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/emoji[0-9]+/emoji[0-9]+\-benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-# > Task :gridlayout:gridlayout:compileDebugAndroidTestJavaWithJavac
-# > Task :leanback:leanback-tab:processDebugAndroidTestManifest
-# > Task :lifecycle:lifecycle-extensions:processDebugAndroidTestManifest
# > Task :leanback:leanback:compileDebugAndroidTestJavaWithJavac
reason: class file for kotlin\.annotation\.AnnotationTarget not found
reason: class file for kotlin\.annotation\.AnnotationRetention not found
warning: unknown enum constant AnnotationTarget\.ANNOTATION_CLASS
-# > Task :navigation:navigation-dynamic-features-runtime:processDebugAndroidTestManifest
-# > Task :mediarouter:mediarouter:compileDebugAndroidTestJavaWithJavac
-# > Task :palette:palette:processDebugAndroidTestManifest
-# > Task :percentlayout:percentlayout:processDebugAndroidTestManifest
-# > Task :preference:preference-ktx:processDebugAndroidTestManifest
# > Task :recyclerview:recyclerview-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/recyclerview/recyclerview\-benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-# > Task :remotecallback:remotecallback:processDebugAndroidTestManifest
-# > Task :recyclerview:recyclerview-selection:compileDebugAndroidTestJavaWithJavac
-# > Task :savedstate:savedstate:processDebugAndroidTestManifest
# > Task :room:room-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/room/benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Task :room:room-benchmark:kaptReleaseAndroidTestKotlin
@@ -291,7 +263,6 @@
\$SUPPORT/room/integration\-tests/noappcompattestapp/src/androidTest/java/androidx/room/integration/noappcompat/BareRelationDatabaseTest\.java:[0-9]+: warning: The return value includes a POJO with a @Relation\. It is usually desired to annotate this method with @Transaction to avoid possibility of inconsistent results between the POJO and its relations\. See https://developer\.android\.com/reference/androidx/room/Transaction\.html for details\.
UserAndPets getUserWithPets\(long id\);
List<UserAndPet> getUsersWithPet\(\);
-# > Task :textclassifier:integration-tests:testapp:compileDebugAndroidTestJavaWithJavac
# > Task :room:integration-tests:room-testapp:compileDebugAndroidTestJavaWithJavac
Note: \$SUPPORT/room/integration\-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/TestUtil\.java uses unchecked or unsafe operations\.
# > Task :activity:integration-tests:testapp:processDebugAndroidTestManifest
@@ -312,16 +283,7 @@
# > Task :compose:material:material:icons:generator:zipHtmlResultsOfTest
Html results of .* zipped into.*\.zip
[0-9]+ problems were found storing the configuration cache, [0-9]+ of which seem unique\.
-\- Task `:docs\-tip\-of\-tree:unzipSampleSources` of type `org\.gradle\.api\.tasks\.Sync`: cannot serialize object of type 'org\.gradle\.api\.internal\.artifacts\.configurations\.DefaultConfiguration', a subtype of 'org\.gradle\.api\.artifacts\.Configuration', as these are not supported with the configuration cache\.
See https://docs\.gradle\.org/[0-9]+\.[0-9]+.*/userguide/configuration_cache\.html\#config_cache:requirements:disallowed_types
-\- Task `:docs\-tip\-of\-tree:unzipSampleSources` of type `org\.gradle\.api\.tasks\.Sync`: cannot deserialize object of type 'org\.gradle\.api\.artifacts\.Configuration' as these are not supported with the configuration cache\.
-\- Task `:docs\-tip\-of\-tree:unzipSampleSources` of type `org\.gradle\.api\.tasks\.Sync`: value 'file collection' is not assignable to 'org\.gradle\.api\.artifacts\.Configuration'
-\- Task `:docs\-tip\-of\-tree:unzipSourcesForDackka` of type `org\.gradle\.api\.tasks\.Sync`: cannot deserialize object of type 'org\.gradle\.api\.Project' as these are not supported with the configuration cache\.
-\- Task `:docs\-tip\-of\-tree:unzipSourcesForDackka` of type `org\.gradle\.api\.tasks\.Sync`: cannot deserialize object of type 'org\.gradle\.api\.artifacts\.Configuration' as these are not supported with the configuration cache\.
-\- Task `:docs\-tip\-of\-tree:unzipSourcesForDackka` of type `org\.gradle\.api\.tasks\.Sync`: value 'file collection' is not assignable to 'org\.gradle\.api\.artifacts\.Configuration'
-\- Task `:docs\-tip\-of\-tree:unzipDocsSources` of type `org\.gradle\.api\.tasks\.Sync`: cannot deserialize object of type 'org\.gradle\.api\.artifacts\.Configuration' as these are not supported with the configuration cache\.
-\- Task `:docs\-tip\-of\-tree:unzipDocsSources` of type `org\.gradle\.api\.tasks\.Sync`: value 'file collection' is not assignable to 'org\.gradle\.api\.artifacts\.Configuration'
-\- Task `:docs\-tip\-of\-tree:unzipSampleSources` of type `org\.gradle\.api\.tasks\.Sync`: cannot serialize object of type 'org\.gradle\.api\.internal\.project\.DefaultProject', a subtype of 'org\.gradle\.api\.Project', as these are not supported with the configuration cache\.
[0-9]+ problem was found storing the configuration cache.
\- Task `:listTaskOutputs` of type `androidx\.build\.ListTaskOutputsTask`: invocation of 'Task\.project' at execution time is unsupported\.
\- Task `[^ ]*checkExternalLicenses` of type `[^ ]*CheckExternalDependencyLicensesTask`: invocation of 'Task\.project' at execution time is unsupported\.
@@ -390,7 +352,6 @@
w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: Unnecessary non\-null assertion \(\!\!\) on a non\-null receiver of type WindowInsetsCompat
w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'consumeStableInsets\(\)\: WindowInsetsCompat\' is deprecated\. Deprecated in Java
w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'consumeDisplayCutout\(\)\: WindowInsetsCompat\' is deprecated\. Deprecated in Java
-# > Task :contentpager:contentpager:compileDebugAndroidTestJavaWithJavac
# > Task :docs-public:dokkaKotlinDocs
No documentation for .*
Found an unresolved type in androidx\.compose\.runtime\.snapshots\.SnapshotStateList\$add\(androidx\.compose\.runtime\.snapshots\.SnapshotStateList\.T\) \(SnapshotStateList\.kt:[0-9]+\)
@@ -597,39 +558,8 @@
\$SUPPORT/compose/ui/ui\-tooling/src/androidAndroidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
No issues found.*
dagger\.lint\.DaggerIssueRegistry in .*/lint\.jar does not specify a vendor; see IssueRegistry#vendor
-# > Task :camera:camera-camera2-pipe:reportLibraryMetrics
-Info: Stripped invalid locals information from [0-9]+ methods\.
-Info: Methods with invalid locals information:
-java\.lang\.Object androidx\.glance\.appwidget\.GlanceAppWidget\.compose\$glance_appwidget_release\(android\.content\.Context\, android\.appwidget\.AppWidgetManager\, int\, java\.lang\.Object\, android\.os\.Bundle\, kotlin\.coroutines\.Continuation\)
-java\.lang\.Object androidx\.glance\.state\.GlanceState\.getDataStore\(android\.content\.Context, androidx\.glance\.state\.GlanceStateDefinition, java\.lang\.String, kotlin\.coroutines\.Continuation\)
-java\.lang\.Object androidx\.wear\.complications\.ComplicationDataSourceInfoRetriever\.retrievePreviewComplicationData\(android\.content\.ComponentName, androidx\.wear\.complications\.data\.ComplicationType, kotlin\.coroutines\.Continuation\)
-java\.lang\.Object androidx\.wear\.watchface\.editor\.BaseEditorSession\.openComplicationDataSourceChooser\$suspendImpl\(androidx\.wear\.watchface\.editor\.BaseEditorSession, int, kotlin\.coroutines\.Continuation\)
-java\.lang\.Object androidx\.compose\.ui\.platform\.GlobalSnapshotManager\$ensureStarted\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
-java\.lang\.Object androidx\.wear\.watchface\.editor\.BaseEditorSession\$fetchComplicationsData\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
-java\.lang\.Object androidx\.camera\.camera[0-9]+\.pipe\.CameraDevicesKt\$find\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
-java\.lang\.Object androidx\.camera\.camera[0-9]+\.pipe\.compat\.VirtualCameraManager\$requestLoop\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
-Information in locals\-table is invalid with respect to the stack map table\. Local refers to non\-present stack map type for register: [0-9]+ with constraint OBJECT\.
-java\.lang\.Object androidx\.camera\.camera[0-9]+\.pipe\.compat\.VirtualCameraManager\.openCameraWithRetry\-RzXb[0-9]+QE\(java\.lang\.String, kotlinx\.coroutines\.CoroutineScope, kotlin\.coroutines\.Continuation\)
-Information in locals\-table is invalid with respect to the stack map table\. Local refers to non\-present stack map type for register: [0-9]+ with constraint INT\.
-Info: Some warnings are typically a sign of using an outdated Java toolchain\. To fix, recompile the source with an updated toolchain\.
-# > Task :paging:paging-samples:reportLibraryMetrics
-java\.lang\.Object androidx\.paging\.samples\.RemoteMediatorSampleKt\$remoteMediatorItemKeyedSample\$ExampleRemoteMediator\.load\(androidx\.paging\.LoadType, androidx\.paging\.PagingState, kotlin\.coroutines\.Continuation\)
-java\.lang\.Object androidx\.paging\.samples\.RemoteMediatorSampleKt\$remoteMediatorPageKeyedSample\$ExampleRemoteMediator\.load\(androidx\.paging\.LoadType, androidx\.paging\.PagingState, kotlin\.coroutines\.Continuation\)
-# > Task :wear:wear-complications-data:reportLibraryMetrics
-Info: Stripped invalid locals information from [0-9]+ method\.
-# > Task :wear:wear-watchface-editor:reportLibraryMetrics
-java\.lang\.Object androidx\.wear\.watchface\.editor\.BaseEditorSession\.getPreviewData\$wear_watchface_editor_release\(androidx\.wear\.complications\.ComplicationDataSourceInfoRetriever, androidx\.wear\.complications\.ComplicationDataSourceInfo, kotlin\.coroutines\.Continuation\)
-# > Task :work:work-runtime-ktx:reportLibraryMetrics
-java\.lang\.Object androidx\.work\.OperationKt\.await\(androidx\.work\.Operation, kotlin\.coroutines\.Continuation\)
-# > Task :compose:material:material:reportLibraryMetrics
-java\.lang\.Object androidx\.compose\.material\.SnackbarHostState\.showSnackbar\(java\.lang\.String, java\.lang\.String, androidx\.compose\.material\.SnackbarDuration, kotlin\.coroutines\.Continuation\)
# > Task :compose:foundation:foundation:androidReleaseSourcesJar
Encountered duplicate path "android[a-zA-Z]*/.+" during copy operation configured with DuplicatesStrategy\.WARN
-# > Task :camera:camera-camera2-pipe:reportLibraryMetrics
-java\.lang\.Object androidx\.camera\.camera[0-9]+\.pipe\.compat\.Camera[0-9]+DeviceCache\$getCameras\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
-Type information in locals\-table is inconsistent\. Cannot constrain type: @Nullable androidx\.camera\.camera[0-9]+\.pipe\.core\.Debug \{\} for value: v[0-9]+\(this_\$iv\$iv\) by constraint INT\.
-# > Task :compose:foundation:foundation:integration-tests:foundation-demos:reportLibraryMetrics
-java\.lang\.Object androidx\.compose\.foundation\.demos\.ListDemosKt\$ListHoistedStateDemo\$[0-9]+\$[0-9]+\$[0-9]+\$[0-9]+\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
# > Task :profileinstaller:profileinstaller-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/profileinstaller/profileinstaller\-benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Task :wear:compose:compose-material-benchmark:processReleaseAndroidTestManifest
@@ -675,3 +605,9 @@
C/C\+\+: (debug|release)\|(x86_64|x86|arm64\-v8a|armeabi\-v7a) :Packaging for\: (amd\-[0-9]+|armhf\-[0-9]+|x86\-[0-9]+)
C/C\+\+: (debug|release)\|(x86_64|x86|arm64\-v8a|armeabi\-v7a) : CMakeLists\.txt:[0-9]+ \(PROJECT\)
C/C\+\+: (debug|release)\|(x86_64|x86|arm64\-v8a|armeabi\-v7a) :Compiling for ARM
+# > Task :glance:glance:reportLibraryMetrics
+Info: Stripped invalid locals information from [0-9]+ method\.
+Info: Methods with invalid locals information:
+java\.lang\.Object androidx\.glance\.state\.GlanceState\.getDataStore\(android\.content\.Context, androidx\.glance\.state\.GlanceStateDefinition, java\.lang\.String, kotlin\.coroutines\.Continuation\)
+Information in locals\-table is invalid with respect to the stack map table\. Local refers to non\-present stack map type for register: [0-9]+ with constraint OBJECT\.
+Info: Some warnings are typically a sign of using an outdated Java toolchain\. To fix, recompile the source with an updated toolchain\.
\ No newline at end of file
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 6b5e28f..162e164 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -31,13 +31,13 @@
docs("androidx.biometric:biometric-ktx:1.2.0-alpha04")
samples("androidx.biometric:biometric-ktx-samples:1.2.0-alpha04")
docs("androidx.browser:browser:1.4.0")
- docs("androidx.camera:camera-camera2:1.1.0-alpha10")
- docs("androidx.camera:camera-core:1.1.0-alpha10")
- docs("androidx.camera:camera-extensions:1.0.0-alpha30")
+ docs("androidx.camera:camera-camera2:1.1.0-alpha11")
+ docs("androidx.camera:camera-core:1.1.0-alpha11")
+ docs("androidx.camera:camera-extensions:1.0.0-alpha31")
stubs(fileTree(dir: "../camera/camera-extensions-stub", include: ["camera-extensions-stub.jar"]))
- docs("androidx.camera:camera-lifecycle:1.1.0-alpha10")
- docs("androidx.camera:camera-video:1.1.0-alpha10")
- docs("androidx.camera:camera-view:1.0.0-alpha30")
+ docs("androidx.camera:camera-lifecycle:1.1.0-alpha11")
+ docs("androidx.camera:camera-video:1.1.0-alpha11")
+ docs("androidx.camera:camera-view:1.0.0-alpha31")
docs("androidx.car.app:app:1.2.0-alpha01")
docs("androidx.car.app:app-automotive:1.2.0-alpha01")
docs("androidx.car.app:app-projected:1.2.0-alpha01")
@@ -92,7 +92,7 @@
docs("androidx.concurrent:concurrent-futures:1.1.0")
docs("androidx.concurrent:concurrent-futures-ktx:1.1.0")
docs("androidx.contentpager:contentpager:1.0.0")
- docs("androidx.coordinatorlayout:coordinatorlayout:1.2.0-alpha01")
+ docs("androidx.coordinatorlayout:coordinatorlayout:1.2.0-beta01")
docs("androidx.core:core-google-shortcuts:1.1.0-alpha02")
docs("androidx.core:core-role:1.1.0-alpha02")
docs("androidx.core:core-animation:1.0.0-alpha02")
@@ -227,7 +227,7 @@
docs("androidx.slice:slice-builders-ktx:1.0.0-alpha08")
docs("androidx.slice:slice-core:1.1.0-alpha02")
docs("androidx.slice:slice-view:1.1.0-alpha02")
- docs("androidx.slidingpanelayout:slidingpanelayout:1.2.0-beta01")
+ docs("androidx.slidingpanelayout:slidingpanelayout:1.2.0-rc01")
docs("androidx.sqlite:sqlite:2.2.0-beta02")
docs("androidx.sqlite:sqlite-framework:2.2.0-beta02")
docs("androidx.sqlite:sqlite-ktx:2.2.0-beta02")
@@ -278,13 +278,13 @@
samples("androidx.wear:wear-input-samples:1.2.0-alpha01")
docs("androidx.wear:wear-input-testing:1.2.0-alpha02")
docs("androidx.webkit:webkit:1.4.0")
- docs("androidx.window:window:1.0.0-beta03")
+ docs("androidx.window:window:1.0.0-beta04")
stubs(fileTree(dir: "../window/stubs/", include: ["window-sidecar-release-0.1.0-alpha01.aar"]))
stubs("androidx.window:window-extensions:1.0.0-alpha01")
- docs("androidx.window:window-java:1.0.0-beta03")
- docs("androidx.window:window-rxjava2:1.0.0-beta03")
- docs("androidx.window:window-rxjava3:1.0.0-beta03")
- docs("androidx.window:window-testing:1.0.0-beta03")
+ docs("androidx.window:window-java:1.0.0-beta04")
+ docs("androidx.window:window-rxjava2:1.0.0-beta04")
+ docs("androidx.window:window-rxjava3:1.0.0-beta04")
+ docs("androidx.window:window-testing:1.0.0-beta04")
docs("androidx.work:work-gcm:2.7.1")
docs("androidx.work:work-multiprocess:2.7.1")
docs("androidx.work:work-runtime:2.7.1")
diff --git a/fragment/fragment-ktx/build.gradle b/fragment/fragment-ktx/build.gradle
index f48c186..fdc614d 100644
--- a/fragment/fragment-ktx/build.gradle
+++ b/fragment/fragment-ktx/build.gradle
@@ -39,7 +39,7 @@
because 'Mirror fragment dependency graph for -ktx artifacts'
}
api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1")
- api("androidx.savedstate:savedstate-ktx:1.1.0") {
+ api(projectOrArtifact(":savedstate:savedstate-ktx")) {
because 'Mirror fragment dependency graph for -ktx artifacts'
}
api(libs.kotlinStdlib)
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index eedd7b9..e273c7d 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -33,7 +33,7 @@
api("androidx.lifecycle:lifecycle-livedata-core:2.3.1")
api("androidx.lifecycle:lifecycle-viewmodel:2.3.1")
api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1")
- api("androidx.savedstate:savedstate:1.1.0")
+ api(projectOrArtifact(":savedstate:savedstate"))
api("androidx.annotation:annotation-experimental:1.0.0")
api(libs.kotlinStdlib)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt
index c544c120..bfffb85 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt
@@ -17,16 +17,22 @@
package androidx.fragment.app
import android.os.Bundle
+import androidx.activity.result.ActivityResultRegistry
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.app.ActivityOptionsCompat
+import androidx.fragment.app.test.ViewModelActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Lifecycle.State.CREATED
import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.ViewModelProvider
import androidx.savedstate.SavedStateRegistry
+import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.testutils.RecreatedActivity
-import androidx.testutils.recreate
+import androidx.testutils.withActivity
import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -34,52 +40,68 @@
@LargeTest
class FragmentSavedStateRegistryTest {
- @Suppress("DEPRECATION")
- @get:Rule
- var activityRule = androidx.test.rule.ActivityTestRule(FragmentSavedStateActivity::class.java)
-
- private fun initializeSavedState(testFragment: Fragment = Fragment()) {
- activityRule.runOnUiThread {
- val fragmentManager = activityRule.activity.supportFragmentManager
- fragmentManager.beginTransaction().add(testFragment, FRAGMENT_TAG).commitNow()
- assertThat(fragmentManager.findFragmentByTag(FRAGMENT_TAG)).isNotNull()
- assertThat(testFragment.lifecycle.currentState.isAtLeast(CREATED)).isTrue()
- val registry = testFragment.savedStateRegistry
- val savedState = registry.consumeRestoredStateForKey(CALLBACK_KEY)
- assertThat(savedState).isNull()
- registry.registerSavedStateProvider(CALLBACK_KEY, DefaultProvider())
- }
+ private fun ActivityScenario<FragmentSavedStateActivity>.initializeSavedState(
+ testFragment: Fragment = Fragment()
+ ) = withActivity {
+ val fragmentManager = supportFragmentManager
+ fragmentManager.beginTransaction().add(testFragment, FRAGMENT_TAG).commitNow()
+ assertThat(fragmentManager.findFragmentByTag(FRAGMENT_TAG)).isNotNull()
+ assertThat(testFragment.lifecycle.currentState.isAtLeast(CREATED)).isTrue()
+ val registry = testFragment.savedStateRegistry
+ val savedState = registry.consumeRestoredStateForKey(CALLBACK_KEY)
+ assertThat(savedState).isNull()
+ registry.registerSavedStateProvider(CALLBACK_KEY, DefaultProvider())
}
@Test
fun savedState() {
- initializeSavedState()
- val recreated = activityRule.recreate()
- activityRule.runOnUiThread {
- assertThat(recreated.fragment().lifecycle.currentState.isAtLeast(CREATED)).isTrue()
- checkDefaultSavedState(recreated.fragment().savedStateRegistry)
+ with(ActivityScenario.launch(FragmentSavedStateActivity::class.java)) {
+ initializeSavedState()
+ recreate()
+ withActivity {
+ assertThat(fragment.lifecycle.currentState.isAtLeast(CREATED)).isTrue()
+ checkDefaultSavedState(fragment.savedStateRegistry)
+ }
}
}
@Test
fun savedStateLateInit() {
- initializeSavedState()
- val recreated = activityRule.recreate()
- activityRule.runOnUiThread {
- recreated.fragment().lifecycle.addObserver(
- LifecycleEventObserver { _, event ->
- if (event == Lifecycle.Event.ON_RESUME) {
- checkDefaultSavedState(recreated.fragment().savedStateRegistry)
+ with(ActivityScenario.launch(FragmentSavedStateActivity::class.java)) {
+ initializeSavedState()
+ recreate()
+ withActivity {
+ fragment.lifecycle.addObserver(
+ LifecycleEventObserver { _, event ->
+ if (event == Lifecycle.Event.ON_RESUME) {
+ checkDefaultSavedState(fragment.savedStateRegistry)
+ }
}
- }
- )
+ )
+ }
}
}
@Test
fun savedStateEarlyRegister() {
- initializeSavedState(OnCreateCheckingFragment())
- activityRule.recreate()
+ with(ActivityScenario.launch(FragmentSavedStateActivity::class.java)) {
+ initializeSavedState(OnCreateCheckingFragment())
+ recreate()
+ }
+ }
+
+ @Test
+ fun savedStateOnActivityResult() {
+ with(ActivityScenario.launch(FragmentSavedStateActivity::class.java)) {
+ val registry = withActivity { registry }
+ initializeSavedState(OnActivityResultCheckingFragment(registry))
+ recreate()
+ moveToState(Lifecycle.State.CREATED)
+ withActivity {
+ (fragment as OnActivityResultCheckingFragment).launcher.launch("")
+ }
+ moveToState(Lifecycle.State.RESUMED)
+ }
}
}
@@ -90,7 +112,32 @@
}
class FragmentSavedStateActivity : RecreatedActivity() {
- fun fragment() = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG)
+ val registry = object : ActivityResultRegistry() {
+ override fun <I : Any?, O : Any?> onLaunch(
+ requestCode: Int,
+ contract: ActivityResultContract<I, O>,
+ input: I,
+ options: ActivityOptionsCompat?
+ ) {
+ if (contract is ActivityResultContracts.GetContent) {
+ dispatchResult(requestCode, null)
+ }
+ }
+ }
+
+ init {
+ supportFragmentManager.fragmentFactory = object : FragmentFactory() {
+ override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
+ return when (loadFragmentClass(classLoader, className)) {
+ OnActivityResultCheckingFragment::class.java ->
+ OnActivityResultCheckingFragment(registry)
+ else -> super.instantiate(classLoader, className)
+ }
+ }
+ }
+ }
+
+ val fragment: Fragment get() = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG)
?: throw IllegalStateException("Fragment under test wasn't found")
}
@@ -103,6 +150,23 @@
}
}
+class OnActivityResultCheckingFragment(
+ activityResultRegistry: ActivityResultRegistry
+) : Fragment() {
+ val viewModel by lazy {
+ ViewModelProvider(this).get(ViewModelActivity.TestSavedStateViewModel::class.java)
+ }
+
+ val launcher = registerForActivityResult(
+ ActivityResultContracts.GetContent(),
+ activityResultRegistry
+ ) {
+ // Accessing the ViewModel is what triggers the startup behavior of
+ // SavedStateRegistryController
+ viewModel
+ }
+}
+
private class DefaultProvider : SavedStateRegistry.SavedStateProvider {
override fun saveState() = Bundle().apply { putString(KEY, VALUE) }
}
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
index 8dc962e..8fcec63 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -572,6 +572,12 @@
// The default factory depends on the SavedStateRegistry so it
// needs to be reset when the SavedStateRegistry is reset
mDefaultFactory = null;
+ registerOnPreAttachListener(new OnPreAttachedListener() {
+ @Override
+ void onPreAttached() {
+ mSavedStateRegistryController.performAttach();
+ }
+ });
}
/**
diff --git a/glance/glance-appwidget/api/current.txt b/glance/glance-appwidget/api/current.txt
index b3a2824..2efcafe 100644
--- a/glance/glance-appwidget/api/current.txt
+++ b/glance/glance-appwidget/api/current.txt
@@ -35,9 +35,7 @@
public final class CompositionLocalsKt {
method public static androidx.compose.runtime.ProvidableCompositionLocal<android.os.Bundle> getLocalAppWidgetOptions();
- method public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.String> getLocalUiKey();
property public static final androidx.compose.runtime.ProvidableCompositionLocal<android.os.Bundle> LocalAppWidgetOptions;
- property public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.String> LocalUiKey;
}
public final class CornerRadiusKt {
@@ -142,7 +140,7 @@
}
public final class LaunchActivityIntentActionKt {
- method public static androidx.glance.action.Action actionLaunchActivity(android.content.Intent intent);
+ method public static androidx.glance.action.Action actionLaunchActivity(android.content.Intent intent, optional androidx.glance.action.ActionParameters parameters);
}
public final class RunCallbackActionKt {
@@ -181,6 +179,15 @@
}
+package androidx.glance.appwidget.state {
+
+ public final class GlanceAppWidgetStateKt {
+ method public static suspend <T> Object? getAppWidgetState(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super T> p);
+ method public static suspend <T> Object? updateAppWidgetState(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, androidx.glance.GlanceId glanceId, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super T>,?> updateState, kotlin.coroutines.Continuation<? super T> p);
+ }
+
+}
+
package androidx.glance.appwidget.translators {
public final class CheckBoxTranslatorKt {
diff --git a/glance/glance-appwidget/api/public_plus_experimental_current.txt b/glance/glance-appwidget/api/public_plus_experimental_current.txt
index b3a2824..2efcafe 100644
--- a/glance/glance-appwidget/api/public_plus_experimental_current.txt
+++ b/glance/glance-appwidget/api/public_plus_experimental_current.txt
@@ -35,9 +35,7 @@
public final class CompositionLocalsKt {
method public static androidx.compose.runtime.ProvidableCompositionLocal<android.os.Bundle> getLocalAppWidgetOptions();
- method public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.String> getLocalUiKey();
property public static final androidx.compose.runtime.ProvidableCompositionLocal<android.os.Bundle> LocalAppWidgetOptions;
- property public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.String> LocalUiKey;
}
public final class CornerRadiusKt {
@@ -142,7 +140,7 @@
}
public final class LaunchActivityIntentActionKt {
- method public static androidx.glance.action.Action actionLaunchActivity(android.content.Intent intent);
+ method public static androidx.glance.action.Action actionLaunchActivity(android.content.Intent intent, optional androidx.glance.action.ActionParameters parameters);
}
public final class RunCallbackActionKt {
@@ -181,6 +179,15 @@
}
+package androidx.glance.appwidget.state {
+
+ public final class GlanceAppWidgetStateKt {
+ method public static suspend <T> Object? getAppWidgetState(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super T> p);
+ method public static suspend <T> Object? updateAppWidgetState(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, androidx.glance.GlanceId glanceId, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super T>,?> updateState, kotlin.coroutines.Continuation<? super T> p);
+ }
+
+}
+
package androidx.glance.appwidget.translators {
public final class CheckBoxTranslatorKt {
diff --git a/glance/glance-appwidget/api/restricted_current.txt b/glance/glance-appwidget/api/restricted_current.txt
index b3a2824..2efcafe 100644
--- a/glance/glance-appwidget/api/restricted_current.txt
+++ b/glance/glance-appwidget/api/restricted_current.txt
@@ -35,9 +35,7 @@
public final class CompositionLocalsKt {
method public static androidx.compose.runtime.ProvidableCompositionLocal<android.os.Bundle> getLocalAppWidgetOptions();
- method public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.String> getLocalUiKey();
property public static final androidx.compose.runtime.ProvidableCompositionLocal<android.os.Bundle> LocalAppWidgetOptions;
- property public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.String> LocalUiKey;
}
public final class CornerRadiusKt {
@@ -142,7 +140,7 @@
}
public final class LaunchActivityIntentActionKt {
- method public static androidx.glance.action.Action actionLaunchActivity(android.content.Intent intent);
+ method public static androidx.glance.action.Action actionLaunchActivity(android.content.Intent intent, optional androidx.glance.action.ActionParameters parameters);
}
public final class RunCallbackActionKt {
@@ -181,6 +179,15 @@
}
+package androidx.glance.appwidget.state {
+
+ public final class GlanceAppWidgetStateKt {
+ method public static suspend <T> Object? getAppWidgetState(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super T> p);
+ method public static suspend <T> Object? updateAppWidgetState(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, androidx.glance.GlanceId glanceId, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super T>,?> updateState, kotlin.coroutines.Continuation<? super T> p);
+ }
+
+}
+
package androidx.glance.appwidget.translators {
public final class CheckBoxTranslatorKt {
diff --git a/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/LayoutGenerator.kt b/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/LayoutGenerator.kt
index 6a82308..74ce99f 100644
--- a/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/LayoutGenerator.kt
+++ b/glance/glance-appwidget/glance-layout-generator/src/main/kotlin/androidx/glance/appwidget/layoutgenerator/LayoutGenerator.kt
@@ -323,6 +323,7 @@
setNamedItemNS(androidId("@id/rootView"))
setNamedItemNS(androidWidth(width))
setNamedItemNS(androidHeight(height))
+ setNamedItemNS(androidAttr("theme", "@style/Glance.AppWidget.Theme"))
}
val stub = createElement("ViewStub")
root.appendChild(stub)
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt
index fd7fcd2..c661d49 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt
@@ -23,36 +23,43 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.glance.Button
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.LocalContext
-import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.action.ActionParameters
import androidx.glance.action.actionLaunchActivity
import androidx.glance.action.actionParametersOf
-import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.action.clickable
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.actionLaunchActivity
+import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.appWidgetBackground
import androidx.glance.appwidget.cornerRadius
+import androidx.glance.appwidget.state.updateAppWidgetState
+import androidx.glance.currentState
import androidx.glance.layout.Alignment
import androidx.glance.layout.Column
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.layout.size
+import androidx.glance.state.PreferencesGlanceStateDefinition
+import androidx.glance.action.toParametersKey
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextDecoration
import androidx.glance.text.TextStyle
-import java.util.concurrent.atomic.AtomicBoolean
class ActionAppWidget : GlanceAppWidget() {
+ override val stateDefinition = PreferencesGlanceStateDefinition
+
@Composable
override fun Content() {
Column(
@@ -61,11 +68,12 @@
verticalAlignment = Alignment.Vertical.CenterVertically,
horizontalAlignment = Alignment.Horizontal.CenterHorizontally
) {
+ val shouldChangeView = currentState<Preferences>()[shouldChangeViewKey] ?: false
Text(
text = "Tap to change me",
style = TextStyle(
fontSize = 16.sp, fontWeight = FontWeight.Bold,
- textDecoration = if (shouldChangeView.get()) {
+ textDecoration = if (shouldChangeView) {
TextDecoration.Underline
} else {
TextDecoration.None
@@ -76,12 +84,12 @@
.clickable(
actionRunCallback<UpdateAction>(
actionParametersOf(
- KEY_USE_UNDERLINE to shouldChangeView.get()
+ shouldChangeViewKey.toParametersKey() to shouldChangeView
)
)
)
)
- if (shouldChangeView.get()) {
+ if (shouldChangeView) {
Image(
ImageProvider(R.drawable.compose),
"Compose",
@@ -108,15 +116,19 @@
}
}
-private val KEY_USE_UNDERLINE = ActionParameters.Key<Boolean>("underline")
-private var shouldChangeView = AtomicBoolean(false)
+private val shouldChangeViewKey = booleanPreferencesKey("shouldChangeView")
/**
- * Action to update the [shouldChangeView] value whenever users clicks on text
+ * Action to update the [shouldChangeViewKey] value whenever users clicks on text
*/
class UpdateAction : ActionCallback {
override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
- shouldChangeView.set(!parameters.getOrDefault(KEY_USE_UNDERLINE, false))
+ updateAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId) { prefs ->
+ prefs.toMutablePreferences().apply {
+ this[shouldChangeViewKey] =
+ !(parameters[shouldChangeViewKey.toParametersKey()] ?: false)
+ }
+ }
ActionAppWidget().update(context, glanceId)
}
}
@@ -128,4 +140,4 @@
class ActionAppWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = ActionAppWidget()
-}
\ No newline at end of file
+}
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt
index fdb499c..2b88886 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt
@@ -21,30 +21,25 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.datastore.core.DataStore
-import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
-import androidx.datastore.preferences.preferencesDataStoreFile
import androidx.glance.Button
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.LocalSize
import androidx.glance.action.ActionParameters
-import androidx.glance.appwidget.action.ActionCallback
-import androidx.glance.appwidget.action.actionRunCallback
-import androidx.glance.action.actionParametersOf
import androidx.glance.appwidget.CheckBox
import androidx.glance.appwidget.CheckBoxColors
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
-import androidx.glance.appwidget.LocalUiKey
import androidx.glance.appwidget.SizeMode
import androidx.glance.appwidget.Switch
import androidx.glance.appwidget.SwitchColors
+import androidx.glance.appwidget.action.ActionCallback
+import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.appWidgetBackground
import androidx.glance.appwidget.cornerRadius
+import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.glance.appwidget.unit.ColorProvider
import androidx.glance.background
import androidx.glance.currentState
@@ -54,18 +49,17 @@
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.padding
-import androidx.glance.state.GlanceState
import androidx.glance.state.GlanceStateDefinition
+import androidx.glance.state.PreferencesGlanceStateDefinition
import androidx.glance.text.FontStyle
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
-import java.io.File
class CompoundButtonAppWidget : GlanceAppWidget() {
override val sizeMode: SizeMode = SizeMode.Exact
- override var stateDefinition: GlanceStateDefinition<*> = MyStateDefinition
+ override var stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition
@Composable
override fun Content() {
@@ -126,13 +120,11 @@
@Composable
fun CountClicks() {
val prefs = currentState<Preferences>()
- val count = prefs[countClicksKey]
-
- val parameters = actionParametersOf(uiNameKey to LocalUiKey.current)
+ val count = prefs[countClicksKey] ?: 0
Row(modifier = GlanceModifier.fillMaxWidth()) {
Button(
text = "Count clicks",
- onClick = actionRunCallback<ClickAction>(parameters)
+ onClick = actionRunCallback<ClickAction>()
)
Text(text = "$count clicks")
}
@@ -140,36 +132,16 @@
class ClickAction : ActionCallback {
override suspend fun onRun(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
- val uiKey = requireNotNull(parameters[uiNameKey]) {
- "Add UI name to parameters, to access the view state."
- }
- GlanceState.updateValue(context, MyStateDefinition, uiKey) { prefs ->
+ updateAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId) { prefs ->
prefs.toMutablePreferences().apply {
- this[countClicksKey] = prefs[countClicksKey]!! + 1
- }.toPreferences()
+ this[countClicksKey] = (prefs[countClicksKey] ?: 0) + 1
+ }
}
CompoundButtonAppWidget().update(context, glanceId)
}
}
-object MyStateDefinition : GlanceStateDefinition<Preferences> {
- override fun getLocation(context: Context, fileKey: String): File =
- context.preferencesDataStoreFile(fileKey)
-
- @Suppress("UNCHECKED_CAST")
- override suspend fun <T> getDataStore(context: Context, fileKey: String): DataStore<T> =
- PreferenceDataStoreFactory.create { context.preferencesDataStoreFile(fileKey) }
- .apply {
- edit { prefs ->
- if (prefs[countClicksKey] == null) {
- prefs[countClicksKey] = 0
- }
- }
- } as DataStore<T>
-}
-
private val countClicksKey = intPreferencesKey("CountClicks")
-private val uiNameKey = ActionParameters.Key<String>("UiKey")
class CompoundButtonAppWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget = CompoundButtonAppWidget()
diff --git a/glance/glance-appwidget/src/androidAndroidTest/res/values-v29/themes.xml b/glance/glance-appwidget/src/androidAndroidTest/res/values-v29/themes.xml
new file mode 100644
index 0000000..1a85e18
--- /dev/null
+++ b/glance/glance-appwidget/src/androidAndroidTest/res/values-v29/themes.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <style name="Glance.AppWidget.Theme" parent=""/>
+</resources>
diff --git a/glance/glance-appwidget/src/androidAndroidTest/res/values/themes.xml b/glance/glance-appwidget/src/androidAndroidTest/res/values/themes.xml
index efd9192..2f50199 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/res/values/themes.xml
+++ b/glance/glance-appwidget/src/androidAndroidTest/res/values/themes.xml
@@ -18,4 +18,5 @@
<style name="Theme.TestDimensionAttr" parent="">
<item name="dimensionAttr">@dimen/testDimension</item>
</style>
+ <style name="Glance.AppWidget.Theme" parent=""/>
</resources>
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt
index cbb494c..ff7c9e6 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt
@@ -32,6 +32,7 @@
import androidx.annotation.IdRes
import androidx.annotation.RequiresApi
import androidx.compose.ui.graphics.toArgb
+import androidx.core.os.bundleOf
import androidx.core.widget.RemoteViewsCompat.setTextViewHeight
import androidx.core.widget.RemoteViewsCompat.setTextViewWidth
import androidx.core.widget.RemoteViewsCompat.setViewBackgroundColor
@@ -127,6 +128,10 @@
is LaunchActivityIntentAction -> action.intent
else -> error("Action type not defined in app widget package: $action")
}
+ val parametersPairs = action.parameters.asMap().map { (key, value) ->
+ key.name to value
+ }.toTypedArray()
+ activityIntent.putExtras(bundleOf(*parametersPairs))
if (translationContext.isLazyCollectionDescendant) {
val fillIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/CompositionLocals.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/CompositionLocals.kt
index 863af1c..d812e74 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/CompositionLocals.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/CompositionLocals.kt
@@ -19,7 +19,6 @@
import android.os.Bundle
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.glance.state.GlanceStateDefinition
/**
* Option Bundle accessible when generating an App Widget.
@@ -28,10 +27,3 @@
*/
public val LocalAppWidgetOptions: ProvidableCompositionLocal<Bundle> =
staticCompositionLocalOf { error("No default app widget options") }
-
-/**
- * Uniquely identifying key for this remote UI, for use with [GlanceStateDefinition].
- */
-// TODO: Look into refactoring the state to not need this key constant
-public val LocalUiKey =
- staticCompositionLocalOf<String> { error("No default UI key") }
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
index d49d35f..56e7a11 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -348,13 +348,11 @@
val recomposer = Recomposer(coroutineContext)
val composition = Composition(applier, recomposer)
val glanceId = AppWidgetId(appWidgetId)
- val uiKey = createUniqueRemoteUiName(appWidgetId)
composition.setContent {
CompositionLocalProvider(
LocalContext provides context,
LocalGlanceId provides glanceId,
LocalAppWidgetOptions provides options,
- LocalUiKey provides uiKey,
LocalState provides state,
LocalSize provides size,
) { Content() }
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt
index e3a38e6..71785ab 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt
@@ -280,11 +280,13 @@
override fun getLocation(context: Context, fileKey: String): File =
context.dataStoreFile(fileKey)
- @Suppress("UNCHECKED_CAST")
- override suspend fun <T> getDataStore(context: Context, fileKey: String): DataStore<T> =
+ override suspend fun getDataStore(
+ context: Context,
+ fileKey: String,
+ ): DataStore<LayoutProto.LayoutConfig> =
DataStoreFactory.create(serializer = LayoutProtoSerializer) {
context.dataStoreFile(fileKey)
- } as DataStore<T>
+ }
}
private fun Alignment.Vertical.toProto() = when (this) {
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentAction.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentAction.kt
index 5ce93e9..7e7f48a 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentAction.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentAction.kt
@@ -19,9 +19,14 @@
import android.app.Activity
import android.content.Intent
import androidx.glance.action.Action
+import androidx.glance.action.ActionParameters
import androidx.glance.action.LaunchActivityAction
+import androidx.glance.action.actionParametersOf
-internal class LaunchActivityIntentAction(val intent: Intent) : LaunchActivityAction
+internal class LaunchActivityIntentAction(
+ val intent: Intent,
+ override val parameters: ActionParameters = actionParametersOf()
+) : LaunchActivityAction
/**
* Creates an [Action] that launches an [Activity] from the given [Intent] when triggered. The
@@ -30,5 +35,10 @@
* This action is supported by app widgets only.
*
* @param intent the intent used to launch the activity
+ * @param parameters the parameters associated with the action. Parameter values will be added to
+ * the activity intent, keyed by the parameter key name string.
*/
-public fun actionLaunchActivity(intent: Intent): Action = LaunchActivityIntentAction(intent)
+public fun actionLaunchActivity(
+ intent: Intent,
+ parameters: ActionParameters = actionParametersOf()
+): Action = LaunchActivityIntentAction(intent, parameters)
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/state/GlanceAppWidgetState.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/state/GlanceAppWidgetState.kt
new file mode 100644
index 0000000..fc8ed71
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/state/GlanceAppWidgetState.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.glance.appwidget.state
+
+import android.content.Context
+import androidx.glance.GlanceId
+import androidx.glance.appwidget.AppWidgetId
+import androidx.glance.appwidget.createUniqueRemoteUiName
+import androidx.glance.state.GlanceState
+import androidx.glance.state.GlanceStateDefinition
+
+/**
+ * Retrieve the state of an app widget.
+ *
+ * The state definition must be the one used for that particular app widget.
+ */
+public suspend fun <T> getAppWidgetState(
+ context: Context,
+ definition: GlanceStateDefinition<T>,
+ glanceId: GlanceId,
+): T {
+ require(glanceId is AppWidgetId) { "The glance ID is not the one of an App Widget" }
+ return GlanceState.getValue(context, definition, createUniqueRemoteUiName(glanceId.appWidgetId))
+}
+
+/**
+ * Update the state of an app widget.
+ *
+ * The state definition must be the one used for that particular app widget.
+ */
+public suspend fun <T> updateAppWidgetState(
+ context: Context,
+ definition: GlanceStateDefinition<T>,
+ glanceId: GlanceId,
+ updateState: suspend (T) -> T,
+): T {
+ require(glanceId is AppWidgetId) { "The glance ID is not the one of an App Widget" }
+ return GlanceState.updateValue(
+ context,
+ definition,
+ createUniqueRemoteUiName(glanceId.appWidgetId),
+ updateState,
+ )
+}
diff --git a/glance/glance-appwidget/src/main/res/values-v29/themes.xml b/glance/glance-appwidget/src/main/res/values-v29/themes.xml
new file mode 100644
index 0000000..4cce739
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/values-v29/themes.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <style name="Glance.AppWidget.Theme" parent="android:Theme.DeviceDefault.DayNight" />
+</resources>
diff --git a/glance/glance-appwidget/src/main/res/values/themes.xml b/glance/glance-appwidget/src/main/res/values/themes.xml
new file mode 100644
index 0000000..efd5acd
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/values/themes.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <style name="Glance.AppWidget.Theme" parent="android:Theme.DeviceDefault"/>
+</resources>
diff --git a/glance/glance-wear/api/current.txt b/glance/glance-wear/api/current.txt
index d1bb2da..66d8ba9 100644
--- a/glance/glance-wear/api/current.txt
+++ b/glance/glance-wear/api/current.txt
@@ -10,17 +10,48 @@
method public static androidx.glance.GlanceModifier border(androidx.glance.GlanceModifier, @DimenRes int width, androidx.glance.unit.ColorProvider color);
}
+ public final class CompositionLocalsKt {
+ method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.glance.wear.TimeInterval> getLocalTimeInterval();
+ property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.glance.wear.TimeInterval> LocalTimeInterval;
+ }
+
public abstract class GlanceTileService extends androidx.wear.tiles.TileService {
ctor public GlanceTileService();
method @androidx.compose.runtime.Composable public abstract void Content();
+ method public androidx.glance.wear.TimelineMode getTimelineMode();
method protected final com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources> onResourcesRequest(androidx.wear.tiles.RequestBuilders.ResourcesRequest requestParams);
method public void onStart(android.content.Intent? intent, int startId);
method protected final com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.TileBuilders.Tile> onTileRequest(androidx.wear.tiles.RequestBuilders.TileRequest requestParams);
+ property public androidx.glance.wear.TimelineMode timelineMode;
}
public final class NormalizeCompositionTreeKt {
}
+ public final class TimeInterval {
+ ctor public TimeInterval(optional java.time.Instant start, optional java.time.Instant end);
+ method public java.time.Instant component1();
+ method public java.time.Instant component2();
+ method public androidx.glance.wear.TimeInterval copy(java.time.Instant start, java.time.Instant end);
+ method public java.time.Instant getEnd();
+ method public java.time.Instant getStart();
+ property public final java.time.Instant end;
+ property public final java.time.Instant start;
+ }
+
+ public sealed interface TimelineMode {
+ }
+
+ public static final class TimelineMode.SingleEntry implements androidx.glance.wear.TimelineMode {
+ field public static final androidx.glance.wear.TimelineMode.SingleEntry INSTANCE;
+ }
+
+ public static final class TimelineMode.TimeBoundEntries implements androidx.glance.wear.TimelineMode {
+ ctor public TimelineMode.TimeBoundEntries(java.util.Set<androidx.glance.wear.TimeInterval> timeIntervals);
+ method public java.util.Set<androidx.glance.wear.TimeInterval> getTimeIntervals();
+ property public final java.util.Set<androidx.glance.wear.TimeInterval> timeIntervals;
+ }
+
public final class WearCompositionTranslatorKt {
}
diff --git a/glance/glance-wear/api/public_plus_experimental_current.txt b/glance/glance-wear/api/public_plus_experimental_current.txt
index d1bb2da..66d8ba9 100644
--- a/glance/glance-wear/api/public_plus_experimental_current.txt
+++ b/glance/glance-wear/api/public_plus_experimental_current.txt
@@ -10,17 +10,48 @@
method public static androidx.glance.GlanceModifier border(androidx.glance.GlanceModifier, @DimenRes int width, androidx.glance.unit.ColorProvider color);
}
+ public final class CompositionLocalsKt {
+ method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.glance.wear.TimeInterval> getLocalTimeInterval();
+ property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.glance.wear.TimeInterval> LocalTimeInterval;
+ }
+
public abstract class GlanceTileService extends androidx.wear.tiles.TileService {
ctor public GlanceTileService();
method @androidx.compose.runtime.Composable public abstract void Content();
+ method public androidx.glance.wear.TimelineMode getTimelineMode();
method protected final com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources> onResourcesRequest(androidx.wear.tiles.RequestBuilders.ResourcesRequest requestParams);
method public void onStart(android.content.Intent? intent, int startId);
method protected final com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.TileBuilders.Tile> onTileRequest(androidx.wear.tiles.RequestBuilders.TileRequest requestParams);
+ property public androidx.glance.wear.TimelineMode timelineMode;
}
public final class NormalizeCompositionTreeKt {
}
+ public final class TimeInterval {
+ ctor public TimeInterval(optional java.time.Instant start, optional java.time.Instant end);
+ method public java.time.Instant component1();
+ method public java.time.Instant component2();
+ method public androidx.glance.wear.TimeInterval copy(java.time.Instant start, java.time.Instant end);
+ method public java.time.Instant getEnd();
+ method public java.time.Instant getStart();
+ property public final java.time.Instant end;
+ property public final java.time.Instant start;
+ }
+
+ public sealed interface TimelineMode {
+ }
+
+ public static final class TimelineMode.SingleEntry implements androidx.glance.wear.TimelineMode {
+ field public static final androidx.glance.wear.TimelineMode.SingleEntry INSTANCE;
+ }
+
+ public static final class TimelineMode.TimeBoundEntries implements androidx.glance.wear.TimelineMode {
+ ctor public TimelineMode.TimeBoundEntries(java.util.Set<androidx.glance.wear.TimeInterval> timeIntervals);
+ method public java.util.Set<androidx.glance.wear.TimeInterval> getTimeIntervals();
+ property public final java.util.Set<androidx.glance.wear.TimeInterval> timeIntervals;
+ }
+
public final class WearCompositionTranslatorKt {
}
diff --git a/glance/glance-wear/api/restricted_current.txt b/glance/glance-wear/api/restricted_current.txt
index d1bb2da..66d8ba9 100644
--- a/glance/glance-wear/api/restricted_current.txt
+++ b/glance/glance-wear/api/restricted_current.txt
@@ -10,17 +10,48 @@
method public static androidx.glance.GlanceModifier border(androidx.glance.GlanceModifier, @DimenRes int width, androidx.glance.unit.ColorProvider color);
}
+ public final class CompositionLocalsKt {
+ method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.glance.wear.TimeInterval> getLocalTimeInterval();
+ property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.glance.wear.TimeInterval> LocalTimeInterval;
+ }
+
public abstract class GlanceTileService extends androidx.wear.tiles.TileService {
ctor public GlanceTileService();
method @androidx.compose.runtime.Composable public abstract void Content();
+ method public androidx.glance.wear.TimelineMode getTimelineMode();
method protected final com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources> onResourcesRequest(androidx.wear.tiles.RequestBuilders.ResourcesRequest requestParams);
method public void onStart(android.content.Intent? intent, int startId);
method protected final com.google.common.util.concurrent.ListenableFuture<androidx.wear.tiles.TileBuilders.Tile> onTileRequest(androidx.wear.tiles.RequestBuilders.TileRequest requestParams);
+ property public androidx.glance.wear.TimelineMode timelineMode;
}
public final class NormalizeCompositionTreeKt {
}
+ public final class TimeInterval {
+ ctor public TimeInterval(optional java.time.Instant start, optional java.time.Instant end);
+ method public java.time.Instant component1();
+ method public java.time.Instant component2();
+ method public androidx.glance.wear.TimeInterval copy(java.time.Instant start, java.time.Instant end);
+ method public java.time.Instant getEnd();
+ method public java.time.Instant getStart();
+ property public final java.time.Instant end;
+ property public final java.time.Instant start;
+ }
+
+ public sealed interface TimelineMode {
+ }
+
+ public static final class TimelineMode.SingleEntry implements androidx.glance.wear.TimelineMode {
+ field public static final androidx.glance.wear.TimelineMode.SingleEntry INSTANCE;
+ }
+
+ public static final class TimelineMode.TimeBoundEntries implements androidx.glance.wear.TimelineMode {
+ ctor public TimelineMode.TimeBoundEntries(java.util.Set<androidx.glance.wear.TimeInterval> timeIntervals);
+ method public java.util.Set<androidx.glance.wear.TimeInterval> getTimeIntervals();
+ property public final java.util.Set<androidx.glance.wear.TimeInterval> timeIntervals;
+ }
+
public final class WearCompositionTranslatorKt {
}
diff --git a/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/CompositionLocals.kt b/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/CompositionLocals.kt
new file mode 100644
index 0000000..99ca2cb
--- /dev/null
+++ b/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/CompositionLocals.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.glance.wear
+
+import androidx.compose.runtime.staticCompositionLocalOf
+
+/**
+ * Time interval of the glance tile UI being generated.
+ */
+public val LocalTimeInterval =
+ staticCompositionLocalOf<TimeInterval?> { error("No default time interval") }
\ No newline at end of file
diff --git a/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/GlanceTileService.kt b/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/GlanceTileService.kt
index 07d72f0..cf6e35a 100644
--- a/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/GlanceTileService.kt
+++ b/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/GlanceTileService.kt
@@ -22,6 +22,7 @@
import androidx.compose.runtime.BroadcastFrameClock
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composition
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Recomposer
import androidx.glance.Applier
import androidx.glance.layout.EmittableBox
@@ -96,25 +97,37 @@
super.onStart(intent, startId)
}
- private suspend fun runComposition(): CompositionResult = coroutineScope {
- val root = EmittableBox()
- val applier = Applier(root)
- val recomposer = Recomposer(currentCoroutineContext())
- val composition = Composition(applier, recomposer)
+ private suspend fun runComposition(
+ timeInterval: TimeInterval? = null
+ ): CompositionResult =
+ coroutineScope {
+ val root = EmittableBox()
+ val applier = Applier(root)
+ val recomposer = Recomposer(currentCoroutineContext())
+ val composition = Composition(applier, recomposer)
- composition.setContent { Content() }
+ composition.setContent {
+ CompositionLocalProvider(
+ LocalTimeInterval provides timeInterval
+ ) { Content() }
+ }
- launch { recomposer.runRecomposeAndApplyChanges() }
+ launch { recomposer.runRecomposeAndApplyChanges() }
- recomposer.close()
- recomposer.join()
+ recomposer.close()
+ recomposer.join()
- normalizeCompositionTree(this@GlanceTileService, root)
+ normalizeCompositionTree(this@GlanceTileService, root)
- translateTopLevelComposition(this@GlanceTileService, root)
- }
+ translateTopLevelComposition(this@GlanceTileService, root)
+ }
- /** Implement with the layout to use in your Tile. */
+ /**
+ * Defines the handling of timeline
+ */
+ public open val timelineMode: TimelineMode = TimelineMode.SingleEntry
+
+ /** Override this method to set the layout to use in your Tile. */
@Composable
public abstract fun Content()
@@ -127,21 +140,46 @@
final override fun onTileRequest(
requestParams: RequestBuilders.TileRequest
): ListenableFuture<TileBuilders.Tile> = coroutineScope.future {
- val content = runComposition().layout
+ val timelineBuilders = TimelineBuilders.Timeline.Builder()
+ if (timelineMode === TimelineMode.SingleEntry) {
+ val content = runComposition().layout
+
+ timelineBuilders
+ .addTimelineEntry(
+ TimelineBuilders.TimelineEntry.Builder()
+ .setLayout(
+ LayoutElementBuilders.Layout.Builder()
+ .setRoot(content)
+ .build()
+ ).build()
+ )
+ } else { // timelineMode is TimelineMode.TimeBoundEntries
+ val timeIntervals = (timelineMode as TimelineMode.TimeBoundEntries).timeIntervals
+ timeIntervals.forEach {
+ val result = runComposition(it).layout
+
+ timelineBuilders
+ .addTimelineEntry(
+ TimelineBuilders.TimelineEntry.Builder()
+ .setValidity(
+ TimelineBuilders.TimeInterval.Builder()
+ .setStartMillis(it.start.toEpochMilli())
+ .setEndMillis(it.end.toEpochMilli())
+ .build()
+ )
+ .setLayout(
+ LayoutElementBuilders.Layout.Builder()
+ .setRoot(result)
+ .build()
+ ).build()
+ )
+ }
+ }
TileBuilders.Tile.Builder()
.setResourcesVersion(ResourcesVersion)
- .setTimeline(
- TimelineBuilders.Timeline.Builder()
- .addTimelineEntry(
- TimelineBuilders.TimelineEntry.Builder()
- .setLayout(
- LayoutElementBuilders.Layout.Builder()
- .setRoot(content)
- .build()
- ).build()
- ).build()
- ).build()
+ .setTimeline(timelineBuilders.build())
+ .build()
}
/**
@@ -153,7 +191,21 @@
final override fun onResourcesRequest(
requestParams: RequestBuilders.ResourcesRequest
): ListenableFuture<ResourceBuilders.Resources> = coroutineScope.future {
- runComposition().resources.setVersion(ResourcesVersion).build()
+ var resourceBuilder: ResourceBuilders.Resources.Builder
+ if (timelineMode === TimelineMode.SingleEntry) {
+ resourceBuilder = runComposition().resources
+ } else {
+ timelineMode is TimelineMode.TimeBoundEntries
+ resourceBuilder = ResourceBuilders.Resources.Builder()
+ val timeIntervals = (timelineMode as TimelineMode.TimeBoundEntries).timeIntervals
+ timeIntervals.forEach {
+ val result = runComposition(it).resources
+ result.build().idToImageMapping.forEach { res ->
+ resourceBuilder.addIdToImageMapping(res.key, res.value)
+ }
+ }
+ }
+ resourceBuilder.setVersion(ResourcesVersion).build()
}
@VisibleForTesting
diff --git a/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/TimelineMode.kt b/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/TimelineMode.kt
new file mode 100644
index 0000000..4e92fa6
--- /dev/null
+++ b/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/TimelineMode.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.glance.wear
+
+import java.time.Instant
+
+/**
+ * TimeInterval class defines a period from [start] to [end]
+ *
+ * @param start The start time of the time interval
+ * @param end The end time of the time interval
+ */
+public data class TimeInterval(
+ val start: Instant = Instant.ofEpochMilli(0),
+ val end: Instant = Instant.ofEpochMilli(Long.MAX_VALUE)
+) {
+ init {
+ require(end > start) {
+ "End time shall come after start time to form a valid interval"
+ }
+ }
+}
+
+public sealed interface TimelineMode {
+ /**
+ * The [GlanceTileService] provides a single UI.
+ * The layout is fixed, and only the information inside the layout changes.
+ */
+ public object SingleEntry : TimelineMode {
+ public override fun toString(): String = "TimelineMode: SingleEntry"
+ }
+
+ /**
+ * The [GlanceTileService] provides a UI for a fixed set of time intervals
+ *
+ * @param timeIntervals Used to build the list of time intervals, the list must not be empty.
+ */
+ public class TimeBoundEntries(val timeIntervals: Set<TimeInterval>) : TimelineMode {
+ init {
+ require(timeIntervals.isNotEmpty()) { "The set of time intervals cannot be empty" }
+ }
+
+ public override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as TimeBoundEntries
+
+ if (timeIntervals != other.timeIntervals) return false
+
+ return true
+ }
+
+ public override fun hashCode(): Int = timeIntervals.hashCode()
+
+ public override fun toString(): String =
+ "TimelineMode.TimeBoundEntries(timeIntervals=$timeIntervals)"
+ }
+}
\ No newline at end of file
diff --git a/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/WearCompositionTranslator.kt b/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/WearCompositionTranslator.kt
index f78e8a6..76ff0b8 100644
--- a/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/WearCompositionTranslator.kt
+++ b/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/WearCompositionTranslator.kt
@@ -133,6 +133,7 @@
else -> error("Unsupported color provider: $this")
}
+// TODO: handle parameters
private fun LaunchActivityAction.toProto(context: Context): ActionBuilders.LaunchAction =
ActionBuilders.LaunchAction.Builder()
.setAndroidActivity(
diff --git a/glance/glance-wear/src/test/kotlin/androidx/glance/wear/GlanceTileServiceTest.kt b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/GlanceTileServiceTest.kt
index fadf753..7133c06 100644
--- a/glance/glance-wear/src/test/kotlin/androidx/glance/wear/GlanceTileServiceTest.kt
+++ b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/GlanceTileServiceTest.kt
@@ -18,9 +18,17 @@
import android.os.Looper
import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import androidx.glance.GlanceModifier
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.layout.ContentScale
+import androidx.glance.layout.size
import androidx.glance.text.Text
+import androidx.glance.wear.test.R
import androidx.wear.tiles.LayoutElementBuilders
import androidx.wear.tiles.RequestBuilders
+import androidx.wear.tiles.TimelineBuilders
import androidx.wear.tiles.testing.TestTileClient
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineDispatcher
@@ -33,6 +41,7 @@
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
+import java.time.Instant
import kotlin.test.assertIs
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalStdlibApi::class)
@@ -41,23 +50,30 @@
private lateinit var fakeCoroutineScope: TestCoroutineScope
private lateinit var tileService: TestGlanceTileService
private lateinit var tileServiceClient: TestTileClient<GlanceTileService>
+ private lateinit var tileServiceWithTimeline: TestGlanceTileServiceWithTimeline
+ private lateinit var tileServiceClientWithTimeline: TestTileClient<GlanceTileService>
@Before
fun setUp() {
fakeCoroutineScope = TestCoroutineScope()
+
tileService = TestGlanceTileService()
tileServiceClient = TestTileClient(
tileService,
fakeCoroutineScope,
fakeCoroutineScope.coroutineContext[CoroutineDispatcher]!!
)
+
+ tileServiceWithTimeline = TestGlanceTileServiceWithTimeline()
+ tileServiceClientWithTimeline = TestTileClient(
+ tileServiceWithTimeline,
+ fakeCoroutineScope,
+ fakeCoroutineScope.coroutineContext[CoroutineDispatcher]!!
+ )
}
@Test
fun tileProviderReturnsTile() = fakeCoroutineScope.runBlockingTest {
- // Add the composition to the service.
- tileService.actualContent = { Text("Hello World!") }
-
// Request is currently un-used, provide an empty one.
val tileRequest = RequestBuilders.TileRequest.Builder().build()
@@ -79,16 +95,62 @@
// It always emits a box as the root-level layout.
val box = assertIs<LayoutElementBuilders.Box>(entry.layout!!.root!!)
- assertThat(box.contents).hasSize(1)
+ assertThat(box.contents).hasSize(2)
val text = assertIs<LayoutElementBuilders.Text>(box.contents[0])
assertThat(text.text!!.value).isEqualTo("Hello World!")
}
@Test
+ fun tileProviderReturnsTimelineTile() = fakeCoroutineScope.runBlockingTest {
+ // Request is currently un-used, provide an empty one.
+ val tileRequest = RequestBuilders.TileRequest.Builder().build()
+
+ // Requests need to be split; we need to allow Robolectric to schedule the service calls on
+ // the main looper, so we can't just do requestTile().await().
+ val tileFuture = tileServiceClientWithTimeline.requestTile(tileRequest)
+ shadowOf(Looper.getMainLooper()).idle()
+ val tile = tileFuture.await()
+
+ // Just uses a simple resource version for now.
+ assertThat(tile.resourcesVersion).isEqualTo(GlanceTileService.ResourcesVersion)
+
+ // No freshness interval (for now)
+ assertThat(tile.freshnessIntervalMillis).isEqualTo(0)
+
+ assertThat(tile.timeline!!.timelineEntries).hasSize(4)
+
+ checkTimelineEntry(
+ tile.timeline!!.timelineEntries[0],
+ 0,
+ Long.MAX_VALUE,
+ "No event"
+ )
+
+ checkTimelineEntry(
+ tile.timeline!!.timelineEntries[1],
+ time1.toEpochMilli(),
+ time2.toEpochMilli(),
+ "Coffee"
+ )
+
+ checkTimelineEntry(
+ tile.timeline!!.timelineEntries[2],
+ time2.toEpochMilli(),
+ time3.toEpochMilli(),
+ "Work"
+ )
+
+ checkTimelineEntry(
+ tile.timeline!!.timelineEntries[3],
+ time4.toEpochMilli(),
+ Long.MAX_VALUE,
+ "Dinner"
+ )
+ }
+
+ @Test
fun tileProviderReturnsResources() = fakeCoroutineScope.runBlockingTest {
- // The service doesn't use resources right now, but it must correctly respond to
- // onResourcesRequest. Just ensure that the version is set correctly.
val resourcesRequest = RequestBuilders.ResourcesRequest.Builder().build()
val resourcesFuture = tileServiceClient.requestResources(resourcesRequest)
@@ -96,14 +158,91 @@
val resources = resourcesFuture.await()
assertThat(resources.version).isEqualTo(GlanceTileService.ResourcesVersion)
+ assertThat(resources.idToImageMapping.size).isEqualTo(1)
+ assertThat(resources.idToImageMapping.containsKey("android_" + R.drawable.oval)).isTrue()
+ }
+
+ @Test
+ fun tileProviderReturnsTimelineResources() = fakeCoroutineScope.runBlockingTest {
+ val resourcesRequest = RequestBuilders.ResourcesRequest.Builder().build()
+
+ val resourcesFuture = tileServiceClientWithTimeline.requestResources(resourcesRequest)
+ shadowOf(Looper.getMainLooper()).idle()
+ val resources = resourcesFuture.await()
+
+ assertThat(resources.version).isEqualTo(GlanceTileService.ResourcesVersion)
+ assertThat(resources.idToImageMapping.size).isEqualTo(2)
+ assertThat(resources.idToImageMapping.containsKey("android_" + R.drawable.oval)).isTrue()
+ assertThat(
+ resources.idToImageMapping.containsKey("android_" + R.drawable.ic_launcher_background)
+ ).isTrue()
+ }
+
+ private fun checkTimelineEntry(
+ entry: TimelineBuilders.TimelineEntry,
+ startMillis: Long,
+ endMillis: Long,
+ textValue: String
+ ) {
+ assertThat(entry.validity!!.startMillis).isEqualTo(startMillis)
+ assertThat(entry.validity!!.endMillis).isEqualTo(endMillis)
+ var box = assertIs<LayoutElementBuilders.Box>(entry.layout!!.root!!)
+ var text = assertIs<LayoutElementBuilders.Text>(box.contents[0])
+ assertThat(text.text!!.value).isEqualTo(textValue)
}
private inner class TestGlanceTileService : GlanceTileService() {
- var actualContent: @Composable () -> Unit = {}
-
@Composable
override fun Content() {
- actualContent()
+ Text("Hello World!")
+ Image(
+ provider = ImageProvider(R.drawable.oval),
+ contentDescription = "Oval",
+ modifier = GlanceModifier.size(40.dp),
+ contentScale = ContentScale.FillBounds
+ )
}
}
+
+ private inner class TestGlanceTileServiceWithTimeline : GlanceTileService() {
+ override val timelineMode = testTimelineMode
+
+ @Composable
+ override fun Content() {
+ when (LocalTimeInterval.current) {
+ testTimelineMode.timeIntervals.elementAt(0) -> { Text("No event") }
+ testTimelineMode.timeIntervals.elementAt(1) -> {
+ Text("Coffee")
+ Image(
+ provider = ImageProvider(R.drawable.oval),
+ contentDescription = "Oval",
+ modifier = GlanceModifier.size(40.dp),
+ contentScale = ContentScale.FillBounds
+ )
+ }
+ testTimelineMode.timeIntervals.elementAt(2) -> {
+ Text("Work")
+ Image(
+ provider = ImageProvider(R.drawable.ic_launcher_background),
+ contentDescription = "Icon",
+ modifier = GlanceModifier.size(40.dp),
+ )
+ }
+ testTimelineMode.timeIntervals.elementAt(3) -> { Text("Dinner") }
+ }
+ }
+ }
+
+ private companion object {
+ private val time1 = Instant.parse("2021-11-12T13:15:30.00Z")
+ private val time2 = Instant.parse("2021-11-12T13:45:30.00Z")
+ private val time3 = Instant.parse("2021-11-12T17:45:30.00Z")
+ private val time4 = Instant.parse("2021-11-12T18:30:30.00Z")
+ val testTimelineMode = TimelineMode.TimeBoundEntries(setOf(
+ TimeInterval(),
+ TimeInterval(time1, time2),
+ TimeInterval(time2, time3),
+ TimeInterval(time4)
+ ))
+ }
}
\ No newline at end of file
diff --git a/glance/glance-wear/src/test/kotlin/androidx/glance/wear/TimelineModeTest.kt b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/TimelineModeTest.kt
new file mode 100644
index 0000000..d044d94
--- /dev/null
+++ b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/TimelineModeTest.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.glance.wear
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import java.time.Instant
+
+class TimelineModeTest {
+
+ @Test
+ fun timeboundEntriesTest() {
+ val time1 = Instant.parse("2021-11-12T13:15:30.00Z")
+ val time2 = Instant.parse("2021-11-12T13:45:30.00Z")
+ val time3 = Instant.parse("2021-11-12T17:45:30.00Z")
+ val time4 = Instant.parse("2021-11-12T18:30:30.00Z")
+
+ val timeBoundEntries = TimelineMode.TimeBoundEntries(setOf(
+ TimeInterval(),
+ TimeInterval(time1, time2),
+ TimeInterval(time2, time3),
+ TimeInterval(time4)
+ ))
+ val intervals = timeBoundEntries.timeIntervals
+
+ assertThat(intervals.size).isEqualTo(4)
+
+ assertThat(intervals.elementAt(0).start.toEpochMilli()).isEqualTo(0)
+ assertThat(intervals.elementAt(0).end.toEpochMilli()).isEqualTo(Long.MAX_VALUE)
+
+ assertThat(intervals.elementAt(1).start).isEqualTo(time1)
+ assertThat(intervals.elementAt(1).end).isEqualTo(time2)
+
+ assertThat(intervals.elementAt(2).start).isEqualTo(time2)
+ assertThat(intervals.elementAt(2).end).isEqualTo(time3)
+
+ assertThat(intervals.elementAt(3).start).isEqualTo(time4)
+ assertThat(intervals.elementAt(3).end.toEpochMilli()).isEqualTo(Long.MAX_VALUE)
+ }
+}
\ No newline at end of file
diff --git a/glance/glance-wear/src/test/kotlin/androidx/glance/wear/WearCompositionTranslatorTest.kt b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/WearCompositionTranslatorTest.kt
index c133aaa..03b2150 100644
--- a/glance/glance-wear/src/test/kotlin/androidx/glance/wear/WearCompositionTranslatorTest.kt
+++ b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/WearCompositionTranslatorTest.kt
@@ -582,7 +582,9 @@
)
assertThat(image.resourceId!!.value).isEqualTo("android_" + R.drawable.oval)
- assertThat(resources.build().idToImageMapping.containsKey(R.drawable.oval.toString()))
+ assertThat(
+ resources.build().idToImageMapping.containsKey("android_" + R.drawable.oval)
+ ).isTrue()
assertThat(image.modifiers!!.semantics!!.contentDescription).isEqualTo("Oval")
}
diff --git a/glance/glance-wear/src/test/res/drawable/ic_launcher_background.xml b/glance/glance-wear/src/test/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..ca3826a
--- /dev/null
+++ b/glance/glance-wear/src/test/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector
+ android:height="108dp"
+ android:width="108dp"
+ android:viewportHeight="108"
+ android:viewportWidth="108"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z"/>
+ <path android:fillColor="#00000000" android:pathData="M9,0L9,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+</vector>
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index 56e1253..5955625 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -117,12 +117,13 @@
method public static androidx.glance.action.MutableActionParameters mutableActionParametersOf(androidx.glance.action.ActionParameters.Pair<?>... pairs);
method public static androidx.glance.action.MutableActionParameters toMutableParameters(androidx.glance.action.ActionParameters);
method public static androidx.glance.action.ActionParameters toParameters(androidx.glance.action.ActionParameters);
+ method public static <T> androidx.glance.action.ActionParameters.Key<T> toParametersKey(androidx.datastore.preferences.core.Preferences.Key<T>);
}
public final class LaunchActivityActionKt {
- method public static androidx.glance.action.Action actionLaunchActivity(android.content.ComponentName componentName);
- method public static <T extends android.app.Activity> androidx.glance.action.Action actionLaunchActivity(Class<T> activity);
- method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionLaunchActivity();
+ method public static androidx.glance.action.Action actionLaunchActivity(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
+ method public static <T extends android.app.Activity> androidx.glance.action.Action actionLaunchActivity(Class<T> activity, optional androidx.glance.action.ActionParameters parameters);
+ method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionLaunchActivity(optional androidx.glance.action.ActionParameters parameters);
}
public final class MutableActionParameters extends androidx.glance.action.ActionParameters {
@@ -276,15 +277,15 @@
package androidx.glance.state {
- public final class GlanceState {
- method public suspend <T> Object? getValue(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, String fileName, kotlin.coroutines.Continuation<? super T> p);
- method public suspend <T> Object? updateValue(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, String fileName, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super T>,?> updateBlock, kotlin.coroutines.Continuation<? super T> p);
- field public static final androidx.glance.state.GlanceState INSTANCE;
+ public interface GlanceStateDefinition<T> {
+ method public suspend Object? getDataStore(android.content.Context context, String fileKey, kotlin.coroutines.Continuation<? super androidx.datastore.core.DataStore<T>> p);
+ method public java.io.File getLocation(android.content.Context context, String fileKey);
}
- public interface GlanceStateDefinition<T> {
- method public suspend <T> Object? getDataStore(android.content.Context context, String fileKey, kotlin.coroutines.Continuation<? super androidx.datastore.core.DataStore<T>> p);
+ public final class PreferencesGlanceStateDefinition implements androidx.glance.state.GlanceStateDefinition<androidx.datastore.preferences.core.Preferences> {
+ method public suspend Object? getDataStore(android.content.Context context, String fileKey, kotlin.coroutines.Continuation<? super androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences>> p);
method public java.io.File getLocation(android.content.Context context, String fileKey);
+ field public static final androidx.glance.state.PreferencesGlanceStateDefinition INSTANCE;
}
}
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index 56e1253..5955625 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -117,12 +117,13 @@
method public static androidx.glance.action.MutableActionParameters mutableActionParametersOf(androidx.glance.action.ActionParameters.Pair<?>... pairs);
method public static androidx.glance.action.MutableActionParameters toMutableParameters(androidx.glance.action.ActionParameters);
method public static androidx.glance.action.ActionParameters toParameters(androidx.glance.action.ActionParameters);
+ method public static <T> androidx.glance.action.ActionParameters.Key<T> toParametersKey(androidx.datastore.preferences.core.Preferences.Key<T>);
}
public final class LaunchActivityActionKt {
- method public static androidx.glance.action.Action actionLaunchActivity(android.content.ComponentName componentName);
- method public static <T extends android.app.Activity> androidx.glance.action.Action actionLaunchActivity(Class<T> activity);
- method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionLaunchActivity();
+ method public static androidx.glance.action.Action actionLaunchActivity(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
+ method public static <T extends android.app.Activity> androidx.glance.action.Action actionLaunchActivity(Class<T> activity, optional androidx.glance.action.ActionParameters parameters);
+ method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionLaunchActivity(optional androidx.glance.action.ActionParameters parameters);
}
public final class MutableActionParameters extends androidx.glance.action.ActionParameters {
@@ -276,15 +277,15 @@
package androidx.glance.state {
- public final class GlanceState {
- method public suspend <T> Object? getValue(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, String fileName, kotlin.coroutines.Continuation<? super T> p);
- method public suspend <T> Object? updateValue(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, String fileName, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super T>,?> updateBlock, kotlin.coroutines.Continuation<? super T> p);
- field public static final androidx.glance.state.GlanceState INSTANCE;
+ public interface GlanceStateDefinition<T> {
+ method public suspend Object? getDataStore(android.content.Context context, String fileKey, kotlin.coroutines.Continuation<? super androidx.datastore.core.DataStore<T>> p);
+ method public java.io.File getLocation(android.content.Context context, String fileKey);
}
- public interface GlanceStateDefinition<T> {
- method public suspend <T> Object? getDataStore(android.content.Context context, String fileKey, kotlin.coroutines.Continuation<? super androidx.datastore.core.DataStore<T>> p);
+ public final class PreferencesGlanceStateDefinition implements androidx.glance.state.GlanceStateDefinition<androidx.datastore.preferences.core.Preferences> {
+ method public suspend Object? getDataStore(android.content.Context context, String fileKey, kotlin.coroutines.Continuation<? super androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences>> p);
method public java.io.File getLocation(android.content.Context context, String fileKey);
+ field public static final androidx.glance.state.PreferencesGlanceStateDefinition INSTANCE;
}
}
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index 56e1253..5955625 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -117,12 +117,13 @@
method public static androidx.glance.action.MutableActionParameters mutableActionParametersOf(androidx.glance.action.ActionParameters.Pair<?>... pairs);
method public static androidx.glance.action.MutableActionParameters toMutableParameters(androidx.glance.action.ActionParameters);
method public static androidx.glance.action.ActionParameters toParameters(androidx.glance.action.ActionParameters);
+ method public static <T> androidx.glance.action.ActionParameters.Key<T> toParametersKey(androidx.datastore.preferences.core.Preferences.Key<T>);
}
public final class LaunchActivityActionKt {
- method public static androidx.glance.action.Action actionLaunchActivity(android.content.ComponentName componentName);
- method public static <T extends android.app.Activity> androidx.glance.action.Action actionLaunchActivity(Class<T> activity);
- method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionLaunchActivity();
+ method public static androidx.glance.action.Action actionLaunchActivity(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
+ method public static <T extends android.app.Activity> androidx.glance.action.Action actionLaunchActivity(Class<T> activity, optional androidx.glance.action.ActionParameters parameters);
+ method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionLaunchActivity(optional androidx.glance.action.ActionParameters parameters);
}
public final class MutableActionParameters extends androidx.glance.action.ActionParameters {
@@ -276,15 +277,15 @@
package androidx.glance.state {
- public final class GlanceState {
- method public suspend <T> Object? getValue(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, String fileName, kotlin.coroutines.Continuation<? super T> p);
- method public suspend <T> Object? updateValue(android.content.Context context, androidx.glance.state.GlanceStateDefinition<T> definition, String fileName, kotlin.jvm.functions.Function2<? super T,? super kotlin.coroutines.Continuation<? super T>,?> updateBlock, kotlin.coroutines.Continuation<? super T> p);
- field public static final androidx.glance.state.GlanceState INSTANCE;
+ public interface GlanceStateDefinition<T> {
+ method public suspend Object? getDataStore(android.content.Context context, String fileKey, kotlin.coroutines.Continuation<? super androidx.datastore.core.DataStore<T>> p);
+ method public java.io.File getLocation(android.content.Context context, String fileKey);
}
- public interface GlanceStateDefinition<T> {
- method public suspend <T> Object? getDataStore(android.content.Context context, String fileKey, kotlin.coroutines.Continuation<? super androidx.datastore.core.DataStore<T>> p);
+ public final class PreferencesGlanceStateDefinition implements androidx.glance.state.GlanceStateDefinition<androidx.datastore.preferences.core.Preferences> {
+ method public suspend Object? getDataStore(android.content.Context context, String fileKey, kotlin.coroutines.Continuation<? super androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences>> p);
method public java.io.File getLocation(android.content.Context context, String fileKey);
+ field public static final androidx.glance.state.PreferencesGlanceStateDefinition INSTANCE;
}
}
diff --git a/glance/glance/build.gradle b/glance/glance/build.gradle
index 1bd9e3b..a01b578 100644
--- a/glance/glance/build.gradle
+++ b/glance/glance/build.gradle
@@ -36,6 +36,8 @@
api("androidx.compose.ui:ui-graphics:1.1.0-beta01")
api("androidx.compose.ui:ui-unit:1.1.0-beta01")
api("androidx.datastore:datastore-core:1.0.0")
+ api("androidx.datastore:datastore-preferences-core:1.0.0")
+ api("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.annotation:annotation:1.1.0")
implementation(libs.kotlinStdlib)
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/action/ActionParameters.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/action/ActionParameters.kt
index 40ecf1d..eb02d71 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/action/ActionParameters.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/action/ActionParameters.kt
@@ -16,6 +16,7 @@
package androidx.glance.action
+import androidx.datastore.preferences.core.Preferences
import java.util.Collections
/**
@@ -197,3 +198,7 @@
* @return a copy of this Parameters
*/
public fun ActionParameters.toParameters(): ActionParameters = toMutableParameters()
+
+/** Creates an action key from a preferences key. */
+public fun <T : Any> Preferences.Key<T>.toParametersKey(): ActionParameters.Key<T> =
+ ActionParameters.Key(name)
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/action/LaunchActivityAction.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/action/LaunchActivityAction.kt
index 387893d..ff1e90b 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/action/LaunchActivityAction.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/action/LaunchActivityAction.kt
@@ -22,39 +22,55 @@
/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public interface LaunchActivityAction : Action
+public interface LaunchActivityAction : Action {
+ abstract val parameters: ActionParameters
+}
/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class LaunchActivityComponentAction(
- public val componentName: ComponentName
+ public val componentName: ComponentName,
+ public override val parameters: ActionParameters
) : LaunchActivityAction
/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class LaunchActivityClassAction(
- public val activityClass: Class<out Activity>
+ public val activityClass: Class<out Activity>,
+ public override val parameters: ActionParameters
) : LaunchActivityAction
/**
* Creates an [Action] that launches the [Activity] specified by the given [ComponentName].
*
* @param componentName component of the activity to launch
+ * @param parameters the parameters associated with the action. Parameter values will be added to
+ * the activity intent, keyed by the parameter key name string.
*/
-public fun actionLaunchActivity(componentName: ComponentName): Action =
- LaunchActivityComponentAction(componentName)
+public fun actionLaunchActivity(
+ componentName: ComponentName,
+ parameters: ActionParameters = actionParametersOf()
+): Action = LaunchActivityComponentAction(componentName, parameters)
/**
* Creates an [Action] that launches the specified [Activity] when triggered.
*
* @param activity class of the activity to launch
+ * @param parameters the parameters associated with the action. Parameter values will be added to
+ * the activity intent, keyed by the parameter key name string.
*/
-public fun <T : Activity> actionLaunchActivity(activity: Class<T>): Action =
- LaunchActivityClassAction(activity)
+public fun <T : Activity> actionLaunchActivity(
+ activity: Class<T>,
+ parameters: ActionParameters = actionParametersOf()
+): Action = LaunchActivityClassAction(activity, parameters)
@Suppress("MissingNullability") /* Shouldn't need to specify @NonNull. b/199284086 */
/**
* Creates an [Action] that launches the specified [Activity] when triggered.
+ *
+ * @param parameters the parameters associated with the action. Parameter values will be added to
+ * the activity intent, keyed by the parameter key name string.
*/
-public inline fun <reified T : Activity> actionLaunchActivity(): Action =
- actionLaunchActivity(T::class.java)
+public inline fun <reified T : Activity> actionLaunchActivity(
+ parameters: ActionParameters = actionParametersOf()
+): Action = actionLaunchActivity(T::class.java, parameters)
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/state/GlanceStateDefinition.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/state/GlanceStateDefinition.kt
index ca1cc33..3be6ce2 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/state/GlanceStateDefinition.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/state/GlanceStateDefinition.kt
@@ -17,7 +17,11 @@
package androidx.glance.state
import android.content.Context
+import androidx.annotation.RestrictTo
import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.preferencesDataStoreFile
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -46,14 +50,17 @@
* @param fileKey The unique string key used to name and identify the data file corresponding to
* a remote UI. Each remote UI has a unique UI key, used to key the data for that UI.
*/
- public suspend fun <T> getDataStore(context: Context, fileKey: String): DataStore<T>
+ public suspend fun getDataStore(context: Context, fileKey: String): DataStore<T>
}
/**
* Data store for data specific to the glanceable view. Stored data should include information
* relevant to the representation of views, but not surface specific view data. For example, the
* month displayed on a calendar rather than actual calendar entries.
+ *
+ * @suppress
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public object GlanceState {
// TODO(b/205496180): Make methods internal
/**
@@ -81,7 +88,7 @@
definition: GlanceStateDefinition<T>,
fileName: String,
updateBlock: suspend (T) -> T
- ) = getDataStore(context, definition, fileName).updateData(updateBlock)
+ ): T = getDataStore(context, definition, fileName).updateData(updateBlock)
@Suppress("UNCHECKED_CAST")
private suspend fun <T> getDataStore(
@@ -91,7 +98,7 @@
): DataStore<T> =
mutex.withLock {
dataStores.getOrPut(fileName) {
- definition.getDataStore<T>(context, fileName)
+ definition.getDataStore(context, fileName)
} as DataStore<T>
}
@@ -100,3 +107,14 @@
// TODO: Move to a single, global source to manage the data lifecycle
private val dataStores: MutableMap<String, DataStore<*>> = mutableMapOf()
}
+
+/**
+ * Base class helping the creation of a state using DataStore's [Preferences].
+ */
+object PreferencesGlanceStateDefinition : GlanceStateDefinition<Preferences> {
+ override fun getLocation(context: Context, fileKey: String): File =
+ context.preferencesDataStoreFile(fileKey)
+
+ override suspend fun getDataStore(context: Context, fileKey: String): DataStore<Preferences> =
+ PreferenceDataStoreFactory.create { context.preferencesDataStoreFile(fileKey) }
+}
diff --git a/glance/glance/src/test/kotlin/androidx/glance/ButtonTest.kt b/glance/glance/src/test/kotlin/androidx/glance/ButtonTest.kt
index 6ae0921..6f49d6a2 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/ButtonTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/ButtonTest.kt
@@ -17,8 +17,10 @@
import android.app.Activity
import androidx.glance.action.ActionModifier
+import androidx.glance.action.ActionParameters
import androidx.glance.action.LaunchActivityAction
import androidx.glance.action.actionLaunchActivity
+import androidx.glance.action.actionParametersOf
import androidx.glance.layout.runTestingComposition
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -39,15 +41,26 @@
@Test
fun createComposableButton() = fakeCoroutineScope.runBlockingTest {
+ val stringKey = ActionParameters.Key<String>("test")
+ val intKey = ActionParameters.Key<Int>("test2")
+ val string = "testString"
+ val int = 12
+
val root = runTestingComposition {
- Button(text = "button", onClick = actionLaunchActivity<Activity>(), enabled = true)
+ Button(text = "button", onClick = actionLaunchActivity<Activity>(
+ actionParametersOf(stringKey to string, intKey to int)
+ ), enabled = true)
}
assertThat(root.children).hasSize(1)
val child = assertIs<EmittableButton>(root.children[0])
assertThat(child.text).isEqualTo("button")
- assertIs<LaunchActivityAction>(child.modifier.findModifier<ActionModifier>()?.action)
+ val action =
+ assertIs<LaunchActivityAction>(child.modifier.findModifier<ActionModifier>()?.action)
assertThat(child.enabled).isTrue()
+ assertThat(action.parameters.asMap()).hasSize(2)
+ assertThat(action.parameters[stringKey]).isEqualTo(string)
+ assertThat(action.parameters[intKey]).isEqualTo(int)
}
@Test
diff --git a/glance/glance/src/test/kotlin/androidx/glance/state/StateDefinitionTest.kt b/glance/glance/src/test/kotlin/androidx/glance/state/StateDefinitionTest.kt
index 6610c26..91a054a 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/state/StateDefinitionTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/state/StateDefinitionTest.kt
@@ -17,12 +17,8 @@
package androidx.glance.state
import android.content.Context
-import androidx.datastore.core.DataStore
-import androidx.datastore.preferences.core.PreferenceDataStoreFactory
-import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
-import androidx.datastore.preferences.preferencesDataStoreFile
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -30,7 +26,6 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
-import java.io.File
import kotlin.test.assertNotNull
@OptIn(ExperimentalCoroutinesApi::class)
@@ -45,7 +40,13 @@
fun emptyStore() {
runBlocking {
val uiString = "testUiString1"
- val store = assertNotNull(GlanceState.getValue(context, EmptyStateDefinition, uiString))
+ val store = assertNotNull(
+ GlanceState.getValue(
+ context,
+ PreferencesGlanceStateDefinition,
+ uiString
+ )
+ )
assertThat(store.contains(counterKey)).isFalse()
assertThat(store.contains(stringKey)).isFalse()
}
@@ -55,20 +56,32 @@
fun counterKey() {
runBlocking {
val uiString = "testUiString2"
- GlanceState.updateValue(context, EmptyStateDefinition, uiString) { prefs ->
+ GlanceState.updateValue(context, PreferencesGlanceStateDefinition, uiString) { prefs ->
prefs.toMutablePreferences().apply { this[counterKey] = 0 }.toPreferences()
}
- var store = assertNotNull(GlanceState.getValue(context, EmptyStateDefinition, uiString))
+ var store = assertNotNull(
+ GlanceState.getValue(
+ context,
+ PreferencesGlanceStateDefinition,
+ uiString
+ )
+ )
assertThat(store.contains(counterKey)).isTrue()
assertThat(store[counterKey]).isEqualTo(0)
- GlanceState.updateValue(context, EmptyStateDefinition, uiString) { prefs ->
+ GlanceState.updateValue(context, PreferencesGlanceStateDefinition, uiString) { prefs ->
prefs.toMutablePreferences().apply {
this[counterKey] = prefs[counterKey]!! + 1
}.toPreferences()
}
- store = assertNotNull(GlanceState.getValue(context, EmptyStateDefinition, uiString))
+ store = assertNotNull(
+ GlanceState.getValue(
+ context,
+ PreferencesGlanceStateDefinition,
+ uiString
+ )
+ )
assertThat(store.contains(counterKey)).isTrue()
assertThat(store[counterKey]).isEqualTo(1)
}
@@ -81,36 +94,37 @@
val storedMessage2 = "Another test string"
runBlocking {
- GlanceState.updateValue(context, EmptyStateDefinition, uiString) { prefs ->
+ GlanceState.updateValue(context, PreferencesGlanceStateDefinition, uiString) { prefs ->
prefs.toMutablePreferences().apply { this[stringKey] = storedMessage1 }
.toPreferences()
}
- var store = assertNotNull(GlanceState.getValue(context, EmptyStateDefinition, uiString))
+ var store = assertNotNull(
+ GlanceState.getValue(
+ context,
+ PreferencesGlanceStateDefinition,
+ uiString
+ )
+ )
assertThat(store.contains(counterKey)).isFalse()
assertThat(store.contains(stringKey)).isTrue()
assertThat(store[stringKey]).isEqualTo(storedMessage1)
- GlanceState.updateValue(context, EmptyStateDefinition, uiString) { prefs ->
+ GlanceState.updateValue(context, PreferencesGlanceStateDefinition, uiString) { prefs ->
prefs.toMutablePreferences().apply {
this[stringKey] = storedMessage2
}.toPreferences()
}
- store = assertNotNull(GlanceState.getValue(context, EmptyStateDefinition, uiString))
+ store = assertNotNull(
+ GlanceState.getValue(
+ context,
+ PreferencesGlanceStateDefinition,
+ uiString
+ )
+ )
assertThat(store.contains(counterKey)).isFalse()
assertThat(store.contains(stringKey)).isTrue()
assertThat(store[stringKey]).isEqualTo(storedMessage2)
}
}
-
- object EmptyStateDefinition : GlanceStateDefinition<Preferences> {
- override fun getLocation(context: Context, fileKey: String): File =
- context.preferencesDataStoreFile(fileKey)
-
- @Suppress("UNCHECKED_CAST")
- override suspend fun <T> getDataStore(context: Context, fileKey: String): DataStore<T> =
- PreferenceDataStoreFactory.create {
- context.preferencesDataStoreFile(fileKey)
- } as DataStore<T>
- }
}
\ No newline at end of file
diff --git a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt
index de040cf..50e6f8d 100644
--- a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt
+++ b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt
@@ -25,6 +25,7 @@
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
@@ -42,6 +43,7 @@
import java.io.File
import java.nio.charset.Charset
+@CacheableTask
abstract class DexInspectorTask : DefaultTask() {
@get:PathSensitive(PathSensitivity.NONE)
@get:InputFile
diff --git a/jetifier/OWNERS b/jetifier/OWNERS
deleted file mode 100644
index 8ae348f..0000000
--- a/jetifier/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-pavlis@google.com
diff --git a/lifecycle/lifecycle-runtime-ktx-lint/build.gradle b/lifecycle/lifecycle-runtime-ktx-lint/build.gradle
index f0497c3..8776e4f 100644
--- a/lifecycle/lifecycle-runtime-ktx-lint/build.gradle
+++ b/lifecycle/lifecycle-runtime-ktx-lint/build.gradle
@@ -28,6 +28,8 @@
compileOnly(libs.kotlinStdlib)
testImplementation(libs.kotlinStdlib)
+ testImplementation(libs.kotlinReflect)
+ testImplementation(libs.kotlinStdlibJdk8)
testImplementation(libs.androidLint)
testImplementation(libs.androidLintTests)
testImplementation(libs.junit)
diff --git a/lifecycle/lifecycle-runtime-ktx-lint/src/main/java/androidx/lifecycle/lint/LifecycleWhenChecks.kt b/lifecycle/lifecycle-runtime-ktx-lint/src/main/java/androidx/lifecycle/lint/LifecycleWhenChecks.kt
index a1d8509..f863f3c 100644
--- a/lifecycle/lifecycle-runtime-ktx-lint/src/main/java/androidx/lifecycle/lint/LifecycleWhenChecks.kt
+++ b/lifecycle/lifecycle-runtime-ktx-lint/src/main/java/androidx/lifecycle/lint/LifecycleWhenChecks.kt
@@ -42,7 +42,9 @@
import org.jetbrains.uast.UIfExpression
import org.jetbrains.uast.ULambdaExpression
import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.USwitchClauseExpression
import org.jetbrains.uast.UTryExpression
+import org.jetbrains.uast.kotlin.KotlinUSwitchEntry
import org.jetbrains.uast.toUElement
import org.jetbrains.uast.tryResolve
import org.jetbrains.uast.visitor.AbstractUastVisitor
@@ -190,6 +192,23 @@
}
return false
}
+
+ override fun visitSwitchClauseExpression(node: USwitchClauseExpression): Boolean {
+ // check each case in the switch statement
+ node.caseValues.forEach { expression ->
+ val method = expression.tryResolve() as? PsiMethod ?: return false
+ if (method.isLifecycleIsAtLeastMethod(context)) {
+ // If the case containing the lifecycle check evaluates to true, check the body
+ withNewState(checkUIAccess = false) {
+ (node as? KotlinUSwitchEntry)?.body?.expressions?.forEach {
+ it.accept(this)
+ }
+ }
+ return true
+ }
+ }
+ return false
+ }
}
private const val DISPATCHER_CLASS_NAME = "androidx.lifecycle.PausingDispatcherKt"
diff --git a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/runtime/lint/LifecycleWhenChecksTest.kt b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/runtime/lint/LifecycleWhenChecksTest.kt
index dbba1ac..e45da05 100644
--- a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/runtime/lint/LifecycleWhenChecksTest.kt
+++ b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/runtime/lint/LifecycleWhenChecksTest.kt
@@ -24,7 +24,6 @@
import com.android.tools.lint.checks.infrastructure.TestFiles.kt
import com.android.tools.lint.checks.infrastructure.TestLintResult
import com.android.tools.lint.checks.infrastructure.TestLintTask
-import com.android.tools.lint.checks.infrastructure.TestMode
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -35,8 +34,6 @@
private fun check(body: String): TestLintResult {
return TestLintTask.lint()
.files(VIEW_STUB, LIFECYCLE_STUB, COROUTINES_STUB, kt(template(body)))
- .allowCompilationErrors(true) // b/193267317
- .skipTestModes(TestMode.IF_TO_WHEN) // b/203245752
.issues(ISSUE)
.run()
}
diff --git a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/runtime/lint/RepeatOnLifecycleDetectorTest.kt b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/runtime/lint/RepeatOnLifecycleDetectorTest.kt
index cce75cd..aa6e8ef 100644
--- a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/runtime/lint/RepeatOnLifecycleDetectorTest.kt
+++ b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/runtime/lint/RepeatOnLifecycleDetectorTest.kt
@@ -107,7 +107,6 @@
*REPEAT_ON_LIFECYCLE_STUBS,
TestFiles.kt(fileToAdd)
)
- .allowCompilationErrors(true) // b/193267317
.issues(RepeatOnLifecycleDetector.ISSUE)
.run()
}
diff --git a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/runtime/lint/WhenMethodsTest.kt b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/runtime/lint/WhenMethodsTest.kt
index 5b26766..701f1da 100644
--- a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/runtime/lint/WhenMethodsTest.kt
+++ b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/runtime/lint/WhenMethodsTest.kt
@@ -51,7 +51,6 @@
private fun check(body: String): TestLintResult {
return TestLintTask.lint()
- .allowCompilationErrors(true) // b/193267317
.files(VIEW_STUB, LIFECYCLE_STUB, COROUTINES_STUB, TestFiles.kt(template(body)))
.issues(LifecycleWhenChecks.ISSUE)
.run()
diff --git a/mediarouter/mediarouter/api/current.txt b/mediarouter/mediarouter/api/current.txt
index 6af2727..a750177 100644
--- a/mediarouter/mediarouter/api/current.txt
+++ b/mediarouter/mediarouter/api/current.txt
@@ -452,7 +452,7 @@
public class MediaRouterParams {
method public int getDialogType();
- method public boolean isMediaTransferReceiverDisabled();
+ method public boolean isMediaTransferReceiverEnabled();
method public boolean isOutputSwitcherEnabled();
method public boolean isTransferToLocalEnabled();
field public static final int DIALOG_TYPE_DEFAULT = 1; // 0x1
@@ -465,7 +465,7 @@
ctor public MediaRouterParams.Builder(androidx.mediarouter.media.MediaRouterParams);
method public androidx.mediarouter.media.MediaRouterParams build();
method public androidx.mediarouter.media.MediaRouterParams.Builder setDialogType(int);
- method public androidx.mediarouter.media.MediaRouterParams.Builder setMediaTransferReceiverDisabled(boolean);
+ method public androidx.mediarouter.media.MediaRouterParams.Builder setMediaTransferReceiverEnabled(boolean);
method public androidx.mediarouter.media.MediaRouterParams.Builder setOutputSwitcherEnabled(boolean);
method public androidx.mediarouter.media.MediaRouterParams.Builder setTransferToLocalEnabled(boolean);
}
diff --git a/mediarouter/mediarouter/api/public_plus_experimental_current.txt b/mediarouter/mediarouter/api/public_plus_experimental_current.txt
index 6af2727..a750177 100644
--- a/mediarouter/mediarouter/api/public_plus_experimental_current.txt
+++ b/mediarouter/mediarouter/api/public_plus_experimental_current.txt
@@ -452,7 +452,7 @@
public class MediaRouterParams {
method public int getDialogType();
- method public boolean isMediaTransferReceiverDisabled();
+ method public boolean isMediaTransferReceiverEnabled();
method public boolean isOutputSwitcherEnabled();
method public boolean isTransferToLocalEnabled();
field public static final int DIALOG_TYPE_DEFAULT = 1; // 0x1
@@ -465,7 +465,7 @@
ctor public MediaRouterParams.Builder(androidx.mediarouter.media.MediaRouterParams);
method public androidx.mediarouter.media.MediaRouterParams build();
method public androidx.mediarouter.media.MediaRouterParams.Builder setDialogType(int);
- method public androidx.mediarouter.media.MediaRouterParams.Builder setMediaTransferReceiverDisabled(boolean);
+ method public androidx.mediarouter.media.MediaRouterParams.Builder setMediaTransferReceiverEnabled(boolean);
method public androidx.mediarouter.media.MediaRouterParams.Builder setOutputSwitcherEnabled(boolean);
method public androidx.mediarouter.media.MediaRouterParams.Builder setTransferToLocalEnabled(boolean);
}
diff --git a/mediarouter/mediarouter/api/restricted_current.txt b/mediarouter/mediarouter/api/restricted_current.txt
index 6af2727..a750177 100644
--- a/mediarouter/mediarouter/api/restricted_current.txt
+++ b/mediarouter/mediarouter/api/restricted_current.txt
@@ -452,7 +452,7 @@
public class MediaRouterParams {
method public int getDialogType();
- method public boolean isMediaTransferReceiverDisabled();
+ method public boolean isMediaTransferReceiverEnabled();
method public boolean isOutputSwitcherEnabled();
method public boolean isTransferToLocalEnabled();
field public static final int DIALOG_TYPE_DEFAULT = 1; // 0x1
@@ -465,7 +465,7 @@
ctor public MediaRouterParams.Builder(androidx.mediarouter.media.MediaRouterParams);
method public androidx.mediarouter.media.MediaRouterParams build();
method public androidx.mediarouter.media.MediaRouterParams.Builder setDialogType(int);
- method public androidx.mediarouter.media.MediaRouterParams.Builder setMediaTransferReceiverDisabled(boolean);
+ method public androidx.mediarouter.media.MediaRouterParams.Builder setMediaTransferReceiverEnabled(boolean);
method public androidx.mediarouter.media.MediaRouterParams.Builder setOutputSwitcherEnabled(boolean);
method public androidx.mediarouter.media.MediaRouterParams.Builder setTransferToLocalEnabled(boolean);
}
diff --git a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterTest.java b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterTest.java
index aa9fff3..186093b 100644
--- a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterTest.java
+++ b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterTest.java
@@ -130,7 +130,7 @@
final int dialogType = MediaRouterParams.DIALOG_TYPE_DYNAMIC_GROUP;
final boolean isOutputSwitcherEnabled = true;
final boolean transferToLocalEnabled = true;
- final boolean transferReceiverDisabled = true;
+ final boolean transferReceiverEnabled = false;
final Bundle extras = new Bundle();
extras.putString(TEST_KEY, TEST_VALUE);
@@ -138,7 +138,7 @@
.setDialogType(dialogType)
.setOutputSwitcherEnabled(isOutputSwitcherEnabled)
.setTransferToLocalEnabled(transferToLocalEnabled)
- .setMediaTransferReceiverDisabled(transferReceiverDisabled)
+ .setMediaTransferReceiverEnabled(transferReceiverEnabled)
.setExtras(extras)
.build();
@@ -147,13 +147,13 @@
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
assertEquals(isOutputSwitcherEnabled, params.isOutputSwitcherEnabled());
assertEquals(transferToLocalEnabled, params.isTransferToLocalEnabled());
- assertEquals(transferReceiverDisabled, params.isMediaTransferReceiverDisabled());
+ assertEquals(transferReceiverEnabled, params.isMediaTransferReceiverEnabled());
} else {
// Earlier than Android R, output switcher cannot be enabled.
// Same for transfer to local.
assertFalse(params.isOutputSwitcherEnabled());
assertFalse(params.isTransferToLocalEnabled());
- assertFalse(params.isMediaTransferReceiverDisabled());
+ assertFalse(params.isMediaTransferReceiverEnabled());
}
extras.remove(TEST_KEY);
@@ -164,8 +164,8 @@
assertEquals(params.getDialogType(), copiedParams.getDialogType());
assertEquals(params.isOutputSwitcherEnabled(), copiedParams.isOutputSwitcherEnabled());
assertEquals(params.isTransferToLocalEnabled(), copiedParams.isTransferToLocalEnabled());
- assertEquals(params.isMediaTransferReceiverDisabled(),
- copiedParams.isMediaTransferReceiverDisabled());
+ assertEquals(params.isMediaTransferReceiverEnabled(),
+ copiedParams.isMediaTransferReceiverEnabled());
assertBundleEquals(params.getExtras(), copiedParams.getExtras());
}
@@ -176,7 +176,7 @@
final int dialogType = MediaRouterParams.DIALOG_TYPE_DYNAMIC_GROUP;
final boolean isOutputSwitcherEnabled = true;
final boolean transferToLocalEnabled = true;
- final boolean transferReceiverDisabled = true;
+ final boolean transferReceiverEnabled = false;
final Bundle paramExtras = new Bundle();
paramExtras.putString(TEST_KEY, TEST_VALUE);
@@ -184,7 +184,7 @@
.setDialogType(dialogType)
.setOutputSwitcherEnabled(isOutputSwitcherEnabled)
.setTransferToLocalEnabled(transferToLocalEnabled)
- .setMediaTransferReceiverDisabled(transferReceiverDisabled)
+ .setMediaTransferReceiverEnabled(transferReceiverEnabled)
.setExtras(paramExtras)
.build();
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 6eb9f22..3334211 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
@@ -2654,6 +2654,8 @@
mMr2Provider = new MediaRoute2Provider(
mApplicationContext, new Mr2ProviderCallback());
addProvider(mMr2Provider);
+ // Make sure mDiscoveryRequestForMr2Provider is updated
+ updateDiscoveryRequest();
mRegisteredProviderWatcher.rescan();
}
@@ -2938,8 +2940,9 @@
}
boolean isMediaTransferEnabled() {
+ // The default value for isMediaTransferReceiverEnabled() is {@code true}.
return mTransferReceiverDeclared
- && (mRouterParams == null || !mRouterParams.isMediaTransferReceiverDisabled());
+ && (mRouterParams == null || mRouterParams.isMediaTransferReceiverEnabled());
}
boolean isTransferToLocalEnabled() {
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouterParams.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouterParams.java
index f580da1..405202f 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouterParams.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouterParams.java
@@ -76,14 +76,14 @@
@DialogType
final int mDialogType;
- final boolean mMediaTransferReceiverDisabled;
+ final boolean mMediaTransferReceiverEnabled;
final boolean mOutputSwitcherEnabled;
final boolean mTransferToLocalEnabled;
final Bundle mExtras;
MediaRouterParams(@NonNull Builder builder) {
mDialogType = builder.mDialogType;
- mMediaTransferReceiverDisabled = builder.mMediaTransferDisabled;
+ mMediaTransferReceiverEnabled = builder.mMediaTransferEnabled;
mOutputSwitcherEnabled = builder.mOutputSwitcherEnabled;
mTransferToLocalEnabled = builder.mTransferToLocalEnabled;
@@ -101,12 +101,12 @@
}
/**
- * Gets whether declared {@link MediaTransferReceiver} is disabled.
+ * Gets whether declared {@link MediaTransferReceiver} is enabled.
*
- * @see Builder#setMediaTransferReceiverDisabled(boolean)
+ * @see Builder#setMediaTransferReceiverEnabled(boolean)
*/
- public boolean isMediaTransferReceiverDisabled() {
- return mMediaTransferReceiverDisabled;
+ public boolean isMediaTransferReceiverEnabled() {
+ return mMediaTransferReceiverEnabled;
}
/**
@@ -146,7 +146,7 @@
public static final class Builder {
@DialogType
int mDialogType = DIALOG_TYPE_DEFAULT;
- boolean mMediaTransferDisabled;
+ boolean mMediaTransferEnabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
boolean mOutputSwitcherEnabled;
boolean mTransferToLocalEnabled;
Bundle mExtras;
@@ -170,7 +170,7 @@
mDialogType = params.mDialogType;
mOutputSwitcherEnabled = params.mOutputSwitcherEnabled;
mTransferToLocalEnabled = params.mTransferToLocalEnabled;
- mMediaTransferDisabled = params.mMediaTransferReceiverDisabled;
+ mMediaTransferEnabled = params.mMediaTransferReceiverEnabled;
mExtras = params.mExtras == null ? null : new Bundle(params.mExtras);
}
@@ -194,12 +194,13 @@
}
/**
- * Sets whether declared {@link MediaTransferReceiver} is disabled. This method will be
- * no-op for Android versions earlier than Android R. The default value is {@code false}.
+ * Sets whether declared {@link MediaTransferReceiver} is enabled. The default value is
+ * {@code true}. This method will be no-op for Android versions earlier than Android R and
+ * it stays {@code false} on devices earlier than Android R.
* <p>
- * It is used to disable media transfer feature when {@link MediaTransferReceiver} is
+ * It can be used to disable media transfer feature when {@link MediaTransferReceiver} is
* declared.
- * If set to {@code true}, media transfer feature will be disabled
+ * If set to {@code false}, media transfer feature will be disabled
* even when {@link MediaTransferReceiver} is declared.
* <p>
* It is not recommended to change this value at runtime.
@@ -207,9 +208,9 @@
* @see MediaTransferReceiver
*/
@NonNull
- public Builder setMediaTransferReceiverDisabled(boolean disabled) {
+ public Builder setMediaTransferReceiverEnabled(boolean enabled) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- mMediaTransferDisabled = disabled;
+ mMediaTransferEnabled = enabled;
}
return this;
}
diff --git a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
index 59d3623..be66cde 100644
--- a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
+++ b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
@@ -714,6 +714,36 @@
}
@Test
+ fun testGetDialogViewModel() {
+ lateinit var navController: NavHostController
+ lateinit var model: TestViewModel
+
+ composeTestRule.setContent {
+ navController = rememberNavController()
+ NavHost(navController, first) {
+ composable(first) { }
+ dialog(second) {
+ model = viewModel(it)
+ }
+ }
+ }
+
+ composeTestRule.runOnIdle {
+ navController.navigate(second)
+ }
+
+ composeTestRule.runOnIdle {
+ navController.popBackStack()
+ }
+
+ assertThat(model.wasCleared).isFalse()
+
+ composeTestRule.waitForIdle()
+
+ assertThat(model.wasCleared).isTrue()
+ }
+
+ @Test
fun testGetGraphViewModelAfterRecompose() {
lateinit var navController: NavHostController
lateinit var model: TestViewModel
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogHost.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogHost.kt
index c8ef92c..700efea 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogHost.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogHost.kt
@@ -17,6 +17,7 @@
package androidx.navigation.compose
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
@@ -47,6 +48,12 @@
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
destination.content(backStackEntry)
}
+
+ DisposableEffect(backStackEntry) {
+ onDispose {
+ dialogNavigator.onTransitionComplete(backStackEntry)
+ }
+ }
}
}
}
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigator.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigator.kt
index 4d433ec..bfac256 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigator.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigator.kt
@@ -61,7 +61,11 @@
}
override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
- state.pop(popUpTo, savedState)
+ state.popWithTransition(popUpTo, savedState)
+ }
+
+ internal fun onTransitionComplete(entry: NavBackStackEntry) {
+ state.markTransitionComplete(entry)
}
/**
diff --git a/preference/OWNERS b/preference/OWNERS
index 50b530e..cc4dc56 100644
--- a/preference/OWNERS
+++ b/preference/OWNERS
@@ -1,2 +1 @@
-pavlis@google.com
lpf@google.com
diff --git a/profileinstaller/integration-tests/init-macrobenchmark/src/androidTest/java/androidx/profileinstaller/integration/macrobenchmark/ProfileinstallerStartupBenchmark.kt b/profileinstaller/integration-tests/init-macrobenchmark/src/androidTest/java/androidx/profileinstaller/integration/macrobenchmark/ProfileinstallerStartupBenchmark.kt
index 83d0a926..4ece2c3 100644
--- a/profileinstaller/integration-tests/init-macrobenchmark/src/androidTest/java/androidx/profileinstaller/integration/macrobenchmark/ProfileinstallerStartupBenchmark.kt
+++ b/profileinstaller/integration-tests/init-macrobenchmark/src/androidTest/java/androidx/profileinstaller/integration/macrobenchmark/ProfileinstallerStartupBenchmark.kt
@@ -20,6 +20,7 @@
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.filters.LargeTest
+import androidx.testutils.BASIC_COMPILATION_MODES
import androidx.testutils.createStartupCompilationParams
import androidx.testutils.measureStartup
import org.junit.Rule
@@ -48,6 +49,8 @@
companion object {
@Parameterized.Parameters(name = "startup={0},compilation={1}")
@JvmStatic
- fun parameters() = createStartupCompilationParams()
+ fun parameters() = createStartupCompilationParams(
+ compilationModes = BASIC_COMPILATION_MODES
+ )
}
}
\ No newline at end of file
diff --git a/room/room-paging/build.gradle b/room/room-paging/build.gradle
index 42d90c5..b25cd0d 100644
--- a/room/room-paging/build.gradle
+++ b/room/room-paging/build.gradle
@@ -47,7 +47,7 @@
implementation(project(":room:room-runtime"))
implementation(project(":room:room-ktx"))
- api("androidx.paging:paging-common:3.1.0-beta01")
+ api("androidx.paging:paging-common:3.1.0")
androidTestImplementation(libs.kotlinCoroutinesTest)
androidTestImplementation(libs.multidex)
diff --git a/room/room-runtime/build.gradle b/room/room-runtime/build.gradle
index 44afc76..48cdb86 100644
--- a/room/room-runtime/build.gradle
+++ b/room/room-runtime/build.gradle
@@ -34,8 +34,8 @@
dependencies {
api(project(":room:room-common"))
- api("androidx.sqlite:sqlite-framework:2.2.0-beta01")
- api("androidx.sqlite:sqlite:2.2.0-beta01")
+ api(project(":sqlite:sqlite-framework"))
+ api(project(":sqlite:sqlite"))
implementation("androidx.arch.core:core-runtime:2.0.1")
compileOnly("androidx.paging:paging-common:2.0.0")
compileOnly("androidx.lifecycle:lifecycle-livedata-core:2.0.0")
diff --git a/room/room-testing/build.gradle b/room/room-testing/build.gradle
index d665ee7..1c0acde 100644
--- a/room/room-testing/build.gradle
+++ b/room/room-testing/build.gradle
@@ -25,8 +25,8 @@
dependencies {
api(project(":room:room-common"))
api(project(":room:room-runtime"))
- api("androidx.sqlite:sqlite:2.2.0-beta01")
- api("androidx.sqlite:sqlite-framework:2.2.0-beta01")
+ api(project(":sqlite:sqlite"))
+ api(project(":sqlite:sqlite-framework"))
api(project(":room:room-migration"))
implementation("androidx.arch.core:core-runtime:2.0.1")
api(libs.junit)
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java
index dc5253e..6f72b20 100644
--- a/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/media/SampleMediaRouterActivity.java
@@ -756,7 +756,7 @@
@Override
public MediaRouterParams getRouterParams() {
return new MediaRouterParams.Builder(super.getRouterParams())
- .setMediaTransferReceiverDisabled(true)
+ .setMediaTransferReceiverEnabled(false)
.build();
}
}
diff --git a/samples/SupportPreferenceDemos/OWNERS b/samples/SupportPreferenceDemos/OWNERS
index f1b8c7e..b2c38b4 100644
--- a/samples/SupportPreferenceDemos/OWNERS
+++ b/samples/SupportPreferenceDemos/OWNERS
@@ -1,2 +1 @@
-pavlis@google.com
lpf@google.com
\ No newline at end of file
diff --git a/slice/slice-view/src/main/res/values-it/strings.xml b/slice/slice-view/src/main/res/values-it/strings.xml
index 9087d7a..62c8052 100644
--- a/slice/slice-view/src/main/res/values-it/strings.xml
+++ b/slice/slice-view/src/main/res/values-it/strings.xml
@@ -22,16 +22,16 @@
<string name="abc_slice_show_more" msgid="1112789899890391107">"Mostra altro"</string>
<string name="abc_slice_updated" msgid="7932359091871934205">"Ultimo aggiornamento: <xliff:g id="TIME">%1$s</xliff:g>"</string>
<plurals name="abc_slice_duration_min" formatted="false" msgid="7664017844210142826">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> min fa</item>
<item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min fa</item>
- <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> min fa</item>
</plurals>
<plurals name="abc_slice_duration_years" formatted="false" msgid="2628491538787454021">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> anno fa</item>
<item quantity="other"><xliff:g id="ID_2">%d</xliff:g> anni fa</item>
- <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> anno fa</item>
</plurals>
<plurals name="abc_slice_duration_days" formatted="false" msgid="8356547162075064530">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> giorno fa</item>
<item quantity="other"><xliff:g id="ID_2">%d</xliff:g> giorni fa</item>
- <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> giorno fa</item>
</plurals>
<string name="abc_slice_error" msgid="1794214973158263497">"Impossibile collegarsi"</string>
</resources>
diff --git a/slice/slice-view/src/main/res/values-pt-rPT/strings.xml b/slice/slice-view/src/main/res/values-pt-rPT/strings.xml
index 34a67ff..08c5c19 100644
--- a/slice/slice-view/src/main/res/values-pt-rPT/strings.xml
+++ b/slice/slice-view/src/main/res/values-pt-rPT/strings.xml
@@ -22,16 +22,16 @@
<string name="abc_slice_show_more" msgid="1112789899890391107">"Mostrar mais"</string>
<string name="abc_slice_updated" msgid="7932359091871934205">"Atualização: <xliff:g id="TIME">%1$s</xliff:g>"</string>
<plurals name="abc_slice_duration_min" formatted="false" msgid="7664017844210142826">
- <item quantity="other">Há <xliff:g id="ID_2">%d</xliff:g> min</item>
<item quantity="one">Há <xliff:g id="ID_1">%d</xliff:g> min</item>
+ <item quantity="other">Há <xliff:g id="ID_2">%d</xliff:g> min</item>
</plurals>
<plurals name="abc_slice_duration_years" formatted="false" msgid="2628491538787454021">
- <item quantity="other">Há <xliff:g id="ID_2">%d</xliff:g> anos</item>
<item quantity="one">Há <xliff:g id="ID_1">%d</xliff:g> ano</item>
+ <item quantity="other">Há <xliff:g id="ID_2">%d</xliff:g> anos</item>
</plurals>
<plurals name="abc_slice_duration_days" formatted="false" msgid="8356547162075064530">
- <item quantity="other">Há <xliff:g id="ID_2">%d</xliff:g> dias</item>
<item quantity="one">Há <xliff:g id="ID_1">%d</xliff:g> dia</item>
+ <item quantity="other">Há <xliff:g id="ID_2">%d</xliff:g> dias</item>
</plurals>
<string name="abc_slice_error" msgid="1794214973158263497">"Não foi possível ligar"</string>
</resources>
diff --git a/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt b/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt
index ad1ce6b..42a98af 100644
--- a/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt
+++ b/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt
@@ -27,14 +27,33 @@
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
/**
- * Default compilation modes to test for all AndroidX macrobenchmarks.
+ * Basic, always-usable compilation modes, when baseline profiles aren't available.
+ *
+ * Over time, it's expected very few macrobenchmarks will reference this directly, as more libraries
+ * gain baseline profiles.
*/
-val COMPILATION_MODES = listOf(
- CompilationMode.None,
- CompilationMode.Interpreted,
- CompilationMode.BaselineProfile,
- CompilationMode.SpeedProfile()
-)
+val BASIC_COMPILATION_MODES = if (Build.VERSION.SDK_INT < 24) {
+ // other modes aren't supported
+ listOf(CompilationMode.None)
+} else {
+ listOf(
+ CompilationMode.None,
+ CompilationMode.Interpreted,
+ CompilationMode.SpeedProfile()
+ )
+}
+
+/**
+ * Default compilation modes to test for all AndroidX macrobenchmarks.
+ *
+ * Baseline profiles are only supported from Nougat (API 24),
+ * currently through Android 11 (API 30)
+ */
+val COMPILATION_MODES = if (Build.VERSION.SDK_INT in 24..30) {
+ listOf(CompilationMode.BaselineProfile)
+} else {
+ emptyList()
+} + BASIC_COMPILATION_MODES
/**
* Temporary, while transitioning to new metrics
@@ -73,7 +92,11 @@
StartupMode.HOT,
StartupMode.WARM,
StartupMode.COLD
- ),
+ ).filter {
+ // skip StartupMode.HOT on Angler, API 23 - it works locally with same build on Bullhead,
+ // but not in Jetpack CI (b/204572406)
+ !(Build.VERSION.SDK_INT == 23 && it == StartupMode.HOT && Build.DEVICE == "angler")
+ },
compilationModes: List<CompilationMode> = COMPILATION_MODES
): List<Array<Any>> = mutableListOf<Array<Any>>().apply {
for (startupMode in startupModes) {
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/TempListUtils.kt b/text/text/src/main/java/androidx/compose/ui/text/android/TempListUtils.kt
index 2de2410..982d05c 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/TempListUtils.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/TempListUtils.kt
@@ -24,6 +24,10 @@
/**
* Iterates through a [List] using the index and calls [action] for each item.
* This does not allocate an iterator like [Iterable.forEach].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
@@ -37,6 +41,10 @@
/**
* Applies the given [transform] function to each element of the original collection
* and appends the results to the given [destination].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T, R, C : MutableCollection<in R>> List<T>.fastMapTo(
@@ -55,6 +63,10 @@
* to each pair of two adjacent elements in this collection.
*
* The returned list is empty if this collection contains less than two elements.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
internal inline fun <T, R> List<T>.fastZipWithNext(transform: (T, T) -> R): List<R> {
diff --git a/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
index 78ff9ef..8b165bb 100644
--- a/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
+++ b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
@@ -40,5 +40,6 @@
implementation projectOrArtifact(":activity:activity-ktx")
implementation 'androidx.core:core-ktx'
implementation(libs.material)
+ implementation(project(":profileinstaller:profileinstaller"))
implementation 'androidx.wear:wear:1.1.0'
}
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index a63d79c..b1cf747 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -145,15 +145,15 @@
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues getButtonsContentPadding();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues getChipsContentPadding();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues getConfirmationContentPadding();
- method public long getIndefiniteDuration();
- method public long getLongDuration();
- method public long getShortDuration();
+ method public long getIndefiniteDurationMillis();
+ method public long getLongDurationMillis();
+ method public long getShortDurationMillis();
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.PaddingValues ButtonsContentPadding;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.PaddingValues ChipsContentPadding;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.PaddingValues ConfirmationContentPadding;
- property public final long IndefiniteDuration;
- property public final long LongDuration;
- property public final long ShortDuration;
+ property public final long IndefiniteDurationMillis;
+ property public final long LongDurationMillis;
+ property public final long ShortDurationMillis;
field public static final androidx.wear.compose.material.DialogDefaults INSTANCE;
}
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index 8855f3c..c6e0939 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -145,15 +145,15 @@
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues getButtonsContentPadding();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues getChipsContentPadding();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues getConfirmationContentPadding();
- method public long getIndefiniteDuration();
- method public long getLongDuration();
- method public long getShortDuration();
+ method public long getIndefiniteDurationMillis();
+ method public long getLongDurationMillis();
+ method public long getShortDurationMillis();
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.PaddingValues ButtonsContentPadding;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.PaddingValues ChipsContentPadding;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.PaddingValues ConfirmationContentPadding;
- property public final long IndefiniteDuration;
- property public final long LongDuration;
- property public final long ShortDuration;
+ property public final long IndefiniteDurationMillis;
+ property public final long LongDurationMillis;
+ property public final long ShortDurationMillis;
field public static final androidx.wear.compose.material.DialogDefaults INSTANCE;
}
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index a63d79c..b1cf747 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -145,15 +145,15 @@
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues getButtonsContentPadding();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues getChipsContentPadding();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues getConfirmationContentPadding();
- method public long getIndefiniteDuration();
- method public long getLongDuration();
- method public long getShortDuration();
+ method public long getIndefiniteDurationMillis();
+ method public long getLongDurationMillis();
+ method public long getShortDurationMillis();
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.PaddingValues ButtonsContentPadding;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.PaddingValues ChipsContentPadding;
property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.PaddingValues ConfirmationContentPadding;
- property public final long IndefiniteDuration;
- property public final long LongDuration;
- property public final long ShortDuration;
+ property public final long IndefiniteDurationMillis;
+ property public final long LongDurationMillis;
+ property public final long ShortDurationMillis;
field public static final androidx.wear.compose.material.DialogDefaults INSTANCE;
}
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Dialog.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Dialog.kt
index 2916b1d..ec24d8e 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Dialog.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Dialog.kt
@@ -196,8 +196,8 @@
* @param scrollState The scroll state for the dialog so that the scroll position can be displayed
* e.g. by the [PositionIndicator] passed to [Scaffold].
* @param durationMillis The number of milliseconds for which the dialog is displayed,
- * must be positive. Expected to be [DialogDefaults.ShortDuration], [DialogDefaults.LongDuration],
- * or [DialogDefaults.IndefiniteDuration].
+ * must be positive. Suggested values are [DialogDefaults.ShortDurationMillis],
+ * [DialogDefaults.LongDurationMillis] or [DialogDefaults.IndefiniteDurationMillis].
* @param backgroundColor [Color] representing the background color for this dialog.
* @param contentColor [Color] representing the content color for this dialog.
* @param iconTintColor Icon [Color] that defaults to the [contentColor],
@@ -211,7 +211,7 @@
modifier: Modifier = Modifier,
icon: @Composable (() -> Unit)? = null,
scrollState: ScrollState = rememberScrollState(),
- durationMillis: Long = DialogDefaults.ShortDuration,
+ durationMillis: Long = DialogDefaults.ShortDurationMillis,
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
iconTintColor: Color = contentColor,
@@ -286,17 +286,17 @@
/**
* Short duration for showing [ConfirmationDialog].
*/
- val ShortDuration = 4000L
+ val ShortDurationMillis = 4000L
/**
* Long duration for showing [ConfirmationDialog].
*/
- val LongDuration = 10000L
+ val LongDurationMillis = 10000L
/**
* Show [ConfirmationDialog] indefinitely (supports swipe-to-dismiss).
*/
- val IndefiniteDuration = Long.MAX_VALUE
+ val IndefiniteDurationMillis = Long.MAX_VALUE
/**
* Spacing between [Button]s.
diff --git a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
index e874449..47863ba 100644
--- a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
+++ b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
@@ -583,6 +583,12 @@
onDestroyLatch.countDown()
backgroundHandlerThread.quitSafely()
}
+
+ override fun setComplicationSlotConfigExtrasChangeCallback(
+ callback: WatchFace.ComplicationSlotConfigExtrasChangeCallback?
+ ) {
+ complicationSlotsManager.configExtrasChangeCallback = callback
+ }
}
if (!shouldTimeout) {
WatchFace.registerEditorDelegate(watchComponentName, editorDelegate)
@@ -1440,6 +1446,57 @@
}
@Test
+ public fun mutable_configExtras() {
+ // Invoke the test data source chooser to record the result.
+ ComplicationHelperActivity.useTestComplicationDataSourceChooserActivity = true
+ // Invoke the data source chooser without checking for permissions first.
+ ComplicationHelperActivity.skipPermissionCheck = true
+
+ val chosenComplicationDataSourceInfo = ComplicationDataSourceInfo(
+ "TestDataSource3App",
+ "TestDataSource3",
+ Icon.createWithBitmap(
+ Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+ ),
+ ComplicationType.LONG_TEXT,
+ dataSource3
+ )
+ TestComplicationDataSourceChooserActivity.lastIntent = null
+ TestComplicationDataSourceChooserActivity.resultIntent = Intent().apply {
+ putExtra(
+ ComplicationDataSourceChooserIntent.EXTRA_PROVIDER_INFO,
+ chosenComplicationDataSourceInfo.toWireComplicationProviderInfo()
+ )
+ }
+
+ val scenario = createOnWatchFaceEditingTestActivity(
+ emptyList(),
+ listOf(leftComplication, rightComplication)
+ )
+
+ lateinit var editorSession: EditorSession
+ scenario.onActivity { activity ->
+ editorSession = activity.editorSession
+ }
+
+ runBlocking {
+ rightComplication.configExtras = Bundle().apply {
+ putString(PROVIDER_CHOOSER_EXTRA_KEY, "Updated")
+ }
+
+ val chosenComplicationDataSource =
+ editorSession.openComplicationDataSourceChooser(RIGHT_COMPLICATION_ID)
+ assertThat(chosenComplicationDataSource).isNotNull()
+
+ assertThat(
+ TestComplicationDataSourceChooserActivity.lastIntent?.extras?.getString(
+ PROVIDER_CHOOSER_EXTRA_KEY
+ )
+ ).isEqualTo("Updated")
+ }
+ }
+
+ @Test
public fun getComplicationIdAt() {
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
diff --git a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
index ab7bdf8..8690ac7 100644
--- a/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
+++ b/wear/watchface/watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
@@ -889,6 +889,7 @@
// Note this has to be done last to ensure tests are not racy.
if (this::editorDelegate.isInitialized) {
+ editorDelegate.setComplicationSlotConfigExtrasChangeCallback(null)
editorDelegate.onDestroy()
}
}
@@ -910,6 +911,14 @@
)
fetchComplicationsDataJob = fetchComplicationsData(backgroundCoroutineScope)
+
+ editorDelegate.setComplicationSlotConfigExtrasChangeCallback(
+ object : WatchFace.ComplicationSlotConfigExtrasChangeCallback {
+ override fun onComplicationSlotConfigExtrasChanged() {
+ maybeUpdateComplicationSlotsState()
+ }
+ }
+ )
}
override val showComplicationDeniedDialogIntent
diff --git a/wear/watchface/watchface/api/current.txt b/wear/watchface/watchface/api/current.txt
index a8af996..f442dc5 100644
--- a/wear/watchface/watchface/api/current.txt
+++ b/wear/watchface/watchface/api/current.txt
@@ -45,6 +45,7 @@
method @UiThread public boolean isEnabled();
method @UiThread public void render(android.graphics.Canvas canvas, java.time.ZonedDateTime zonedDateTime, androidx.wear.watchface.RenderParameters renderParameters);
method @UiThread public void renderHighlightLayer(android.graphics.Canvas canvas, java.time.ZonedDateTime zonedDateTime, androidx.wear.watchface.RenderParameters renderParameters);
+ method public void setConfigExtras(android.os.Bundle value);
property @UiThread public final int accessibilityTraversalIndex;
property public final int boundsType;
property public final androidx.wear.watchface.CanvasComplicationFactory canvasComplicationFactory;
diff --git a/wear/watchface/watchface/api/public_plus_experimental_current.txt b/wear/watchface/watchface/api/public_plus_experimental_current.txt
index a8af996..f442dc5 100644
--- a/wear/watchface/watchface/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface/api/public_plus_experimental_current.txt
@@ -45,6 +45,7 @@
method @UiThread public boolean isEnabled();
method @UiThread public void render(android.graphics.Canvas canvas, java.time.ZonedDateTime zonedDateTime, androidx.wear.watchface.RenderParameters renderParameters);
method @UiThread public void renderHighlightLayer(android.graphics.Canvas canvas, java.time.ZonedDateTime zonedDateTime, androidx.wear.watchface.RenderParameters renderParameters);
+ method public void setConfigExtras(android.os.Bundle value);
property @UiThread public final int accessibilityTraversalIndex;
property public final int boundsType;
property public final androidx.wear.watchface.CanvasComplicationFactory canvasComplicationFactory;
diff --git a/wear/watchface/watchface/api/restricted_current.txt b/wear/watchface/watchface/api/restricted_current.txt
index 958905b..50cb2dc 100644
--- a/wear/watchface/watchface/api/restricted_current.txt
+++ b/wear/watchface/watchface/api/restricted_current.txt
@@ -45,6 +45,7 @@
method @UiThread public boolean isEnabled();
method @UiThread public void render(android.graphics.Canvas canvas, java.time.ZonedDateTime zonedDateTime, androidx.wear.watchface.RenderParameters renderParameters);
method @UiThread public void renderHighlightLayer(android.graphics.Canvas canvas, java.time.ZonedDateTime zonedDateTime, androidx.wear.watchface.RenderParameters renderParameters);
+ method public void setConfigExtras(android.os.Bundle value);
property @UiThread public final int accessibilityTraversalIndex;
property public final int boundsType;
property public final androidx.wear.watchface.CanvasComplicationFactory canvasComplicationFactory;
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index 8e16e8c..7d4df5a 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -208,7 +208,8 @@
* Editors need to know the initial state of a complication slot to predict the effects of making a
* style change.
* @param configExtras Extras to be merged into the Intent sent when invoking the complication data
- * source chooser activity.
+ * source chooser activity. This features is intended for OEM watch faces where they have elements
+ * that behave like a complication but are in fact entirely watch face specific.
* @param fixedComplicationDataSource Whether or not the complication data source is fixed (i.e.
* can't be changed by the user). This is useful for watch faces built around specific
* complications.
@@ -226,7 +227,7 @@
defaultDataSourceType: ComplicationType,
@get:JvmName("isInitiallyEnabled")
public val initiallyEnabled: Boolean,
- public val configExtras: Bundle,
+ configExtras: Bundle,
@get:JvmName("isFixedComplicationDataSource")
public val fixedComplicationDataSource: Boolean,
public val tapFilter: ComplicationTapFilter
@@ -238,6 +239,17 @@
internal lateinit var complicationSlotsManager: ComplicationSlotsManager
/**
+ * Extras to be merged into the Intent sent when invoking the complication data source chooser
+ * activity.
+ */
+ public var configExtras: Bundle = configExtras
+ set(value) {
+ field = value
+ complicationSlotsManager.configExtrasChangeCallback
+ ?.onComplicationSlotConfigExtrasChanged()
+ }
+
+ /**
* The [CanvasComplication] used to render the complication. This can't be used until after
* [WatchFaceService.createWatchFace] has completed.
*/
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
index 5def43e..5da65c6 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
@@ -118,6 +118,11 @@
private val complicationListeners = HashSet<TapCallback>()
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public var configExtrasChangeCallback: WatchFace.ComplicationSlotConfigExtrasChangeCallback? =
+ null
+
@VisibleForTesting
internal constructor(
complicationSlotCollection: Collection<ComplicationSlot>,
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index d8e9b4e..91b47c0 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -254,6 +254,21 @@
/** Signals that the activity is going away and resources should be released. */
public fun onDestroy()
+
+ /** Sets a callback to observe an y changes to [ComplicationSlot.configExtras]. */
+ public fun setComplicationSlotConfigExtrasChangeCallback(
+ callback: ComplicationSlotConfigExtrasChangeCallback?
+ )
+ }
+
+ /**
+ * Used to inform EditorSession about changes to [ComplicationSlot.configExtras].
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public interface ComplicationSlotConfigExtrasChangeCallback {
+ public fun onComplicationSlotConfigExtrasChanged()
}
/**
@@ -758,6 +773,12 @@
return screenShot
}
+ override fun setComplicationSlotConfigExtrasChangeCallback(
+ callback: WatchFace.ComplicationSlotConfigExtrasChangeCallback?
+ ) {
+ complicationSlotsManager.configExtrasChangeCallback = callback
+ }
+
override fun onDestroy(): Unit = TraceEvent("WFEditorDelegate.onDestroy").use {
if (watchState.isHeadless) {
this@WatchFaceImpl.onDestroy()
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
index 362edee..8a207d8 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
@@ -170,6 +170,8 @@
}
fun onDestroy() {
- engine = null
+ uiThreadCoroutineScope.launch {
+ engine = null
+ }
}
}