commit | b9b2bea3a0881736b85c426275358a99a8cd1e74 | [log] [tgz] |
---|---|---|
author | Misha Kulaha | Mon Aug 21 15:52:17 2023 +0000 |
committer | Gerrit Code Review | Mon Aug 21 15:52:17 2023 +0000 |
tree | f09ee33db3c5b32ffa03fb32df88d9976d2ce1c9 | |
parent | 324b5cec7771aa42305ab94f03cb273a365b5f4d [diff] | |
parent | 31ba2f8f6cab9f13c4b5a86c6ce98d6938855369 [diff] |
Merge "Update typefonts for M3 Cards" into androidx-main
diff --git a/browser/browser/api/api_lint.ignore b/browser/browser/api/api_lint.ignore index 68c85ec..bec6b20 100644 --- a/browser/browser/api/api_lint.ignore +++ b/browser/browser/api/api_lint.ignore
@@ -157,6 +157,8 @@ androidx.browser.customtabs.CustomTabsIntent does not declare a `getSecondaryToolbarViews()` method matching method androidx.browser.customtabs.CustomTabsIntent.Builder.setSecondaryToolbarViews(android.widget.RemoteViews,int[],android.app.PendingIntent) MissingGetterMatchingBuilder: androidx.browser.customtabs.CustomTabsIntent.Builder#setSession(androidx.browser.customtabs.CustomTabsSession): androidx.browser.customtabs.CustomTabsIntent does not declare a `getSession()` method matching method androidx.browser.customtabs.CustomTabsIntent.Builder.setSession(androidx.browser.customtabs.CustomTabsSession) +MissingGetterMatchingBuilder: androidx.browser.customtabs.CustomTabsIntent.Builder#setShareIdentityEnabled(boolean): + androidx.browser.customtabs.CustomTabsIntent does not declare a `isShareIdentityEnabled()` method matching method androidx.browser.customtabs.CustomTabsIntent.Builder.setShareIdentityEnabled(boolean) MissingGetterMatchingBuilder: androidx.browser.customtabs.CustomTabsIntent.Builder#setShareState(int): androidx.browser.customtabs.CustomTabsIntent does not declare a `getShareState()` method matching method androidx.browser.customtabs.CustomTabsIntent.Builder.setShareState(int) MissingGetterMatchingBuilder: androidx.browser.customtabs.CustomTabsIntent.Builder#setShowTitle(boolean):
diff --git a/browser/browser/api/current.txt b/browser/browser/api/current.txt index 4878f05..68c6b14 100644 --- a/browser/browser/api/current.txt +++ b/browser/browser/api/current.txt
@@ -199,6 +199,7 @@ method public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarViews(android.widget.RemoteViews, int[]?, android.app.PendingIntent?); method public androidx.browser.customtabs.CustomTabsIntent.Builder setSendToExternalDefaultHandlerEnabled(boolean); method public androidx.browser.customtabs.CustomTabsIntent.Builder setSession(androidx.browser.customtabs.CustomTabsSession); + method public androidx.browser.customtabs.CustomTabsIntent.Builder setShareIdentityEnabled(boolean); method public androidx.browser.customtabs.CustomTabsIntent.Builder setShareState(int); method public androidx.browser.customtabs.CustomTabsIntent.Builder setShowOnToolbarEnabled(boolean); method public androidx.browser.customtabs.CustomTabsIntent.Builder setShowTitle(boolean);
diff --git a/browser/browser/api/restricted_current.txt b/browser/browser/api/restricted_current.txt index 2eab941..e978f0e 100644 --- a/browser/browser/api/restricted_current.txt +++ b/browser/browser/api/restricted_current.txt
@@ -210,6 +210,7 @@ method public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarViews(android.widget.RemoteViews, int[]?, android.app.PendingIntent?); method public androidx.browser.customtabs.CustomTabsIntent.Builder setSendToExternalDefaultHandlerEnabled(boolean); method public androidx.browser.customtabs.CustomTabsIntent.Builder setSession(androidx.browser.customtabs.CustomTabsSession); + method public androidx.browser.customtabs.CustomTabsIntent.Builder setShareIdentityEnabled(boolean); method public androidx.browser.customtabs.CustomTabsIntent.Builder setShareState(int); method public androidx.browser.customtabs.CustomTabsIntent.Builder setShowOnToolbarEnabled(boolean); method public androidx.browser.customtabs.CustomTabsIntent.Builder setShowTitle(boolean);
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java index d19d42f..53d1504 100644 --- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java +++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
@@ -20,6 +20,7 @@ import static androidx.annotation.Dimension.PX; import android.app.Activity; +import android.app.ActivityOptions; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -535,12 +536,13 @@ private final CustomTabColorSchemeParams.Builder mDefaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder(); @Nullable private ArrayListmMenuItems; - @Nullable private Bundle mStartAnimationBundle; + @Nullable private ActivityOptions mActivityOptions; @Nullable private ArrayListmActionButtons; @Nullable private SparseArraymColorSchemeParamBundles; @Nullable private Bundle mDefaultColorSchemeBundle; @ShareState private int mShareState = SHARE_STATE_DEFAULT; private boolean mInstantAppsEnabled = true; + private boolean mShareIdentity; /** * Creates a {@link CustomTabsIntent.Builder} object associated with no @@ -913,8 +915,13 @@ @SuppressWarnings("NullAway") // TODO: b/141869399 public Builder setStartAnimations( @NonNull Context context, @AnimRes int enterResId, @AnimRes int exitResId) { - mStartAnimationBundle = ActivityOptionsCompat.makeCustomAnimation( - context, enterResId, exitResId).toBundle(); + // We use ActivityOptions, not ActivityOptionsCompat, to build the start activity + // options, since we might set another option (share identity, which is not + // available yet via ActivityOptionsCompat) before turning it to a Bundle. + // TODO(b/296463161): Update androidx.core.core lib to support the new option via + // ActivityOptionsCompat and use it here instead of ActivityOptions. + mActivityOptions = ActivityOptions.makeCustomAnimation( + context, enterResId, exitResId); return this; } @@ -1161,6 +1168,16 @@ } /** + * Allow Custom Tabs to obtain the caller's identity i.e. package name. + * @param enabled Whether the identity sharing is enabled. + */ + @NonNull + public Builder setShareIdentityEnabled(boolean enabled) { + mShareIdentity = enabled; + return this; + } + + /** * Combines all the options that have been set and returns a new {@link CustomTabsIntent} * object. */ @@ -1195,7 +1212,14 @@ setCurrentLocaleAsDefaultAcceptLanguage(); } - return new CustomTabsIntent(mIntent, mStartAnimationBundle); + Bundle bundle = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + setShareIdentityEnabled(); + } + if (mActivityOptions != null) { + bundle = mActivityOptions.toBundle(); + } + return new CustomTabsIntent(mIntent, bundle); } /** @@ -1214,6 +1238,14 @@ } } } + + @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + private void setShareIdentityEnabled() { + if (mActivityOptions == null) { + mActivityOptions = Api23Impl.makeBasicActivityOptions(); + } + Api34Impl.setShareIdentityEnabled(mActivityOptions, mShareIdentity); + } } /** @@ -1395,6 +1427,14 @@ return intent.getBooleanExtra(EXTRA_SHOW_ON_TOOLBAR, false); } + @RequiresApi(api = Build.VERSION_CODES.M) + private static class Api23Impl { + @DoNotInline + static ActivityOptions makeBasicActivityOptions() { + return ActivityOptions.makeBasic(); + } + } + @RequiresApi(api = Build.VERSION_CODES.N) private static class Api24Impl { @DoNotInline @@ -1404,4 +1444,12 @@ return (defaultLocaleList.size() > 0) ? defaultLocaleList.get(0).toLanguageTag(): null; } } + + @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + private static class Api34Impl { + @DoNotInline + static void setShareIdentityEnabled(ActivityOptions activityOptions, boolean enabled) { + activityOptions.setShareIdentityEnabled(enabled); + } + } }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraCoordinatorAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraCoordinatorAdapter.kt index 06d84bf..bfb2ddb 100644 --- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraCoordinatorAdapter.kt +++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraCoordinatorAdapter.kt
@@ -20,7 +20,7 @@ import androidx.annotation.VisibleForTesting import androidx.camera.camera2.pipe.CameraDevices import androidx.camera.camera2.pipe.CameraId -import androidx.camera.camera2.pipe.integration.internal.CameraGraphCreator +import androidx.camera.camera2.pipe.CameraPipe import androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop import androidx.camera.core.CameraInfo @@ -32,15 +32,26 @@ @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java class CameraCoordinatorAdapter( + private var cameraPipe: CameraPipe?, cameraDevices: CameraDevices, - private val cameraGraphCreator: CameraGraphCreator ) : CameraCoordinator { - @VisibleForTesting val cameraInternalMap = mutableMapOf() - @VisibleForTesting var concurrentCameraIdsSet = mutableSetOf>() - @VisibleForTesting var concurrentCameraIdMap = mutableMapOf>() - @VisibleForTesting var activeConcurrentCameraInfosList = mutableListOf() - @VisibleForTesting var concurrentMode: Int = CAMERA_OPERATING_MODE_UNSPECIFIED - @VisibleForTesting var concurrentModeOn = false + @VisibleForTesting + val cameraInternalMap = mutableMapOf() + + @VisibleForTesting + var concurrentCameraIdsSet = mutableSetOf>() + + @VisibleForTesting + var concurrentCameraIdMap = mutableMapOf>() + + @VisibleForTesting + var activeConcurrentCameraInfosList = mutableListOf() + + @VisibleForTesting + var concurrentMode: Int = CAMERA_OPERATING_MODE_UNSPECIFIED + + @VisibleForTesting + var concurrentModeOn = false init { concurrentCameraIdsSet = cameraDevices.awaitConcurrentCameraIds()!!.toMutableSet() @@ -85,8 +96,16 @@ override fun setActiveConcurrentCameraInfos(cameraInfos: MutableList) { activeConcurrentCameraInfosList = cameraInfos - for (cameraInternalAdapter in cameraInternalMap.values) { - cameraInternalAdapter.resumeRefresh() + val graphConfigs = cameraInternalMap.values.map { + checkNotNull(it.getDeferredCameraGraphConfig()) { + "Every CameraInternal instance is expected to have a deferred CameraGraph config " + + "when the active concurrent CameraInfos are set!" + } + } + val cameraGraphs = checkNotNull(cameraPipe).createCameraGraphs(graphConfigs) + check(cameraGraphs.size == cameraInternalMap.size) + for ((cameraInternalAdapter, cameraGraph) in cameraInternalMap.values.zip(cameraGraphs)) { + cameraInternalAdapter.resumeDeferredCameraGraphCreation(cameraGraph) } } @@ -114,12 +133,11 @@ override fun setCameraOperatingMode(@CameraOperatingMode cameraOperatingMode: Int) { concurrentMode = cameraOperatingMode concurrentModeOn = cameraOperatingMode == CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT - cameraGraphCreator.setConcurrentModeOn(concurrentModeOn) for (cameraInternalAdapter in cameraInternalMap.values) { if (cameraOperatingMode == CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT) { - cameraInternalAdapter.pauseRefresh() + cameraInternalAdapter.setCameraGraphCreationMode(createImmediately = false) } else if (cameraOperatingMode == CameraCoordinator.CAMERA_OPERATING_MODE_SINGLE) { - cameraInternalAdapter.resumeRefresh() + cameraInternalAdapter.setCameraGraphCreationMode(createImmediately = true) } } } @@ -131,6 +149,7 @@ } override fun shutdown() { + cameraPipe = null cameraInternalMap.clear() concurrentCameraIdsSet.clear() concurrentCameraIdMap.clear()
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt index 1c3dc2c..a22309f 100644 --- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt +++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
@@ -72,7 +72,9 @@ private var mAvailableCamerasSelector: CameraSelector? = availableCamerasSelector private var mAvailableCameraIds: Listprivate val cameraCoordinator: CameraCoordinatorAdapter = CameraCoordinatorAdapter( - appComponent.getCameraDevices(), appComponent.getCameraGraphCreator()) + appComponent.getCameraPipe(), + appComponent.getCameraDevices(), + ) init { debug { "Created CameraFactoryAdapter" }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt index 5b39179..5020ead 100644 --- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt +++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
@@ -19,6 +19,7 @@ package androidx.camera.camera2.pipe.integration.adapter import androidx.annotation.RequiresApi +import androidx.camera.camera2.pipe.CameraGraph import androidx.camera.camera2.pipe.CameraPipe import androidx.camera.camera2.pipe.core.Log.debug import androidx.camera.camera2.pipe.integration.config.CameraConfig @@ -60,11 +61,15 @@ // TODO: Consider preloading the list of camera ids and metadata. } - fun pauseRefresh() = threads.scope.launch(threads.backgroundDispatcher) { - useCaseManager.pauseRefresh() + internal fun setCameraGraphCreationMode(createImmediately: Boolean) { + useCaseManager.setCameraGraphCreationMode(createImmediately) } - fun resumeRefresh() = threads.scope.launch(threads.backgroundDispatcher) { - useCaseManager.resumeRefresh() + + internal fun getDeferredCameraGraphConfig(): CameraGraph.Config? = + useCaseManager.getDeferredCameraGraphConfig() + + internal fun resumeDeferredCameraGraphCreation(cameraGraph: CameraGraph) { + useCaseManager.resumeDeferredComponentCreation(cameraGraph) } // Load / unload methods
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt index 31d67ce..2ba8e67 100644 --- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt +++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
@@ -23,7 +23,6 @@ import androidx.camera.camera2.pipe.CameraDevices import androidx.camera.camera2.pipe.CameraPipe import androidx.camera.camera2.pipe.integration.impl.CameraInteropStateCallbackRepository -import androidx.camera.camera2.pipe.integration.internal.CameraGraphCreator import androidx.camera.core.impl.CameraFactory import androidx.camera.core.impl.CameraThreadConfig import dagger.Component @@ -79,8 +78,6 @@ fun getCameraPipe(): CameraPipe fun getCameraDevices(): CameraDevices - fun getCameraGraphCreator(): CameraGraphCreator - @Component.Builder interface Builder { fun config(config: CameraAppConfig): Builder
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt index aa17277..84bd53e 100644 --- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt +++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -41,7 +41,6 @@ import androidx.camera.camera2.pipe.integration.config.CameraScope import androidx.camera.camera2.pipe.integration.config.UseCaseCameraComponent import androidx.camera.camera2.pipe.integration.config.UseCaseCameraConfig -import androidx.camera.camera2.pipe.integration.internal.CameraGraphCreator import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop import androidx.camera.core.UseCase @@ -54,7 +53,6 @@ import javax.inject.Provider import kotlinx.coroutines.Job import kotlinx.coroutines.joinAll -import kotlinx.coroutines.runBlocking /** * This class keeps track of the currently attached and active [UseCase]'s for a specific camera. @@ -87,7 +85,6 @@ @CameraScope class UseCaseManager @Inject constructor( private val cameraPipe: CameraPipe, - private val cameraGraphCreator: CameraGraphCreator, private val callbackMap: CameraCallbackMap, private val requestListener: ComboRequestListener, private val cameraConfig: CameraConfig, @@ -115,7 +112,10 @@ private var activeResumeEnabled = false @GuardedBy("lock") - private var refreshAttached = true + private var shouldCreateCameraGraphImmediately = true + + @GuardedBy("lock") + private var deferredUseCaseManagerConfig: UseCaseManagerConfig? = null private val meteringRepeating by lazy { MeteringRepeating.Builder( @@ -141,13 +141,17 @@ private val allControls = controls.toMutableSet().apply { add(camera2CameraControl) } - fun pauseRefresh() = synchronized(lock) { - refreshAttached = false + internal fun setCameraGraphCreationMode(createImmediately: Boolean) = synchronized(lock) { + shouldCreateCameraGraphImmediately = createImmediately + if (shouldCreateCameraGraphImmediately) { + // Clear the UseCaseManager configuration that haven't been "resumed" when we return + // to single camera operating mode early. + deferredUseCaseManagerConfig = null + } } - fun resumeRefresh() = synchronized(lock) { - refreshAttached = true - refreshAttachedUseCases(attachedUseCases) + internal fun getDeferredCameraGraphConfig() = synchronized(lock) { + deferredUseCaseManagerConfig?.cameraGraphConfig } /** @@ -283,9 +287,6 @@ @GuardedBy("lock") private fun refreshAttachedUseCases(newUseCases: Set) { - if (!refreshAttached) { - return - } val useCases = newUseCases.toList() // Close prior camera graph @@ -315,28 +316,53 @@ val graphConfig = createCameraGraphConfig( sessionConfigAdapter, streamConfigMap, callbackMap, - requestListener, cameraConfig, cameraQuirks, cameraGraphFlags) - val cameraGraph = - runBlocking { cameraGraphCreator.createCameraGraph(cameraPipe, graphConfig) } + requestListener, cameraConfig, cameraQuirks, cameraGraphFlags + ) - // Create and configure the new camera component. - _activeComponent = - builder.config( - UseCaseCameraConfig( - useCases, - sessionConfigAdapter, - cameraStateAdapter, - cameraGraph, - streamConfigMap - ) - ) - .build() - for (control in allControls) { - control.useCaseCamera = camera + val useCaseManagerConfig = UseCaseManagerConfig( + useCases, + sessionConfigAdapter, + graphConfig, + streamConfigMap + ) + if (!shouldCreateCameraGraphImmediately) { + deferredUseCaseManagerConfig = useCaseManagerConfig + return } - camera?.setActiveResumeMode(activeResumeEnabled) + val cameraGraph = cameraPipe.create(useCaseManagerConfig.cameraGraphConfig) + beginComponentCreation(useCaseManagerConfig, cameraGraph) + } - refreshRunningUseCases() + internal fun resumeDeferredComponentCreation(cameraGraph: CameraGraph) { + val config = synchronized(lock) { deferredUseCaseManagerConfig } + checkNotNull(config) + beginComponentCreation(config, cameraGraph) + } + + private fun beginComponentCreation( + useCaseManagerConfig: UseCaseManagerConfig, + cameraGraph: CameraGraph + ) { + with(useCaseManagerConfig) { + // Create and configure the new camera component. + _activeComponent = + builder.config( + UseCaseCameraConfig( + useCases, + sessionConfigAdapter, + cameraStateAdapter, + cameraGraph, + streamConfigMap + ) + ) + .build() + for (control in allControls) { + control.useCaseCamera = camera + } + camera?.setActiveResumeMode(activeResumeEnabled) + + refreshRunningUseCases() + } } @GuardedBy("lock") @@ -464,6 +490,13 @@ } companion object { + internal data class UseCaseManagerConfig( + val useCases: List, + val sessionConfigAdapter: SessionConfigAdapter, + val cameraGraphConfig: CameraGraph.Config, + val streamConfigMap: MutableMap+ ) + fun SessionConfig.toCamera2ImplConfig(): Camera2ImplConfig { return Camera2ImplConfig(implementationOptions) }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/CameraGraphCreator.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/CameraGraphCreator.kt deleted file mode 100644 index f0aabe8..0000000 --- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/CameraGraphCreator.kt +++ /dev/null
@@ -1,73 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.camera.camera2.pipe.integration.internal - -import androidx.annotation.GuardedBy -import androidx.annotation.RequiresApi -import androidx.camera.camera2.pipe.CameraGraph -import androidx.camera.camera2.pipe.CameraPipe -import javax.inject.Inject -import javax.inject.Singleton -import kotlinx.coroutines.CompletableDeferred - -@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java -@Singleton -class CameraGraphCreator @Inject constructor() { - private val lock = Any() - - @GuardedBy("lock") - var currentExpectedConfigs = 1 - - @GuardedBy("lock") - val currentConfigs = mutableListOf() - - private var pendingDeferred: CompletableDeferred? = null - - fun setConcurrentModeOn(on: Boolean) = synchronized(lock) { - currentExpectedConfigs = if (on) { - 2 - } else { - 1 - } - } - - suspend fun createCameraGraph(cameraPipe: CameraPipe, config: CameraGraph.Config): CameraGraph { - var deferred: CompletableDeferred? = null - synchronized(lock) { - currentConfigs.add(config) - if (currentConfigs.size != currentExpectedConfigs) { - deferred = CompletableDeferred() - pendingDeferred = deferred - } - } - if (deferred != null) { - return deferred!!.await() - } - synchronized(lock) { - if (currentExpectedConfigs == 1) { - val cameraGraph = cameraPipe.create(config) - currentConfigs.clear() - return cameraGraph - } else { - val cameraGraphs = cameraPipe.createCameraGraphs(currentConfigs) - pendingDeferred?.complete(cameraGraphs.first()) - currentConfigs.clear() - return cameraGraphs[1] - } - } - } -}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraCoordinatorAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraCoordinatorAdapterTest.kt index 4e9619b..442f7b1 100644 --- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraCoordinatorAdapterTest.kt +++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CameraCoordinatorAdapterTest.kt
@@ -16,25 +16,31 @@ package androidx.camera.camera2.pipe.integration.adapter +import android.content.Context import android.os.Build import androidx.camera.camera2.pipe.CameraBackendId +import androidx.camera.camera2.pipe.CameraGraph import androidx.camera.camera2.pipe.CameraId -import androidx.camera.camera2.pipe.integration.internal.CameraGraphCreator +import androidx.camera.camera2.pipe.CameraPipe import androidx.camera.camera2.pipe.integration.testing.FakeCameraInfoAdapterCreator +import androidx.camera.camera2.pipe.testing.FakeCameraBackend import androidx.camera.camera2.pipe.testing.FakeCameraDevices import androidx.camera.camera2.pipe.testing.FakeCameraMetadata import androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT import androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_SINGLE import androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_UNSPECIFIED import androidx.camera.core.impl.CameraInfoInternal +import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.reset import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import org.robolectric.annotation.Config import org.robolectric.annotation.internal.DoNotInstrument @@ -43,7 +49,9 @@ @Config(minSdk = Build.VERSION_CODES.LOLLIPOP) class CameraCoordinatorAdapterTest { - private val cameraMetadata = FakeCameraMetadata() + private val cameraMetadata0 = FakeCameraMetadata(cameraId = CameraId("0")) + private val cameraMetadata1 = FakeCameraMetadata(cameraId = CameraId("1")) + private val cameraMetadata2 = FakeCameraMetadata(cameraId = CameraId("2")) private val cameraDevices = FakeCameraDevices( defaultCameraBackendId = CameraBackendId("0"), @@ -51,16 +59,46 @@ setOf(CameraBackendId("0"), CameraBackendId("1")), setOf(CameraBackendId("0"), CameraBackendId("2")) ), - cameraMetadataMap = mapOf(CameraBackendId("0") to listOf(cameraMetadata)) + cameraMetadataMap = mapOf( + CameraBackendId("0") to listOf( + cameraMetadata0, + cameraMetadata1, + cameraMetadata2 + ) + ) ) - private val mockCameraGraphCreator: CameraGraphCreator = mock() private val mockCameraInternalAdapter0: CameraInternalAdapter = mock() private val mockCameraInternalAdapter1: CameraInternalAdapter = mock() private val mockCameraInternalAdapter2: CameraInternalAdapter = mock() - private val cameraCoordinatorAdapter = CameraCoordinatorAdapter( - cameraDevices, mockCameraGraphCreator) + private val mockCameraGraphConfig0 = CameraGraph.Config( + camera = CameraId("0"), streams = emptyList() + ) + private val mockCameraGraphConfig1 = CameraGraph.Config( + camera = CameraId("1"), streams = emptyList() + ) + private val mockCameraGraphConfig2 = CameraGraph.Config( + camera = CameraId("2"), streams = emptyList() + ) + + private val context: Context = ApplicationProvider.getApplicationContext() + private val fakeCameraBackend = FakeCameraBackend( + fakeCameras = mapOf( + cameraMetadata0.camera to cameraMetadata0, + cameraMetadata1.camera to cameraMetadata1, + cameraMetadata2.camera to cameraMetadata2 + ) + ) + private val cameraPipe = CameraPipe( + CameraPipe.Config( + context, + cameraBackendConfig = CameraPipe.CameraBackendConfig( + internalBackend = fakeCameraBackend + ), + ) + ) + private val cameraCoordinatorAdapter = CameraCoordinatorAdapter(cameraPipe, cameraDevices) @Before fun setUp() { @@ -79,9 +117,17 @@ @Test fun setAndGetActiveConcurrentCameraInfos() { + whenever(mockCameraInternalAdapter0.getDeferredCameraGraphConfig()) + .thenReturn(mockCameraGraphConfig0) + whenever(mockCameraInternalAdapter1.getDeferredCameraGraphConfig()) + .thenReturn(mockCameraGraphConfig1) + whenever(mockCameraInternalAdapter2.getDeferredCameraGraphConfig()) + .thenReturn(mockCameraGraphConfig2) + cameraCoordinatorAdapter.activeConcurrentCameraInfos = mutableListOf( FakeCameraInfoAdapterCreator.createCameraInfoAdapter(cameraId = CameraId("0")), - FakeCameraInfoAdapterCreator.createCameraInfoAdapter(cameraId = CameraId("1"))) + FakeCameraInfoAdapterCreator.createCameraInfoAdapter(cameraId = CameraId("1")) + ) assertThat(cameraCoordinatorAdapter.activeConcurrentCameraInfos.size).isEqualTo(2) val cameraInfo0 = cameraCoordinatorAdapter.activeConcurrentCameraInfos[0] @@ -90,17 +136,25 @@ val cameraInfo1 = cameraCoordinatorAdapter.activeConcurrentCameraInfos[1] as CameraInfoInternal assertThat(cameraInfo1.cameraId).isEqualTo("1") - verify(mockCameraInternalAdapter0).resumeRefresh() - verify(mockCameraInternalAdapter1).resumeRefresh() + verify(mockCameraInternalAdapter0).resumeDeferredCameraGraphCreation(any()) + verify(mockCameraInternalAdapter1).resumeDeferredCameraGraphCreation(any()) } @Test fun getPairedConcurrentCameraId() { + whenever(mockCameraInternalAdapter0.getDeferredCameraGraphConfig()) + .thenReturn(mockCameraGraphConfig0) + whenever(mockCameraInternalAdapter1.getDeferredCameraGraphConfig()) + .thenReturn(mockCameraGraphConfig1) + whenever(mockCameraInternalAdapter2.getDeferredCameraGraphConfig()) + .thenReturn(mockCameraGraphConfig2) + assertThat(cameraCoordinatorAdapter.getPairedConcurrentCameraId("0")).isNull() cameraCoordinatorAdapter.activeConcurrentCameraInfos = mutableListOf( FakeCameraInfoAdapterCreator.createCameraInfoAdapter(cameraId = CameraId("0")), - FakeCameraInfoAdapterCreator.createCameraInfoAdapter(cameraId = CameraId("1"))) + FakeCameraInfoAdapterCreator.createCameraInfoAdapter(cameraId = CameraId("1")) + ) assertThat(cameraCoordinatorAdapter.getPairedConcurrentCameraId("0")).isEqualTo("1") } @@ -109,11 +163,10 @@ fun setAndGetCameraOperatingMode() { cameraCoordinatorAdapter.cameraOperatingMode = CAMERA_OPERATING_MODE_CONCURRENT - verify(mockCameraInternalAdapter0).pauseRefresh() - verify(mockCameraInternalAdapter0, never()).resumeRefresh() - verify(mockCameraInternalAdapter1).pauseRefresh() - verify(mockCameraInternalAdapter1, never()).resumeRefresh() - verify(mockCameraGraphCreator).setConcurrentModeOn(true) + verify(mockCameraInternalAdapter0).setCameraGraphCreationMode(createImmediately = false) + verify(mockCameraInternalAdapter0, never()).resumeDeferredCameraGraphCreation(any()) + verify(mockCameraInternalAdapter1).setCameraGraphCreationMode(createImmediately = false) + verify(mockCameraInternalAdapter1, never()).resumeDeferredCameraGraphCreation(any()) assertThat(cameraCoordinatorAdapter.cameraOperatingMode) .isEqualTo(CAMERA_OPERATING_MODE_CONCURRENT) @@ -121,25 +174,32 @@ reset(mockCameraInternalAdapter1) cameraCoordinatorAdapter.cameraOperatingMode = CAMERA_OPERATING_MODE_SINGLE - verify(mockCameraInternalAdapter0).resumeRefresh() - verify(mockCameraInternalAdapter1).resumeRefresh() - verify(mockCameraGraphCreator).setConcurrentModeOn(false) + verify(mockCameraInternalAdapter0).setCameraGraphCreationMode(createImmediately = true) + verify(mockCameraInternalAdapter1).setCameraGraphCreationMode(createImmediately = true) assertThat(cameraCoordinatorAdapter.cameraOperatingMode) .isEqualTo(CAMERA_OPERATING_MODE_SINGLE) reset(mockCameraInternalAdapter0) reset(mockCameraInternalAdapter1) cameraCoordinatorAdapter.cameraOperatingMode = CAMERA_OPERATING_MODE_UNSPECIFIED - verify(mockCameraInternalAdapter0, never()).resumeRefresh() - verify(mockCameraInternalAdapter1, never()).resumeRefresh() + verify(mockCameraInternalAdapter0, never()).resumeDeferredCameraGraphCreation(any()) + verify(mockCameraInternalAdapter1, never()).resumeDeferredCameraGraphCreation(any()) } @Test fun shutdown() { + whenever(mockCameraInternalAdapter0.getDeferredCameraGraphConfig()) + .thenReturn(mockCameraGraphConfig0) + whenever(mockCameraInternalAdapter1.getDeferredCameraGraphConfig()) + .thenReturn(mockCameraGraphConfig1) + whenever(mockCameraInternalAdapter2.getDeferredCameraGraphConfig()) + .thenReturn(mockCameraGraphConfig2) + cameraCoordinatorAdapter.cameraOperatingMode = CAMERA_OPERATING_MODE_CONCURRENT cameraCoordinatorAdapter.activeConcurrentCameraInfos = mutableListOf( FakeCameraInfoAdapterCreator.createCameraInfoAdapter(cameraId = CameraId("0")), - FakeCameraInfoAdapterCreator.createCameraInfoAdapter(cameraId = CameraId("1"))) + FakeCameraInfoAdapterCreator.createCameraInfoAdapter(cameraId = CameraId("1")) + ) cameraCoordinatorAdapter.shutdown() @@ -148,7 +208,8 @@ assertThat(cameraCoordinatorAdapter.concurrentCameraIdMap).isEmpty() assertThat(cameraCoordinatorAdapter.concurrentCameraIdsSet).isEmpty() assertThat(cameraCoordinatorAdapter.cameraOperatingMode).isEqualTo( - CAMERA_OPERATING_MODE_UNSPECIFIED) + CAMERA_OPERATING_MODE_UNSPECIFIED + ) assertThat(cameraCoordinatorAdapter.concurrentModeOn).isFalse() } }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt index f06d683..9a1df6c35 100644 --- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt +++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -30,7 +30,6 @@ import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector import androidx.camera.camera2.pipe.integration.config.CameraConfig import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera.RunningUseCasesChangeListener -import androidx.camera.camera2.pipe.integration.internal.CameraGraphCreator import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop import androidx.camera.camera2.pipe.integration.testing.FakeCamera2CameraControlCompat @@ -345,7 +344,6 @@ val fakeCamera = FakeCamera() return UseCaseManager( cameraPipe = CameraPipe(CameraPipe.Config(ApplicationProvider.getApplicationContext())), - cameraGraphCreator = CameraGraphCreator(), cameraConfig = CameraConfig(cameraId), callbackMap = CameraCallbackMap(), requestListener = ComboRequestListener(),
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/CameraGraphCreatorTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/CameraGraphCreatorTest.kt deleted file mode 100644 index 02100cc..0000000 --- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/CameraGraphCreatorTest.kt +++ /dev/null
@@ -1,145 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.camera.camera2.pipe.integration.internal - -import android.content.Context -import android.graphics.Rect -import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.CameraMetadata -import android.os.Build -import android.util.Size -import androidx.camera.camera2.pipe.CameraGraph -import androidx.camera.camera2.pipe.CameraId -import androidx.camera.camera2.pipe.CameraPipe -import androidx.camera.camera2.pipe.CameraStream -import androidx.camera.camera2.pipe.StreamFormat -import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.async -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.annotation.Config -import org.robolectric.annotation.internal.DoNotInstrument -import org.robolectric.shadow.api.Shadow -import org.robolectric.shadows.ShadowCameraCharacteristics -import org.robolectric.shadows.ShadowCameraManager -import org.robolectric.shadows.StreamConfigurationMapBuilder - -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(RobolectricCameraPipeTestRunner::class) -@DoNotInstrument -@Config(minSdk = Build.VERSION_CODES.LOLLIPOP) -class CameraGraphCreatorTest { - - private val cameraGraphCreator: CameraGraphCreator = CameraGraphCreator() - private val context = ApplicationProvider.getApplicationContext() as Context - private val cameraPipe = CameraPipe(CameraPipe.Config(context)) - - private val stream1Config = CameraStream.Config.create( - Size(640, 480), StreamFormat.YUV_420_888) - private val stream2Config = CameraStream.Config.create( - Size(1280, 720), StreamFormat.YUV_420_888) - private val cameraGraph1Config = CameraGraph.Config(CameraId("0"), listOf(stream1Config)) - private val cameraGraph2Config = CameraGraph.Config(CameraId("1"), listOf(stream2Config)) - - @Before - fun setUp() { - setupCameras() - } - - @Test - fun createCameraGraph_singleMode() = runTest { - cameraGraphCreator.setConcurrentModeOn(false) - val cameraGraph = cameraGraphCreator.createCameraGraph(cameraPipe, cameraGraph1Config) - - advanceUntilIdle() - assertThat(cameraGraph).isNotNull() - } - - @Test - fun createCameraGraph_concurrentMode() = runTest { - cameraGraphCreator.setConcurrentModeOn(true) - - val cameraGraph0 = async { - cameraGraphCreator.createCameraGraph(cameraPipe, cameraGraph1Config) - } - advanceUntilIdle() - assertThat(cameraGraph0.isCompleted).isFalse() - - val cameraGraph1 = async { - cameraGraphCreator.createCameraGraph(cameraPipe, cameraGraph2Config) - } - advanceUntilIdle() - - assertThat(cameraGraph0.isCompleted).isTrue() - assertThat(cameraGraph1.isCompleted).isTrue() - assertThat(cameraGraph0.await()).isNotNull() - assertThat(cameraGraph1.await()).isNotNull() - } - - private fun setupCameras() { - val capabilities = - intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) - - initCharacteristics("0", CameraCharacteristics.LENS_FACING_BACK, capabilities) - initCharacteristics("1", CameraCharacteristics.LENS_FACING_FRONT, capabilities) - } - - private fun initCharacteristics(cameraId: String, lensFacing: Int, capabilities: IntArray?) { - val sensorWidth = 640 - val sensorHeight = 480 - - val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics() - val shadowCharacteristics = - Shadow.extract(characteristics).apply { - - set(CameraCharacteristics.LENS_FACING, lensFacing) - - set( - CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE, - Rect(0, 0, sensorWidth, sensorHeight) - ) - - set( - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY - ) - - set( - CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP, - StreamConfigurationMapBuilder.newBuilder().build() - ) - } - - capabilities?.let { - shadowCharacteristics.set( - CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, capabilities - ) - } - - // Add the camera to the camera service - (Shadow.extract( - ApplicationProvider.getApplicationContext() - .getSystemService(Context.CAMERA_SERVICE) - ) as ShadowCameraManager).addCamera(cameraId, characteristics) - } -}
diff --git a/camera/camera-effects/src/androidTest/java/androidx/camera/effects/opengl/GlContextDeviceTest.kt b/camera/camera-effects/src/androidTest/java/androidx/camera/effects/opengl/GlContextDeviceTest.kt index d6b064d..2ea329a 100644 --- a/camera/camera-effects/src/androidTest/java/androidx/camera/effects/opengl/GlContextDeviceTest.kt +++ b/camera/camera-effects/src/androidTest/java/androidx/camera/effects/opengl/GlContextDeviceTest.kt
@@ -74,4 +74,9 @@ glContext.registerSurface(surface) glContext.drawAndSwap(surface, TIMESTAMP_NS) } + + @Test + fun registerSurfaceWithoutDrawingOrReleasing_noException() { + glContext.registerSurface(surface) + } }
diff --git a/camera/camera-effects/src/androidTest/java/androidx/camera/effects/opengl/GlRendererDeviceTest.kt b/camera/camera-effects/src/androidTest/java/androidx/camera/effects/opengl/GlRendererDeviceTest.kt new file mode 100644 index 0000000..795082f --- /dev/null +++ b/camera/camera-effects/src/androidTest/java/androidx/camera/effects/opengl/GlRendererDeviceTest.kt
@@ -0,0 +1,53 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.camera.effects.opengl + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SdkSuppress +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumentation tests for [GlRenderer]. + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +@SdkSuppress(minSdkVersion = 21) +class GlRendererDeviceTest { + + private val glRenderer = GlRenderer() + + @Before + fun setUp() { + glRenderer.init() + } + + @After + fun tearDown() { + glRenderer.release() + } + + // TODO(b/295407763): verify the input/output of the OpenGL renderer + @Test + fun placeholder() { + assertThat(true).isTrue() + } +}
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlContext.java b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlContext.java index ee796b3e..56919d0 100644 --- a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlContext.java +++ b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlContext.java
@@ -71,15 +71,13 @@ void init() { checkState(Objects.equals(mEglDisplay, EGL14.EGL_NO_DISPLAY), "Already initialized"); - // TODO(b/295407763): make sure EGLDisplay, EGLConfig, and EGLContext are released when - // there is exception. // Create EGLDisplay. - EGLDisplay eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); - if (Objects.equals(eglDisplay, EGL14.EGL_NO_DISPLAY)) { + mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (Objects.equals(mEglDisplay, EGL14.EGL_NO_DISPLAY)) { throw new IllegalStateException("Unable to get EGL14 display"); } int[] version = new int[2]; - if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) { + if (!EGL14.eglInitialize(mEglDisplay, version, 0, version, 1)) { throw new IllegalStateException("Unable to initialize EGL14"); } @@ -103,35 +101,28 @@ EGLConfig[] configs = new EGLConfig[1]; int[] numConfigs = new int[1]; if (!EGL14.eglChooseConfig( - eglDisplay, attribToChooseConfig, 0, configs, 0, configs.length, - numConfigs, 0 + mEglDisplay, attribToChooseConfig, 0, configs, 0, configs.length, numConfigs, 0 )) { throw new IllegalStateException("Unable to find a suitable EGLConfig"); } - EGLConfig eglConfig = configs[0]; + mEglConfig = configs[0]; int[] attribToCreateContext = { EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE }; // Create EGLContext. - EGLContext eglContext = EGL14.eglCreateContext( - eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT, + mEglContext = EGL14.eglCreateContext( + mEglDisplay, mEglConfig, EGL14.EGL_NO_CONTEXT, attribToCreateContext, 0 ); checkEglErrorOrThrow("eglCreateContext"); int[] values = new int[1]; EGL14.eglQueryContext( - eglDisplay, eglContext, EGL14.EGL_CONTEXT_CLIENT_VERSION, values, - 0 + mEglDisplay, mEglContext, EGL14.EGL_CONTEXT_CLIENT_VERSION, values, 0 ); Logger.d(TAG, "EGLContext created, client version " + values[0]); - // All successful. Track the created objects. - mEglDisplay = eglDisplay; - mEglConfig = eglConfig; - mEglContext = eglContext; - // Create a temporary surface to make it current. mTempSurface = create1x1PBufferSurface(); makeCurrent(mTempSurface); @@ -207,18 +198,19 @@ } } - boolean release() { - if (!isInitialized()) { - return false; + void release() { + if (!Objects.equals(mEglDisplay, EGL14.EGL_NO_DISPLAY)) { + EGL14.eglMakeCurrent( + mEglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, + EGL14.EGL_NO_CONTEXT + ); } - EGL14.eglMakeCurrent( - mEglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, - EGL14.EGL_NO_CONTEXT - ); // Destroy EGLSurfaces for (EglSurface eglSurface : mRegisteredSurfaces.values()) { - destroyEglSurface(eglSurface); + if (eglSurface != null) { + destroyEglSurface(eglSurface); + } } mRegisteredSurfaces.clear(); @@ -230,15 +222,17 @@ mCurrentSurface = null; // Destroy EGLContext and terminate display. - EGL14.eglDestroyContext(mEglDisplay, mEglContext); - EGL14.eglTerminate(mEglDisplay); - EGL14.eglReleaseThread(); + if (!Objects.equals(mEglContext, EGL14.EGL_NO_CONTEXT)) { + EGL14.eglDestroyContext(mEglDisplay, mEglContext); + mEglContext = EGL14.EGL_NO_CONTEXT; + } + if (!Objects.equals(mEglDisplay, EGL14.EGL_NO_DISPLAY)) { + EGL14.eglTerminate(mEglDisplay); + mEglDisplay = EGL14.EGL_NO_DISPLAY; + } - // Clear the created configurations. - mEglDisplay = EGL14.EGL_NO_DISPLAY; - mEglContext = EGL14.EGL_NO_CONTEXT; + EGL14.eglReleaseThread(); mEglConfig = null; - return true; } // --- Private methods ---
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlRenderer.java b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlRenderer.java index 984a25a..50488dc 100644 --- a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlRenderer.java +++ b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlRenderer.java
@@ -16,12 +16,22 @@ package androidx.camera.effects.opengl; +import static androidx.camera.effects.opengl.Utils.checkGlErrorOrThrow; +import static androidx.camera.effects.opengl.Utils.configureExternalTexture; +import static androidx.camera.effects.opengl.Utils.configureTexture2D; +import static androidx.camera.effects.opengl.Utils.createTextureId; +import static androidx.core.util.Preconditions.checkState; + import android.graphics.Bitmap; +import android.opengl.GLES11Ext; import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.os.Build; import android.util.Size; import android.view.Surface; import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; /** @@ -36,9 +46,27 @@ * *It also allows the caller to upload a bitmap and overlay it when rendering to Surface.
*/ +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @RestrictTo(RestrictTo.Scope.LIBRARY) public final class GlRenderer { + private static final String TAG = "GlRenderer"; + + private boolean mInitialized = false; + + private Thread mGlThread = null; + private final GlContext mGlContext = new GlContext(); + private final GlProgramOverlay mGlProgramOverlay = new GlProgramOverlay(); + private final GlProgramCopy mGlProgramCopy = new GlProgramCopy(); + + // Texture IDs. + private int mInputTextureId = -1; + private int mOverlayTextureId = -1; + @NonNull + private int[] mQueueTextureIds = new int[0]; + private int mQueueTextureWidth = -1; + private int mQueueTextureHeight = -1; + // --- Public methods --- /** @@ -46,8 +74,22 @@ * *Must be called before any other methods.
*/ - void init() { - throw new UnsupportedOperationException("TODO: implement this"); + public void init() { + checkState(!mInitialized, "Already initialized"); + mInitialized = true; + mGlThread = Thread.currentThread(); + try { + mGlContext.init(); + mGlProgramCopy.init(); + mGlProgramOverlay.init(); + mInputTextureId = createTextureId(); + configureExternalTexture(mInputTextureId); + mOverlayTextureId = createTextureId(); + configureTexture2D(mOverlayTextureId); + } catch (IllegalStateException | IllegalArgumentException e) { + release(); + throw e; + } } /** @@ -55,15 +97,41 @@ * *Once released, it can never be accessed again.
*/ - void release() { - throw new UnsupportedOperationException("TODO: implement this"); + public void release() { + checkGlThreadAndInitialized(); + + mInitialized = false; + mGlThread = null; + mQueueTextureWidth = -1; + mQueueTextureHeight = -1; + + mGlContext.release(); + mGlProgramOverlay.release(); + mGlProgramCopy.release(); + + if (mInputTextureId != -1) { + GLES20.glDeleteTextures(1, new int[]{mInputTextureId}, 0); + checkGlErrorOrThrow("glDeleteTextures"); + mInputTextureId = -1; + } + if (mOverlayTextureId != -1) { + GLES20.glDeleteTextures(1, new int[]{mOverlayTextureId}, 0); + checkGlErrorOrThrow("glDeleteTextures"); + mOverlayTextureId = -1; + } + if (mQueueTextureIds.length > 0) { + GLES20.glDeleteTextures(mQueueTextureIds.length, mQueueTextureIds, 0); + checkGlErrorOrThrow("glDeleteTextures"); + mQueueTextureIds = new int[0]; + } } /** * Gets the external input texture ID created during initialization. */ public int getInputTextureId() { - throw new UnsupportedOperationException("TODO: implement this"); + checkGlThreadAndInitialized(); + return mInputTextureId; } /** @@ -79,14 +147,48 @@ */ @NonNull public int[] createBufferTextureIds(int queueDepth, @NonNull Size size) { - throw new UnsupportedOperationException("TODO: implement this"); + checkGlThreadAndInitialized(); + // Delete the current buffer if it exists. + if (mQueueTextureIds.length > 0) { + GLES20.glDeleteTextures(mQueueTextureIds.length, mQueueTextureIds, 0); + checkGlErrorOrThrow("glDeleteTextures"); + } + + mQueueTextureIds = new int[queueDepth]; + // If the queue depth is 0, return an empty array. There is no need to create textures. + if (queueDepth == 0) { + return mQueueTextureIds; + } + + // Create the textures. + GLES20.glGenTextures(queueDepth, mQueueTextureIds, 0); + checkGlErrorOrThrow("glGenTextures"); + mQueueTextureWidth = size.getWidth(); + mQueueTextureHeight = size.getHeight(); + for (int textureId : mQueueTextureIds) { + configureTexture2D(textureId); + GLES20.glTexImage2D( + GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGB, size.getWidth(), size.getHeight(), 0, + GLES20.GL_RGB, + GLES20.GL_UNSIGNED_BYTE, + null + ); + } + return mQueueTextureIds; } /** * Uploads the {@link Bitmap} to the overlay texture. */ public void uploadOverlay(@NonNull Bitmap overlay) { - throw new UnsupportedOperationException("TODO: implement this"); + checkGlThreadAndInitialized(); + + GLES20.glActiveTexture(GLES20.GL_TEXTURE1); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mOverlayTextureId); + checkGlErrorOrThrow("glBindTexture"); + + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, overlay, 0); + checkGlErrorOrThrow("texImage2D"); } /** @@ -96,7 +198,8 @@ * {@link #renderQueueTextureToSurface}. */ public void registerOutputSurface(@NonNull Surface surface) { - throw new UnsupportedOperationException("TODO: implement this"); + checkGlThreadAndInitialized(); + mGlContext.registerSurface(surface); } /** @@ -106,7 +209,8 @@ * {@link #renderQueueTextureToSurface} with the {@link Surface} throws an exception. */ public void unregisterOutputSurface(@NonNull Surface surface) { - throw new UnsupportedOperationException("TODO: implement this"); + checkGlThreadAndInitialized(); + mGlContext.unregisterSurface(surface); } /** @@ -117,7 +221,9 @@ */ public void renderInputToSurface(long timestampNs, @NonNull float[] textureTransform, @NonNull Surface surface) { - throw new UnsupportedOperationException("TODO: implement this"); + checkGlThreadAndInitialized(); + mGlProgramOverlay.draw(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mInputTextureId, + mOverlayTextureId, textureTransform, mGlContext, surface, timestampNs); } /** @@ -128,9 +234,9 @@ * {@link #createBufferTextureIds}. */ public void renderQueueTextureToSurface(int textureId, long timestampNs, - @NonNull float[] textureTransform, - @NonNull Surface surface) { - throw new UnsupportedOperationException("TODO: implement this"); + @NonNull float[] textureTransform, @NonNull Surface surface) { + mGlProgramOverlay.draw(GLES20.GL_TEXTURE_2D, textureId, mOverlayTextureId, + textureTransform, mGlContext, surface, timestampNs); } /** @@ -139,8 +245,14 @@ *The texture ID must be from the latest return value of{@link #createBufferTextureIds}.
*/ public void renderInputToQueueTexture(int textureId) { - throw new UnsupportedOperationException("TODO: implement this"); + mGlProgramCopy.draw(mInputTextureId, textureId, mQueueTextureWidth, mQueueTextureHeight); } // --- Private methods --- + + private void checkGlThreadAndInitialized() { + checkState(mInitialized, "OpenGlRenderer is not initialized"); + checkState(mGlThread == Thread.currentThread(), + "Method call must be called on the GL thread."); + } }
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/Utils.java b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/Utils.java index 1a4f66c..e8f2d49 100644 --- a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/Utils.java +++ b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/Utils.java
@@ -19,6 +19,7 @@ import static androidx.camera.effects.opengl.GlProgram.VERTEX_SIZE; import android.opengl.EGL14; +import android.opengl.GLES11Ext; import android.opengl.GLES20; import androidx.annotation.NonNull; @@ -87,4 +88,47 @@ throw new IllegalStateException("Unable to locate '" + label + "' in program"); } } + + /** + * Creates a single texture ID. + */ + static int createTextureId() { + int[] textureIds = new int[1]; + GLES20.glGenTextures(1, textureIds, 0); + checkGlErrorOrThrow("glGenTextures"); + return textureIds[0]; + } + + /** + * Configures the texture as a 2D texture. + */ + static void configureTexture2D(int textureId) { + GLES20.glActiveTexture(GLES20.GL_TEXTURE1); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE); + } + + /** + * Configures the texture as an external texture. + */ + static void configureExternalTexture(int textureId) { + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId); + checkGlErrorOrThrow("glBindTexture " + textureId); + GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_NEAREST); + GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE); + checkGlErrorOrThrow("glTexParameter"); + } }
diff --git a/collection/collection/api/current.txt b/collection/collection/api/current.txt index 4261c54..16b4531 100644 --- a/collection/collection/api/current.txt +++ b/collection/collection/api/current.txt
@@ -133,7 +133,16 @@ } public final class FloatListKt { + method public static androidx.collection.FloatList emptyFloatList(); + method public static androidx.collection.FloatList floatListOf(); + method public static androidx.collection.FloatList floatListOf(float element1); + method public static androidx.collection.FloatList floatListOf(float element1, float element2); + method public static androidx.collection.FloatList floatListOf(float element1, float element2, float element3); + method public static androidx.collection.FloatList floatListOf(float... elements); method public static inline androidx.collection.MutableFloatList mutableFloatListOf(); + method public static androidx.collection.MutableFloatList mutableFloatListOf(float element1); + method public static androidx.collection.MutableFloatList mutableFloatListOf(float element1, float element2); + method public static androidx.collection.MutableFloatList mutableFloatListOf(float element1, float element2, float element3); method public static inline androidx.collection.MutableFloatList mutableFloatListOf(float... elements); } @@ -158,7 +167,15 @@ public final class FloatSetKt { method public static androidx.collection.FloatSet emptyFloatSet(); + method public static androidx.collection.FloatSet floatSetOf(); + method public static androidx.collection.FloatSet floatSetOf(float element1); + method public static androidx.collection.FloatSet floatSetOf(float element1, float element2); + method public static androidx.collection.FloatSet floatSetOf(float element1, float element2, float element3); + method public static androidx.collection.FloatSet floatSetOf(float... elements); method public static androidx.collection.MutableFloatSet mutableFloatSetOf(); + method public static androidx.collection.MutableFloatSet mutableFloatSetOf(float element1); + method public static androidx.collection.MutableFloatSet mutableFloatSetOf(float element1, float element2); + method public static androidx.collection.MutableFloatSet mutableFloatSetOf(float element1, float element2, float element3); method public static androidx.collection.MutableFloatSet mutableFloatSetOf(float... elements); } @@ -201,7 +218,16 @@ } public final class IntListKt { + method public static androidx.collection.IntList emptyIntList(); + method public static androidx.collection.IntList intListOf(); + method public static androidx.collection.IntList intListOf(int element1); + method public static androidx.collection.IntList intListOf(int element1, int element2); + method public static androidx.collection.IntList intListOf(int element1, int element2, int element3); + method public static androidx.collection.IntList intListOf(int... elements); method public static inline androidx.collection.MutableIntList mutableIntListOf(); + method public static androidx.collection.MutableIntList mutableIntListOf(int element1); + method public static androidx.collection.MutableIntList mutableIntListOf(int element1, int element2); + method public static androidx.collection.MutableIntList mutableIntListOf(int element1, int element2, int element3); method public static inline androidx.collection.MutableIntList mutableIntListOf(int... elements); } @@ -226,7 +252,15 @@ public final class IntSetKt { method public static androidx.collection.IntSet emptyIntSet(); + method public static androidx.collection.IntSet intSetOf(); + method public static androidx.collection.IntSet intSetOf(int element1); + method public static androidx.collection.IntSet intSetOf(int element1, int element2); + method public static androidx.collection.IntSet intSetOf(int element1, int element2, int element3); + method public static androidx.collection.IntSet intSetOf(int... elements); method public static androidx.collection.MutableIntSet mutableIntSetOf(); + method public static androidx.collection.MutableIntSet mutableIntSetOf(int element1); + method public static androidx.collection.MutableIntSet mutableIntSetOf(int element1, int element2); + method public static androidx.collection.MutableIntSet mutableIntSetOf(int element1, int element2, int element3); method public static androidx.collection.MutableIntSet mutableIntSetOf(int... elements); } @@ -269,7 +303,16 @@ } public final class LongListKt { + method public static androidx.collection.LongList emptyLongList(); + method public static androidx.collection.LongList longListOf(); + method public static androidx.collection.LongList longListOf(long element1); + method public static androidx.collection.LongList longListOf(long element1, long element2); + method public static androidx.collection.LongList longListOf(long element1, long element2, long element3); + method public static androidx.collection.LongList longListOf(long... elements); method public static inline androidx.collection.MutableLongList mutableLongListOf(); + method public static androidx.collection.MutableLongList mutableLongListOf(long element1); + method public static androidx.collection.MutableLongList mutableLongListOf(long element1, long element2); + method public static androidx.collection.MutableLongList mutableLongListOf(long element1, long element2, long element3); method public static inline androidx.collection.MutableLongList mutableLongListOf(long... elements); } @@ -294,7 +337,15 @@ public final class LongSetKt { method public static androidx.collection.LongSet emptyLongSet(); + method public static androidx.collection.LongSet longSetOf(); + method public static androidx.collection.LongSet longSetOf(long element1); + method public static androidx.collection.LongSet longSetOf(long element1, long element2); + method public static androidx.collection.LongSet longSetOf(long element1, long element2, long element3); + method public static androidx.collection.LongSet longSetOf(long... elements); method public static androidx.collection.MutableLongSet mutableLongSetOf(); + method public static androidx.collection.MutableLongSet mutableLongSetOf(long element1); + method public static androidx.collection.MutableLongSet mutableLongSetOf(long element1, long element2); + method public static androidx.collection.MutableLongSet mutableLongSetOf(long element1, long element2, long element3); method public static androidx.collection.MutableLongSet mutableLongSetOf(long... elements); } @@ -652,7 +703,15 @@ public final class ScatterSetKt { method public staticandroidx.collection.ScatterSet method public staticemptyScatterSet(); androidx.collection.MutableScatterSet + method public staticmutableScatterSetOf(); androidx.collection.MutableScatterSet + method public staticmutableScatterSetOf(E element1); androidx.collection.MutableScatterSet + method public staticmutableScatterSetOf(E element1, E element2); androidx.collection.MutableScatterSet method public staticmutableScatterSetOf(E element1, E element2, E element3); androidx.collection.MutableScatterSet + method public staticmutableScatterSetOf(E?... elements); androidx.collection.ScatterSet + method public staticscatterSetOf(); androidx.collection.ScatterSet + method public staticscatterSetOf(E element1); androidx.collection.ScatterSet + method public staticscatterSetOf(E element1, E element2); androidx.collection.ScatterSet + method public staticscatterSetOf(E element1, E element2, E element3); androidx.collection.ScatterSet } public class SimpleArrayMapscatterSetOf(E?... elements); {
diff --git a/collection/collection/api/restricted_current.txt b/collection/collection/api/restricted_current.txt index 7759811..29dd414 100644 --- a/collection/collection/api/restricted_current.txt +++ b/collection/collection/api/restricted_current.txt
@@ -135,7 +135,16 @@ } public final class FloatListKt { + method public static androidx.collection.FloatList emptyFloatList(); + method public static androidx.collection.FloatList floatListOf(); + method public static androidx.collection.FloatList floatListOf(float element1); + method public static androidx.collection.FloatList floatListOf(float element1, float element2); + method public static androidx.collection.FloatList floatListOf(float element1, float element2, float element3); + method public static androidx.collection.FloatList floatListOf(float... elements); method public static inline androidx.collection.MutableFloatList mutableFloatListOf(); + method public static androidx.collection.MutableFloatList mutableFloatListOf(float element1); + method public static androidx.collection.MutableFloatList mutableFloatListOf(float element1, float element2); + method public static androidx.collection.MutableFloatList mutableFloatListOf(float element1, float element2, float element3); method public static inline androidx.collection.MutableFloatList mutableFloatListOf(float... elements); } @@ -163,7 +172,15 @@ public final class FloatSetKt { method public static androidx.collection.FloatSet emptyFloatSet(); + method public static androidx.collection.FloatSet floatSetOf(); + method public static androidx.collection.FloatSet floatSetOf(float element1); + method public static androidx.collection.FloatSet floatSetOf(float element1, float element2); + method public static androidx.collection.FloatSet floatSetOf(float element1, float element2, float element3); + method public static androidx.collection.FloatSet floatSetOf(float... elements); method public static androidx.collection.MutableFloatSet mutableFloatSetOf(); + method public static androidx.collection.MutableFloatSet mutableFloatSetOf(float element1); + method public static androidx.collection.MutableFloatSet mutableFloatSetOf(float element1, float element2); + method public static androidx.collection.MutableFloatSet mutableFloatSetOf(float element1, float element2, float element3); method public static androidx.collection.MutableFloatSet mutableFloatSetOf(float... elements); } @@ -208,7 +225,16 @@ } public final class IntListKt { + method public static androidx.collection.IntList emptyIntList(); + method public static androidx.collection.IntList intListOf(); + method public static androidx.collection.IntList intListOf(int element1); + method public static androidx.collection.IntList intListOf(int element1, int element2); + method public static androidx.collection.IntList intListOf(int element1, int element2, int element3); + method public static androidx.collection.IntList intListOf(int... elements); method public static inline androidx.collection.MutableIntList mutableIntListOf(); + method public static androidx.collection.MutableIntList mutableIntListOf(int element1); + method public static androidx.collection.MutableIntList mutableIntListOf(int element1, int element2); + method public static androidx.collection.MutableIntList mutableIntListOf(int element1, int element2, int element3); method public static inline androidx.collection.MutableIntList mutableIntListOf(int... elements); } @@ -236,7 +262,15 @@ public final class IntSetKt { method public static androidx.collection.IntSet emptyIntSet(); + method public static androidx.collection.IntSet intSetOf(); + method public static androidx.collection.IntSet intSetOf(int element1); + method public static androidx.collection.IntSet intSetOf(int element1, int element2); + method public static androidx.collection.IntSet intSetOf(int element1, int element2, int element3); + method public static androidx.collection.IntSet intSetOf(int... elements); method public static androidx.collection.MutableIntSet mutableIntSetOf(); + method public static androidx.collection.MutableIntSet mutableIntSetOf(int element1); + method public static androidx.collection.MutableIntSet mutableIntSetOf(int element1, int element2); + method public static androidx.collection.MutableIntSet mutableIntSetOf(int element1, int element2, int element3); method public static androidx.collection.MutableIntSet mutableIntSetOf(int... elements); } @@ -281,7 +315,16 @@ } public final class LongListKt { + method public static androidx.collection.LongList emptyLongList(); + method public static androidx.collection.LongList longListOf(); + method public static androidx.collection.LongList longListOf(long element1); + method public static androidx.collection.LongList longListOf(long element1, long element2); + method public static androidx.collection.LongList longListOf(long element1, long element2, long element3); + method public static androidx.collection.LongList longListOf(long... elements); method public static inline androidx.collection.MutableLongList mutableLongListOf(); + method public static androidx.collection.MutableLongList mutableLongListOf(long element1); + method public static androidx.collection.MutableLongList mutableLongListOf(long element1, long element2); + method public static androidx.collection.MutableLongList mutableLongListOf(long element1, long element2, long element3); method public static inline androidx.collection.MutableLongList mutableLongListOf(long... elements); } @@ -309,7 +352,15 @@ public final class LongSetKt { method public static androidx.collection.LongSet emptyLongSet(); + method public static androidx.collection.LongSet longSetOf(); + method public static androidx.collection.LongSet longSetOf(long element1); + method public static androidx.collection.LongSet longSetOf(long element1, long element2); + method public static androidx.collection.LongSet longSetOf(long element1, long element2, long element3); + method public static androidx.collection.LongSet longSetOf(long... elements); method public static androidx.collection.MutableLongSet mutableLongSetOf(); + method public static androidx.collection.MutableLongSet mutableLongSetOf(long element1); + method public static androidx.collection.MutableLongSet mutableLongSetOf(long element1, long element2); + method public static androidx.collection.MutableLongSet mutableLongSetOf(long element1, long element2, long element3); method public static androidx.collection.MutableLongSet mutableLongSetOf(long... elements); } @@ -682,7 +733,15 @@ public final class ScatterSetKt { method public staticandroidx.collection.ScatterSet method public staticemptyScatterSet(); androidx.collection.MutableScatterSet + method public staticmutableScatterSetOf(); androidx.collection.MutableScatterSet + method public staticmutableScatterSetOf(E element1); androidx.collection.MutableScatterSet + method public staticmutableScatterSetOf(E element1, E element2); androidx.collection.MutableScatterSet method public staticmutableScatterSetOf(E element1, E element2, E element3); androidx.collection.MutableScatterSet + method public staticmutableScatterSetOf(E?... elements); androidx.collection.ScatterSet + method public staticscatterSetOf(); androidx.collection.ScatterSet + method public staticscatterSetOf(E element1); androidx.collection.ScatterSet + method public staticscatterSetOf(E element1, E element2); androidx.collection.ScatterSet + method public staticscatterSetOf(E element1, E element2, E element3); androidx.collection.ScatterSet } public class SimpleArrayMapscatterSetOf(E?... elements); {
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt index 5aa24f6..cf0c648 100644 --- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt +++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
@@ -839,13 +839,80 @@ @Suppress("PrivatePropertyName") private val EmptyFloatArray = FloatArray(0) +private val EmptyFloatList: FloatList = MutableFloatList(0) + /** - * Creates and returns an empty [MutableFloatList] with the default capacity. + * @return a read-only [FloatList] with nothing in it. + */ +public fun emptyFloatList(): FloatList = EmptyFloatList + +/** + * @return a read-only [FloatList] with nothing in it. + */ +public fun floatListOf(): FloatList = EmptyFloatList + +/** + * @return a new read-only [FloatList] with [element1] as the only item in the list. + */ +public fun floatListOf(element1: Float): FloatList = mutableFloatListOf(element1) + +/** + * @return a new read-only [FloatList] with 2 elements, [element1] and [element2], in order. + */ +public fun floatListOf(element1: Float, element2: Float): FloatList = + mutableFloatListOf(element1, element2) + +/** + * @return a new read-only [FloatList] with 3 elements, [element1], [element2], and [element3], + * in order. + */ +public fun floatListOf(element1: Float, element2: Float, element3: Float): FloatList = + mutableFloatListOf(element1, element2, element3) + +/** + * @return a new read-only [FloatList] with [elements] in order. + */ +public fun floatListOf(vararg elements: Float): FloatList = + MutableFloatList(elements.size).apply { plusAssign(elements) } + +/** + * @return a new empty [MutableFloatList] with the default capacity. */ public inline fun mutableFloatListOf(): MutableFloatList = MutableFloatList() /** - * Creates and returns a [MutableFloatList] with the given values. + * @return a new [MutableFloatList] with [element1] as the only item in the list. + */ +public fun mutableFloatListOf(element1: Float): MutableFloatList { + val list = MutableFloatList(1) + list += element1 + return list +} + +/** + * @return a new [MutableFloatList] with 2 elements, [element1] and [element2], in order. + */ +public fun mutableFloatListOf(element1: Float, element2: Float): MutableFloatList { + val list = MutableFloatList(2) + list += element1 + list += element2 + return list +} + +/** + * @return a new [MutableFloatList] with 3 elements, [element1], [element2], and [element3], + * in order. + */ +public fun mutableFloatListOf(element1: Float, element2: Float, element3: Float): MutableFloatList { + val list = MutableFloatList(3) + list += element1 + list += element2 + list += element3 + return list +} + +/** + * @return a new [MutableFloatList] with the given elements, in order. */ public inline fun mutableFloatListOf(vararg elements: Float): MutableFloatList = - MutableFloatList(elements.size).also { it.addAll(elements) } + MutableFloatList(elements.size).apply { plusAssign(elements) }
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt index ec17dcb..278902c4 100644 --- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt +++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
@@ -43,17 +43,75 @@ public fun emptyFloatSet(): FloatSet = EmptyFloatSet /** + * Returns an empty, read-only [ScatterSet]. + */ +@Suppress("UNCHECKED_CAST") +public fun floatSetOf(): FloatSet = EmptyFloatSet + +/** + * Returns a new read-only [FloatSet] with only [element1] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun floatSetOf(element1: Float): FloatSet = mutableFloatSetOf(element1) + +/** + * Returns a new read-only [FloatSet] with only [element1] and [element2] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun floatSetOf(element1: Float, element2: Float): FloatSet = + mutableFloatSetOf(element1, element2) + +/** + * Returns a new read-only [FloatSet] with only [element1], [element2], and [element3] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun floatSetOf(element1: Float, element2: Float, element3: Float): FloatSet = + mutableFloatSetOf(element1, element2, element3) + +/** + * Returns a new read-only [FloatSet] with only [elements] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun floatSetOf(vararg elements: Float): FloatSet = + MutableFloatSet(elements.size).apply { plusAssign(elements) } + +/** * Returns a new [MutableFloatSet]. */ public fun mutableFloatSetOf(): MutableFloatSet = MutableFloatSet() /** + * Returns a new [MutableFloatSet] with only [element1] in it. + */ +public fun mutableFloatSetOf(element1: Float): MutableFloatSet = + MutableFloatSet(1).apply { + plusAssign(element1) + } + +/** + * Returns a new [MutableFloatSet] with only [element1] and [element2] in it. + */ +public fun mutableFloatSetOf(element1: Float, element2: Float): MutableFloatSet = + MutableFloatSet(2).apply { + plusAssign(element1) + plusAssign(element2) + } + +/** + * Returns a new [MutableFloatSet] with only [element1], [element2], and [element3] in it. + */ +public fun mutableFloatSetOf(element1: Float, element2: Float, element3: Float): MutableFloatSet = + MutableFloatSet(3).apply { + plusAssign(element1) + plusAssign(element2) + plusAssign(element3) + } + +/** * Returns a new [MutableFloatSet] with the specified elements. */ public fun mutableFloatSetOf(vararg elements: Float): MutableFloatSet = - MutableFloatSet(elements.size).apply { - addAll(elements) - } + MutableFloatSet(elements.size).apply { plusAssign(elements) } /** * [FloatSet] is a container with a [Set]-like interface designed to avoid
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt index dbeb31b..8c6122db 100644 --- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt +++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
@@ -483,7 +483,7 @@ * @constructor Creates a [MutableIntList] with a [capacity] of `initialCapacity`. */ public class MutableIntList( - initialCapacity: Int = DefaultCapacity + initialCapacity: Int = 16 ) : IntList(initialCapacity) { /** * Returns the total number of elements that can be held before the [MutableIntList] must @@ -835,20 +835,84 @@ } } -@Suppress("ConstPropertyName") -private const val DefaultCapacity = 16 - // Empty array used when nothing is allocated @Suppress("PrivatePropertyName") private val EmptyIntArray = IntArray(0) +private val EmptyIntList: IntList = MutableIntList(0) + /** - * Creates and returns an empty [MutableIntList] with the default capacity. + * @return a read-only [IntList] with nothing in it. + */ +public fun emptyIntList(): IntList = EmptyIntList + +/** + * @return a read-only [IntList] with nothing in it. + */ +public fun intListOf(): IntList = EmptyIntList + +/** + * @return a new read-only [IntList] with [element1] as the only item in the list. + */ +public fun intListOf(element1: Int): IntList = mutableIntListOf(element1) + +/** + * @return a new read-only [IntList] with 2 elements, [element1] and [element2], in order. + */ +public fun intListOf(element1: Int, element2: Int): IntList = + mutableIntListOf(element1, element2) + +/** + * @return a new read-only [IntList] with 3 elements, [element1], [element2], and [element3], + * in order. + */ +public fun intListOf(element1: Int, element2: Int, element3: Int): IntList = + mutableIntListOf(element1, element2, element3) + +/** + * @return a new read-only [IntList] with [elements] in order. + */ +public fun intListOf(vararg elements: Int): IntList = + MutableIntList(elements.size).apply { plusAssign(elements) } + +/** + * @return a new empty [MutableIntList] with the default capacity. */ public inline fun mutableIntListOf(): MutableIntList = MutableIntList() /** - * Creates and returns a [MutableIntList] with the given values. + * @return a new [MutableIntList] with [element1] as the only item in the list. + */ +public fun mutableIntListOf(element1: Int): MutableIntList { + val list = MutableIntList(1) + list += element1 + return list +} + +/** + * @return a new [MutableIntList] with 2 elements, [element1] and [element2], in order. + */ +public fun mutableIntListOf(element1: Int, element2: Int): MutableIntList { + val list = MutableIntList(2) + list += element1 + list += element2 + return list +} + +/** + * @return a new [MutableIntList] with 3 elements, [element1], [element2], and [element3], + * in order. + */ +public fun mutableIntListOf(element1: Int, element2: Int, element3: Int): MutableIntList { + val list = MutableIntList(3) + list += element1 + list += element2 + list += element3 + return list +} + +/** + * @return a new [MutableIntList] with the given elements, in order. */ public inline fun mutableIntListOf(vararg elements: Int): MutableIntList = - MutableIntList(elements.size).also { it.addAll(elements) } + MutableIntList(elements.size).apply { plusAssign(elements) }
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt index fc29782..2db0f7f 100644 --- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt +++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
@@ -34,7 +34,7 @@ // Default empty set to avoid allocations private val EmptyIntSet = MutableIntSet(0) -// An empty array of Ints +// An empty array of ints private val EmptyIntArray = IntArray(0) /** @@ -43,17 +43,75 @@ public fun emptyIntSet(): IntSet = EmptyIntSet /** + * Returns an empty, read-only [ScatterSet]. + */ +@Suppress("UNCHECKED_CAST") +public fun intSetOf(): IntSet = EmptyIntSet + +/** + * Returns a new read-only [IntSet] with only [element1] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun intSetOf(element1: Int): IntSet = mutableIntSetOf(element1) + +/** + * Returns a new read-only [IntSet] with only [element1] and [element2] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun intSetOf(element1: Int, element2: Int): IntSet = + mutableIntSetOf(element1, element2) + +/** + * Returns a new read-only [IntSet] with only [element1], [element2], and [element3] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun intSetOf(element1: Int, element2: Int, element3: Int): IntSet = + mutableIntSetOf(element1, element2, element3) + +/** + * Returns a new read-only [IntSet] with only [elements] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun intSetOf(vararg elements: Int): IntSet = + MutableIntSet(elements.size).apply { plusAssign(elements) } + +/** * Returns a new [MutableIntSet]. */ public fun mutableIntSetOf(): MutableIntSet = MutableIntSet() /** + * Returns a new [MutableIntSet] with only [element1] in it. + */ +public fun mutableIntSetOf(element1: Int): MutableIntSet = + MutableIntSet(1).apply { + plusAssign(element1) + } + +/** + * Returns a new [MutableIntSet] with only [element1] and [element2] in it. + */ +public fun mutableIntSetOf(element1: Int, element2: Int): MutableIntSet = + MutableIntSet(2).apply { + plusAssign(element1) + plusAssign(element2) + } + +/** + * Returns a new [MutableIntSet] with only [element1], [element2], and [element3] in it. + */ +public fun mutableIntSetOf(element1: Int, element2: Int, element3: Int): MutableIntSet = + MutableIntSet(3).apply { + plusAssign(element1) + plusAssign(element2) + plusAssign(element3) + } + +/** * Returns a new [MutableIntSet] with the specified elements. */ public fun mutableIntSetOf(vararg elements: Int): MutableIntSet = - MutableIntSet(elements.size).apply { - addAll(elements) - } + MutableIntSet(elements.size).apply { plusAssign(elements) } /** * [IntSet] is a container with a [Set]-like interface designed to avoid
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt index 85679d4..94dfd82 100644 --- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt +++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
@@ -483,7 +483,7 @@ * @constructor Creates a [MutableLongList] with a [capacity] of `initialCapacity`. */ public class MutableLongList( - initialCapacity: Int = DefaultCapacity + initialCapacity: Int = 16 ) : LongList(initialCapacity) { /** * Returns the total number of elements that can be held before the [MutableLongList] must @@ -835,20 +835,84 @@ } } -@Suppress("ConstPropertyName") -private const val DefaultCapacity = 16 - // Empty array used when nothing is allocated @Suppress("PrivatePropertyName") private val EmptyLongArray = LongArray(0) +private val EmptyLongList: LongList = MutableLongList(0) + /** - * Creates and returns an empty [MutableLongList] with the default capacity. + * @return a read-only [LongList] with nothing in it. + */ +public fun emptyLongList(): LongList = EmptyLongList + +/** + * @return a read-only [LongList] with nothing in it. + */ +public fun longListOf(): LongList = EmptyLongList + +/** + * @return a new read-only [LongList] with [element1] as the only item in the list. + */ +public fun longListOf(element1: Long): LongList = mutableLongListOf(element1) + +/** + * @return a new read-only [LongList] with 2 elements, [element1] and [element2], in order. + */ +public fun longListOf(element1: Long, element2: Long): LongList = + mutableLongListOf(element1, element2) + +/** + * @return a new read-only [LongList] with 3 elements, [element1], [element2], and [element3], + * in order. + */ +public fun longListOf(element1: Long, element2: Long, element3: Long): LongList = + mutableLongListOf(element1, element2, element3) + +/** + * @return a new read-only [LongList] with [elements] in order. + */ +public fun longListOf(vararg elements: Long): LongList = + MutableLongList(elements.size).apply { plusAssign(elements) } + +/** + * @return a new empty [MutableLongList] with the default capacity. */ public inline fun mutableLongListOf(): MutableLongList = MutableLongList() /** - * Creates and returns a [MutableLongList] with the given values. + * @return a new [MutableLongList] with [element1] as the only item in the list. + */ +public fun mutableLongListOf(element1: Long): MutableLongList { + val list = MutableLongList(1) + list += element1 + return list +} + +/** + * @return a new [MutableLongList] with 2 elements, [element1] and [element2], in order. + */ +public fun mutableLongListOf(element1: Long, element2: Long): MutableLongList { + val list = MutableLongList(2) + list += element1 + list += element2 + return list +} + +/** + * @return a new [MutableLongList] with 3 elements, [element1], [element2], and [element3], + * in order. + */ +public fun mutableLongListOf(element1: Long, element2: Long, element3: Long): MutableLongList { + val list = MutableLongList(3) + list += element1 + list += element2 + list += element3 + return list +} + +/** + * @return a new [MutableLongList] with the given elements, in order. */ public inline fun mutableLongListOf(vararg elements: Long): MutableLongList = - MutableLongList(elements.size).also { it.addAll(elements) } + MutableLongList(elements.size).apply { plusAssign(elements) }
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt index 4cfe132..f292716 100644 --- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt +++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
@@ -34,7 +34,7 @@ // Default empty set to avoid allocations private val EmptyLongSet = MutableLongSet(0) -// An empty array of Longs +// An empty array of longs private val EmptyLongArray = LongArray(0) /** @@ -43,17 +43,75 @@ public fun emptyLongSet(): LongSet = EmptyLongSet /** + * Returns an empty, read-only [ScatterSet]. + */ +@Suppress("UNCHECKED_CAST") +public fun longSetOf(): LongSet = EmptyLongSet + +/** + * Returns a new read-only [LongSet] with only [element1] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun longSetOf(element1: Long): LongSet = mutableLongSetOf(element1) + +/** + * Returns a new read-only [LongSet] with only [element1] and [element2] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun longSetOf(element1: Long, element2: Long): LongSet = + mutableLongSetOf(element1, element2) + +/** + * Returns a new read-only [LongSet] with only [element1], [element2], and [element3] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun longSetOf(element1: Long, element2: Long, element3: Long): LongSet = + mutableLongSetOf(element1, element2, element3) + +/** + * Returns a new read-only [LongSet] with only [elements] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun longSetOf(vararg elements: Long): LongSet = + MutableLongSet(elements.size).apply { plusAssign(elements) } + +/** * Returns a new [MutableLongSet]. */ public fun mutableLongSetOf(): MutableLongSet = MutableLongSet() /** + * Returns a new [MutableLongSet] with only [element1] in it. + */ +public fun mutableLongSetOf(element1: Long): MutableLongSet = + MutableLongSet(1).apply { + plusAssign(element1) + } + +/** + * Returns a new [MutableLongSet] with only [element1] and [element2] in it. + */ +public fun mutableLongSetOf(element1: Long, element2: Long): MutableLongSet = + MutableLongSet(2).apply { + plusAssign(element1) + plusAssign(element2) + } + +/** + * Returns a new [MutableLongSet] with only [element1], [element2], and [element3] in it. + */ +public fun mutableLongSetOf(element1: Long, element2: Long, element3: Long): MutableLongSet = + MutableLongSet(3).apply { + plusAssign(element1) + plusAssign(element2) + plusAssign(element3) + } + +/** * Returns a new [MutableLongSet] with the specified elements. */ public fun mutableLongSetOf(vararg elements: Long): MutableLongSet = - MutableLongSet(elements.size).apply { - addAll(elements) - } + MutableLongSet(elements.size).apply { plusAssign(elements) } /** * [LongSet] is a container with a [Set]-like interface designed to avoid
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt index 3b13f66..2eac81f 100644 --- a/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt +++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ScatterSet.kt
@@ -42,17 +42,75 @@ public funemptyScatterSet(): ScatterSet /** + * Returns an empty, read-only [ScatterSet]. + */ +@Suppress("UNCHECKED_CAST") +public fun= EmptyScatterSet as ScatterSet scatterSetOf(): ScatterSet + +/** + * Returns a new read-only [ScatterSet] with only [element1] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun= EmptyScatterSet as ScatterSet scatterSetOf(element1: E): ScatterSet + +/** + * Returns a new read-only [ScatterSet] with only [element1] and [element2] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun= mutableScatterSetOf(element1) scatterSetOf(element1: E, element2: E): ScatterSet + mutableScatterSetOf(element1, element2) + +/** + * Returns a new read-only [ScatterSet] with only [element1], [element2], and [element3] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun= scatterSetOf(element1: E, element2: E, element3: E): ScatterSet + mutableScatterSetOf(element1, element2, element3) + +/** + * Returns a new read-only [ScatterSet] with only [elements] in it. + */ +@Suppress("UNCHECKED_CAST") +public fun= scatterSetOf(vararg elements: E): ScatterSet + MutableScatterSet= (elements.size).apply { plusAssign(elements) } + +/** * Returns a new [MutableScatterSet]. */ public funmutableScatterSetOf(): MutableScatterSet /** + * Returns a new [MutableScatterSet] with only [element1] in it. + */ +public fun= MutableScatterSet() mutableScatterSetOf(element1: E): MutableScatterSet + MutableScatterSet= (1).apply { + plusAssign(element1) + } + +/** + * Returns a new [MutableScatterSet] with only [element1] and [element2] in it. + */ +public funmutableScatterSetOf(element1: E, element2: E): MutableScatterSet + MutableScatterSet= (2).apply { + plusAssign(element1) + plusAssign(element2) + } + +/** + * Returns a new [MutableScatterSet] with only [element1], [element2], and [element3] in it. + */ +public funmutableScatterSetOf(element1: E, element2: E, element3: E): MutableScatterSet + MutableScatterSet= (3).apply { + plusAssign(element1) + plusAssign(element2) + plusAssign(element3) + } + +/** * Returns a new [MutableScatterSet] with the specified contents. */ public funmutableScatterSetOf(vararg elements: E): MutableScatterSet - MutableScatterSet= (elements.size).apply { - addAll(elements) - } + MutableScatterSet(elements.size).apply { plusAssign(elements) } /** * [ScatterSet] is a container with a [Set]-like interface based on a flat
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt index 63d4ce6..b4e501b 100644 --- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt +++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
@@ -629,4 +629,88 @@ l.sortDescending() assertEquals(mutableFloatListOf(5f, 4f, 3f, 2f, 1f), l) } + + @Test + fun testEmptyFloatList() { + val l = emptyFloatList() + assertEquals(0, l.size) + } + + @Test + fun floatListOfEmpty() { + val l = floatListOf() + assertEquals(0, l.size) + } + + @Test + fun floatListOfOneValue() { + val l = floatListOf(2f) + assertEquals(1, l.size) + assertEquals(2f, l[0]) + } + + @Test + fun floatListOfTwoValues() { + val l = floatListOf(2f, 1f) + assertEquals(2, l.size) + assertEquals(2f, l[0]) + assertEquals(1f, l[1]) + } + + @Test + fun floatListOfThreeValues() { + val l = floatListOf(2f, 10f, -1f) + assertEquals(3, l.size) + assertEquals(2f, l[0]) + assertEquals(10f, l[1]) + assertEquals(-1f, l[2]) + } + + @Test + fun floatListOfFourValues() { + val l = floatListOf(2f, 10f, -1f, 10f) + assertEquals(4, l.size) + assertEquals(2f, l[0]) + assertEquals(10f, l[1]) + assertEquals(-1f, l[2]) + assertEquals(10f, l[3]) + } + + @Test + fun mutableFloatListOfOneValue() { + val l = mutableFloatListOf(2f) + assertEquals(1, l.size) + assertEquals(1, l.capacity) + assertEquals(2f, l[0]) + } + + @Test + fun mutableFloatListOfTwoValues() { + val l = mutableFloatListOf(2f, 1f) + assertEquals(2, l.size) + assertEquals(2, l.capacity) + assertEquals(2f, l[0]) + assertEquals(1f, l[1]) + } + + @Test + fun mutableFloatListOfThreeValues() { + val l = mutableFloatListOf(2f, 10f, -1f) + assertEquals(3, l.size) + assertEquals(3, l.capacity) + assertEquals(2f, l[0]) + assertEquals(10f, l[1]) + assertEquals(-1f, l[2]) + } + + @Test + fun mutableFloatListOfFourValues() { + val l = mutableFloatListOf(2f, 10f, -1f, 10f) + assertEquals(4, l.size) + assertEquals(4, l.capacity) + assertEquals(2f, l[0]) + assertEquals(10f, l[1]) + assertEquals(-1f, l[2]) + assertEquals(10f, l[3]) + } }
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt index df1c181..98f7b59 100644 --- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt +++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
@@ -20,6 +20,7 @@ import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertNotEquals +import kotlin.test.assertSame import kotlin.test.assertTrue class FloatSetTest { @@ -442,4 +443,84 @@ assertTrue(set.trim() > 0) assertEquals(capacity, set.capacity) } + + @Test + fun floatSetOfEmpty() { + assertSame(emptyFloatSet(), floatSetOf()) + assertEquals(0, floatSetOf().size) + } + + @Test + fun floatSetOfOne() { + val set = floatSetOf(1f) + assertEquals(1, set.size) + assertEquals(1f, set.first()) + } + + @Test + fun floatSetOfTwo() { + val set = floatSetOf(1f, 2f) + assertEquals(2, set.size) + assertTrue(1f in set) + assertTrue(2f in set) + assertFalse(5f in set) + } + + @Test + fun floatSetOfThree() { + val set = floatSetOf(1f, 2f, 3f) + assertEquals(3, set.size) + assertTrue(1f in set) + assertTrue(2f in set) + assertTrue(3f in set) + assertFalse(5f in set) + } + + @Test + fun floatSetOfFour() { + val set = floatSetOf(1f, 2f, 3f, 4f) + assertEquals(4, set.size) + assertTrue(1f in set) + assertTrue(2f in set) + assertTrue(3f in set) + assertTrue(4f in set) + assertFalse(5f in set) + } + + @Test + fun mutableFloatSetOfOne() { + val set = mutableFloatSetOf(1f) + assertEquals(1, set.size) + assertEquals(1f, set.first()) + } + + @Test + fun mutableFloatSetOfTwo() { + val set = mutableFloatSetOf(1f, 2f) + assertEquals(2, set.size) + assertTrue(1f in set) + assertTrue(2f in set) + assertFalse(5f in set) + } + + @Test + fun mutableFloatSetOfThree() { + val set = mutableFloatSetOf(1f, 2f, 3f) + assertEquals(3, set.size) + assertTrue(1f in set) + assertTrue(2f in set) + assertTrue(3f in set) + assertFalse(5f in set) + } + + @Test + fun mutableFloatSetOfFour() { + val set = mutableFloatSetOf(1f, 2f, 3f, 4f) + assertEquals(4, set.size) + assertTrue(1f in set) + assertTrue(2f in set) + assertTrue(3f in set) + assertTrue(4f in set) + assertFalse(5f in set) + } }
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt index 068527c..66d89af 100644 --- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt +++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
@@ -628,4 +628,88 @@ l.sortDescending() assertEquals(mutableIntListOf(5, 4, 3, 2, 1), l) } + + @Test + fun testEmptyIntList() { + val l = emptyIntList() + assertEquals(0, l.size) + } + + @Test + fun intListOfEmpty() { + val l = intListOf() + assertEquals(0, l.size) + } + + @Test + fun intListOfOneValue() { + val l = intListOf(2) + assertEquals(1, l.size) + assertEquals(2, l[0]) + } + + @Test + fun intListOfTwoValues() { + val l = intListOf(2, 1) + assertEquals(2, l.size) + assertEquals(2, l[0]) + assertEquals(1, l[1]) + } + + @Test + fun intListOfThreeValues() { + val l = intListOf(2, 10, -1) + assertEquals(3, l.size) + assertEquals(2, l[0]) + assertEquals(10, l[1]) + assertEquals(-1, l[2]) + } + + @Test + fun intListOfFourValues() { + val l = intListOf(2, 10, -1, 10) + assertEquals(4, l.size) + assertEquals(2, l[0]) + assertEquals(10, l[1]) + assertEquals(-1, l[2]) + assertEquals(10, l[3]) + } + + @Test + fun mutableIntListOfOneValue() { + val l = mutableIntListOf(2) + assertEquals(1, l.size) + assertEquals(1, l.capacity) + assertEquals(2, l[0]) + } + + @Test + fun mutableIntListOfTwoValues() { + val l = mutableIntListOf(2, 1) + assertEquals(2, l.size) + assertEquals(2, l.capacity) + assertEquals(2, l[0]) + assertEquals(1, l[1]) + } + + @Test + fun mutableIntListOfThreeValues() { + val l = mutableIntListOf(2, 10, -1) + assertEquals(3, l.size) + assertEquals(3, l.capacity) + assertEquals(2, l[0]) + assertEquals(10, l[1]) + assertEquals(-1, l[2]) + } + + @Test + fun mutableIntListOfFourValues() { + val l = mutableIntListOf(2, 10, -1, 10) + assertEquals(4, l.size) + assertEquals(4, l.capacity) + assertEquals(2, l[0]) + assertEquals(10, l[1]) + assertEquals(-1, l[2]) + assertEquals(10, l[3]) + } }
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt index 121b0a6..9d326e1 100644 --- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt +++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
@@ -20,6 +20,7 @@ import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertNotEquals +import kotlin.test.assertSame import kotlin.test.assertTrue class IntSetTest { @@ -442,4 +443,84 @@ assertTrue(set.trim() > 0) assertEquals(capacity, set.capacity) } + + @Test + fun intSetOfEmpty() { + assertSame(emptyIntSet(), intSetOf()) + assertEquals(0, intSetOf().size) + } + + @Test + fun intSetOfOne() { + val set = intSetOf(1) + assertEquals(1, set.size) + assertEquals(1, set.first()) + } + + @Test + fun intSetOfTwo() { + val set = intSetOf(1, 2) + assertEquals(2, set.size) + assertTrue(1 in set) + assertTrue(2 in set) + assertFalse(5 in set) + } + + @Test + fun intSetOfThree() { + val set = intSetOf(1, 2, 3) + assertEquals(3, set.size) + assertTrue(1 in set) + assertTrue(2 in set) + assertTrue(3 in set) + assertFalse(5 in set) + } + + @Test + fun intSetOfFour() { + val set = intSetOf(1, 2, 3, 4) + assertEquals(4, set.size) + assertTrue(1 in set) + assertTrue(2 in set) + assertTrue(3 in set) + assertTrue(4 in set) + assertFalse(5 in set) + } + + @Test + fun mutableIntSetOfOne() { + val set = mutableIntSetOf(1) + assertEquals(1, set.size) + assertEquals(1, set.first()) + } + + @Test + fun mutableIntSetOfTwo() { + val set = mutableIntSetOf(1, 2) + assertEquals(2, set.size) + assertTrue(1 in set) + assertTrue(2 in set) + assertFalse(5 in set) + } + + @Test + fun mutableIntSetOfThree() { + val set = mutableIntSetOf(1, 2, 3) + assertEquals(3, set.size) + assertTrue(1 in set) + assertTrue(2 in set) + assertTrue(3 in set) + assertFalse(5 in set) + } + + @Test + fun mutableIntSetOfFour() { + val set = mutableIntSetOf(1, 2, 3, 4) + assertEquals(4, set.size) + assertTrue(1 in set) + assertTrue(2 in set) + assertTrue(3 in set) + assertTrue(4 in set) + assertFalse(5 in set) + } }
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt index e9893fa..45aa039 100644 --- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt +++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
@@ -628,4 +628,88 @@ l.sortDescending() assertEquals(mutableLongListOf(5L, 4L, 3L, 2L, 1L), l) } + + @Test + fun testEmptyLongList() { + val l = emptyLongList() + assertEquals(0, l.size) + } + + @Test + fun longListOfEmpty() { + val l = longListOf() + assertEquals(0, l.size) + } + + @Test + fun longListOfOneValue() { + val l = longListOf(2L) + assertEquals(1, l.size) + assertEquals(2L, l[0]) + } + + @Test + fun longListOfTwoValues() { + val l = longListOf(2L, 1L) + assertEquals(2, l.size) + assertEquals(2L, l[0]) + assertEquals(1L, l[1]) + } + + @Test + fun longListOfThreeValues() { + val l = longListOf(2L, 10L, -1L) + assertEquals(3, l.size) + assertEquals(2L, l[0]) + assertEquals(10L, l[1]) + assertEquals(-1L, l[2]) + } + + @Test + fun longListOfFourValues() { + val l = longListOf(2L, 10L, -1L, 10L) + assertEquals(4, l.size) + assertEquals(2L, l[0]) + assertEquals(10L, l[1]) + assertEquals(-1L, l[2]) + assertEquals(10L, l[3]) + } + + @Test + fun mutableLongListOfOneValue() { + val l = mutableLongListOf(2L) + assertEquals(1, l.size) + assertEquals(1, l.capacity) + assertEquals(2L, l[0]) + } + + @Test + fun mutableLongListOfTwoValues() { + val l = mutableLongListOf(2L, 1L) + assertEquals(2, l.size) + assertEquals(2, l.capacity) + assertEquals(2L, l[0]) + assertEquals(1L, l[1]) + } + + @Test + fun mutableLongListOfThreeValues() { + val l = mutableLongListOf(2L, 10L, -1L) + assertEquals(3, l.size) + assertEquals(3, l.capacity) + assertEquals(2L, l[0]) + assertEquals(10L, l[1]) + assertEquals(-1L, l[2]) + } + + @Test + fun mutableLongListOfFourValues() { + val l = mutableLongListOf(2L, 10L, -1L, 10L) + assertEquals(4, l.size) + assertEquals(4, l.capacity) + assertEquals(2L, l[0]) + assertEquals(10L, l[1]) + assertEquals(-1L, l[2]) + assertEquals(10L, l[3]) + } }
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt index de7549d..1278fcf 100644 --- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt +++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
@@ -20,6 +20,7 @@ import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertNotEquals +import kotlin.test.assertSame import kotlin.test.assertTrue class LongSetTest { @@ -442,4 +443,84 @@ assertTrue(set.trim() > 0) assertEquals(capacity, set.capacity) } + + @Test + fun longSetOfEmpty() { + assertSame(emptyLongSet(), longSetOf()) + assertEquals(0, longSetOf().size) + } + + @Test + fun longSetOfOne() { + val set = longSetOf(1L) + assertEquals(1, set.size) + assertEquals(1L, set.first()) + } + + @Test + fun longSetOfTwo() { + val set = longSetOf(1L, 2L) + assertEquals(2, set.size) + assertTrue(1L in set) + assertTrue(2L in set) + assertFalse(5L in set) + } + + @Test + fun longSetOfThree() { + val set = longSetOf(1L, 2L, 3L) + assertEquals(3, set.size) + assertTrue(1L in set) + assertTrue(2L in set) + assertTrue(3L in set) + assertFalse(5L in set) + } + + @Test + fun longSetOfFour() { + val set = longSetOf(1L, 2L, 3L, 4L) + assertEquals(4, set.size) + assertTrue(1L in set) + assertTrue(2L in set) + assertTrue(3L in set) + assertTrue(4L in set) + assertFalse(5L in set) + } + + @Test + fun mutableLongSetOfOne() { + val set = mutableLongSetOf(1L) + assertEquals(1, set.size) + assertEquals(1L, set.first()) + } + + @Test + fun mutableLongSetOfTwo() { + val set = mutableLongSetOf(1L, 2L) + assertEquals(2, set.size) + assertTrue(1L in set) + assertTrue(2L in set) + assertFalse(5L in set) + } + + @Test + fun mutableLongSetOfThree() { + val set = mutableLongSetOf(1L, 2L, 3L) + assertEquals(3, set.size) + assertTrue(1L in set) + assertTrue(2L in set) + assertTrue(3L in set) + assertFalse(5L in set) + } + + @Test + fun mutableLongSetOfFour() { + val set = mutableLongSetOf(1L, 2L, 3L, 4L) + assertEquals(4, set.size) + assertTrue(1L in set) + assertTrue(2L in set) + assertTrue(3L in set) + assertTrue(4L in set) + assertFalse(5L in set) + } }
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterSetTest.kt index 201b2ef..60de883 100644 --- a/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterSetTest.kt +++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ScatterSetTest.kt
@@ -21,6 +21,7 @@ import kotlin.test.assertFalse import kotlin.test.assertNotEquals import kotlin.test.assertNull +import kotlin.test.assertSame import kotlin.test.assertTrue class ScatterSetTest { @@ -648,4 +649,84 @@ assertTrue(set.trim() > 0) assertEquals(capacity, set.capacity) } + + @Test + fun scatterSetOfEmpty() { + assertSame(emptyScatterSet(), scatterSetOf + assertEquals(0, scatterSetOf()) ().size) + } + + @Test + fun scatterSetOfOne() { + val set = scatterSetOf("Hello") + assertEquals(1, set.size) + assertEquals("Hello", set.first()) + } + + @Test + fun scatterSetOfTwo() { + val set = scatterSetOf("Hello", "World") + assertEquals(2, set.size) + assertTrue("Hello" in set) + assertTrue("World" in set) + assertFalse("Bonjour" in set) + } + + @Test + fun scatterSetOfThree() { + val set = scatterSetOf("Hello", "World", "Hola") + assertEquals(3, set.size) + assertTrue("Hello" in set) + assertTrue("World" in set) + assertTrue("Hola" in set) + assertFalse("Bonjour" in set) + } + + @Test + fun scatterSetOfFour() { + val set = scatterSetOf("Hello", "World", "Hola", "Mundo") + assertEquals(4, set.size) + assertTrue("Hello" in set) + assertTrue("World" in set) + assertTrue("Hola" in set) + assertTrue("Mundo" in set) + assertFalse("Bonjour" in set) + } + + @Test + fun mutableScatterSetOfOne() { + val set = mutableScatterSetOf("Hello") + assertEquals(1, set.size) + assertEquals("Hello", set.first()) + } + + @Test + fun mutableScatterSetOfTwo() { + val set = mutableScatterSetOf("Hello", "World") + assertEquals(2, set.size) + assertTrue("Hello" in set) + assertTrue("World" in set) + assertFalse("Bonjour" in set) + } + + @Test + fun mutableScatterSetOfThree() { + val set = mutableScatterSetOf("Hello", "World", "Hola") + assertEquals(3, set.size) + assertTrue("Hello" in set) + assertTrue("World" in set) + assertTrue("Hola" in set) + assertFalse("Bonjour" in set) + } + + @Test + fun mutableScatterSetOfFour() { + val set = mutableScatterSetOf("Hello", "World", "Hola", "Mundo") + assertEquals(4, set.size) + assertTrue("Hello" in set) + assertTrue("World" in set) + assertTrue("Hola" in set) + assertTrue("Mundo" in set) + assertFalse("Bonjour" in set) + } }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt index 6ae0c2a..36e5ddc 100644 --- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt +++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
@@ -17,6 +17,8 @@ package androidx.compose.material import android.os.Build +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -26,8 +28,13 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Favorite import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.testutils.assertShape import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.LayoutCoordinates @@ -47,6 +54,7 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest @@ -148,14 +156,18 @@ rule.setMaterialContent { Column { Spacer( - Modifier.requiredSize(10.dp).weight(1f).onGloballyPositioned { - item1Bounds = it.boundsInRoot() - } + Modifier + .requiredSize(10.dp) + .weight(1f) + .onGloballyPositioned { + item1Bounds = it.boundsInRoot() + } ) FloatingActionButton( onClick = {}, - modifier = Modifier.weight(1f) + modifier = Modifier + .weight(1f) .onGloballyPositioned { buttonBounds = it.boundsInRoot() } @@ -163,7 +175,10 @@ Text("Button") } - Spacer(Modifier.requiredSize(10.dp).weight(1f)) + Spacer( + Modifier + .requiredSize(10.dp) + .weight(1f)) } } @@ -257,7 +272,8 @@ } ) { Box( - Modifier.size(2.dp) + Modifier + .size(2.dp) .onGloballyPositioned { contentCoordinates = it } ) } @@ -286,7 +302,8 @@ ExtendedFloatingActionButton( text = { Box( - Modifier.size(2.dp) + Modifier + .size(2.dp) .onGloballyPositioned { contentCoordinates = it } ) }, @@ -319,13 +336,15 @@ ExtendedFloatingActionButton( text = { Box( - Modifier.size(2.dp) + Modifier + .size(2.dp) .onGloballyPositioned { textCoordinates = it } ) }, icon = { Box( - Modifier.size(10.dp) + Modifier + .size(10.dp) .onGloballyPositioned { iconCoordinates = it } ) }, @@ -355,4 +374,112 @@ } } } + + @Test + fun floatingActionButtonElevation_newInteraction() { + val interactionSource = MutableInteractionSource() + val defaultElevation = 1.dp + val pressedElevation = 2.dp + val hoveredElevation = 3.dp + val focusedElevation = 4.dp + lateinit var elevation: State+ + rule.setMaterialContent { + val fabElevation = FloatingActionButtonDefaults.elevation( + defaultElevation = defaultElevation, + pressedElevation = pressedElevation, + hoveredElevation = hoveredElevation, + focusedElevation = focusedElevation + ) + + elevation = fabElevation.elevation(interactionSource) + } + + rule.runOnIdle { + assertThat(elevation.value).isEqualTo(defaultElevation) + } + + rule.runOnIdle { + interactionSource.tryEmit(PressInteraction.Press(Offset.Zero)) + } + + rule.runOnIdle { + assertThat(elevation.value).isEqualTo(pressedElevation) + } + } + + @Test + fun floatingActionButtonElevation_newValue() { + val interactionSource = MutableInteractionSource() + var defaultElevation by mutableStateOf(1.dp) + val pressedElevation = 2.dp + val hoveredElevation = 3.dp + val focusedElevation = 4.dp + lateinit var elevation: State + + rule.setMaterialContent { + val fabElevation = FloatingActionButtonDefaults.elevation( + defaultElevation = defaultElevation, + pressedElevation = pressedElevation, + hoveredElevation = hoveredElevation, + focusedElevation = focusedElevation + ) + + elevation = fabElevation.elevation(interactionSource) + } + + rule.runOnIdle { + assertThat(elevation.value).isEqualTo(defaultElevation) + } + + rule.runOnIdle { + defaultElevation = 5.dp + } + + rule.runOnIdle { + assertThat(elevation.value).isEqualTo(5.dp) + } + } + + @Test + fun floatingActionButtonElevation_newValueDuringInteraction() { + val interactionSource = MutableInteractionSource() + val defaultElevation = 1.dp + var pressedElevation by mutableStateOf(2.dp) + val hoveredElevation = 3.dp + val focusedElevation = 4.dp + lateinit var elevation: State + + rule.setMaterialContent { + val fabElevation = FloatingActionButtonDefaults.elevation( + defaultElevation = defaultElevation, + pressedElevation = pressedElevation, + hoveredElevation = hoveredElevation, + focusedElevation = focusedElevation + ) + + elevation = fabElevation.elevation(interactionSource) + } + + rule.runOnIdle { + assertThat(elevation.value).isEqualTo(defaultElevation) + } + + rule.runOnIdle { + interactionSource.tryEmit(PressInteraction.Press(Offset.Zero)) + } + + rule.runOnIdle { + assertThat(elevation.value).isEqualTo(pressedElevation) + } + + rule.runOnIdle { + pressedElevation = 5.dp + } + + // We are still pressed, so we should now show the updated value for the pressed state + rule.runOnIdle { + assertThat(elevation.value).isEqualTo(5.dp) + } + } }
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/DatePickerBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/DatePickerBenchmark.kt index e5e5664..e5ba192 100644 --- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/DatePickerBenchmark.kt +++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/DatePickerBenchmark.kt
@@ -27,6 +27,7 @@ import androidx.compose.testutils.benchmark.benchmarkFirstCompose import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -42,40 +43,58 @@ private val dateInputTestCaseFactory = { DateInputTestCase() } @Test + fun datePicker_firstPixel() { + benchmarkRule.benchmarkFirstRenderUntilStable(datePickerTestCaseFactory) + } + + @Test + fun dateInput_firstPixel() { + benchmarkRule.benchmarkFirstRenderUntilStable(dateInputTestCaseFactory) + } + + @Ignore + @Test fun first_compose_pickerMode() { benchmarkRule.benchmarkFirstCompose(datePickerTestCaseFactory) } + @Ignore @Test fun first_compose_inputMode() { benchmarkRule.benchmarkFirstCompose(dateInputTestCaseFactory) } + @Ignore @Test fun datePicker_measure() { benchmarkRule.benchmarkMeasureUntilStable(datePickerTestCaseFactory) } + @Ignore @Test fun dateInput_measure() { benchmarkRule.benchmarkMeasureUntilStable(dateInputTestCaseFactory) } + @Ignore @Test fun datePicker_layout() { benchmarkRule.benchmarkLayoutUntilStable(datePickerTestCaseFactory) } + @Ignore @Test fun dateInput_layout() { benchmarkRule.benchmarkLayoutUntilStable(dateInputTestCaseFactory) } + @Ignore @Test fun datePicker_draw() { benchmarkRule.benchmarkDrawUntilStable(datePickerTestCaseFactory) } + @Ignore @Test fun dateInput_draw() { benchmarkRule.benchmarkDrawUntilStable(dateInputTestCaseFactory)
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/DateRangePickerBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/DateRangePickerBenchmark.kt index b9f97a3..61aec86 100644 --- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/DateRangePickerBenchmark.kt +++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/DateRangePickerBenchmark.kt
@@ -28,8 +28,10 @@ import androidx.compose.testutils.benchmark.benchmarkFirstDraw import androidx.compose.testutils.benchmark.benchmarkFirstLayout import androidx.compose.testutils.benchmark.benchmarkFirstMeasure +import androidx.compose.testutils.benchmark.benchmarkToFirstPixel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -45,40 +47,58 @@ private val dateRangeInputTestCaseFactory = { DateRangeInputTestCase() } @Test + fun dateRangePicker_firstPixel() { + benchmarkRule.benchmarkToFirstPixel(dateRangePickerTestCaseFactory) + } + + @Test + fun dateRangeInput_firstPixel() { + benchmarkRule.benchmarkFirstRenderUntilStable(dateRangeInputTestCaseFactory) + } + + @Ignore + @Test fun first_compose_pickerMode() { benchmarkRule.benchmarkFirstCompose(dateRangePickerTestCaseFactory) } + @Ignore @Test fun first_compose_inputMode() { benchmarkRule.benchmarkFirstCompose(dateRangeInputTestCaseFactory) } + @Ignore @Test fun dateRangePicker_measure() { benchmarkRule.benchmarkFirstMeasure(dateRangePickerTestCaseFactory) } + @Ignore @Test fun dateRangeInput_measure() { benchmarkRule.benchmarkMeasureUntilStable(dateRangeInputTestCaseFactory) } + @Ignore @Test fun dateRangePicker_layout() { benchmarkRule.benchmarkFirstLayout(dateRangePickerTestCaseFactory) } + @Ignore @Test fun dateRangeInput_layout() { benchmarkRule.benchmarkLayoutUntilStable(dateRangeInputTestCaseFactory) } + @Ignore @Test fun dateRangePicker_draw() { benchmarkRule.benchmarkFirstDraw(dateRangePickerTestCaseFactory) } + @Ignore @Test fun dateRangeInput_draw() { benchmarkRule.benchmarkDrawUntilStable(dateRangeInputTestCaseFactory)
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt index 4d11904..9dfb619 100644 --- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt +++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
@@ -16,6 +16,8 @@ package androidx.compose.material3 +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -28,6 +30,7 @@ import androidx.compose.material3.tokens.FabPrimarySmallTokens import androidx.compose.material3.tokens.FabPrimaryTokens import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -56,6 +59,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -540,6 +544,127 @@ .assertHeightIsEqualTo(FabPrimaryTokens.ContainerHeight) .assertWidthIsEqualTo(FabPrimaryTokens.ContainerWidth) } + + @Test + fun floatingActionButtonElevation_newInteraction() { + val interactionSource = MutableInteractionSource() + val defaultElevation = 1.dp + val pressedElevation = 2.dp + val hoveredElevation = 3.dp + val focusedElevation = 4.dp + lateinit var tonalElevation: State+ lateinit var shadowElevation: State + + rule.setMaterialContent(lightColorScheme()) { + val fabElevation = FloatingActionButtonDefaults.elevation( + defaultElevation = defaultElevation, + pressedElevation = pressedElevation, + hoveredElevation = hoveredElevation, + focusedElevation = focusedElevation + ) + + tonalElevation = fabElevation.tonalElevation(interactionSource) + shadowElevation = fabElevation.shadowElevation(interactionSource) + } + + rule.runOnIdle { + assertThat(tonalElevation.value).isEqualTo(defaultElevation) + assertThat(shadowElevation.value).isEqualTo(defaultElevation) + } + + rule.runOnIdle { + interactionSource.tryEmit(PressInteraction.Press(Offset.Zero)) + } + + rule.runOnIdle { + assertThat(tonalElevation.value).isEqualTo(pressedElevation) + assertThat(shadowElevation.value).isEqualTo(pressedElevation) + } + } + + @Test + fun floatingActionButtonElevation_newValue() { + val interactionSource = MutableInteractionSource() + var defaultElevation by mutableStateOf(1.dp) + val pressedElevation = 2.dp + val hoveredElevation = 3.dp + val focusedElevation = 4.dp + lateinit var tonalElevation: State + lateinit var shadowElevation: State + + rule.setMaterialContent(lightColorScheme()) { + val fabElevation = FloatingActionButtonDefaults.elevation( + defaultElevation = defaultElevation, + pressedElevation = pressedElevation, + hoveredElevation = hoveredElevation, + focusedElevation = focusedElevation + ) + + tonalElevation = fabElevation.tonalElevation(interactionSource) + shadowElevation = fabElevation.shadowElevation(interactionSource) + } + + rule.runOnIdle { + assertThat(tonalElevation.value).isEqualTo(defaultElevation) + assertThat(shadowElevation.value).isEqualTo(defaultElevation) + } + + rule.runOnIdle { + defaultElevation = 5.dp + } + + rule.runOnIdle { + assertThat(tonalElevation.value).isEqualTo(5.dp) + assertThat(shadowElevation.value).isEqualTo(5.dp) + } + } + + @Test + fun floatingActionButtonElevation_newValueDuringInteraction() { + val interactionSource = MutableInteractionSource() + val defaultElevation = 1.dp + var pressedElevation by mutableStateOf(2.dp) + val hoveredElevation = 3.dp + val focusedElevation = 4.dp + lateinit var tonalElevation: State + lateinit var shadowElevation: State + + rule.setMaterialContent(lightColorScheme()) { + val fabElevation = FloatingActionButtonDefaults.elevation( + defaultElevation = defaultElevation, + pressedElevation = pressedElevation, + hoveredElevation = hoveredElevation, + focusedElevation = focusedElevation + ) + + tonalElevation = fabElevation.tonalElevation(interactionSource) + shadowElevation = fabElevation.shadowElevation(interactionSource) + } + + rule.runOnIdle { + assertThat(tonalElevation.value).isEqualTo(defaultElevation) + assertThat(shadowElevation.value).isEqualTo(defaultElevation) + } + + rule.runOnIdle { + interactionSource.tryEmit(PressInteraction.Press(Offset.Zero)) + } + + rule.runOnIdle { + assertThat(tonalElevation.value).isEqualTo(pressedElevation) + assertThat(shadowElevation.value).isEqualTo(pressedElevation) + } + + rule.runOnIdle { + pressedElevation = 5.dp + } + + // We are still pressed, so we should now show the updated value for the pressed state + rule.runOnIdle { + assertThat(tonalElevation.value).isEqualTo(5.dp) + assertThat(shadowElevation.value).isEqualTo(5.dp) + } + } } fun assertWithinOnePixel(expected: Offset, actual: Offset) {
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/state/ComposeStateReadBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/state/ComposeStateReadBenchmark.kt new file mode 100644 index 0000000..44a1a9f --- /dev/null +++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/state/ComposeStateReadBenchmark.kt
@@ -0,0 +1,202 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.runtime.benchmark.state + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.compose.runtime.Applier +import androidx.compose.runtime.Composition +import androidx.compose.runtime.Recomposer +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.snapshots.SnapshotStateObserver +import androidx.test.filters.LargeTest +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@LargeTest +@RunWith(Parameterized::class) +class ComposeStateReadBenchmark(private val readContext: ReadContext) { + enum class ReadContext { + Composition, + Measure; + } + + companion object { + private const val MEASURE_OBSERVATION_DEPTH = 5 + private val OnCommitInvalidatingMeasure: (Any) -> Unit = {} + + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun parameters() = arrayOf(ReadContext.Composition, ReadContext.Measure) + } + + @get:Rule + val benchmarkRule = BenchmarkRule() + + @Test + fun readState() { + val state = mutableIntStateOf(0) + + benchmarkRead { + state.value + } + } + + @Test + fun readDerivedState() { + val stateA = mutableIntStateOf(0) + val stateB = mutableIntStateOf(0) + val derivedState = derivedStateOf { stateA.value + stateB.value } + + derivedState.value // precompute result + + benchmarkRead { + derivedState.value + } + } + + @Test + fun readDerivedState_secondRead() { + val stateA = mutableIntStateOf(0) + val stateB = mutableIntStateOf(0) + val derivedState = derivedStateOf { stateA.value + stateB.value } + + derivedState.value // precompute result + + benchmarkRead(before = { derivedState.value }) { + derivedState.value + } + } + + @Test + fun readDerivedState_afterWrite() { + val stateA = mutableIntStateOf(0) + val stateB = mutableIntStateOf(0) + val derivedState = derivedStateOf { stateA.value + stateB.value } + + derivedState.value // precompute result + + benchmarkRead(before = { stateA.value += 1 }) { + derivedState.value + } + } + + @Test + fun readState_afterWrite() { + val stateA = mutableIntStateOf(0) + + benchmarkRead(before = { stateA.value += 1 }) { + stateA.value + } + } + + @Test + fun readState_preinitialized() { + val stateA = mutableIntStateOf(0) + val stateB = mutableIntStateOf(0) + + benchmarkRead(before = { stateA.value }) { + stateB.value + } + } + + @Test + fun readDerivedState_preinitialized() { + val stateA = mutableIntStateOf(0) + val stateB = mutableIntStateOf(0) + + val derivedStateA = derivedStateOf { stateA.value + stateB.value } + val derivedStateB = derivedStateOf { stateB.value + stateA.value } + + benchmarkRead(before = { derivedStateA.value }) { + derivedStateB.value + } + } + + private fun benchmarkRead( + before: () -> Unit = {}, + after: () -> Unit = {}, + measure: () -> Unit + ) { + val benchmarkState = benchmarkRule.getState() + benchmarkRule.measureRepeated { + benchmarkState.pauseTiming() + runInReadObservationScope { + before() + benchmarkState.resumeTiming() + + measure() + + benchmarkState.pauseTiming() + after() + } + benchmarkRule.getState().resumeTiming() + } + } + + private fun runInReadObservationScope(scopeBlock: () -> Unit) { + when (readContext) { + ReadContext.Composition -> createComposition().setContent { scopeBlock() } + ReadContext.Measure -> { + SnapshotStateObserver { it() }.apply { + val nodes = List(MEASURE_OBSERVATION_DEPTH) { Any() } + start() + recursiveObserve(nodes, nodes.size, scopeBlock) + stop() + } + } + } + } + + private fun SnapshotStateObserver.recursiveObserve( + nodes: List, + depth: Int, + block: () -> Unit + ) { + if (depth == 0) { + block() + return + } + observeReads(nodes[depth - 1], OnCommitInvalidatingMeasure) { + recursiveObserve(nodes, depth - 1, block) + } + } + + private fun createComposition( + coroutineContext: CoroutineContext = EmptyCoroutineContext + ): Composition { + val applier = UnitApplier() + val recomposer = Recomposer(coroutineContext) + return Composition(applier, recomposer) + } + + private class UnitApplier : Applier{ + override val current: Unit = Unit + override fun clear() {} + override fun move(from: Int, to: Int, count: Int) {} + override fun remove(index: Int, count: Int) {} + override fun up() {} + override fun insertTopDown(index: Int, instance: Unit) {} + override fun insertBottomUp(index: Int, instance: Unit) {} + override fun down(node: Unit) {} + } +}
diff --git a/compose/runtime/runtime/src/androidMain/kotlin/androidx/compose/runtime/ActualAndroid.android.kt b/compose/runtime/runtime/src/androidMain/kotlin/androidx/compose/runtime/ActualAndroid.android.kt index e712b4a..13d9104 100644 --- a/compose/runtime/runtime/src/androidMain/kotlin/androidx/compose/runtime/ActualAndroid.android.kt +++ b/compose/runtime/runtime/src/androidMain/kotlin/androidx/compose/runtime/ActualAndroid.android.kt
@@ -113,3 +113,5 @@ internal actual fun logError(message: String, e: Throwable) { Log.e(LogTag, message, e) } + +internal actual val MainThreadId: Long = Looper.getMainLooper()?.thread?.id ?: -1
diff --git a/compose/runtime/runtime/src/desktopMain/kotlin/androidx/compose/runtime/ActualDesktop.desktop.kt b/compose/runtime/runtime/src/desktopMain/kotlin/androidx/compose/runtime/ActualDesktop.desktop.kt index 6594283..a150271 100644 --- a/compose/runtime/runtime/src/desktopMain/kotlin/androidx/compose/runtime/ActualDesktop.desktop.kt +++ b/compose/runtime/runtime/src/desktopMain/kotlin/androidx/compose/runtime/ActualDesktop.desktop.kt
@@ -102,3 +102,5 @@ System.err.println(message) e.printStackTrace(System.err) } + +internal actual val MainThreadId: Long = -1
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt index e73fbb9..71576e4 100644 --- a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt +++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt
@@ -50,19 +50,34 @@ private val map = AtomicReference(emptyThreadMap) private val writeMutex = Any() + private var mainThreadValue: T? = null + @Suppress("UNCHECKED_CAST") - actual fun get(): T? = map.get().get(Thread.currentThread().id) as T? + actual fun get(): T? { + val threadId = Thread.currentThread().id + return if (threadId == MainThreadId) { + mainThreadValue + } else { + map.get().get(Thread.currentThread().id) as T? + } + } actual fun set(value: T?) { val key = Thread.currentThread().id - synchronized(writeMutex) { - val current = map.get() - if (current.trySet(key, value)) return - map.set(current.newWith(key, value)) + if (key == MainThreadId) { + mainThreadValue = value + } else { + synchronized(writeMutex) { + val current = map.get() + if (current.trySet(key, value)) return + map.set(current.newWith(key, value)) + } } } } +internal expect val MainThreadId: Long + internal actual fun identityHashCode(instance: Any?): Int = System.identityHashCode(instance) @PublishedApi
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/drawscope/DrawScopeTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/drawscope/DrawScopeTest.kt index 55a81a0..7f26707 100644 --- a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/drawscope/DrawScopeTest.kt +++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/drawscope/DrawScopeTest.kt
@@ -23,8 +23,10 @@ import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.ClipOp import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageBitmapConfig import androidx.compose.ui.graphics.LinearGradientShader import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.PaintingStyle @@ -40,6 +42,7 @@ import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.toPixelMap import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -1770,6 +1773,52 @@ } } + @Test + fun testBrushResetOnSubsequentDrawWithAlphaBitmap() { + val width = 200 + val height = 200 + val brush = Brush.horizontalGradient( + listOf(Color.Transparent, Color.Blue, Color.Transparent) + ) + val maskBitmap = ImageBitmap(width / 2, height / 2, ImageBitmapConfig.Alpha8) + val maskCanvas = Canvas(maskBitmap) + maskCanvas.drawRect( + Rect(0f, 0f, width.toFloat(), height.toFloat()), + Paint().apply { color = Color.Green } + ) + val colorFilter = ColorFilter.tint(Color.Red) + testDrawScopeAndCanvasAreEquivalent( + width, + height, + { + // Drawing an ImageBitmap after drawing a brush should unset the + // previously configured brush + drawRect(brush) + inset(width / 4f, height / 4f) { + drawImage(maskBitmap, colorFilter = colorFilter) + } + }, + { canvas -> + val paint = Paint().apply { + brush.applyTo(Size(width.toFloat(), height.toFloat()), this, 1f) + } + canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint) + canvas.save() + canvas.translate(width / 4f, height / 4f) + canvas.drawImageRect( + maskBitmap, + srcOffset = IntOffset.Zero, + srcSize = IntSize(width, height), + dstOffset = IntOffset.Zero, + dstSize = IntSize(width, height), + Paint().apply { + this.colorFilter = colorFilter + }) + canvas.restore() + } + ) + } + private inline fun testDrawTransformDefault(block: WrappedDrawTransform.() -> Unit) { val width = 100 val height = 150
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt index 36fe832..c8ea35b 100644 --- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt +++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt
@@ -614,8 +614,10 @@ ): Paint = selectPaint(style).apply { if (brush != null) { brush.applyTo(size, this, alpha) - } else if (this.alpha != alpha) { - this.alpha = alpha + } else { + if (this.shader != null) this.shader = null + if (this.color != Color.Black) this.color = Color.Black + if (this.alpha != alpha) this.alpha = alpha } if (this.colorFilter != colorFilter) this.colorFilter = colorFilter if (this.blendMode != blendMode) this.blendMode = blendMode
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt index dda58d9..48e4a35 100644 --- a/compose/ui/ui/api/current.txt +++ b/compose/ui/ui/api/current.txt
@@ -1967,9 +1967,11 @@ public final class RotaryScrollEvent { method public float getHorizontalScrollPixels(); + method public int getInputDeviceId(); method public long getUptimeMillis(); method public float getVerticalScrollPixels(); property public final float horizontalScrollPixels; + property public final int inputDeviceId; property public final long uptimeMillis; property public final float verticalScrollPixels; }
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt index 1557073..e254bab 100644 --- a/compose/ui/ui/api/restricted_current.txt +++ b/compose/ui/ui/api/restricted_current.txt
@@ -1967,9 +1967,11 @@ public final class RotaryScrollEvent { method public float getHorizontalScrollPixels(); + method public int getInputDeviceId(); method public long getUptimeMillis(); method public float getVerticalScrollPixels(); property public final float horizontalScrollPixels; + property public final int inputDeviceId; property public final long uptimeMillis; property public final float verticalScrollPixels; }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt index 3a813aa..0e0a75b 100644 --- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt +++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
@@ -26,6 +26,7 @@ import androidx.activity.ComponentActivity import androidx.annotation.RequiresApi import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.height @@ -863,7 +864,11 @@ contentDescription = null, modifier = Modifier .testTag(testTag) - .background(Color.Red) + .background( + Brush.horizontalGradient( + listOf(Color.Transparent, Color.Yellow, Color.Transparent) + ) + ) .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen }, contentScale = ContentScale.FillBounds, colorFilter = colorFilter
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt index 6d7da47..e350b81 100644 --- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt +++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
@@ -64,7 +64,7 @@ private val sentEvent: Any = when (nodeType) { KeyInput, InterruptedSoftKeyboardInput -> KeyEvent(AndroidKeyEvent(ACTION_DOWN, KEYCODE_A)) - RotaryInput -> RotaryScrollEvent(1f, 1f, 0L) + RotaryInput -> RotaryScrollEvent(1f, 1f, 0L, 0) } private var receivedEvent: Any? = null private val initialFocus = FocusRequester()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt index c2b3768..31db4be 100644 --- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt +++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
@@ -18,8 +18,11 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReusableContent import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -858,4 +861,45 @@ } assertThat(events).hasSize(2) } + + @Test + @MediumTest + fun lambdaIsRecapturedWhenReused() { + val tag = "box" + val events = mutableListOf() + + @Composable + fun BoxWithKey(key: Int) { + // imitating one of the recommended patterns for Modifier.pointerInput() where we use + // rememberUpdatedState in order to have the latest value inside the suspending lambda. + // technically the state backing rememberUpdatedState will be recreated when the reuse + // happens so Modifier.pointerInput() have to update it's lambda to the new one even + // given that the key (Unit) didn't change. + val currentKey by rememberUpdatedState(key) + Box( + Modifier + .testTag(tag) + .fillMaxSize() + .pointerInput(Unit) { + events.add(currentKey) + }) + } + + var key by mutableStateOf(0) + + rule.setContent { + ReusableContent(key = key) { + BoxWithKey(key) + } + } + + rule.runOnIdle { + key++ + } + + rule.onNodeWithTag(tag).performTouchInput { + down(Offset.Zero) + } + assertThat(events).isEqualTo(listOf(key)) + } }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEventTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEventTest.kt index 06d53a3..e6eb02c 100644 --- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEventTest.kt +++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEventTest.kt
@@ -274,6 +274,41 @@ } @Test + fun rotaryEventHasDeviceId() { + val DEVICE_ID = 1234 + + // Arrange. + ContentWithInitialFocus { + Box( + modifier = Modifier + .onRotaryScrollEvent { + receivedEvent = it + true + } + .focusable(initiallyFocused = true) + ) + } + + // Act. + rule.runOnIdle { + rootView.dispatchGenericMotionEvent( + MotionEventBuilder.newBuilder() + .setAction(ACTION_SCROLL) + .setSource(SOURCE_ROTARY_ENCODER) + .setDeviceId(DEVICE_ID) + .build() + ) + } + + // Assert. + rule.runOnIdle { + with(checkNotNull(receivedEvent)) { + assertThat(inputDeviceId).isEqualTo(DEVICE_ID) + } + } + } + + @Test fun rotaryEventUsesTestTime() { val TIME_DELTA = 1234L
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt index 205b312..44ae78e 100644 --- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt +++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
@@ -2382,7 +2382,10 @@ val content = if (showContent) { subcompose(0) { Box { - AndroidView(::View, Modifier.fillMaxSize().testTag("AndroidView")) + AndroidView(::View, + Modifier + .fillMaxSize() + .testTag("AndroidView")) } } } else emptyList() @@ -2495,7 +2498,9 @@ Box { SubcomposeLayout { constraints -> val placeable = measure(Unit, constraints) { - Box(modifier = Modifier.size(10.dp).then(measureCountModifier)) + Box(modifier = Modifier + .size(10.dp) + .then(measureCountModifier)) DisposableEffect(Unit) { val capturedSlotId = slotId @@ -2539,6 +2544,42 @@ } } + @Test + fun slotIsProperlyDeactivatedAfterUpdatingReusePolicy() { + var state by mutableStateOf(SubcomposeLayoutState(SubcomposeSlotReusePolicy(1))) + var shouldCompose by mutableStateOf(true) + var disposed = false + rule.setContent { + SubcomposeLayout(state) { constraints -> + val placeables = if (shouldCompose) { + subcompose(Unit) { + DisposableEffect(Unit) { + onDispose { + disposed = true + } + } + }.map { + it.measure(constraints) + } + } else { + emptyList() + } + layout(100, 100) { + placeables.forEach { it.place(0, 0) } + } + } + } + + rule.runOnIdle { + state = SubcomposeLayoutState(SubcomposeSlotReusePolicy(1)) + shouldCompose = false + } + + rule.runOnIdle { + assertThat(disposed).isTrue() + } + } + private fun SubcomposeMeasureScope.measure( slotId: Any, constraints: Constraints,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEvent.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEvent.android.kt new file mode 100644 index 0000000..8c3e583 --- /dev/null +++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEvent.android.kt
@@ -0,0 +1,66 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.input.rotary + +/** + * This event represents a rotary input event. + * + * Some Wear OS devices contain a physical rotating side button, or a rotating bezel. When the user + * turns the button or rotates the bezel, a [RotaryScrollEvent] is sent to the item in focus. + */ +actual class RotaryScrollEvent internal constructor( + /** + * The amount to scroll (in pixels) in response to a [RotaryScrollEvent] in a container that + * can scroll vertically. + */ + actual val verticalScrollPixels: Float, + + /** + * The amount to scroll (in pixels) in response to a [RotaryScrollEvent] in a container that + * can scroll horizontally. + */ + actual val horizontalScrollPixels: Float, + + /** + * The time in milliseconds at which this even occurred. The start (`0`) time is + * platform-dependent. + */ + actual val uptimeMillis: Long, + + /** + * The id for the input device that this event came from + */ + val inputDeviceId: Int +) { + override fun equals(other: Any?): Boolean = other is RotaryScrollEvent && + other.verticalScrollPixels == verticalScrollPixels && + other.horizontalScrollPixels == horizontalScrollPixels && + other.uptimeMillis == uptimeMillis && + other.inputDeviceId == inputDeviceId + + override fun hashCode(): Int = 0 + .let { verticalScrollPixels.hashCode() } + .let { 31 * it + horizontalScrollPixels.hashCode() } + .let { 31 * it + uptimeMillis.hashCode() } + .let { 31 * it + inputDeviceId.hashCode() } + + override fun toString(): String = "RotaryScrollEvent(" + + "verticalScrollPixels=$verticalScrollPixels," + + "horizontalScrollPixels=$horizontalScrollPixels," + + "uptimeMillis=$uptimeMillis," + + "deviceId=$inputDeviceId)" +}
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 2f3b345..9fe5321 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
@@ -1399,7 +1399,8 @@ val rotaryEvent = RotaryScrollEvent( verticalScrollPixels = axisValue * getScaledVerticalScrollFactor(config, context), horizontalScrollPixels = axisValue * getScaledHorizontalScrollFactor(config, context), - uptimeMillis = event.eventTime + uptimeMillis = event.eventTime, + inputDeviceId = event.deviceId ) return focusOwner.dispatchRotaryEvent(rotaryEvent) }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEvent.kt index c79101e..10ec62d 100644 --- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEvent.kt +++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEvent.kt
@@ -18,41 +18,23 @@ /** * This event represents a rotary input event. - * - * Some Wear OS devices contain a physical rotating side button, or a rotating bezel. When the user - * turns the button or rotates the bezel, a [RotaryScrollEvent] is sent to the item in focus. */ -class RotaryScrollEvent internal constructor( +expect class RotaryScrollEvent { /** * The amount to scroll (in pixels) in response to a [RotaryScrollEvent] in a container that * can scroll vertically. */ - val verticalScrollPixels: Float, + val verticalScrollPixels: Float /** * The amount to scroll (in pixels) in response to a [RotaryScrollEvent] in a container that * can scroll horizontally. */ - val horizontalScrollPixels: Float, + val horizontalScrollPixels: Float /** * The time in milliseconds at which this even occurred. The start (`0`) time is * platform-dependent. */ val uptimeMillis: Long -) { - override fun equals(other: Any?): Boolean = other is RotaryScrollEvent && - other.verticalScrollPixels == verticalScrollPixels && - other.horizontalScrollPixels == horizontalScrollPixels && - other.uptimeMillis == uptimeMillis - - override fun hashCode(): Int = 0 - .let { verticalScrollPixels.hashCode() } - .let { 31 * it + horizontalScrollPixels.hashCode() } - .let { 31 * it + uptimeMillis.hashCode() } - - override fun toString(): String = "RotaryScrollEvent(" + - "verticalScrollPixels=$verticalScrollPixels," + - "horizontalScrollPixels=$horizontalScrollPixels," + - "uptimeMillis=$uptimeMillis)" }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt index 5c52ed5..8abb20b 100644 --- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt +++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -567,15 +567,16 @@ node.resetLayoutState() if (deactivate) { nodeState.composition?.deactivate() + nodeState.activeState = mutableStateOf(false) + } else { + nodeState.active = false } // create a new instance to avoid change notifications - nodeState.activeState = mutableStateOf(false) nodeState.slotId = ReusedSlotId } } } slotIdToNode.clear() - Snapshot.sendApplyNotifications() } makeSureStateIsConsistent() @@ -673,7 +674,6 @@ nodeState.activeState = mutableStateOf(true) nodeState.forceReuse = true nodeState.forceRecompose = true - Snapshot.sendApplyNotifications() node } }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt index 20e43e3..b205277 100644 --- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt +++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
@@ -23,6 +23,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.areObjectsOfSameType +import androidx.compose.ui.input.pointer.SuspendPointerInputElement import androidx.compose.ui.layout.ModifierInfo private val SentinelHead = object : Modifier.Node() { @@ -218,6 +219,16 @@ tailToHead { if (it.isAttached) it.reset() } + current?.let { elements -> + elements.forEachIndexed { i, element -> + // we need to make sure the suspending pointer input modifier node is updated after + // being reset so we use the latest lambda, even if the keys provided as input + // didn't change. + if (element is SuspendPointerInputElement) { + elements[i] = ForceUpdateElement + } + } + } runDetachLifecycle() markAsDetached() } @@ -797,7 +808,7 @@ internal fun actionForModifiers(prev: Modifier.Element, next: Modifier.Element): Int { return if (prev == next) ActionReuse - else if (areObjectsOfSameType(prev, next)) + else if (areObjectsOfSameType(prev, next) || prev === ForceUpdateElement) ActionUpdate else ActionReplace @@ -833,3 +844,20 @@ } return result } + +@Suppress("ModifierNodeInspectableProperties") +private object ForceUpdateElement : ModifierNodeElement() { + override fun create(): Modifier.Node { + throw IllegalStateException("Shouldn't be called") + } + + override fun update(node: Modifier.Node) { + throw IllegalStateException("Shouldn't be called") + } + + override fun hashCode(): Int = 100 + + override fun equals(other: Any?): Boolean = other === this + + override fun toString() = "ForceUpdateElement" +}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEvent.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEvent.desktop.kt new file mode 100644 index 0000000..02ae758 --- /dev/null +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEvent.desktop.kt
@@ -0,0 +1,59 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.compose.ui.input.rotary + +/** + * This event represents a rotary input event. + * + * Some devices contain a physical rotating side button, or a rotating bezel. When the user + * turns the button or rotates the bezel, a [RotaryScrollEvent] is sent to the item in focus. + */ +actual class RotaryScrollEvent internal constructor( + /** + * The amount to scroll (in pixels) in response to a [RotaryScrollEvent] in a container that + * can scroll vertically. + */ + actual val verticalScrollPixels: Float, + + /** + * The amount to scroll (in pixels) in response to a [RotaryScrollEvent] in a container that + * can scroll horizontally. + */ + actual val horizontalScrollPixels: Float, + + /** + * The time in milliseconds at which this even occurred. The start (`0`) time is + * platform-dependent. + */ + actual val uptimeMillis: Long, + +) { + override fun equals(other: Any?): Boolean = other is RotaryScrollEvent && + other.verticalScrollPixels == verticalScrollPixels && + other.horizontalScrollPixels == horizontalScrollPixels && + other.uptimeMillis == uptimeMillis + + override fun hashCode(): Int = 0 + .let { verticalScrollPixels.hashCode() } + .let { 31 * it + horizontalScrollPixels.hashCode() } + .let { 31 * it + uptimeMillis.hashCode() } + + override fun toString(): String = "RotaryScrollEvent(" + + "verticalScrollPixels=$verticalScrollPixels," + + "horizontalScrollPixels=$horizontalScrollPixels," + + "uptimeMillis=$uptimeMillis)" +}
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore index e0af7f0..f56473d 100644 --- a/development/build_log_simplifier/messages.ignore +++ b/development/build_log_simplifier/messages.ignore
@@ -720,13 +720,3 @@ w: file://\$SUPPORT/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils\.kt:[0-9]+:[0-9]+ 'getter for windowLayoutParams: EspressoOptional!' is deprecated\. Deprecated in Java # b/271306193 remove after aosp/2589888 :emoji:emoji:spdxSbomForRelease spdx sboms require a version but project: noto\-emoji\-compat\-flatbuffers has no specified version -# > Configure project :androidx-demos -WARNING: The option setting 'android\.experimental\.disableCompileSdkChecks=true' is experimental\. -The current default is 'false'\. -WARNING: The option setting 'android\.r[0-9]+\.maxWorkers=[0-9]+' is experimental\. -# > Task :compose:ui:ui:compileReleaseKotlinAndroid -e: Daemon compilation failed: Could not connect to Kotlin compile daemon -java\.lang\.RuntimeException: Could not connect to Kotlin compile daemon -Errors were stored into \$SUPPORT/\.gradle/kotlin/errors/errors\-[0-9]+\.log -# > Task :vectordrawable:integration-tests:testapp:createReleaseCompatibleScreenManifests -exception: info: \[ksp\] loaded provider\(s\): \[androidx\.room\.RoomKspProcessor\$Provider\]
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle index b16457b..ed38f46 100644 --- a/fragment/fragment/build.gradle +++ b/fragment/fragment/build.gradle
@@ -29,7 +29,7 @@ api("androidx.collection:collection:1.1.0") api("androidx.viewpager:viewpager:1.0.0") api("androidx.loader:loader:1.0.0") - api(project(":activity:activity")) + api(projectOrArtifact(":activity:activity")) api("androidx.lifecycle:lifecycle-runtime:2.6.1") api("androidx.lifecycle:lifecycle-livedata-core:2.6.1") api("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLFrameBufferRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLFrameBufferRendererTest.kt index 1d69de6..ac06484 100644 --- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLFrameBufferRendererTest.kt +++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLFrameBufferRendererTest.kt
@@ -431,18 +431,27 @@ renderLatch.countDown() } } + var activity: SurfaceViewTestActivity? = null var renderer: GLFrameBufferRenderer? = null var surfaceView: SurfaceView? try { val scenario = ActivityScenario.launch(SurfaceViewTestActivity::class.java) .moveToState(Lifecycle.State.CREATED) .onActivity { + activity = it surfaceView = it.getSurfaceView() renderer = GLFrameBufferRenderer.Builder(surfaceView!!, callbacks).build() } scenario.moveToState(Lifecycle.State.RESUMED) assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS)) + + val destroyLatch = CountDownLatch(1) + activity?.setOnDestroyCallback { + destroyLatch.countDown() + } + scenario.moveToState(Lifecycle.State.DESTROYED) + assertTrue(destroyLatch.await(3000, TimeUnit.MILLISECONDS)) } finally { renderer.blockingRelease() }
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SurfaceViewTestActivity.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SurfaceViewTestActivity.kt index d21b990..831cc8b 100644 --- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SurfaceViewTestActivity.kt +++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/SurfaceViewTestActivity.kt
@@ -36,6 +36,8 @@ setContentView(surfaceView, ViewGroup.LayoutParams(WIDTH, HEIGHT)) } + private var mOnDestroyCallback: (() -> Unit)? = null + fun getSurfaceView(): TestSurfaceView = mSurfaceView companion object { @@ -76,4 +78,13 @@ } } } + + fun setOnDestroyCallback(callback: (() -> Unit)?) { + mOnDestroyCallback = callback + } + + override fun onDestroy() { + super.onDestroy() + mOnDestroyCallback?.invoke() + } }
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContractTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContractTest.kt index 0c7cdb5d..370aadb 100644 --- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContractTest.kt +++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContractTest.kt
@@ -27,6 +27,7 @@ import androidx.test.filters.MediumTest import androidx.test.filters.SdkSuppress import com.google.common.truth.Truth.assertThat +import kotlin.test.assertFailsWith import org.junit.Test import org.junit.runner.RunWith @@ -38,8 +39,27 @@ private val context: Context = ApplicationProvider.getApplicationContext() @Test + fun createIntent_nonHealthPermission_throwsIAE() { + val requestPermissionContract = HealthPermissionsRequestContract() + assertFailsWith{ + requestPermissionContract.createIntent( + context, + setOf(HealthPermission.READ_STEPS, "NON_HEALTH_PERMISSION") + ) + } + } + + @Test + fun createIntent_noPermissions_throwsIAE() { + val requestPermissionContract = HealthPermissionsRequestContract() + assertFailsWith{ + requestPermissionContract.createIntent(context, setOf()) + } + } + + @Test @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - fun requestHealthPermissions_createIntent_hasPlatformIntentAction() { + fun createIntent_hasPlatformIntentAction() { val intent = HealthPermissionsRequestContract() .createIntent(context, setOf(HealthPermission.READ_STEPS)) @@ -51,7 +71,7 @@ @Test @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.TIRAMISU) - fun requestHealthPermissions_createIntent_hasApkIntentAction() { + fun createIntent_hasApkIntentAction() { val intent = HealthPermissionsRequestContract() .createIntent(context, setOf(HealthPermission.READ_STEPS)) @@ -61,7 +81,7 @@ @Test @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.TIRAMISU) - fun requestHealthPermissions_createIntent_hasApkIntentActionAndProvider() { + fun createIntent_hasApkIntentActionAndProvider() { val intent = HealthPermissionsRequestContract("some.provider") .createIntent(context, setOf(HealthPermission.READ_STEPS))
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContract.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContract.kt index 1251304..679412f 100644 --- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContract.kt +++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/contracts/HealthPermissionsRequestContract.kt
@@ -22,6 +22,7 @@ import androidx.activity.result.contract.ActivityResultContract import androidx.health.connect.client.HealthConnectClient import androidx.health.connect.client.permission.HealthDataRequestPermissionsInternal +import androidx.health.connect.client.permission.HealthPermission import androidx.health.connect.client.permission.platform.HealthDataRequestPermissionsUpsideDownCake /** @@ -50,6 +51,10 @@ * @see ActivityResultContract.createIntent */ override fun createIntent(context: Context, input: Set): Intent { + require(input.all { it.startsWith(HealthPermission.PERMISSION_PREFIX) }) { + "Unsupported health connect permission" + } + require(input.isNotEmpty()) { "At least one permission is required!" } return delegate.createIntent(context, input) }
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsInternal.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsInternal.kt index 28c1ee5..6ddad08 100644 --- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsInternal.kt +++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsInternal.kt
@@ -32,8 +32,7 @@ * An [ActivityResultContract] to request Health Connect permissions. * * @param providerPackageName Optional provider package name for the backing implementation of - * choice. - * + * choice. * @see androidx.activity.ComponentActivity.registerForActivityResult */ @RestrictTo(RestrictTo.Scope.LIBRARY) @@ -42,8 +41,6 @@ ) : ActivityResultContract, Set override fun createIntent(context: Context, input: Set>() { ): Intent { - require(input.isNotEmpty()) { "At least one permission is required!" } - val protoPermissionList = input .asSequence()
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCake.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCake.kt index e6994c5..e4dd720 100644 --- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCake.kt +++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCake.kt
@@ -20,7 +20,6 @@ import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions import androidx.annotation.RestrictTo -import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_PREFIX /** * An [ActivityResultContract] to request Health Connect system permissions. @@ -33,12 +32,8 @@ private val requestPermissions = RequestMultiplePermissions() - override fun createIntent(context: Context, input: Set): Intent { - require(input.all { it.startsWith(PERMISSION_PREFIX) }) { - "Unsupported health connect permission" - } - return requestPermissions.createIntent(context, input.toTypedArray()) - } + override fun createIntent(context: Context, input: Set): Intent = + requestPermissions.createIntent(context, input.toTypedArray()) override fun parseResult(resultCode: Int, intent: Intent?): Set= requestPermissions.parseResult(resultCode, intent).filterValues { it }.keys
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCakeTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCakeTest.kt index 2bbd4f4..f284bd6a 100644 --- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCakeTest.kt +++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/platform/HealthDataRequestPermissionsUpsideDownCakeTest.kt
@@ -25,7 +25,6 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat -import kotlin.test.assertFailsWith import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -45,7 +44,9 @@ val requestPermissionContract = HealthDataRequestPermissionsUpsideDownCake() val intent = requestPermissionContract.createIntent( - context, setOf(HealthPermission.READ_STEPS, HealthPermission.WRITE_DISTANCE)) + context, + setOf(HealthPermission.READ_STEPS, HealthPermission.WRITE_DISTANCE) + ) assertThat(intent.action).isEqualTo(RequestMultiplePermissions.ACTION_REQUEST_PERMISSIONS) assertThat(intent.getStringArrayExtra(RequestMultiplePermissions.EXTRA_PERMISSIONS)) @@ -54,15 +55,6 @@ } @Test - fun createIntent_nonHealthPermission_throwsIAE() { - val requestPermissionContract = HealthDataRequestPermissionsUpsideDownCake() - assertFailsWith{ - requestPermissionContract.createIntent( - context, setOf(HealthPermission.READ_STEPS, "NON_HEALTH_PERMISSION")) - } - } - - @Test fun parseIntent() { val requestPermissionContract = HealthDataRequestPermissionsUpsideDownCake() @@ -73,14 +65,18 @@ HealthPermission.READ_STEPS, HealthPermission.WRITE_STEPS, HealthPermission.WRITE_DISTANCE, - HealthPermission.READ_HEART_RATE)) + HealthPermission.READ_HEART_RATE + ) + ) intent.putExtra( RequestMultiplePermissions.EXTRA_PERMISSION_GRANT_RESULTS, intArrayOf( PackageManager.PERMISSION_GRANTED, PackageManager.PERMISSION_DENIED, PackageManager.PERMISSION_GRANTED, - PackageManager.PERMISSION_DENIED)) + PackageManager.PERMISSION_DENIED + ) + ) val result = requestPermissionContract.parseResult(Activity.RESULT_OK, intent)
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanFilter.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanFilter.java index 207b941..91f2d12 100644 --- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanFilter.java +++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanFilter.java
@@ -52,8 +52,16 @@ // Measurement matrix public @NonNull Matrix H; - // Kalman gain - public @NonNull Matrix K; + // Buffers to minimize matrix allocations on every MotionEvent + private @NonNull Matrix mBufferXDimOne; + private @NonNull Matrix mBufferXDimXDim; + private @NonNull Matrix mBufferXDimXDim2; + private @NonNull Matrix mBufferXDimZDim; + private @NonNull Matrix mBufferXDimZDim2; + private @NonNull Matrix mBufferZDimOne; + private @NonNull Matrix mBufferZDimXDim; + private @NonNull Matrix mBufferZDimZDim; + private @NonNull Matrix mBufferZDimTwiceZDim; public KalmanFilter(int xDim, int zDim) { x = new Matrix(xDim, 1); @@ -62,7 +70,15 @@ R = Matrix.identity(zDim); F = new Matrix(xDim, xDim); H = new Matrix(zDim, xDim); - K = new Matrix(xDim, zDim); + mBufferXDimZDim = new Matrix(xDim, zDim); + mBufferXDimZDim2 = new Matrix(xDim, zDim); + mBufferXDimOne = new Matrix(xDim, 1); + mBufferXDimXDim = new Matrix(xDim, xDim); + mBufferXDimXDim2 = new Matrix(xDim, xDim); + mBufferZDimOne = new Matrix(zDim, 1); + mBufferZDimXDim = new Matrix(zDim, xDim); + mBufferZDimZDim = new Matrix(zDim, zDim); + mBufferZDimTwiceZDim = new Matrix(zDim, 2 * zDim); } /** Resets the internal state of this Kalman filter. */ @@ -70,7 +86,6 @@ // NOTE: It is not necessary to reset Q, R, F, and H matrices. x.fill(0); Matrix.setIdentity(P); - K.fill(0); } /** @@ -78,16 +93,24 @@ * estimate for the current timestep. */ public void predict() { - x = F.dot(x); - P = F.dot(P).dotTranspose(F).plus(Q); + Matrix originalX = x; + x = F.dot(x, mBufferXDimOne); + mBufferXDimOne = originalX; + + F.dot(P, mBufferXDimXDim).dotTranspose(F, P).plus(Q); } /** Updates the state estimate to incorporate the new observation z. */ public void update(@NonNull Matrix z) { - Matrix y = z.minus(H.dot(x)); - Matrix tS = H.dot(P).dotTranspose(H).plus(R); - K = P.dotTranspose(H).dot(tS.inverse()); - x = x.plus(K.dot(y)); - P = P.minus(K.dot(H).dot(P)); + z.minus(H.dot(x, mBufferZDimOne)); + H.dot(P, mBufferZDimXDim) + .dotTranspose(H, mBufferZDimZDim) + .plus(R) + .inverse(mBufferZDimTwiceZDim); + + P.dotTranspose(H, mBufferXDimZDim2).dot(mBufferZDimZDim, mBufferXDimZDim); + + x.plus(mBufferXDimZDim.dot(z, mBufferXDimOne)); + P.minus(mBufferXDimZDim.dot(H, mBufferXDimXDim).dot(P, mBufferXDimXDim2)); } }
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/matrix/Matrix.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/matrix/Matrix.java index 0294b18..399263d 100644 --- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/matrix/Matrix.java +++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/matrix/Matrix.java
@@ -230,27 +230,6 @@ * Calculates the matrix product of this matrix and {@code that}. * * @param that the other matrix - * @return newly created matrix representing the matrix product of this and that - * @throws IllegalArgumentException if the dimensions differ - */ - public @NonNull Matrix dot(@NonNull Matrix that) { - try { - return dot(that, new Matrix(mRows, that.mCols)); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException( - String.format( - Locale.ROOT, - "The matrices dimensions are not conformant for a dot matrix " - + "operation. this:%s that:%s", - shortString(), - that.shortString())); - } - } - - /** - * Calculates the matrix product of this matrix and {@code that}. - * - * @param that the other matrix * @param result matrix to hold the result * @return result, filled with the matrix product * @throws IllegalArgumentException if the dimensions differ @@ -281,15 +260,26 @@ /** * Calculates the inverse of a square matrix * + * @param scratch the matrix [rows, 2*cols] to hold the temporary information + * * @return newly created matrix representing the matrix inverse * @throws ArithmeticException if the matrix is not invertible */ - public @NonNull Matrix inverse() { + public @NonNull Matrix inverse(@NonNull Matrix scratch) { if (!(mRows == mCols)) { throw new IllegalArgumentException( String.format(Locale.ROOT, "The matrix is not square. this:%s", shortString())); } - final Matrix scratch = new Matrix(mRows, 2 * mCols); + + if (scratch.mRows != mRows || scratch.mCols != 2 * mCols) { + throw new IllegalArgumentException( + String.format( + Locale.ROOT, + "The scratch matrix size is not correct. this:%s", + scratch.shortString() + ) + ); + } for (int i = 0; i < mRows; i++) { for (int j = 0; j < mCols; j++) { @@ -349,27 +339,6 @@ * Calculates the matrix product with the transpose of a second matrix. * * @param that the other matrix - * @return newly created matrix representing the matrix product of this and that.transpose() - * @throws IllegalArgumentException if shapes are not conformant - */ - public @NonNull Matrix dotTranspose(@NonNull Matrix that) { - try { - return dotTranspose(that, new Matrix(mRows, that.mRows)); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException( - String.format( - Locale.ROOT, - "The matrices dimensions are not conformant for a transpose " - + "operation. this:%s that:%s", - shortString(), - that.shortString())); - } - } - - /** - * Calculates the matrix product with the transpose of a second matrix. - * - * @param that the other matrix * @param result space to hold the result * @return result, filled with the matrix product of this and that.transpose() * @throws IllegalArgumentException if shapes are not conformant
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java index 5ad04f2..02cb445 100644 --- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java +++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
@@ -360,8 +360,7 @@ /* package */ void addMemberToDynamicGroup(@NonNull MediaRouter.RouteInfo route) { if (!(mSelectedRouteController instanceof MediaRouteProvider.DynamicGroupRouteController)) { - throw new IllegalStateException( - "There is no currently selected " + "dynamic group route."); + throw new IllegalStateException("There is no currently selected dynamic group route."); } MediaRouter.RouteInfo.DynamicGroupState state = getDynamicGroupState(route); if (mSelectedRoute.getMemberRoutes().contains(route) @@ -376,8 +375,7 @@ /* package */ void removeMemberFromDynamicGroup(@NonNull MediaRouter.RouteInfo route) { if (!(mSelectedRouteController instanceof MediaRouteProvider.DynamicGroupRouteController)) { - throw new IllegalStateException( - "There is no currently selected " + "dynamic group route."); + throw new IllegalStateException("There is no currently selected dynamic group route."); } MediaRouter.RouteInfo.DynamicGroupState state = getDynamicGroupState(route); if (!mSelectedRoute.getMemberRoutes().contains(route) @@ -396,8 +394,7 @@ /* package */ void transferToRoute(@NonNull MediaRouter.RouteInfo route) { if (!(mSelectedRouteController instanceof MediaRouteProvider.DynamicGroupRouteController)) { - throw new IllegalStateException( - "There is no currently selected dynamic group " + "route."); + throw new IllegalStateException("There is no currently selected dynamic group route."); } MediaRouter.RouteInfo.DynamicGroupState state = getDynamicGroupState(route); if (state == null || !state.isTransferable()) {
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt index d000958..52c924c 100644 --- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt +++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
@@ -21,6 +21,8 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.navigation.NavOptions @@ -35,7 +37,8 @@ import androidx.testutils.withActivity import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage -import org.junit.Ignore +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit import org.junit.Test import org.junit.runner.RunWith @@ -75,7 +78,7 @@ .that(navController.currentBackStackEntry!!.lifecycle.currentState) .isEqualTo(Lifecycle.State.RESUMED) } - @Ignore("b/276806142") + @Test fun fragmentNavigateClearBackStack() = withNavigationActivity { navController.setGraph(R.navigation.nav_simple) @@ -98,6 +101,14 @@ TestClearViewModel::class.java ] val originalFragment = fm?.findFragmentById(R.id.nav_host) as Fragment + val destroyCountDownLatch = CountDownLatch(1) + originalFragment.lifecycle.addObserver(object : LifecycleEventObserver { + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (event == Lifecycle.Event.ON_DESTROY) { + destroyCountDownLatch.countDown() + } + } + }) val originalFragmentViewModel = ViewModelProvider(originalFragment)[ TestClearViewModel::class.java ] @@ -125,6 +136,7 @@ assertThat(fm.findFragmentById(R.id.nav_host)).isEqualTo(currentTopFragment) assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.empty_fragment_2) assertThat(navigator.backStack.value.size).isEqualTo(2) + assertThat(destroyCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue() assertThat(originalFragmentViewModel.cleared).isTrue() assertThat(originalEntryViewModel.cleared).isTrue() }
diff --git a/paging/paging-compose/src/androidAndroidTest/kotlin/androidx/paging/compose/LazyPagingItemsTest.kt b/paging/paging-compose/src/androidAndroidTest/kotlin/androidx/paging/compose/LazyPagingItemsTest.kt index 76209f0..4399ddd 100644 --- a/paging/paging-compose/src/androidAndroidTest/kotlin/androidx/paging/compose/LazyPagingItemsTest.kt +++ b/paging/paging-compose/src/androidAndroidTest/kotlin/androidx/paging/compose/LazyPagingItemsTest.kt
@@ -56,7 +56,6 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -976,7 +975,6 @@ } } - @Ignore // b/294941531 @Test fun cachedData_loadStates() { val flow = createPager().flow.cachedIn(TestScope(UnconfinedTestDispatcher())) @@ -994,7 +992,10 @@ rule.waitUntil { dispatcher.scheduler.advanceUntilIdle() // let items load - lazyPagingItems.itemCount == maxItem + lazyPagingItems.itemCount == maxItem && + lazyPagingItems.loadState.source.refresh is LoadState.NotLoading && + lazyPagingItems.loadState.source.prepend is LoadState.NotLoading && + lazyPagingItems.loadState.source.append is LoadState.NotLoading } assertThat(lazyPagingItems.loadState).isEqualTo(
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/FeaturedCarousel.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/FeaturedCarousel.kt index c8953f1..ac7fe19 100644 --- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/FeaturedCarousel.kt +++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/FeaturedCarousel.kt
@@ -66,8 +66,8 @@ import androidx.compose.ui.unit.dp import androidx.tv.material3.Carousel import androidx.tv.material3.CarouselDefaults -import androidx.tv.material3.CarouselState import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.rememberCarouselState @Composable fun FeaturedCarouselContent() { @@ -122,7 +122,7 @@ Color.LightGray.copy(alpha = 0.3f), ) - val carouselState = remember { CarouselState() } + val carouselState = rememberCarouselState() var carouselFocused by remember { mutableStateOf(false) } Carousel( itemCount = backgrounds.size, @@ -141,9 +141,9 @@ ) }, contentTransformStartToEnd = - fadeIn(tween(1000)).togetherWith(fadeOut(tween(1000))), + fadeIn(tween(1000)).togetherWith(fadeOut(tween(1000))), contentTransformEndToStart = - fadeIn(tween(1000)).togetherWith(fadeOut(tween(1000))) + fadeIn(tween(1000)).togetherWith(fadeOut(tween(1000))) ) { itemIndex -> Box( modifier = Modifier
diff --git a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt index e5fa951..2f07358 100644 --- a/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt +++ b/tv/integration-tests/presentation/src/main/java/androidx/tv/integration/presentation/FeaturedCarousel.kt
@@ -33,7 +33,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowRight import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -44,6 +43,7 @@ import androidx.tv.material3.CarouselState import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Text +import androidx.tv.material3.rememberCarouselState @OptIn(ExperimentalTvMaterial3Api::class) @Composable @@ -51,7 +51,7 @@ movies: List= featuredCarouselMovies, modifier: Modifier = Modifier ) { - val carouselState: CarouselState = remember { CarouselState() } + val carouselState: CarouselState = rememberCarouselState() val slidesCount = movies.size Carousel(
diff --git a/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt index 320ab30..c3ed957 100644 --- a/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt +++ b/tv/samples/src/main/java/androidx/tv/samples/CarouselSamples.kt
@@ -52,8 +52,8 @@ import androidx.compose.ui.unit.dp import androidx.tv.material3.Carousel import androidx.tv.material3.CarouselDefaults -import androidx.tv.material3.CarouselState import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.rememberCarouselState @OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class) @Sampled @@ -142,7 +142,7 @@ Color.Yellow.copy(alpha = 0.3f), Color.Green.copy(alpha = 0.3f) ) - val carouselState = remember { CarouselState() } + val carouselState = rememberCarouselState() Carousel( itemCount = backgrounds.size,
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt index 3dca4bb..dabf56f 100644 --- a/tv/tv-material/api/current.txt +++ b/tv/tv-material/api/current.txt
@@ -143,6 +143,7 @@ public final class CarouselKt { method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Carousel(int itemCount, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.CarouselState carouselState, optional long autoScrollDurationMillis, optional androidx.compose.animation.ContentTransform contentTransformStartToEnd, optional androidx.compose.animation.ContentTransform contentTransformEndToStart, optional kotlin.jvm.functions.Function1 super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> carouselIndicator, kotlin.jvm.functions.Function2 super androidx.compose.animation.AnimatedContentScope,? super java.lang.Integer,kotlin.Unit> content); + method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static androidx.tv.material3.CarouselState rememberCarouselState(optional int initialActiveItemIndex); } @SuppressCompatibility @androidx.compose.runtime.Stable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselState { @@ -150,6 +151,12 @@ method public int getActiveItemIndex(); method public androidx.tv.material3.ScrollPauseHandle pauseAutoScroll(int itemIndex); property public final int activeItemIndex; + field public static final androidx.tv.material3.CarouselState.Companion Companion; + } + + public static final class CarouselState.Companion { + method public androidx.compose.runtime.saveable.SavergetSaver(); + property public final androidx.compose.runtime.saveable.SaverSaver; } @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CheckboxColors {
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt index 3dca4bb..dabf56f 100644 --- a/tv/tv-material/api/restricted_current.txt +++ b/tv/tv-material/api/restricted_current.txt
@@ -143,6 +143,7 @@ public final class CarouselKt { method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Carousel(int itemCount, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.CarouselState carouselState, optional long autoScrollDurationMillis, optional androidx.compose.animation.ContentTransform contentTransformStartToEnd, optional androidx.compose.animation.ContentTransform contentTransformEndToStart, optional kotlin.jvm.functions.Function1 super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> carouselIndicator, kotlin.jvm.functions.Function2 super androidx.compose.animation.AnimatedContentScope,? super java.lang.Integer,kotlin.Unit> content); + method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static androidx.tv.material3.CarouselState rememberCarouselState(optional int initialActiveItemIndex); } @SuppressCompatibility @androidx.compose.runtime.Stable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CarouselState { @@ -150,6 +151,12 @@ method public int getActiveItemIndex(); method public androidx.tv.material3.ScrollPauseHandle pauseAutoScroll(int itemIndex); property public final int activeItemIndex; + field public static final androidx.tv.material3.CarouselState.Companion Companion; + } + + public static final class CarouselState.Companion { + method public androidx.compose.runtime.saveable.SavergetSaver(); + property public final androidx.compose.runtime.saveable.SaverSaver; } @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CheckboxColors {
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt index 5538671..5b302d8 100644 --- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt +++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
@@ -122,7 +122,7 @@ @Test fun carousel_onUserTriggeredPause_stopsScroll() { rule.setContent { - val carouselState = remember { CarouselState() } + val carouselState = rememberCarouselState() SampleCarousel(carouselState = carouselState) { BasicText(text = "Text ${it + 1}") LaunchedEffect(carouselState) { carouselState.pauseAutoScroll(it) } @@ -142,7 +142,7 @@ fun carousel_onUserTriggeredPauseAndResume_resumeScroll() { var pauseHandle: ScrollPauseHandle? = null rule.setContent { - val carouselState = remember { CarouselState() } + val carouselState = rememberCarouselState() SampleCarousel(carouselState = carouselState) { BasicText(text = "Text ${it + 1}") LaunchedEffect(carouselState) { @@ -176,7 +176,7 @@ var pauseHandle1: ScrollPauseHandle? = null var pauseHandle2: ScrollPauseHandle? = null rule.setContent { - val carouselState = remember { CarouselState() } + val carouselState = rememberCarouselState() SampleCarousel(carouselState = carouselState) { BasicText(text = "Text ${it + 1}") LaunchedEffect(carouselState) { @@ -219,7 +219,7 @@ fun carousel_onRepeatedResumesOnSamePauseHandle_ignoresSubsequentResumeCalls() { var pauseHandle1: ScrollPauseHandle? = null rule.setContent { - val carouselState = remember { CarouselState() } + val carouselState = rememberCarouselState() var pauseHandle2: ScrollPauseHandle? = null SampleCarousel(carouselState = carouselState) { BasicText(text = "Text ${it + 1}") @@ -429,7 +429,7 @@ .fillMaxWidth() .testTag("featured-carousel") .border(2.dp, Color.Black), - carouselState = remember { CarouselState() }, + carouselState = rememberCarouselState(), itemCount = 3, autoScrollDurationMillis = delayBetweenItems ) { @@ -833,7 +833,7 @@ @OptIn(ExperimentalTvMaterial3Api::class) @Composable private fun SampleCarousel( - carouselState: CarouselState = remember { CarouselState() }, + carouselState: CarouselState = rememberCarouselState(), itemCount: Int = 3, timeToDisplayItemMillis: Long = delayBetweenItems, content: @Composable AnimatedContentScope.(index: Int) -> Unit
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt index 2da1c2d..23ffe4e 100644 --- a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt +++ b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
@@ -44,6 +44,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment @@ -107,7 +109,7 @@ fun Carousel( itemCount: Int, modifier: Modifier = Modifier, - carouselState: CarouselState = remember { CarouselState() }, + carouselState: CarouselState = rememberCarouselState(), autoScrollDurationMillis: Long = CarouselDefaults.TimeToDisplayItemMillis, contentTransformStartToEnd: ContentTransform = CarouselDefaults.contentTransform, contentTransformEndToStart: ContentTransform = CarouselDefaults.contentTransform, @@ -260,6 +262,7 @@ carouselState.moveToPreviousItem(itemCount) outerBoxFocusRequester.requestFocus() } + fun showNextItem() { carouselState.moveToNextItem(itemCount) outerBoxFocusRequester.requestFocus() @@ -293,6 +296,7 @@ updateItemBasedOnLayout(direction, isLtr) KeyEventPropagation.StopPropagation } + else -> KeyEventPropagation.StopPropagation } @@ -315,6 +319,7 @@ when { shouldFocusExitCarousel(it, carouselState, itemCount, isLtr) -> FocusRequester.Default + else -> FocusRequester.Cancel } } @@ -350,6 +355,22 @@ } /** + * Creates a [CarouselState] that is remembered across compositions. + * + * Changes to the provided initial values will **not** result in the state being recreated or + * changed in any way if it has already been created. + * + * @param initialActiveItemIndex the index of the first active item + */ +@ExperimentalTvMaterial3Api +@Composable +fun rememberCarouselState(initialActiveItemIndex: Int = 0): CarouselState { + return rememberSaveable(saver = CarouselState.Saver) { + CarouselState(initialActiveItemIndex) + } +} + +/** * State of the Carousel which allows the user to specify the first item that is shown when the * Carousel is instantiated in the constructor. * @@ -410,6 +431,16 @@ // Go to next item activeItemIndex = floorMod(activeItemIndex + 1, itemCount) } + + companion object { + /** + * The default [Saver] implementation for [CarouselState]. + */ + val Saver: Saver= Saver( + save = { it.activeItemIndex }, + restore = { CarouselState(it) } + ) + } } @ExperimentalTvMaterial3Api
diff --git a/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/RepeatableClickable.kt b/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/RepeatableClickable.kt deleted file mode 100644 index 545a576..0000000 --- a/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/RepeatableClickable.kt +++ /dev/null
@@ -1,107 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.wear.compose.materialcore - -import androidx.compose.foundation.layout.Box -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -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 -import org.junit.Assert.assertEquals -import org.junit.Rule -import org.junit.Test - -public class RepeatableClickable { - @get:Rule - public val rule = createComposeRule() - - @Test - fun touch_hold_shorter_than_threshold() { - var clickCounter = 0 - - boxWithRepeatableClickable(rule, 300) { - clickCounter++ - } - - assertEquals(0, clickCounter) - } - - @Test - fun touch_hold_equals_to_threshold() { - var clickCounter = 0 - - boxWithRepeatableClickable(rule, 500) { - clickCounter++ - } - - assertEquals(1, clickCounter) - } - - @Test - fun touch_hold_longer_than_threshold() { - var clickCounter = 0 - - boxWithRepeatableClickable(rule, 620) { - clickCounter++ - } - - assertEquals(3, clickCounter) - } - - @Test - fun touch_hold_disabled() { - var clickCounter = 0 - - boxWithRepeatableClickable(rule, 500, false) { - clickCounter++ - } - - assertEquals(0, clickCounter) - } - - private fun boxWithRepeatableClickable( - rule: ComposeContentTestRule, - holdDelay: Long, - enabled: Boolean = true, - initialDelay: Long = 500L, - incrementalDelay: Long = 60L, - onClick: () -> Unit - ) { - - rule.setContent { - Box( - modifier = Modifier - .testTag(TEST_TAG) - .repeatableClickable( - enabled = enabled, - initialDelay = initialDelay, - incrementalDelay = incrementalDelay - ) { - onClick() - } - ) {} - } - - rule.onNodeWithTag(TEST_TAG).performTouchInput { - down(center) - advanceEventTime(holdDelay) - up() - } - } -}
diff --git a/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/RepeatableClickableTest.kt b/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/RepeatableClickableTest.kt new file mode 100644 index 0000000..895034c --- /dev/null +++ b/wear/compose/compose-material-core/src/androidTest/kotlin/androidx/wear/compose/materialcore/RepeatableClickableTest.kt
@@ -0,0 +1,165 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.wear.compose.materialcore + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.platform.testTag +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 +import androidx.compose.ui.unit.dp +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test + +public class RepeatableClickableTest { + @get:Rule + public val rule = createComposeRule() + + @Test + fun touch_hold_shorter_than_threshold_performs_click() { + var repeatableClickCounter = 0 + var clicked = false + + boxWithRepeatableClickable(rule, + holdDelay = INITIAL_DELAY / 2, + onRepeatableClick = { repeatableClickCounter++ }, + onClick = { clicked = true } + ) + assertEquals(0, repeatableClickCounter) + assertEquals(true, clicked) + } + + @Test + fun touch_hold_equals_to_threshold_performs_repeatable_click() { + var repeatableClickCounter = 0 + var clicked = false + + boxWithRepeatableClickable(rule, + holdDelay = INITIAL_DELAY, + onRepeatableClick = { repeatableClickCounter++ }, + onClick = { clicked = true } + ) + assertEquals(1, repeatableClickCounter) + assertEquals(false, clicked) + } + + @Test + fun touch_hold_longer_than_threshold_performs_multiple_repeatable_clicks() { + var repeatableClickCounter = 0 + var clicked = false + + boxWithRepeatableClickable(rule, + holdDelay = INITIAL_DELAY + INCREMENTAL_DELAY * 2, + onRepeatableClick = { repeatableClickCounter++ }, + onClick = { clicked = true } + ) + + assertEquals(3, repeatableClickCounter) + assertEquals(false, clicked) + } + + @Test + fun touch_hold_disabled() { + var repeatableClickCounter = 0 + var clicked = false + + boxWithRepeatableClickable(rule, + holdDelay = INITIAL_DELAY, + enabled = false, + onRepeatableClick = { repeatableClickCounter++ }, + onClick = { clicked = true } + ) + + assertEquals(0, repeatableClickCounter) + assertEquals(false, clicked) + } + + @Test + fun touch_hold_release_outside_of_bounds_shorter_than_threshold() { + var repeatableClickCounter = 0 + var clicked = false + + boxWithRepeatableClickable(rule, + holdDelay = INITIAL_DELAY / 2, + enabled = true, + releaseOutsideOfBox = true, + onRepeatableClick = { repeatableClickCounter++ }, + onClick = { clicked = true } + ) + + assertEquals(0, repeatableClickCounter) + assertEquals(false, clicked) + } + + private fun boxWithRepeatableClickable( + rule: ComposeContentTestRule, + holdDelay: Long, + enabled: Boolean = true, + initialDelay: Long = INITIAL_DELAY, + incrementalDelay: Long = INCREMENTAL_DELAY, + releaseOutsideOfBox: Boolean = false, + onClick: () -> Unit, + onRepeatableClick: () -> Unit + ) { + rule.setContent { + Box( + modifier = Modifier + .fillMaxSize() + ) { + Box( + modifier = Modifier + .testTag(TEST_TAG) + .size(50.dp) + .align(Alignment.Center) + .repeatableClickable( + enabled = enabled, + initialDelay = initialDelay, + incrementalDelay = incrementalDelay, + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = onClick, + onRepeatableClick = onRepeatableClick + ) + ) {} + } + } + + rule.onNodeWithTag(TEST_TAG).performTouchInput { + down(center) + advanceEventTime(holdDelay) + if (releaseOutsideOfBox) { + // Move to -1f,-1f coordinates which are outside of the current component + moveTo(Offset(-1f, -1f)) + } + up() + } + } + + companion object { + private const val INITIAL_DELAY = 500L + private const val INCREMENTAL_DELAY = 60L + } +}
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/RepeatableClickable.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/RepeatableClickable.kt index da01843..b5ac77f 100644 --- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/RepeatableClickable.kt +++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/RepeatableClickable.kt
@@ -17,46 +17,106 @@ package androidx.wear.compose.materialcore import androidx.annotation.RestrictTo +import androidx.compose.foundation.Indication +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.semantics.Role import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch /** * This modifier provides functionality to increment or decrement values repeatedly - * by holding down the composable + * by holding down the composable. + * Should be used instead of clickable modifier to achieve clickable and repeatable + * clickable behavior. Can't be used along with clickable modifier as it already implements it. + * + * Callbacks [onClick] and [onRepeatableClick] are different. [onClick] is triggered only + * when the hold duration is shorter than [initialDelay] and no repeatable clicks happened. + * [onRepeatableClick] is repeatedly triggered when the hold duration is longer + * than [initialDelay] with [incrementalDelay] intervals. + * + * @param interactionSource [MutableInteractionSource] that will be used to dispatch + * [PressInteraction.Press] when this clickable is pressed. Only the initial (first) press will be + * recorded and dispatched with [MutableInteractionSource]. + * @param indication indication to be shown when modified element is pressed. By default, + * indication from [LocalIndication] will be used. Pass `null` to show no indication, or + * current value from [LocalIndication] to show theme default + * @param enabled Controls the enabled state. When `false`, [onClick], and this modifier will + * appear disabled for accessibility services + * @param onClickLabel semantic / accessibility label for the [onClick] action + * @param role the type of user interface element. Accessibility services might use this + * to describe the element or do customizations + * @param initialDelay The initial delay before the click starts repeating, in ms + * @param incrementalDelay The delay between each repeated click, in ms + * @param onClick will be called when user clicks on the element + * @param onRepeatableClick will be called after the [initialDelay] with [incrementalDelay] + * between each call until the touch is released */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public fun Modifier.repeatableClickable( - enabled: Boolean, +fun Modifier.repeatableClickable( + interactionSource: MutableInteractionSource, + indication: Indication?, + enabled: Boolean = true, + onClickLabel: String? = null, + role: Role? = null, initialDelay: Long = 500L, incrementalDelay: Long = 60L, - onClick: () -> Unit + onClick: () -> Unit, + onRepeatableClick: () -> Unit = onClick ): Modifier = composed { - + val currentOnRepeatableClick by rememberUpdatedState(onRepeatableClick) val currentOnClick by rememberUpdatedState(onClick) + // This flag is used for checking whether the onClick should be ignored or not. + // If this flag is true, then it means that repeatable click happened and onClick + // shouldn't be triggered. + var ignoreOnClick by remember { mutableStateOf(false) } - pointerInput(enabled) { - coroutineScope { - awaitEachGesture { - awaitFirstDown() - val repeatingJob = launch { - delay(initialDelay) - while (enabled) { - currentOnClick() - delay(incrementalDelay) + // Repeatable logic should always follow the clickable, as the lowest modifier finishes first, + // and we have to be sure that repeatable goes before clickable. + clickable( + interactionSource = interactionSource, + indication = indication, + enabled = enabled, + onClickLabel = onClickLabel, + role = role, + onClick = { + if (!ignoreOnClick) { + currentOnClick() + } + ignoreOnClick = false + }, + ) + .pointerInput(enabled) { + coroutineScope { + awaitEachGesture { + awaitFirstDown() + ignoreOnClick = false + val repeatingJob = launch { + delay(initialDelay) + ignoreOnClick = true + while (enabled) { + currentOnRepeatableClick() + delay(incrementalDelay) + } } + // Waiting for up or cancellation of the gesture. + waitForUpOrCancellation() + repeatingJob.cancel() } - waitForUpOrCancellation() - repeatingJob.cancel() } } - } }
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Slider.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Slider.kt index 065d3d0..637a82c 100644 --- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Slider.kt +++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Slider.kt
@@ -18,7 +18,6 @@ import androidx.annotation.RestrictTo import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight @@ -48,13 +47,12 @@ modifier = Modifier .width(buttonControlSize) .fillMaxHeight() - .clickable( + .repeatableClickable( enabled = enabled, onClick = onClick, interactionSource = remember { MutableInteractionSource() }, - indication = LocalIndication.current, + indication = LocalIndication.current ) - .repeatableClickable(enabled = enabled, onClick = onClick) .then(modifier), contentAlignment = contentAlignment ) {
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Stepper.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Stepper.kt index d1a25d0..70428a4 100644 --- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Stepper.kt +++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Stepper.kt
@@ -18,7 +18,6 @@ import androidx.annotation.RestrictTo import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -39,7 +38,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp /** @@ -156,10 +154,12 @@ modifier = Modifier .fillMaxWidth() .weight(StepperDefaults.ButtonWeight) - .clickable( - interactionSource, null, onClick = onClick, enabled = enabled, role = Role.Button + .repeatableClickable( + enabled = enabled, + onClick = onClick, + interactionSource = interactionSource, + indication = null ) - .repeatableClickable(enabled = enabled, onClick = onClick) .wrapContentWidth() .indication(interactionSource, rememberRipple(bounded = false)) .padding(paddingValues),