Merge "[GH] [Room][Compiler Processing] Originating element improvements" into androidx-main
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index 5d0169f..20451ba 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -40,6 +40,7 @@
androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
androidTestImplementation projectOrArtifact(":compose:material:material")
+ androidTestImplementation projectOrArtifact(":compose:test-utils")
androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
androidTestImplementation(ANDROIDX_TEST_RUNNER)
androidTestImplementation(ANDROIDX_TEST_EXT_KTX)
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index 5cb2672..78c9e69 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -108,7 +108,7 @@
const val REACTIVE_STREAMS = "org.reactivestreams:reactive-streams:1.0.0"
const val RX_JAVA = "io.reactivex.rxjava2:rxjava:2.2.9"
const val RX_JAVA3 = "io.reactivex.rxjava3:rxjava:3.0.0"
-val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.2.21"
+val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.2.22"
val SKIKO = "org.jetbrains.skiko:skiko-jvm:$SKIKO_VERSION"
val SKIKO_LINUX_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-linux-x64:$SKIKO_VERSION"
val SKIKO_MACOS_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-macos-x64:$SKIKO_VERSION"
diff --git a/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt
index ec49186..7a95cae 100644
--- a/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt
@@ -298,7 +298,7 @@
dependencies.add(project.dependencies.create(DACKKA_DEPENDENCY))
}
- project.tasks.register("dackkaDocs", DackkaTask::class.java) { task ->
+ val dackkaTask = project.tasks.register("dackkaDocs", DackkaTask::class.java) { task ->
task.apply {
dependsOn(dackkaConfiguration)
dependsOn(unzipDocsTask)
@@ -314,6 +314,26 @@
sourcesDir = unzippedDocsSources
}
}
+
+ project.tasks.register("zipDackkaDocs", Zip::class.java) { task ->
+ task.apply {
+ dependsOn(dackkaTask)
+ from(generatedDocsDir)
+
+ val baseName = "dackka-$docsType-docs"
+ val buildId = getBuildId()
+ archiveBaseName.set(baseName)
+ archiveVersion.set(buildId)
+ destinationDirectory.set(project.getDistributionDirectory())
+ group = JavaBasePlugin.DOCUMENTATION_GROUP
+
+ val filePath = "${project.getDistributionDirectory().canonicalPath}/"
+ val fileName = "$baseName-$buildId.zip"
+ val destinationFile = filePath + fileName
+ description = "Zips Java and Kotlin documentation (generated via Dackka in the" +
+ " style of d.android.com) into $destinationFile"
+ }
+ }
}
private fun configureDokka(
diff --git a/camera/camera-camera2/src/androidTest/AndroidManifest.xml b/camera/camera-camera2/src/androidTest/AndroidManifest.xml
index b53d4ff..4a5d228 100644
--- a/camera/camera-camera2/src/androidTest/AndroidManifest.xml
+++ b/camera/camera-camera2/src/androidTest/AndroidManifest.xml
@@ -1,42 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2019 The Android Open Source Project
+<!--
+ Copyright 2021 The Android Open Source Project
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
+ 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
+ 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.
--->
+ 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.
+ -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="androidx.camera.camera2.test">
-
-
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- <application>
- <activity
- android:name="androidx.camera.testing.activity.CameraXTestActivity"
- android:label="CameraX TestActivity">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- </activity>
- <activity
- android:name="androidx.camera.testing.activity.Camera2TestActivity"
- android:label="Camera2 TestActivity">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- </activity>
- </application>
</manifest>
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/CameraDisconnectTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/CameraDisconnectTest.java
deleted file mode 100644
index 2b8db23..0000000
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/CameraDisconnectTest.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2;
-
-import static androidx.camera.testing.CoreAppTestUtil.ForegroundOccupiedError;
-import static androidx.camera.testing.CoreAppTestUtil.assumeCanTestCameraDisconnect;
-import static androidx.camera.testing.CoreAppTestUtil.assumeCompatibleDevice;
-import static androidx.camera.testing.CoreAppTestUtil.prepareDeviceUI;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-
-import androidx.camera.core.CameraX;
-import androidx.camera.testing.CameraUtil;
-import androidx.camera.testing.activity.Camera2TestActivity;
-import androidx.camera.testing.activity.CameraXTestActivity;
-import androidx.test.core.app.ActivityScenario;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.espresso.Espresso;
-import androidx.test.espresso.IdlingRegistry;
-import androidx.test.espresso.IdlingResource;
-import androidx.test.espresso.idling.CountingIdlingResource;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicReference;
-
-@RunWith(AndroidJUnit4.class)
-@LargeTest
-public class CameraDisconnectTest {
-
- @Rule
- public TestRule mCameraRule = CameraUtil.grantCameraPermissionAndPreTest();
-
- @Before
- public void setUp() throws ForegroundOccupiedError {
- assumeCompatibleDevice();
- assumeCanTestCameraDisconnect();
-
- final Context context = ApplicationProvider.getApplicationContext();
- CameraX.initialize(context, Camera2Config.defaultConfig());
-
- // Clear the device UI and check if there is no dialog or lock screen on the top of the
- // window before start the test.
- prepareDeviceUI(InstrumentationRegistry.getInstrumentation());
- }
-
- @After
- public void tearDown() throws ExecutionException, InterruptedException, TimeoutException {
- CameraX.shutdown().get(10_000, TimeUnit.MILLISECONDS);
- }
-
- @Test
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M) // Known issue, checkout b/147393563.
- public void testDisconnect_launchCamera2App() {
- // Launch CameraX test activity
- final ActivityScenario<CameraXTestActivity> cameraXActivity = ActivityScenario.launch(
- CameraXTestActivity.class);
-
- // Wait for preview to become active
- final AtomicReference<CountingIdlingResource> cameraXPreviewReady = new AtomicReference<>();
- cameraXActivity.onActivity(activity -> cameraXPreviewReady.set(activity.getPreviewReady()));
- waitFor(cameraXPreviewReady.get());
-
- // Get id of camera opened by CameraX test activity
- final AtomicReference<String> cameraId = new AtomicReference<>();
- cameraXActivity.onActivity(activity -> cameraId.set(activity.getCameraId()));
- assertThat(cameraId.get()).isNotNull();
-
- // Launch Camera2 test activity. It should cause the camera to disconnect from CameraX.
- final Intent intent = new Intent(ApplicationProvider.getApplicationContext(),
- Camera2TestActivity.class);
- intent.putExtra(Camera2TestActivity.EXTRA_CAMERA_ID, cameraId.get());
- final ActivityScenario<Camera2TestActivity> camera2Activity = ActivityScenario.launch(
- intent);
-
- // Wait for preview to become active
- final AtomicReference<CountingIdlingResource> camera2PreviewReady = new AtomicReference<>();
- camera2Activity.onActivity(activity -> camera2PreviewReady.set(activity.mPreviewReady));
- waitFor(camera2PreviewReady.get());
-
- // Close Camera2 test activity, and verify the CameraX Preview resumes successfully.
- camera2Activity.close();
- waitFor(cameraXPreviewReady.get());
-
- // Close CameraX test activity
- cameraXActivity.close();
- }
-
- private static void waitFor(IdlingResource idlingResource) {
- IdlingRegistry.getInstance().register(idlingResource);
- Espresso.onIdle();
- IdlingRegistry.getInstance().unregister(idlingResource);
- }
-
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 2bd0527..991bec0 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -113,6 +113,8 @@
import androidx.camera.core.internal.IoConfig;
import androidx.camera.core.internal.TargetConfig;
import androidx.camera.core.internal.YuvToJpegProcessor;
+import androidx.camera.core.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.core.internal.compat.quirk.ImageCaptureWashedOutImageQuirk;
import androidx.camera.core.internal.compat.quirk.SoftwareJpegEncodingPreferredQuirk;
import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability;
import androidx.camera.core.internal.utils.ImageUtil;
@@ -303,6 +305,14 @@
*/
private boolean mUseSoftwareJpeg = false;
+ /**
+ * Whether the torch flash will be used.
+ *
+ * <p>When the flag is set, torch will be opened and closed to replace the flash fired by flash
+ * mode.
+ */
+ private final boolean mUseTorchFlash;
+
////////////////////////////////////////////////////////////////////////////////////////////
// [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
////////////////////////////////////////////////////////////////////////////////////////////
@@ -351,6 +361,11 @@
} else {
mEnableCheck3AConverged = false; // skip 3A convergence in MIN_LATENCY mode
}
+
+ mUseTorchFlash = DeviceQuirks.get(ImageCaptureWashedOutImageQuirk.class) != null;
+ if (mUseTorchFlash) {
+ Logger.d(TAG, "Open and close torch to replace the flash fired by flash mode.");
+ }
}
@UiThread
@@ -521,7 +536,7 @@
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@Override
- UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+ protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
@NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
if (builder.getUseCaseConfig().retrieveOption(OPTION_CAPTURE_PROCESSOR, null)
!= null && Build.VERSION.SDK_INT >= 29) {
@@ -1320,9 +1335,12 @@
.transformAsync(captureResult -> {
state.mPreCaptureState = captureResult;
triggerAfIfNeeded(state);
- if (isAePrecaptureRequired(state)) {
- // trigger AE precapture and await the result.
- return triggerAePrecapture(state);
+ if (isFlashRequired(state)) {
+ if (mUseTorchFlash) {
+ return openTorch(state);
+ } else {
+ return triggerAePrecapture(state);
+ }
}
return Futures.immediateFuture(null);
}, mExecutor)
@@ -1337,10 +1355,37 @@
* <p>For example, cancel 3A scan, close torch if necessary.
*/
void postTakePicture(final TakePictureState state) {
+ closeTorch(state);
cancelAfAeTrigger(state);
unlockFlashMode();
}
+ @NonNull
+ private ListenableFuture<Void> openTorch(@NonNull TakePictureState state) {
+ CameraInternal camera = getCamera();
+ if (camera != null && camera.getCameraInfo().getTorchState().getValue() == TorchState.ON) {
+ // Torch is already opened.
+ return Futures.immediateFuture(null);
+ }
+
+ Logger.d(TAG, "openTorch");
+
+ // Create a new future in order to ignore any fail from CameraControl.enableTorch().
+ return CallbackToFutureAdapter.getFuture(completer -> {
+ getCameraControl().enableTorch(state.mIsTorchOpened = true).addListener(
+ () -> completer.set(null), CameraXExecutors.directExecutor());
+ return "openTorch";
+ });
+ }
+
+ private void closeTorch(@NonNull TakePictureState state) {
+ if (state.mIsTorchOpened) {
+ // Add listener to avoid FutureReturnValueIgnored error.
+ getCameraControl().enableTorch(state.mIsTorchOpened = false).addListener(() -> {
+ }, CameraXExecutors.directExecutor());
+ }
+ }
+
/**
* Gets a capture result or not according to current configuration.
*
@@ -1373,7 +1418,7 @@
return Futures.immediateFuture(null);
}
- boolean isAePrecaptureRequired(TakePictureState state) {
+ boolean isFlashRequired(@NonNull TakePictureState state) {
switch (getFlashMode()) {
case FLASH_MODE_ON:
return true;
@@ -1386,9 +1431,7 @@
}
ListenableFuture<Boolean> check3AConverged(TakePictureState state) {
- // Skip the 3A converged check if enableCheck3AConverged is false and AE precapture is
- // not triggered.
- if (!mEnableCheck3AConverged && !state.mIsAePrecaptureTriggered) {
+ if (!mEnableCheck3AConverged && !state.mIsAePrecaptureTriggered && !state.mIsTorchOpened) {
return Futures.immediateFuture(false);
}
@@ -1467,10 +1510,12 @@
}
/** Issues a request to start auto exposure scan. */
- ListenableFuture<CameraCaptureResult> triggerAePrecapture(TakePictureState state) {
+ ListenableFuture<Void> triggerAePrecapture(TakePictureState state) {
Logger.d(TAG, "triggerAePrecapture");
state.mIsAePrecaptureTriggered = true;
- return getCameraControl().triggerAePrecapture();
+ // Transform type from CameraCaptureResult to Void
+ return Futures.transform(getCameraControl().triggerAePrecapture(), captureResult -> null,
+ CameraXExecutors.directExecutor());
}
/** Issues a request to cancel auto focus and/or auto exposure scan. */
@@ -2059,6 +2104,7 @@
*/
static final class TakePictureState {
CameraCaptureResult mPreCaptureState = EmptyCameraCaptureResult.create();
+ boolean mIsTorchOpened = false;
boolean mIsAfTriggered = false;
boolean mIsAePrecaptureTriggered = false;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index e9af877..99b6dc6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -468,7 +468,7 @@
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@Override
- UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+ protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
@NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
if (builder.getMutableConfig().retrieveOption(OPTION_PREVIEW_CAPTURE_PROCESSOR, null)
!= null) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index ca32637..247b2311 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -243,7 +243,7 @@
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
- UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+ protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
@NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
return builder.getUseCaseConfig();
}
@@ -652,7 +652,7 @@
*
* @hide
*/
- @RestrictTo(Scope.LIBRARY)
+ @RestrictTo(Scope.LIBRARY_GROUP)
public void setViewPortCropRect(@NonNull Rect viewPortCropRect) {
mViewPortCropRect = viewPortCropRect;
}
@@ -662,7 +662,7 @@
*
* @hide
*/
- @RestrictTo(Scope.LIBRARY)
+ @RestrictTo(Scope.LIBRARY_GROUP)
@Nullable
public Rect getViewPortCropRect() {
return mViewPortCropRect;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
index 0a15ab7..debf4f3 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
@@ -47,6 +47,10 @@
quirks.add(new ImageCaptureRotationOptionQuirk());
}
+ if (ImageCaptureWashedOutImageQuirk.load()) {
+ quirks.add(new ImageCaptureWashedOutImageQuirk());
+ }
+
return quirks;
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/ImageCaptureWashedOutImageQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/ImageCaptureWashedOutImageQuirk.java
new file mode 100644
index 0000000..62fa918
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/ImageCaptureWashedOutImageQuirk.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.camera.core.impl.Quirk;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Quirk that prevents from getting washed out image while taking picture with flash ON/AUTO mode.
+ *
+ * <p>See b/176399765 and b/181966663.
+ */
+public class ImageCaptureWashedOutImageQuirk implements Quirk {
+
+ // List of devices with the issue. See b/181966663.
+ private static final List<String> DEVICE_MODELS = Arrays.asList(
+ // Galaxy S7
+ "SM-G9300",
+ "SM-G930R",
+ "SM-G930A",
+ "SM-G930V",
+ "SM-G930T",
+ "SM-G930U",
+ "SM-G930P",
+
+ // Galaxy S7+
+ "SM-SC02H",
+ "SM-SCV33",
+ "SM-G9350",
+ "SM-G935R",
+ "SM-G935A",
+ "SM-G935V",
+ "SM-G935T",
+ "SM-G935U",
+ "SM-G935P"
+ );
+
+ static boolean load() {
+ return "SAMSUNG".equals(Build.BRAND.toUpperCase(Locale.US))
+ && DEVICE_MODELS.contains(Build.MODEL.toUpperCase(Locale.US));
+ }
+}
diff --git a/camera/camera-testing/src/main/AndroidManifest.xml b/camera/camera-testing/src/main/AndroidManifest.xml
index 20e22e1..958dea7 100644
--- a/camera/camera-testing/src/main/AndroidManifest.xml
+++ b/camera/camera-testing/src/main/AndroidManifest.xml
@@ -17,6 +17,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="androidx.camera.testing">
<uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS"/>
+ <uses-permission android:name="android.permission.CAMERA" />
<application>
<activity
android:name="androidx.camera.testing.activity.ForegroundTestActivity"
@@ -26,6 +27,22 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
+ <activity
+ android:name="androidx.camera.testing.activity.CameraXTestActivity"
+ android:label="CameraX TestActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <activity
+ android:name="androidx.camera.testing.activity.Camera2TestActivity"
+ android:label="Camera2 TestActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
</application>
</manifest>
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/activity/CameraXTestActivity.java b/camera/camera-testing/src/main/java/androidx/camera/testing/activity/CameraXTestActivity.java
index 0952135..30e7dea 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/activity/CameraXTestActivity.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/activity/CameraXTestActivity.java
@@ -17,11 +17,13 @@
package androidx.camera.testing.activity;
+import static androidx.camera.testing.SurfaceTextureProvider.createSurfaceTextureProvider;
+
import android.graphics.SurfaceTexture;
import android.os.Bundle;
import android.util.Size;
-import android.view.Surface;
import android.view.TextureView;
+import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -32,10 +34,10 @@
import androidx.camera.core.Logger;
import androidx.camera.core.Preview;
import androidx.camera.core.impl.CameraInternal;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.internal.CameraUseCaseAdapter;
import androidx.camera.testing.CameraUtil;
import androidx.camera.testing.R;
+import androidx.camera.testing.SurfaceTextureProvider;
import androidx.test.espresso.idling.CountingIdlingResource;
import java.util.Collections;
@@ -113,7 +115,6 @@
public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surfaceTexture,
int width, int height) {
Logger.d(TAG, "SurfaceTexture available");
- setSurfaceProvider(surfaceTexture);
}
@Override
@@ -138,6 +139,24 @@
}
});
+ mPreview.setSurfaceProvider(createSurfaceTextureProvider(
+ new SurfaceTextureProvider.SurfaceTextureCallback() {
+ @Override
+ public void onSurfaceTextureReady(@NonNull SurfaceTexture surfaceTexture,
+ @NonNull Size resolution) {
+ ViewGroup viewGroup = (ViewGroup) textureView.getParent();
+ viewGroup.removeView(textureView);
+ viewGroup.addView(textureView, resolution.getWidth(),
+ resolution.getHeight());
+ textureView.setSurfaceTexture(surfaceTexture);
+ }
+
+ @Override
+ public void onSafeToRelease(@NonNull SurfaceTexture surfaceTexture) {
+ surfaceTexture.release();
+ }
+ }));
+
try {
final CameraX cameraX = CameraX.getOrCreateInstance(this).get();
final LinkedHashSet<CameraInternal> cameras =
@@ -158,25 +177,6 @@
cameraSelector).getCameraInfoInternal().getCameraId();
}
- void setSurfaceProvider(@NonNull SurfaceTexture surfaceTexture) {
- if (mPreview == null) {
- return;
- }
- mPreview.setSurfaceProvider((surfaceRequest) -> {
- final Size resolution = surfaceRequest.getResolution();
- surfaceTexture.setDefaultBufferSize(resolution.getWidth(), resolution.getHeight());
-
- final Surface surface = new Surface(surfaceTexture);
- surfaceRequest.provideSurface(
- surface,
- CameraXExecutors.directExecutor(),
- (surfaceResponse) -> {
- surface.release();
- surfaceTexture.release();
- });
- });
- }
-
@Nullable
public String getCameraId() {
return mCameraId;
diff --git a/camera/camera-testing/src/main/res/layout/activity_camera_main.xml b/camera/camera-testing/src/main/res/layout/activity_camera_main.xml
index fdc92c0..5a08f695 100644
--- a/camera/camera-testing/src/main/res/layout/activity_camera_main.xml
+++ b/camera/camera-testing/src/main/res/layout/activity_camera_main.xml
@@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<RelativeLayout
+<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -23,4 +23,4 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
-</RelativeLayout>
+</FrameLayout>
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
new file mode 100644
index 0000000..b94f63e
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
@@ -0,0 +1,300 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video
+
+import android.content.Context
+import android.graphics.SurfaceTexture
+import android.os.Build
+import android.view.Surface
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.MutableStateObservable
+import androidx.camera.core.impl.Observable
+import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.GLUtil
+import androidx.camera.video.QualitySelector.QUALITY_HIGHEST
+import androidx.camera.video.QualitySelector.QUALITY_LOWEST
+import androidx.camera.video.VideoOutput.StreamState
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.ArrayBlockingQueue
+import java.util.concurrent.Executors
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class VideoCaptureDeviceTest {
+
+ @get:Rule
+ val cameraRule = CameraUtil.grantCameraPermissionAndPreTest()
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val context: Context = ApplicationProvider.getApplicationContext()
+ private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+
+ private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
+ private lateinit var cameraInfo: CameraInfo
+
+ @Before
+ fun setUp() {
+ CameraX.initialize(context, Camera2Config.defaultConfig()).get()
+
+ cameraUseCaseAdapter = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector)
+ cameraInfo = cameraUseCaseAdapter.cameraInfo
+ }
+
+ @After
+ fun tearDown() {
+ if (this::cameraUseCaseAdapter.isInitialized) {
+ instrumentation.runOnMainSync {
+ cameraUseCaseAdapter.apply {
+ removeUseCases(useCases)
+ }
+ }
+ }
+ CameraX.shutdown().get(10, TimeUnit.SECONDS)
+ }
+
+ @Test
+ fun addUseCases_canReceiveFrame() = runBlocking {
+ // Arrange.
+ val videoOutput = TestVideoOutput()
+ val videoCapture = VideoCapture.withOutput(videoOutput)
+
+ // Act.
+ instrumentation.runOnMainSync {
+ cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
+ }
+
+ // Assert.
+ val surfaceRequest = videoOutput.nextSurfaceRequest(5, TimeUnit.SECONDS)
+ val frameUpdateSemaphore = surfaceRequest.provideUpdatingSurface()
+ assertThat(frameUpdateSemaphore.tryAcquire(5, 10, TimeUnit.SECONDS)).isTrue()
+ }
+
+ @Test
+ fun changeStreamState_canReceiveFrame() = runBlocking {
+ // Arrange.
+ val videoOutput = TestVideoOutput(streamState = StreamState.INACTIVE)
+ val videoCapture = VideoCapture.withOutput(videoOutput)
+
+ // Act.
+ instrumentation.runOnMainSync {
+ cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
+ }
+
+ // Assert.
+ val surfaceRequest = videoOutput.nextSurfaceRequest(5, TimeUnit.SECONDS)
+ val frameUpdateSemaphore = surfaceRequest.provideUpdatingSurface()
+ // No frame should be updated by INACTIVE state
+ assertThat(frameUpdateSemaphore.tryAcquire(1, 2, TimeUnit.SECONDS)).isFalse()
+
+ // Act.
+ videoOutput.setStreamState(StreamState.ACTIVE)
+
+ // Assert.
+ assertThat(frameUpdateSemaphore.tryAcquire(5, 10, TimeUnit.SECONDS)).isTrue()
+ }
+
+ @Test
+ fun addUseCases_setSupportedQuality_getCorrectResolution() {
+ assumeTrue(QualitySelector.getSupportedQualities(cameraInfo).isNotEmpty())
+ // Cuttlefish API 29 has inconsistent resolution issue. See b/184015059.
+ assumeFalse(Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29)
+
+ // Arrange.
+ val qualityList = QualitySelector.getSupportedQualities(cameraInfo) + arrayOf(
+ QUALITY_HIGHEST,
+ QUALITY_LOWEST
+ )
+ qualityList.forEach { quality ->
+ val targetResolution = QualitySelector.getResolution(cameraInfo, quality)
+ val videoOutput = TestVideoOutput(
+ mediaSpec = MediaSpec.builder().configureVideo {
+ it.setQualitySelector(QualitySelector.of(quality))
+ }.build()
+ )
+ val videoCapture = VideoCapture.withOutput(videoOutput)
+
+ // Act.
+ instrumentation.runOnMainSync {
+ cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
+ }
+
+ // Assert.
+ val surfaceRequest = videoOutput.nextSurfaceRequest(5, TimeUnit.SECONDS)
+ assertWithMessage("Set quality value by $quality")
+ .that(surfaceRequest.resolution).isEqualTo(targetResolution)
+
+ // Cleanup.
+ instrumentation.runOnMainSync {
+ cameraUseCaseAdapter.apply {
+ removeUseCases(listOf(videoCapture))
+ }
+ }
+ }
+ }
+
+ @Test
+ fun addUseCases_setQualityWithRotation_getCorrectResolution() {
+ assumeTrue(QualitySelector.getSupportedQualities(cameraInfo).isNotEmpty())
+ // Cuttlefish API 29 has inconsistent resolution issue. See b/184015059.
+ assumeFalse(Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29)
+
+ val targetResolution = QualitySelector.getResolution(cameraInfo, QUALITY_LOWEST)
+
+ arrayOf(
+ Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, Surface.ROTATION_270
+ ).forEach { rotation ->
+ // Arrange.
+ val videoOutput = TestVideoOutput(
+ mediaSpec = MediaSpec.builder().configureVideo {
+ it.setQualitySelector(QualitySelector.of(QUALITY_LOWEST))
+ }.build()
+ )
+ val videoCapture = VideoCapture.withOutput(videoOutput)
+
+ // Act.
+ instrumentation.runOnMainSync {
+ cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
+ }
+
+ // Assert.
+ val surfaceRequest = videoOutput.nextSurfaceRequest(5, TimeUnit.SECONDS)
+ assertWithMessage("Set rotation value by $rotation")
+ .that(surfaceRequest.resolution).isEqualTo(targetResolution)
+
+ // Cleanup.
+ instrumentation.runOnMainSync {
+ cameraUseCaseAdapter.apply {
+ removeUseCases(listOf(videoCapture))
+ }
+ }
+ }
+ }
+
+ @Test
+ fun useCaseCanBeReused() = runBlocking {
+ // Arrange.
+ val videoOutput = TestVideoOutput()
+ val videoCapture = VideoCapture.withOutput(videoOutput)
+
+ // Act.
+ instrumentation.runOnMainSync {
+ cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
+ }
+
+ // Assert.
+ var surfaceRequest = videoOutput.nextSurfaceRequest(5, TimeUnit.SECONDS)
+ var frameUpdateSemaphore = surfaceRequest.provideUpdatingSurface()
+ assertThat(frameUpdateSemaphore.tryAcquire(5, 10, TimeUnit.SECONDS)).isTrue()
+
+ // Act.
+ // Reuse use case
+ instrumentation.runOnMainSync {
+ cameraUseCaseAdapter.apply {
+ removeUseCases(listOf(videoCapture))
+ }
+ cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
+ }
+
+ // Assert.
+ surfaceRequest = videoOutput.nextSurfaceRequest(5, TimeUnit.SECONDS)
+ frameUpdateSemaphore = surfaceRequest.provideUpdatingSurface()
+ assertThat(frameUpdateSemaphore.tryAcquire(5, 10, TimeUnit.SECONDS)).isTrue()
+ }
+
+ private class TestVideoOutput(
+ streamState: StreamState = StreamState.ACTIVE,
+ mediaSpec: MediaSpec = MediaSpec.builder().build()
+ ) : VideoOutput {
+ private val surfaceRequests = ArrayBlockingQueue<SurfaceRequest>(10)
+
+ private val streamStateObservable: MutableStateObservable<StreamState> =
+ MutableStateObservable.withInitialState(streamState)
+
+ private val mediaSpecObservable: MutableStateObservable<MediaSpec> =
+ MutableStateObservable.withInitialState(mediaSpec)
+
+ override fun onSurfaceRequested(surfaceRequest: SurfaceRequest) {
+ surfaceRequests.put(surfaceRequest)
+ }
+
+ override fun getStreamState(): Observable<StreamState> = streamStateObservable
+
+ override fun getMediaSpec(): Observable<MediaSpec> = mediaSpecObservable
+
+ fun nextSurfaceRequest(timeout: Long, timeUnit: TimeUnit): SurfaceRequest {
+ return surfaceRequests.poll(timeout, timeUnit)
+ }
+
+ fun setStreamState(streamState: StreamState) = streamStateObservable.setState(streamState)
+
+ fun setMediaSpec(mediaSpec: MediaSpec) = mediaSpecObservable.setState(mediaSpec)
+ }
+
+ private suspend fun SurfaceRequest.provideUpdatingSurface(): Semaphore {
+ var isReleased = false
+ val frameUpdateSemaphore = Semaphore(0)
+ val executor = Executors.newFixedThreadPool(1)
+
+ val surfaceTexture = withContext(executor.asCoroutineDispatcher()) {
+ SurfaceTexture(0).apply {
+ setDefaultBufferSize(640, 480)
+ detachFromGLContext()
+ attachToGLContext(GLUtil.getTexIdFromGLContext())
+ setOnFrameAvailableListener {
+ frameUpdateSemaphore.release()
+ executor.execute {
+ if (!isReleased) {
+ updateTexImage()
+ }
+ }
+ }
+ }
+ }
+ val surface = Surface(surfaceTexture)
+
+ provideSurface(surface, executor) {
+ surfaceTexture.release()
+ surface.release()
+ executor.shutdown()
+ isReleased = true
+ }
+
+ return frameUpdateSemaphore
+ }
+}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureIntegrationTest.kt
similarity index 99%
rename from camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureTest.kt
rename to camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureIntegrationTest.kt
index 0ed0e9c..9513f29 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureIntegrationTest.kt
@@ -76,7 +76,7 @@
@LargeTest
@RunWith(Parameterized::class)
-class VideoCaptureTest(
+class VideoCaptureIntegrationTest(
private var cameraSelector: CameraSelector
) {
companion object {
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 347532b..900c7f8 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -16,9 +16,757 @@
package androidx.camera.video;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_DEFAULT_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ROTATION;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_ATTACHED_USE_CASES_UPDATE_LISTENER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAMERA_SELECTOR;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
+import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_CLASS;
+import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME;
+import static androidx.camera.core.internal.ThreadConfig.OPTION_BACKGROUND_EXECUTOR;
+import static androidx.camera.core.internal.UseCaseEventConfig.OPTION_USE_CASE_EVENT_CALLBACK;
+import static androidx.camera.video.impl.VideoCaptureConfig.OPTION_VIDEO_OUTPUT;
+
+import android.graphics.Rect;
+import android.util.Pair;
+import android.util.Size;
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.camera.core.AspectRatio;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ExperimentalUseCaseGroup;
+import androidx.camera.core.Logger;
+import androidx.camera.core.SurfaceRequest;
+import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CameraInfoInternal;
+import androidx.camera.core.impl.CaptureConfig;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.ConfigProvider;
+import androidx.camera.core.impl.DeferrableSurface;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
+import androidx.camera.core.impl.MutableConfig;
+import androidx.camera.core.impl.MutableOptionsBundle;
+import androidx.camera.core.impl.Observable;
+import androidx.camera.core.impl.Observable.Observer;
+import androidx.camera.core.impl.OptionsBundle;
+import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.impl.utils.Threads;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.internal.ThreadConfig;
+import androidx.camera.video.VideoOutput.StreamState;
+import androidx.camera.video.impl.VideoCaptureConfig;
+import androidx.core.util.Consumer;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
/**
- * A use case for taking a video.
+ * A use case that provides camera stream suitable for video application.
+ *
+ * <p>VideoCapture is used to create a camera stream suitable for video application. This stream
+ * is used by the implementation of {@link VideoOutput}. Calling {@link #withOutput(VideoOutput)}
+ * can generate a VideoCapture use case binding to the given VideoOutput.
+ *
+ * <p>When binding VideoCapture, VideoCapture will initialize the camera stream according to the
+ * resolution found by the {@link QualitySelector} in VideoOutput. Then VideoCapture will invoke
+ * {@link VideoOutput#onSurfaceRequested(SurfaceRequest)} to request VideoOutput to provide a
+ * {@link Surface} via {@link SurfaceRequest#provideSurface} to complete the initialization
+ * process. After VideoCapture is bound, updating the QualitySelector in VideoOutput will have no
+ * effect. If it needs to change the resolution of the camera stream after VideoCapture is bound,
+ * it has to unbind the original VideoCapture, update the QualitySelector in VideoOutput and then
+ * re-bind the VideoCapture. If the implementation of VideoOutput does not support modifying the
+ * QualitySelector afterwards, it has to create a new VideoOutput and VideoCapture for re-bind.
+ *
+ * @param <T> the type of VideoOutput
*/
-public class VideoCapture {
- private VideoCapture() {}
+public final class VideoCapture<T extends VideoOutput> extends UseCase {
+ private static final String TAG = "VideoCapture";
+ private static final Defaults DEFAULT_CONFIG = new Defaults();
+
+ private DeferrableSurface mDeferrableSurface;
+ private SurfaceRequest mSurfaceRequest;
+
+ /**
+ * Create a VideoCapture builder with a {@link VideoOutput}.
+ *
+ * @param videoOutput the associated VideoOutput.
+ * @return the new Builder
+ */
+ @NonNull
+ public static <T extends VideoOutput> VideoCapture<T> withOutput(@NonNull T videoOutput) {
+ return new VideoCapture.Builder<T>(videoOutput).build();
+ }
+
+ /**
+ * Creates a new video capture use case from the given configuration.
+ *
+ * @param config for this use case instance
+ */
+ VideoCapture(@NonNull VideoCaptureConfig<T> config) {
+ super(config);
+ }
+
+ /**
+ * Gets the {@link VideoOutput} associated to this VideoCapture.
+ */
+ @SuppressWarnings("unchecked")
+ @NonNull
+ public T getOutput() {
+ return ((VideoCaptureConfig<T>) getCurrentConfig()).getVideoOutput();
+ }
+
+ /**
+ * Sets the desired rotation of the output video.
+ *
+ * <p>In most cases this should be set to the current rotation returned by {@link
+ * Display#getRotation()}.
+ *
+ * @param rotation Desired rotation of the output video.
+ */
+ public void setTargetRotation(@RotationValue int rotation) {
+ setTargetRotationInternal(rotation);
+ }
+
+ @Override
+ public void onAttached() {
+ getOutput().getStreamState().addObserver(CameraXExecutors.mainThreadExecutor(),
+ mStreamStateObserver);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ protected Size onSuggestedResolutionUpdated(@NonNull Size suggestedResolution) {
+ String cameraId = getCameraId();
+ VideoCaptureConfig<T> config = (VideoCaptureConfig<T>) getCurrentConfig();
+
+ SessionConfig.Builder sessionConfigBuilder = createPipeline(cameraId, config,
+ suggestedResolution);
+ updateSessionConfig(sessionConfigBuilder.build());
+
+ return suggestedResolution;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public void onDetached() {
+ clearPipeline();
+
+ getOutput().getStreamState().removeObserver(mStreamStateObserver);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return TAG + ":" + getName();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ @Nullable
+ public UseCaseConfig<?> getDefaultConfig(boolean applyDefaultConfig,
+ @NonNull UseCaseConfigFactory factory) {
+ Config captureConfig = factory.getConfig(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE);
+
+ if (applyDefaultConfig) {
+ captureConfig = Config.mergeConfigs(captureConfig, DEFAULT_CONFIG.getConfig());
+ }
+
+ return captureConfig == null ? null :
+ getUseCaseConfigBuilder(captureConfig).getUseCaseConfig();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ @Override
+ protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+ @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
+
+ updateTargetResolutionByQuality(cameraInfo, builder);
+
+ return builder.getUseCaseConfig();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @NonNull
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ public UseCaseConfig.Builder<?, ?, ?> getUseCaseConfigBuilder(@NonNull Config config) {
+ return Builder.fromConfig(config);
+ }
+
+ @UiThread
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
+ @NonNull
+ private SessionConfig.Builder createPipeline(@NonNull String cameraId,
+ @NonNull VideoCaptureConfig<T> config,
+ @NonNull Size resolution) {
+ Threads.checkMainThread();
+
+ mSurfaceRequest = new SurfaceRequest(resolution, getCamera(), false);
+ config.getVideoOutput().onSurfaceRequested(mSurfaceRequest);
+ Rect cropRect = getViewPortCropRect() != null ? getViewPortCropRect() : new Rect(0, 0,
+ resolution.getWidth(), resolution.getHeight());
+ mSurfaceRequest.updateTransformationInfo(SurfaceRequest.TransformationInfo.of(cropRect,
+ getRelativeRotation(getCamera()), getTargetRotationInternal()));
+ mDeferrableSurface = mSurfaceRequest.getDeferrableSurface();
+
+ SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
+ sessionConfigBuilder.addSurface(mDeferrableSurface);
+ sessionConfigBuilder.addErrorListener(
+ (sessionConfig, error) -> resetPipeline(cameraId, config, resolution));
+
+ return sessionConfigBuilder;
+ }
+
+ /**
+ * Clear the internal pipeline so that the pipeline can be set up again.
+ */
+ @UiThread
+ private void clearPipeline() {
+ Threads.checkMainThread();
+
+ if (mDeferrableSurface != null) {
+ mDeferrableSurface.close();
+ mDeferrableSurface = null;
+ }
+
+ mSurfaceRequest = null;
+ }
+
+ @UiThread
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ void resetPipeline(@NonNull String cameraId,
+ @NonNull VideoCaptureConfig<T> config,
+ @NonNull Size resolution) {
+ clearPipeline();
+
+ // Ensure the attached camera has not changed before resetting.
+ // TODO(b/143915543): Ensure this never gets called by a camera that is not attached
+ // to this use case so we don't need to do this check.
+ if (isCurrentCamera(cameraId)) {
+ // Only reset the pipeline when the bound camera is the same.
+ SessionConfig.Builder sessionConfigBuilder = createPipeline(cameraId, config,
+ resolution);
+ updateSessionConfig(sessionConfigBuilder.build());
+ notifyReset();
+ }
+ }
+
+ /**
+ * Provides a base static default configuration for the VideoCapture
+ *
+ * <p>These values may be overridden by the implementation. They only provide a minimum set of
+ * defaults that are implementation independent.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public static final class Defaults implements ConfigProvider<VideoCaptureConfig<?>> {
+ /** Surface occupancy priority to this use case */
+ private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 3;
+ private static final VideoOutput DEFAULT_VIDEO_OUTPUT =
+ SurfaceRequest::willNotProvideSurface;
+ static final Size DEFAULT_RESOLUTION = new Size(1920, 1080);
+
+ private static final VideoCaptureConfig<?> DEFAULT_CONFIG;
+
+ static {
+ Builder<?> builder = new Builder<>(DEFAULT_VIDEO_OUTPUT)
+ .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
+
+ DEFAULT_CONFIG = builder.getUseCaseConfig();
+ }
+
+ @NonNull
+ @Override
+ public VideoCaptureConfig<?> getConfig() {
+ return DEFAULT_CONFIG;
+ }
+ }
+
+ @Nullable
+ private MediaSpec getMediaSpec() {
+ return fetchObservableValue(getOutput().getMediaSpec(), null);
+ }
+
+ private final Observer<StreamState> mStreamStateObserver = new Observer<StreamState>() {
+ @Override
+ public void onNewData(@Nullable StreamState streamState) {
+ Logger.d(TAG, "Receive streamState = " + streamState);
+ if (streamState == StreamState.ACTIVE) {
+ notifyActive();
+ } else {
+ notifyInactive();
+ }
+ }
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ Logger.w(TAG, "Receive onError from StreamState observer", t);
+ }
+ };
+
+ /**
+ * Set {@link ImageOutputConfig#OPTION_TARGET_RESOLUTION} according to the resolution found
+ * by the {@link QualitySelector} in VideoOutput.
+ *
+ * <p>If the device doesn't have any supported quality, a default resolution will be set.
+ *
+ * @throws IllegalArgumentException if not able to find a resolution by the QualitySelector
+ * in VideoOutput.
+ */
+ private void updateTargetResolutionByQuality(@NonNull CameraInfoInternal cameraInfo,
+ @NonNull UseCaseConfig.Builder<?, ?, ?> builder) throws IllegalArgumentException {
+ MediaSpec mediaSpec = getMediaSpec();
+
+ Preconditions.checkArgument(mediaSpec != null,
+ "Unable to update target resolution by null MediaSpec.");
+
+ Size resolution;
+ if (QualitySelector.getSupportedQualities(cameraInfo).isEmpty()) {
+ // When the device does not have any supported quality, even the most flexible
+ // QualitySelector such as QualitySelector.of(QUALITY_HIGHEST), still cannot
+ // find any resolution. This should be a rare case but will cause VideoCapture
+ // to always fail to bind. The workaround is to set a default resolution.
+ Logger.w(TAG,
+ "Can't find any supported quality on the device, use default resolution.");
+ resolution = Defaults.DEFAULT_RESOLUTION;
+ } else {
+ QualitySelector qualitySelector = mediaSpec.getVideoSpec().getQualitySelector();
+
+ int quality = qualitySelector.select(cameraInfo);
+
+ Logger.d(TAG, "Found quality " + quality + " by qualitySelector " + qualitySelector);
+
+ if (quality != QualitySelector.QUALITY_NONE) {
+ resolution = QualitySelector.getResolution(cameraInfo, quality);
+ } else {
+ throw new IllegalArgumentException(
+ "Unable to find a resolution by QualitySelector: " + qualitySelector);
+ }
+ }
+
+ int relativeRotation = cameraInfo.getSensorRotationDegrees(getTargetRotationInternal());
+ boolean needRotate = relativeRotation == 90 || relativeRotation == 270;
+ if (needRotate) {
+ resolution = new Size(/* width= */resolution.getHeight(),
+ /* height= */resolution.getWidth());
+ }
+ Logger.d(TAG,
+ "relativeRotation = " + relativeRotation + " and final resolution = " + resolution);
+
+ builder.getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION, resolution);
+ }
+
+ /**
+ * Gets the snapshot value of the given {@link Observable}.
+ *
+ * <p>Note: Set {@code valueIfMissing} to a non-{@code null} value doesn't mean the method
+ * will never return a {@code null} value. The observable could contain exact {@code null}
+ * value.
+ *
+ * @param observable the observable
+ * @param valueIfMissing if the observable doesn't contain value.
+ * @param <T> the value type
+ * @return the snapshot value of the given {@link Observable}.
+ */
+ @Nullable
+ private static <T> T fetchObservableValue(@NonNull Observable<T> observable,
+ @Nullable T valueIfMissing) {
+ ListenableFuture<T> future = observable.fetchData();
+ if (!future.isDone()) {
+ return valueIfMissing;
+ }
+ try {
+ return future.get();
+ } catch (ExecutionException | InterruptedException e) {
+ // Should not happened
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Builder for a {@link VideoCapture}.
+ *
+ * @param <T> the type of VideoOutput
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @SuppressWarnings("ObjectToString")
+ public static final class Builder<T extends VideoOutput> implements
+ UseCaseConfig.Builder<VideoCapture<T>, VideoCaptureConfig<T>, Builder<T>>,
+ ImageOutputConfig.Builder<Builder<T>>, ThreadConfig.Builder<Builder<T>> {
+ private final MutableOptionsBundle mMutableConfig;
+
+ /** Creates a new Builder object. */
+ Builder(@NonNull T videoOutput) {
+ this(createInitialBundle(videoOutput));
+ }
+
+ @SuppressWarnings("unchecked")
+ private Builder(@NonNull MutableOptionsBundle mutableConfig) {
+ mMutableConfig = mutableConfig;
+
+ if (!mMutableConfig.containsOption(OPTION_VIDEO_OUTPUT)) {
+ throw new IllegalArgumentException("VideoOutput is required");
+ }
+
+ Class<?> oldConfigClass =
+ mutableConfig.retrieveOption(OPTION_TARGET_CLASS, null);
+ if (oldConfigClass != null && !oldConfigClass.equals(VideoCapture.class)) {
+ throw new IllegalArgumentException(
+ "Invalid target class configuration for "
+ + Builder.this
+ + ": "
+ + oldConfigClass);
+ }
+
+ setTargetClass((Class<VideoCapture<T>>) (Type) VideoCapture.class);
+ }
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ static Builder<? extends VideoOutput> fromConfig(@NonNull Config configuration) {
+ return new Builder<>(MutableOptionsBundle.from(configuration));
+ }
+
+ /**
+ * Generates a Builder from another Config object
+ *
+ * @param configuration An immutable configuration to pre-populate this builder.
+ * @return The new Builder.
+ */
+ @NonNull
+ public static <T extends VideoOutput> Builder<T> fromConfig(
+ @NonNull VideoCaptureConfig<T> configuration) {
+ return new Builder<>(MutableOptionsBundle.from(configuration));
+ }
+
+ @NonNull
+ private static <T extends VideoOutput> MutableOptionsBundle createInitialBundle(
+ @NonNull T videoOutput) {
+ MutableOptionsBundle bundle = MutableOptionsBundle.create();
+ bundle.insertOption(OPTION_VIDEO_OUTPUT, videoOutput);
+ return bundle;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ @NonNull
+ public MutableConfig getMutableConfig() {
+ return mMutableConfig;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ @Override
+ public VideoCaptureConfig<T> getUseCaseConfig() {
+ return new VideoCaptureConfig<>(OptionsBundle.from(mMutableConfig));
+ }
+
+ /**
+ * Builds an immutable {@link VideoCaptureConfig} from the current state.
+ *
+ * @return A {@link VideoCaptureConfig} populated with the current state.
+ */
+ @Override
+ @NonNull
+ public VideoCapture<T> build() {
+ return new VideoCapture<>(getUseCaseConfig());
+ }
+
+ // Implementations of TargetConfig.Builder default methods
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ @NonNull
+ public Builder<T> setTargetClass(@NonNull Class<VideoCapture<T>> targetClass) {
+ getMutableConfig().insertOption(OPTION_TARGET_CLASS, targetClass);
+
+ // If no name is set yet, then generate a unique name
+ if (null == getMutableConfig().retrieveOption(OPTION_TARGET_NAME, null)) {
+ String targetName = targetClass.getCanonicalName() + "-" + UUID.randomUUID();
+ setTargetName(targetName);
+ }
+
+ return this;
+ }
+
+ /**
+ * Sets the name of the target object being configured, used only for debug logging.
+ *
+ * <p>The name should be a value that can uniquely identify an instance of the object being
+ * configured.
+ *
+ * <p>If not set, the target name will default to an unique name automatically generated
+ * with the class canonical name and random UUID.
+ *
+ * @param targetName A unique string identifier for the instance of the class being
+ * configured.
+ * @return the current Builder.
+ */
+ @Override
+ @NonNull
+ public Builder<T> setTargetName(@NonNull String targetName) {
+ getMutableConfig().insertOption(OPTION_TARGET_NAME, targetName);
+ return this;
+ }
+
+ // Implementations of ImageOutputConfig.Builder default methods
+
+ /**
+ * Sets the aspect ratio of the intended target for images from this configuration.
+ *
+ * <p>It is not allowed to set both target aspect ratio and target resolution on the same
+ * use case.
+ *
+ * <p>The target aspect ratio is used as a hint when determining the resulting output aspect
+ * ratio which may differ from the request, possibly due to device constraints.
+ * Application code should check the resulting output's resolution.
+ *
+ * <p>If not set, resolutions with aspect ratio 16:9 will be considered in higher
+ * priority.
+ *
+ * @param aspectRatio A {@link AspectRatio} representing the ratio of the target's width
+ * and height.
+ * @return The current Builder.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ @Override
+ public Builder<T> setTargetAspectRatio(@AspectRatio.Ratio int aspectRatio) {
+ getMutableConfig().insertOption(OPTION_TARGET_ASPECT_RATIO, aspectRatio);
+ return this;
+ }
+
+ /**
+ * Sets the rotation of the intended target for images from this configuration.
+ *
+ * <p>This is one of four valid values: {@link Surface#ROTATION_0}, {@link
+ * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
+ * Rotation values are relative to the "natural" rotation, {@link Surface#ROTATION_0}.
+ *
+ * <p>If not set, the target rotation will default to the value of
+ * {@link Display#getRotation()} of the default display at the time the use case is
+ * created. The use case is fully created once it has been attached to a camera.
+ *
+ * @param rotation The rotation of the intended target.
+ * @return The current Builder.
+ */
+ @NonNull
+ @Override
+ public Builder<T> setTargetRotation(@RotationValue int rotation) {
+ getMutableConfig().insertOption(OPTION_TARGET_ROTATION, rotation);
+ return this;
+ }
+
+ /**
+ * setTargetResolution is not supported on VideoCapture
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ @Override
+ public Builder<T> setTargetResolution(@NonNull Size resolution) {
+ throw new UnsupportedOperationException("setTargetResolution is not supported.");
+ }
+
+ /**
+ * Sets the default resolution of the intended target from this configuration.
+ *
+ * @param resolution The default resolution to choose from supported output sizes list.
+ * @return The current Builder.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ @Override
+ public Builder<T> setDefaultResolution(@NonNull Size resolution) {
+ getMutableConfig().insertOption(OPTION_DEFAULT_RESOLUTION, resolution);
+ return this;
+ }
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ @Override
+ public Builder<T> setMaxResolution(@NonNull Size resolution) {
+ getMutableConfig().insertOption(OPTION_MAX_RESOLUTION, resolution);
+ return this;
+ }
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ @NonNull
+ public Builder<T> setSupportedResolutions(
+ @NonNull List<Pair<Integer, Size[]>> resolutions) {
+ getMutableConfig().insertOption(OPTION_SUPPORTED_RESOLUTIONS, resolutions);
+ return this;
+ }
+
+ // Implementations of ThreadConfig.Builder default methods
+
+ /**
+ * Sets the default executor that will be used for background tasks.
+ *
+ * <p>If not set, the background executor will default to an automatically generated
+ * {@link Executor}.
+ *
+ * @param executor The executor which will be used for background tasks.
+ * @return the current Builder.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ @NonNull
+ public Builder<T> setBackgroundExecutor(@NonNull Executor executor) {
+ getMutableConfig().insertOption(OPTION_BACKGROUND_EXECUTOR, executor);
+ return this;
+ }
+
+ // Implementations of UseCaseConfig.Builder default methods
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ @NonNull
+ public Builder<T> setDefaultSessionConfig(@NonNull SessionConfig sessionConfig) {
+ getMutableConfig().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
+ return this;
+ }
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ @NonNull
+ public Builder<T> setDefaultCaptureConfig(@NonNull CaptureConfig captureConfig) {
+ getMutableConfig().insertOption(OPTION_DEFAULT_CAPTURE_CONFIG, captureConfig);
+ return this;
+ }
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ @NonNull
+ public Builder<T> setSessionOptionUnpacker(
+ @NonNull SessionConfig.OptionUnpacker optionUnpacker) {
+ getMutableConfig().insertOption(OPTION_SESSION_CONFIG_UNPACKER, optionUnpacker);
+ return this;
+ }
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ @NonNull
+ public Builder<T> setCaptureOptionUnpacker(
+ @NonNull CaptureConfig.OptionUnpacker optionUnpacker) {
+ getMutableConfig().insertOption(OPTION_CAPTURE_CONFIG_UNPACKER, optionUnpacker);
+ return this;
+ }
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ @NonNull
+ public Builder<T> setSurfaceOccupancyPriority(int priority) {
+ getMutableConfig().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
+ return this;
+ }
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY)
+ @Override
+ @NonNull
+ public Builder<T> setCameraSelector(@NonNull CameraSelector cameraSelector) {
+ getMutableConfig().insertOption(OPTION_CAMERA_SELECTOR, cameraSelector);
+ return this;
+ }
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ @NonNull
+ public Builder<T> setUseCaseEventCallback(
+ @NonNull EventCallback useCaseEventCallback) {
+ getMutableConfig().insertOption(OPTION_USE_CASE_EVENT_CALLBACK, useCaseEventCallback);
+ return this;
+ }
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Override
+ @NonNull
+ public Builder<T> setAttachedUseCasesUpdateListener(
+ @NonNull Consumer<Collection<UseCase>> attachedUseCasesUpdateListener) {
+ getMutableConfig().insertOption(OPTION_ATTACHED_USE_CASES_UPDATE_LISTENER,
+ attachedUseCasesUpdateListener);
+ return this;
+ }
+ }
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java
new file mode 100644
index 0000000..f5a628a
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.impl;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.ImageFormatConstants;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.OptionsBundle;
+import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.core.internal.ThreadConfig;
+import androidx.camera.video.VideoCapture;
+import androidx.camera.video.VideoOutput;
+
+/**
+ * Config for a video capture use case.
+ *
+ * <p>In the earlier stage, the VideoCapture is deprioritized.
+ *
+ * @param <T> the type of VideoOutput
+ */
+public final class VideoCaptureConfig<T extends VideoOutput>
+ implements UseCaseConfig<VideoCapture<T>>,
+ ImageOutputConfig,
+ ThreadConfig {
+
+ // Option Declarations:
+ // *********************************************************************************************
+
+ public static final Option<VideoOutput> OPTION_VIDEO_OUTPUT =
+ Option.create("camerax.video.VideoCapture.videoOutput", VideoOutput.class);
+
+ // *********************************************************************************************
+
+ private final OptionsBundle mConfig;
+
+ public VideoCaptureConfig(@NonNull OptionsBundle config) {
+ mConfig = config;
+ }
+
+ @SuppressWarnings("unchecked")
+ @NonNull
+ public T getVideoOutput() {
+ return (T) retrieveOption(OPTION_VIDEO_OUTPUT);
+ }
+
+ /**
+ * Retrieves the format of the image that is fed as input.
+ *
+ * <p>This should always be PRIVATE for VideoCapture.
+ */
+ @Override
+ public int getInputFormat() {
+ return ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+ }
+
+ @NonNull
+ @Override
+ public Config getConfig() {
+ return mConfig;
+ }
+}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
new file mode 100644
index 0000000..a22f778
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video
+
+import android.content.Context
+import android.os.Build
+import android.util.Size
+import android.view.Surface
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.CameraFactory
+import androidx.camera.core.impl.ImageOutputConfig
+import androidx.camera.core.impl.MutableStateObservable
+import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.testing.CamcorderProfileUtil
+import androidx.camera.testing.CamcorderProfileUtil.PROFILE_2160P
+import androidx.camera.testing.CamcorderProfileUtil.PROFILE_720P
+import androidx.camera.testing.CamcorderProfileUtil.RESOLUTION_2160P
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.fakes.FakeAppConfig
+import androidx.camera.testing.fakes.FakeCamcorderProfileProvider
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraFactory
+import androidx.camera.testing.fakes.FakeCameraInfoInternal
+import androidx.camera.video.QualitySelector.QUALITY_FHD
+import androidx.camera.video.QualitySelector.QUALITY_UHD
+import androidx.camera.video.VideoOutput.StreamState
+import androidx.core.util.Consumer
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+private val ANY_SIZE = Size(640, 480)
+private const val CAMERA_ID_0 = "0"
+private const val CAMERA_ID_1 = "1"
+private val CAMERA_0_PROFILE_HIGH = CamcorderProfileUtil.asHighQuality(PROFILE_2160P)
+private val CAMERA_0_PROFILE_LOW = CamcorderProfileUtil.asLowQuality(PROFILE_720P)
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class VideoCaptureTest {
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+ private lateinit var cameraUseCaseAdapter0: CameraUseCaseAdapter
+ private lateinit var cameraUseCaseAdapter1: CameraUseCaseAdapter
+
+ @Before
+ fun setUp() {
+ // Prepare camera0
+ // Camera 0 support 2160P(UHD) and 720P(HD)
+ val cameraInfo0 = FakeCameraInfoInternal(CAMERA_ID_0).apply {
+ camcorderProfileProvider = FakeCamcorderProfileProvider.Builder()
+ .addProfile(CAMERA_0_PROFILE_HIGH)
+ .addProfile(PROFILE_2160P)
+ .addProfile(PROFILE_720P)
+ .addProfile(CAMERA_0_PROFILE_LOW)
+ .build()
+ }
+ val camera0 = FakeCamera(CAMERA_ID_0, null, cameraInfo0)
+
+ // Prepare camera 1
+ // camera1 has no supported quality
+ val cameraInfo1 = FakeCameraInfoInternal(CAMERA_ID_1, 0, CameraSelector.LENS_FACING_FRONT)
+ val camera1 = FakeCamera(CAMERA_ID_1, null, cameraInfo1)
+
+ val cameraFactoryProvider =
+ CameraFactory.Provider { _, _, _ ->
+ FakeCameraFactory().apply {
+ insertDefaultBackCamera(CAMERA_ID_0) { camera0 }
+ insertDefaultFrontCamera(CAMERA_ID_1) { camera1 }
+ }
+ }
+
+ val cameraXConfig = CameraXConfig.Builder.fromConfig(FakeAppConfig.create())
+ .setCameraFactoryProvider(cameraFactoryProvider)
+ .build()
+ CameraX.initialize(context, cameraXConfig).get()
+
+ cameraUseCaseAdapter0 =
+ CameraUtil.createCameraUseCaseAdapter(context, CameraSelector.DEFAULT_BACK_CAMERA)
+ cameraUseCaseAdapter1 =
+ CameraUtil.createCameraUseCaseAdapter(context, CameraSelector.DEFAULT_FRONT_CAMERA)
+ }
+
+ @After
+ fun tearDown() {
+ if (this::cameraUseCaseAdapter0.isInitialized) {
+ cameraUseCaseAdapter0.apply {
+ removeUseCases(useCases)
+ }
+ }
+ if (this::cameraUseCaseAdapter1.isInitialized) {
+ cameraUseCaseAdapter1.apply {
+ removeUseCases(useCases)
+ }
+ }
+ CameraX.shutdown().get()
+ }
+
+ @Test
+ fun setTargetResolution_throwsException() {
+ val videoOutput = createVideoOutput()
+
+ assertThrows(UnsupportedOperationException::class.java) {
+ VideoCapture.Builder(videoOutput)
+ .setTargetResolution(ANY_SIZE)
+ .build()
+ }
+ }
+
+ @Test
+ fun canGetVideoOutput() {
+ // Arrange.
+ val videoOutput = createVideoOutput()
+
+ // Act.
+ val videoCapture = VideoCapture.withOutput(videoOutput)
+
+ // Assert.
+ assertThat(videoCapture.output).isEqualTo(videoOutput)
+ }
+
+ @Test
+ fun addUseCases_receiveOnSurfaceRequest() {
+ // Arrange.
+ var surfaceRequest: SurfaceRequest? = null
+ val videoOutput = createVideoOutput(surfaceRequestListener = { surfaceRequest = it })
+ val videoCapture = VideoCapture.Builder(videoOutput)
+ .setSessionOptionUnpacker { _, _ -> }
+ .build()
+
+ // Act.
+ cameraUseCaseAdapter0.addUseCases(listOf(videoCapture))
+
+ // Assert.
+ assertThat(surfaceRequest).isNotNull()
+ }
+
+ @Test
+ fun addUseCases_withNullMediaSpec_throwException() {
+ // Arrange.
+ val videoOutput = createVideoOutput(mediaSpec = null)
+ val videoCapture = VideoCapture.Builder(videoOutput)
+ .setSessionOptionUnpacker { _, _ -> }
+ .build()
+
+ // Assert.
+ assertThrows(CameraUseCaseAdapter.CameraException::class.java) {
+ // Act.
+ cameraUseCaseAdapter0.addUseCases(listOf(videoCapture))
+ }
+ }
+
+ @Test
+ fun setQualitySelector_sameResolutionAsQualitySelector() {
+ // Arrange.
+ // Camera 0 support 2160P(UHD) and 720P(HD)
+ val videoOutput = createVideoOutput(
+ mediaSpec = MediaSpec.builder().configureVideo {
+ it.setQualitySelector(QualitySelector.of(QUALITY_UHD))
+ }.build()
+ )
+ val videoCapture = VideoCapture.Builder(videoOutput)
+ .setSessionOptionUnpacker { _, _ -> }
+ .build()
+
+ // Act.
+ cameraUseCaseAdapter0.addUseCases(listOf(videoCapture))
+
+ // Assert.
+ val targetResolution = videoCapture.currentConfig.retrieveOption(
+ ImageOutputConfig.OPTION_TARGET_RESOLUTION
+ )
+ assertThat(targetResolution).isEqualTo(RESOLUTION_2160P)
+ }
+
+ @Test
+ fun setQualitySelector_notSupportedQuality_throwException() {
+ // Arrange.
+ // Camera 0 support 2160P(UHD) and 720P(HD)
+ val videoOutput = createVideoOutput(
+ mediaSpec = MediaSpec.builder().configureVideo {
+ it.setQualitySelector(QualitySelector.of(QUALITY_FHD))
+ }.build()
+ )
+ val videoCapture = VideoCapture.Builder(videoOutput)
+ .setSessionOptionUnpacker { _, _ -> }
+ .build()
+
+ // Assert.
+ assertThrows(CameraUseCaseAdapter.CameraException::class.java) {
+ // Act.
+ cameraUseCaseAdapter0.addUseCases(listOf(videoCapture))
+ }
+ }
+
+ @Test
+ fun noSupportedQuality_useDefaultResolution() {
+ // Arrange.
+ val videoOutput = createVideoOutput(
+ mediaSpec = MediaSpec.builder().configureVideo {
+ it.setQualitySelector(QualitySelector.of(QUALITY_UHD))
+ }.build()
+ )
+ val videoCapture = VideoCapture.Builder(videoOutput)
+ .setSessionOptionUnpacker { _, _ -> }
+ .build()
+
+ // Act.
+ // camera1 has no supported quality
+ cameraUseCaseAdapter1.addUseCases(listOf(videoCapture))
+
+ // Assert.
+ val targetResolution = videoCapture.currentConfig.retrieveOption(
+ ImageOutputConfig.OPTION_TARGET_RESOLUTION
+ )
+ assertThat(targetResolution).isEqualTo(VideoCapture.Defaults.DEFAULT_RESOLUTION)
+ }
+
+ @Test
+ fun removeUseCases_receiveResultOfSurfaceRequest() {
+ // Arrange.
+ var surfaceResult: SurfaceRequest.Result? = null
+ val videoOutput = createVideoOutput { surfaceRequest ->
+ surfaceRequest.provideSurface(
+ mock(Surface::class.java),
+ CameraXExecutors.directExecutor(),
+ { surfaceResult = it }
+ )
+ }
+ val videoCapture = VideoCapture.Builder(videoOutput)
+ .setSessionOptionUnpacker { _, _ -> }
+ .build()
+
+ // Act.
+ cameraUseCaseAdapter0.addUseCases(listOf(videoCapture))
+
+ // Assert.
+ // Surface is in use, should not receive any result.
+ assertThat(surfaceResult).isNull()
+
+ // Act.
+ cameraUseCaseAdapter0.removeUseCases(listOf(videoCapture))
+
+ // Assert.
+ assertThat(surfaceResult!!.resultCode).isEqualTo(
+ SurfaceRequest.Result.RESULT_SURFACE_USED_SUCCESSFULLY
+ )
+ }
+
+ @Test
+ fun addUseCases_transformationInfoUpdated() {
+ // Arrange.
+ val listener = mock(SurfaceRequest.TransformationInfoListener::class.java)
+ val videoOutput = createVideoOutput(
+ surfaceRequestListener = {
+ it.setTransformationInfoListener(
+ CameraXExecutors.directExecutor(),
+ listener
+ )
+ }
+ )
+
+ val videoCapture = VideoCapture.Builder(videoOutput)
+ .setSessionOptionUnpacker { _, _ -> }
+ .build()
+
+ // Act.
+ cameraUseCaseAdapter0.addUseCases(listOf(videoCapture))
+
+ // Assert.
+ verify(listener).onTransformationInfoUpdate(any())
+ }
+
+ private fun createVideoOutput(
+ streamState: StreamState = StreamState.ACTIVE,
+ mediaSpec: MediaSpec? = MediaSpec.builder().build(),
+ surfaceRequestListener: Consumer<SurfaceRequest> = Consumer { it.willNotProvideSurface() }
+ ): TestVideoOutput = TestVideoOutput(streamState, mediaSpec, surfaceRequestListener)
+
+ private class TestVideoOutput constructor(
+ streamState: StreamState,
+ mediaSpec: MediaSpec?,
+ val surfaceRequestCallback: Consumer<SurfaceRequest>
+ ) : VideoOutput {
+
+ private val streamStateObservable: MutableStateObservable<StreamState> =
+ MutableStateObservable.withInitialState(streamState)
+
+ private val mediaSpecObservable: MutableStateObservable<MediaSpec> =
+ MutableStateObservable.withInitialState(mediaSpec)
+
+ override fun onSurfaceRequested(surfaceRequest: SurfaceRequest) {
+ surfaceRequestCallback.accept(surfaceRequest)
+ }
+
+ override fun getStreamState(): Observable<StreamState> = streamStateObservable
+
+ override fun getMediaSpec(): Observable<MediaSpec> = mediaSpecObservable
+
+ fun setStreamState(streamState: StreamState) = streamStateObservable.setState(streamState)
+
+ fun setMediaSpec(mediaSpec: MediaSpec) = mediaSpecObservable.setState(mediaSpec)
+ }
+}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt
new file mode 100644
index 0000000..45de9b6
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraDisconnectTest.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.core
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.CameraX
+import androidx.camera.core.CameraXConfig
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CoreAppTestUtil
+import androidx.camera.testing.activity.Camera2TestActivity
+import androidx.camera.testing.activity.CameraXTestActivity
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.IdlingPolicies
+import androidx.test.espresso.IdlingRegistry
+import androidx.test.espresso.IdlingResource
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.util.concurrent.TimeUnit
+
+/** Tests for [CameraX] which varies use case combinations to run. */
+@LargeTest
+@RunWith(Parameterized::class)
+class CameraDisconnectTest(
+ private val implName: String,
+ private val cameraConfig: CameraXConfig
+) {
+
+ @get:Rule
+ val cameraRule = CameraUtil.grantCameraPermissionAndPreTest()
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data() = listOf(
+ arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
+ arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
+ )
+ }
+
+ @Suppress("DEPRECATION")
+ @get:Rule
+ val cameraXTestActivityRule = androidx.test.rule.ActivityTestRule(
+ CameraXTestActivity::class.java, true, false
+ )
+
+ @Suppress("DEPRECATION")
+ @get:Rule
+ val camera2ActivityRule = androidx.test.rule.ActivityTestRule(
+ Camera2TestActivity::class.java, true, false
+ )
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Before
+ fun setUp() {
+ IdlingPolicies.setIdlingResourceTimeout(10, TimeUnit.SECONDS)
+ CoreAppTestUtil.assumeCompatibleDevice()
+ CoreAppTestUtil.assumeCanTestCameraDisconnect()
+ runBlocking {
+ CameraX.initialize(context, cameraConfig).get(10, TimeUnit.SECONDS)
+ }
+
+ // Clear the device UI and check if there is no dialog or lock screen on the top of the
+ // window before start the test.
+ CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
+ }
+
+ @After
+ fun tearDown() {
+ if (cameraXTestActivityRule.activity != null) {
+ cameraXTestActivityRule.finishActivity()
+ }
+
+ if (camera2ActivityRule.activity != null) {
+ camera2ActivityRule.finishActivity()
+ }
+
+ runBlocking {
+ CameraX.shutdown().get(10, TimeUnit.SECONDS)
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M) // Known issue, checkout b/147393563.
+ fun testCameraDisconnect() {
+
+ // TODO(b/184603071): Migrate the ActivityTestRule to ActivityScenario
+ // Launch CameraX test activity
+ with(cameraXTestActivityRule.launchActivity(Intent())) {
+
+ // Wait for preview to become active
+ waitForCameraXPreview()
+
+ // Get id of camera opened by CameraX test activity
+ Truth.assertThat(cameraId).isNotNull()
+
+ // Launch Camera2 test activity. It should cause the camera to disconnect from CameraX.
+ val intent = Intent(
+ context,
+ Camera2TestActivity::class.java
+ ).apply {
+ putExtra(Camera2TestActivity.EXTRA_CAMERA_ID, cameraId)
+ }
+ camera2ActivityRule.launchActivity(intent)
+
+ // Wait for preview to become active
+ camera2ActivityRule.activity.waitForCamera2Preview()
+
+ // Close Camera2 test activity, and verify the CameraX Preview resumes successfully.
+ camera2ActivityRule.finishActivity()
+
+ // Verify the CameraX Preview can resume successfully.
+ waitForCameraXPreview()
+ }
+ }
+
+ private fun CameraXTestActivity.waitForCameraXPreview() {
+ waitFor(previewReady)
+ }
+
+ private fun Camera2TestActivity.waitForCamera2Preview() {
+ waitFor(mPreviewReady)
+ }
+
+ private fun waitFor(idlingResource: IdlingResource) {
+ IdlingRegistry.getInstance().register(idlingResource)
+ Espresso.onIdle()
+ IdlingRegistry.getInstance().unregister(idlingResource)
+ }
+}
\ No newline at end of file
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index ef336ff..0e876b3 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -59,6 +59,7 @@
androidTestImplementation(JUNIT)
androidTestImplementation(project(":compose:animation:animation"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:test-utils"))
lintPublish project(":compose:animation:animation-core-lint")
}
@@ -104,6 +105,7 @@
implementation(JUNIT)
implementation(project(":compose:animation:animation"))
implementation(project(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:test-utils"))
}
}
}
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index 0ab8607..b4b428d 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -54,6 +54,7 @@
androidTestImplementation(project(":compose:foundation:foundation"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(ANDROIDX_TEST_RULES)
androidTestImplementation(ANDROIDX_TEST_RUNNER)
androidTestImplementation(JUNIT)
@@ -104,6 +105,7 @@
implementation(JUNIT)
implementation(project(":compose:foundation:foundation"))
implementation(project(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:test-utils"))
}
}
}
diff --git a/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyBenchmark.kt b/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyBenchmark.kt
new file mode 100644
index 0000000..a3ffd44
--- /dev/null
+++ b/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyBenchmark.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.testutils.benchmark
+
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.ComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkDrawPerf
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkFirstDraw
+import androidx.compose.testutils.benchmark.benchmarkFirstLayout
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class EmptyBenchmark {
+
+ @get:Rule
+ val benchmarkRule = ComposeBenchmarkRule()
+
+ private val textCaseFactory = { EmptyTestCase() }
+
+ @Test
+ fun first_compose() {
+ benchmarkRule.benchmarkFirstCompose(textCaseFactory)
+ }
+
+ @Test
+ fun first_measure() {
+ benchmarkRule.benchmarkFirstMeasure(textCaseFactory)
+ }
+
+ @Test
+ fun first_layout() {
+ benchmarkRule.benchmarkFirstLayout(textCaseFactory)
+ }
+
+ @Test
+ fun first_draw() {
+ benchmarkRule.benchmarkFirstDraw(textCaseFactory)
+ }
+
+ @Test
+ fun draw() {
+ benchmarkRule.benchmarkDrawPerf(textCaseFactory)
+ }
+}
+
+class EmptyTestCase : ComposeTestCase {
+ @Composable
+ override fun Content() {
+ }
+}
\ No newline at end of file
diff --git a/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyFirstFastBenchmark.kt b/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyFirstFastBenchmark.kt
new file mode 100644
index 0000000..e5b3694
--- /dev/null
+++ b/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyFirstFastBenchmark.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.testutils.benchmark
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkFirstComposeFast
+import androidx.compose.testutils.benchmark.benchmarkFirstDrawFast
+import androidx.compose.testutils.benchmark.benchmarkFirstLayoutFast
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasureFast
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class EmptyFirstFastBenchmark {
+
+ @get:Rule
+ val benchmarkRule = ComposeBenchmarkRule()
+
+ private val testCaseFactory = { EmptyLayeredTestCase() }
+
+ @Test
+ fun first_compose() {
+ benchmarkRule.benchmarkFirstComposeFast(testCaseFactory)
+ }
+
+ @Test
+ fun first_measure() {
+ benchmarkRule.benchmarkFirstMeasureFast(testCaseFactory)
+ }
+
+ @Test
+ fun first_layout() {
+ benchmarkRule.benchmarkFirstLayoutFast(testCaseFactory)
+ }
+
+ @Test
+ fun first_draw() {
+ benchmarkRule.benchmarkFirstDrawFast(testCaseFactory)
+ }
+}
+
+class EmptyLayeredTestCase : LayeredComposeTestCase {
+ @Composable
+ override fun MeasuredContent() {}
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ Box {
+ content()
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarkFirstExtensions.kt b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarkFirstExtensions.kt
index 74f1e03..1765fa0 100644
--- a/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarkFirstExtensions.kt
+++ b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarkFirstExtensions.kt
@@ -24,7 +24,6 @@
import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.LayeredComposeTestCase
import androidx.compose.testutils.assertNoPendingChanges
-import androidx.compose.testutils.benchmark.android.AndroidTestCase
import androidx.compose.testutils.doFramesUntilNoChangesPending
import org.junit.Assert.assertTrue
@@ -32,7 +31,7 @@
* Measures the time of the first composition right after the given test case is added to an
* already existing hierarchy.
*/
-fun ComposeBenchmarkRule.benchmarkFirstCompose(caseFactory: () -> LayeredComposeTestCase) {
+fun ComposeBenchmarkRule.benchmarkFirstComposeFast(caseFactory: () -> LayeredComposeTestCase) {
runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) {
measureRepeated {
runWithTimingDisabled {
@@ -54,7 +53,7 @@
* Measures the time of the first measure right after the given test case is added to an already
* existing hierarchy.
*/
-fun ComposeBenchmarkRule.benchmarkFirstMeasure(caseFactory: () -> LayeredComposeTestCase) {
+fun ComposeBenchmarkRule.benchmarkFirstMeasureFast(caseFactory: () -> LayeredComposeTestCase) {
runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) {
measureRepeated {
runWithTimingDisabled {
@@ -79,7 +78,7 @@
* Measures the time of the first layout right after the given test case is added to an already
* existing hierarchy.
*/
-fun ComposeBenchmarkRule.benchmarkFirstLayout(caseFactory: () -> LayeredComposeTestCase) {
+fun ComposeBenchmarkRule.benchmarkFirstLayoutFast(caseFactory: () -> LayeredComposeTestCase) {
runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) {
measureRepeated {
runWithTimingDisabled {
@@ -105,7 +104,7 @@
* Measures the time of the first draw right after the given test case is added to an already
* existing hierarchy.
*/
-fun ComposeBenchmarkRule.benchmarkFirstDraw(caseFactory: () -> LayeredComposeTestCase) {
+fun ComposeBenchmarkRule.benchmarkFirstDrawFast(caseFactory: () -> LayeredComposeTestCase) {
runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) {
measureRepeated {
runWithTimingDisabled {
@@ -131,85 +130,6 @@
}
/**
- * Measures the time of the first set content of the given Android test case.
- */
-fun AndroidBenchmarkRule.benchmarkFirstSetContent(caseFactory: () -> AndroidTestCase) {
- runBenchmarkFor(caseFactory) {
- measureRepeated {
- setupContent()
- runWithTimingDisabled {
- disposeContent()
- }
- }
- }
-}
-
-/**
- * Measures the time of the first measure of the given test case.
- */
-fun AndroidBenchmarkRule.benchmarkFirstMeasure(caseFactory: () -> AndroidTestCase) {
- runBenchmarkFor(caseFactory) {
- measureRepeated {
- runWithTimingDisabled {
- setupContent()
- requestLayout()
- }
-
- measure()
-
- runWithTimingDisabled {
- disposeContent()
- }
- }
- }
-}
-
-/**
- * Measures the time of the first layout of the given test case.
- */
-fun AndroidBenchmarkRule.benchmarkFirstLayout(caseFactory: () -> AndroidTestCase) {
- runBenchmarkFor(caseFactory) {
- measureRepeated {
- runWithTimingDisabled {
- setupContent()
- requestLayout()
- measure()
- }
-
- layout()
-
- runWithTimingDisabled {
- disposeContent()
- }
- }
- }
-}
-
-/**
- * Measures the time of the first draw of the given test case.
- */
-fun AndroidBenchmarkRule.benchmarkFirstDraw(caseFactory: () -> AndroidTestCase) {
- runBenchmarkFor(caseFactory) {
- measureRepeated {
- runWithTimingDisabled {
- setupContent()
- requestLayout()
- measure()
- layout()
- drawPrepare()
- }
-
- draw()
-
- runWithTimingDisabled {
- drawFinish()
- disposeContent()
- }
- }
- }
-}
-
-/**
* Runs recompositions until there are no changes pending.
*
* @param maxAmountOfStep Max amount of recomposition to perform before giving up and throwing
diff --git a/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarksExtensions.kt b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarksExtensions.kt
index ea64883..c53a78e 100644
--- a/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarksExtensions.kt
+++ b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarksExtensions.kt
@@ -26,6 +26,7 @@
import androidx.compose.testutils.benchmark.android.AndroidTestCase
import androidx.compose.testutils.doFramesUntilNoChangesPending
import androidx.compose.testutils.recomposeAssertHadChanges
+import androidx.compose.testutils.setupContent
/**
* Measures measure and layout performance of the given test case by toggling measure constraints.
@@ -145,6 +146,180 @@
}
/**
+ * Measures the time of the first composition of the given compose test case.
+ *
+ * @param assertNoPendingRecompositions whether the benchmark will fail if there are pending
+ * recompositions after the first composition. By default this is true to enforce correctness in
+ * the benchmark, but for components that have an initial animation after being composed this can
+ * be turned off to benchmark just the first composition without any pending animations.
+ */
+fun ComposeBenchmarkRule.benchmarkFirstCompose(
+ caseFactory: () -> ComposeTestCase,
+ assertNoPendingRecompositions: Boolean = true
+) {
+ runBenchmarkFor(caseFactory) {
+ measureRepeated {
+ runWithTimingDisabled {
+ createTestCase()
+ }
+
+ emitContent()
+
+ runWithTimingDisabled {
+ if (assertNoPendingRecompositions) {
+ assertNoPendingChanges()
+ }
+ disposeContent()
+ }
+ }
+ }
+}
+
+/**
+ * Measures the time of the first set content of the given Android test case.
+ */
+fun AndroidBenchmarkRule.benchmarkFirstSetContent(caseFactory: () -> AndroidTestCase) {
+ runBenchmarkFor(caseFactory) {
+ measureRepeated {
+ setupContent()
+ runWithTimingDisabled {
+ disposeContent()
+ }
+ }
+ }
+}
+
+/**
+ * Measures the time of the first measure of the given test case.
+ */
+fun ComposeBenchmarkRule.benchmarkFirstMeasure(caseFactory: () -> ComposeTestCase) {
+ runBenchmarkFor(caseFactory) {
+ measureRepeated {
+ runWithTimingDisabled {
+ setupContent()
+ requestLayout()
+ }
+
+ measure()
+
+ runWithTimingDisabled {
+ disposeContent()
+ }
+ }
+ }
+}
+
+/**
+ * Measures the time of the first measure of the given test case.
+ */
+fun AndroidBenchmarkRule.benchmarkFirstMeasure(caseFactory: () -> AndroidTestCase) {
+ runBenchmarkFor(caseFactory) {
+ measureRepeated {
+ runWithTimingDisabled {
+ setupContent()
+ requestLayout()
+ }
+
+ measure()
+
+ runWithTimingDisabled {
+ disposeContent()
+ }
+ }
+ }
+}
+
+/**
+ * Measures the time of the first layout of the given test case.
+ */
+fun ComposeBenchmarkRule.benchmarkFirstLayout(caseFactory: () -> ComposeTestCase) {
+ runBenchmarkFor(caseFactory) {
+ measureRepeated {
+ runWithTimingDisabled {
+ setupContent()
+ requestLayout()
+ measure()
+ }
+
+ layout()
+
+ runWithTimingDisabled {
+ disposeContent()
+ }
+ }
+ }
+}
+
+/**
+ * Measures the time of the first layout of the given test case.
+ */
+fun AndroidBenchmarkRule.benchmarkFirstLayout(caseFactory: () -> AndroidTestCase) {
+ runBenchmarkFor(caseFactory) {
+ measureRepeated {
+ runWithTimingDisabled {
+ setupContent()
+ requestLayout()
+ measure()
+ }
+
+ layout()
+
+ runWithTimingDisabled {
+ disposeContent()
+ }
+ }
+ }
+}
+
+/**
+ * Measures the time of the first draw of the given test case.
+ */
+fun ComposeBenchmarkRule.benchmarkFirstDraw(caseFactory: () -> ComposeTestCase) {
+ runBenchmarkFor(caseFactory) {
+ measureRepeated {
+ runWithTimingDisabled {
+ setupContent()
+ requestLayout()
+ measure()
+ layout()
+ drawPrepare()
+ }
+
+ draw()
+
+ runWithTimingDisabled {
+ drawFinish()
+ disposeContent()
+ }
+ }
+ }
+}
+
+/**
+ * Measures the time of the first draw of the given test case.
+ */
+fun AndroidBenchmarkRule.benchmarkFirstDraw(caseFactory: () -> AndroidTestCase) {
+ runBenchmarkFor(caseFactory) {
+ measureRepeated {
+ runWithTimingDisabled {
+ setupContent()
+ requestLayout()
+ measure()
+ layout()
+ drawPrepare()
+ }
+
+ draw()
+
+ runWithTimingDisabled {
+ drawFinish()
+ disposeContent()
+ }
+ }
+ }
+}
+
+/**
* Measures recomposition time of the hierarchy after changing a state.
*
* @param assertOneRecomposition whether the benchmark will fail if there are pending
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
index ce00673..b64b7f3 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
@@ -1214,7 +1214,6 @@
}
protected fun dexSafeName(name: Name): Name {
- val unsafeSymbolsRegex = "[ <>]".toRegex()
return if (
name.isSpecial || name.asString().contains(unsafeSymbolsRegex)
) {
@@ -1226,6 +1225,8 @@
}
}
+private val unsafeSymbolsRegex = "[ <>]".toRegex()
+
fun IrFunction.composerParam(): IrValueParameter? {
for (param in valueParameters.asReversed()) {
if (param.isComposerParam()) return param
diff --git a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/NestedBoxesTestCase.kt b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/NestedBoxesTestCase.kt
index 237f9fb..4e5a26b 100644
--- a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/NestedBoxesTestCase.kt
+++ b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/NestedBoxesTestCase.kt
@@ -18,7 +18,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
/**
* Test case representing a layout hierarchy of nested boxes.
@@ -26,10 +26,10 @@
class NestedBoxesTestCase(
private val depth: Int,
private val children: Int
-) : LayeredComposeTestCase {
+) : ComposeTestCase {
@Composable
- override fun MeasuredContent() {
+ override fun Content() {
Box {
Boxes(depth - 1, children)
}
diff --git a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnSharedModelTestCase.kt b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnSharedModelTestCase.kt
index 48fb82f..34791dd 100644
--- a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnSharedModelTestCase.kt
+++ b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnSharedModelTestCase.kt
@@ -20,9 +20,10 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
+import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.ToggleableTestCase
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -37,18 +38,20 @@
*/
class RectsInColumnSharedModelTestCase(
private val amountOfRectangles: Int
-) : LayeredComposeTestCase, ToggleableTestCase {
+) : ComposeTestCase, ToggleableTestCase {
private val color = mutableStateOf(Color.Black)
@Composable
- override fun MeasuredContent() {
- Column {
- repeat(amountOfRectangles) { i ->
- if (i == 0) {
- Box(Modifier.size(100.dp, 50.dp).background(color = color.value))
- } else {
- Box(Modifier.size(100.dp, 50.dp).background(color = Color.Green))
+ override fun Content() {
+ MaterialTheme {
+ Column {
+ repeat(amountOfRectangles) { i ->
+ if (i == 0) {
+ Box(Modifier.size(100.dp, 50.dp).background(color = color.value))
+ } else {
+ Box(Modifier.size(100.dp, 50.dp).background(color = Color.Green))
+ }
}
}
}
diff --git a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnTestCase.kt b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnTestCase.kt
index 88548cc..fce4d49 100644
--- a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnTestCase.kt
+++ b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/RectsInColumnTestCase.kt
@@ -20,11 +20,13 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.ToggleableTestCase
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -38,15 +40,19 @@
*/
class RectsInColumnTestCase(
private val amountOfRectangles: Int
-) : LayeredComposeTestCase, ToggleableTestCase {
+) : ComposeTestCase, ToggleableTestCase {
private val states = mutableListOf<MutableState<Color>>()
@Composable
- override fun MeasuredContent() {
- Column {
- repeat(amountOfRectangles) {
- ColoredRectWithModel()
+ override fun Content() {
+ MaterialTheme {
+ Surface {
+ Column {
+ repeat(amountOfRectangles) {
+ ColoredRectWithModel()
+ }
+ }
}
}
}
diff --git a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/SpacingBenchmark.kt b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/SpacingBenchmark.kt
index 54c3b2e..ca00d7b 100644
--- a/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/SpacingBenchmark.kt
+++ b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/SpacingBenchmark.kt
@@ -24,7 +24,7 @@
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.ToggleableTestCase
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.benchmarkDrawPerf
@@ -158,7 +158,7 @@
}
}
-private sealed class PaddingTestCase : LayeredComposeTestCase, ToggleableTestCase {
+private sealed class PaddingTestCase : ComposeTestCase, ToggleableTestCase {
var paddingState: MutableState<Dp>? = null
@@ -169,7 +169,7 @@
}
@Composable
- override fun MeasuredContent() {
+ override fun Content() {
val padding = remember { mutableStateOf(5.dp) }
paddingState = padding
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index 30c0190a..d4582c7 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -50,6 +50,7 @@
androidTestImplementation(project(":compose:foundation:foundation"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(project(":activity:activity-compose"))
androidTestImplementation(ANDROIDX_TEST_RULES)
androidTestImplementation(ANDROIDX_TEST_RUNNER)
@@ -93,6 +94,7 @@
androidAndroidTest.dependencies {
implementation(project(":compose:foundation:foundation"))
implementation(project(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:test-utils"))
implementation(project(":activity:activity-compose"))
implementation(ANDROIDX_TEST_RULES)
diff --git a/compose/foundation/foundation/api/1.0.0-beta05.txt b/compose/foundation/foundation/api/1.0.0-beta05.txt
index f85c7ad..16ae359 100644
--- a/compose/foundation/foundation/api/1.0.0-beta05.txt
+++ b/compose/foundation/foundation/api/1.0.0-beta05.txt
@@ -149,7 +149,7 @@
method public suspend Object? drag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}
- public interface FlingBehavior {
+ @androidx.compose.runtime.Stable public interface FlingBehavior {
method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float> p);
}
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index f85c7ad..16ae359 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -149,7 +149,7 @@
method public suspend Object? drag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}
- public interface FlingBehavior {
+ @androidx.compose.runtime.Stable public interface FlingBehavior {
method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float> p);
}
diff --git a/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta05.txt b/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta05.txt
index 7453b13..ce801c7 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta05.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta05.txt
@@ -157,7 +157,7 @@
method public suspend Object? drag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}
- public interface FlingBehavior {
+ @androidx.compose.runtime.Stable public interface FlingBehavior {
method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float> p);
}
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 7453b13..ce801c7 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -157,7 +157,7 @@
method public suspend Object? drag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}
- public interface FlingBehavior {
+ @androidx.compose.runtime.Stable public interface FlingBehavior {
method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float> p);
}
diff --git a/compose/foundation/foundation/api/restricted_1.0.0-beta05.txt b/compose/foundation/foundation/api/restricted_1.0.0-beta05.txt
index f85c7ad..16ae359 100644
--- a/compose/foundation/foundation/api/restricted_1.0.0-beta05.txt
+++ b/compose/foundation/foundation/api/restricted_1.0.0-beta05.txt
@@ -149,7 +149,7 @@
method public suspend Object? drag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}
- public interface FlingBehavior {
+ @androidx.compose.runtime.Stable public interface FlingBehavior {
method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float> p);
}
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index f85c7ad..16ae359 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -149,7 +149,7 @@
method public suspend Object? drag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.DragScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}
- public interface FlingBehavior {
+ @androidx.compose.runtime.Stable public interface FlingBehavior {
method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float> p);
}
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/NestedScrollerTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/NestedScrollerTestCase.kt
index 2db5f25..2ad14ed 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/NestedScrollerTestCase.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/NestedScrollerTestCase.kt
@@ -28,10 +28,12 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.ToggleableTestCase
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -43,16 +45,20 @@
/**
* Test case that puts many horizontal scrollers in a vertical scroller
*/
-class NestedScrollerTestCase : LayeredComposeTestCase, ToggleableTestCase {
+class NestedScrollerTestCase : ComposeTestCase, ToggleableTestCase {
// ScrollerPosition must now be constructed during composition to obtain the Density
private lateinit var scrollState: ScrollState
@Composable
- override fun MeasuredContent() {
+ override fun Content() {
scrollState = rememberScrollState()
- LazyColumn {
- items(5) { index ->
- SquareRow(index == 0)
+ MaterialTheme {
+ Surface {
+ LazyColumn {
+ items(5) { index ->
+ SquareRow(index == 0)
+ }
+ }
}
}
}
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerTestCase.kt
index ec48333..1a2fcc1 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerTestCase.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/ScrollerTestCase.kt
@@ -24,7 +24,7 @@
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.ToggleableTestCase
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -34,11 +34,11 @@
/**
* Test case that puts a large number of boxes in a column in a vertical scroller to force scrolling.
*/
-class ScrollerTestCase : LayeredComposeTestCase, ToggleableTestCase {
+class ScrollerTestCase : ComposeTestCase, ToggleableTestCase {
private lateinit var scrollState: ScrollState
@Composable
- override fun MeasuredContent() {
+ override fun Content() {
scrollState = rememberScrollState()
Column(Modifier.verticalScroll(scrollState)) {
Column(Modifier.fillMaxHeight()) {
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/SimpleComponentImplementationBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/SimpleComponentImplementationBenchmark.kt
index a23c9cd..0e9330f 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/SimpleComponentImplementationBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/SimpleComponentImplementationBenchmark.kt
@@ -28,7 +28,7 @@
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.ToggleableTestCase
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.benchmarkDrawPerf
@@ -231,7 +231,7 @@
class ComponentWithModifiersTestCase : SimpleComponentImplementationTestCase() {
@Composable
- override fun MeasuredContent() {
+ override fun Content() {
val innerSize = getInnerSize()
Box(
Modifier.size(48.dp)
@@ -249,7 +249,7 @@
class ComponentWithRedrawTestCase : SimpleComponentImplementationTestCase() {
@Composable
- override fun MeasuredContent() {
+ override fun Content() {
val innerSize = getInnerSize()
val stroke = Stroke()
Canvas(Modifier.size(48.dp)) {
@@ -261,7 +261,7 @@
class ComponentWithTwoLayoutNodesTestCase : SimpleComponentImplementationTestCase() {
@Composable
- override fun MeasuredContent() {
+ override fun Content() {
Box(
modifier = Modifier
.size(48.dp)
@@ -280,7 +280,7 @@
}
}
-abstract class SimpleComponentImplementationTestCase : LayeredComposeTestCase, ToggleableTestCase {
+abstract class SimpleComponentImplementationTestCase : ComposeTestCase, ToggleableTestCase {
private var state: MutableState<Dp>? = null
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/TrailingLambdaBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/TrailingLambdaBenchmark.kt
index f823649..335d283 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/TrailingLambdaBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/TrailingLambdaBenchmark.kt
@@ -23,7 +23,7 @@
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.ToggleableTestCase
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.benchmarkFirstCompose
@@ -44,7 +44,7 @@
@Test
fun withTrailingLambdas_compose() {
- benchmarkRule.benchmarkFirstCompose { WithTrailingLambdas() }
+ benchmarkRule.benchmarkFirstCompose({ WithTrailingLambdas() })
}
@Test
@@ -54,7 +54,7 @@
@Test
fun withoutTrailingLambdas_compose() {
- benchmarkRule.benchmarkFirstCompose { WithoutTrailingLambdas() }
+ benchmarkRule.benchmarkFirstCompose({ WithoutTrailingLambdas() })
}
@Test
@@ -63,12 +63,12 @@
}
}
-private sealed class TrailingLambdaTestCase : LayeredComposeTestCase, ToggleableTestCase {
+private sealed class TrailingLambdaTestCase : ComposeTestCase, ToggleableTestCase {
var numberState: MutableState<Int>? = null
@Composable
- override fun MeasuredContent() {
+ override fun Content() {
val number = remember { mutableStateOf(5) }
numberState = number
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextBasicBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextBasicBenchmark.kt
index f7f3edd..6ddf510 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextBasicBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextBasicBenchmark.kt
@@ -18,10 +18,10 @@
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.benchmarkDrawPerf
-import androidx.compose.testutils.benchmark.benchmarkFirstCompose
-import androidx.compose.testutils.benchmark.benchmarkFirstDraw
-import androidx.compose.testutils.benchmark.benchmarkFirstLayout
-import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.compose.testutils.benchmark.benchmarkFirstComposeFast
+import androidx.compose.testutils.benchmark.benchmarkFirstDrawFast
+import androidx.compose.testutils.benchmark.benchmarkFirstLayoutFast
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasureFast
import androidx.compose.testutils.benchmark.benchmarkLayoutPerf
import androidx.compose.testutils.benchmark.toggleStateBenchmarkDraw
import androidx.compose.testutils.benchmark.toggleStateBenchmarkLayout
@@ -85,7 +85,7 @@
*/
@Test
fun first_compose() {
- benchmarkRule.benchmarkFirstCompose(caseFactory)
+ benchmarkRule.benchmarkFirstComposeFast(caseFactory)
}
/**
@@ -94,7 +94,7 @@
*/
@Test
fun first_measure() {
- benchmarkRule.benchmarkFirstMeasure(caseFactory)
+ benchmarkRule.benchmarkFirstMeasureFast(caseFactory)
}
/**
@@ -103,7 +103,7 @@
*/
@Test
fun first_layout() {
- benchmarkRule.benchmarkFirstLayout(caseFactory)
+ benchmarkRule.benchmarkFirstLayoutFast(caseFactory)
}
/**
@@ -111,7 +111,7 @@
*/
@Test
fun first_draw() {
- benchmarkRule.benchmarkFirstDraw(caseFactory)
+ benchmarkRule.benchmarkFirstDrawFast(caseFactory)
}
/**
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt
index e64f1de..14f1941 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt
@@ -18,10 +18,10 @@
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.benchmarkDrawPerf
-import androidx.compose.testutils.benchmark.benchmarkFirstCompose
-import androidx.compose.testutils.benchmark.benchmarkFirstDraw
-import androidx.compose.testutils.benchmark.benchmarkFirstLayout
-import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.compose.testutils.benchmark.benchmarkFirstComposeFast
+import androidx.compose.testutils.benchmark.benchmarkFirstDrawFast
+import androidx.compose.testutils.benchmark.benchmarkFirstLayoutFast
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasureFast
import androidx.compose.testutils.benchmark.benchmarkLayoutPerf
import androidx.compose.testutils.benchmark.toggleStateBenchmarkDraw
import androidx.compose.testutils.benchmark.toggleStateBenchmarkLayout
@@ -77,7 +77,7 @@
*/
@Test
fun first_compose() {
- benchmarkRule.benchmarkFirstCompose(caseFactory)
+ benchmarkRule.benchmarkFirstComposeFast(caseFactory)
}
/**
@@ -87,7 +87,7 @@
*/
@Test
fun first_measure() {
- benchmarkRule.benchmarkFirstMeasure(caseFactory)
+ benchmarkRule.benchmarkFirstMeasureFast(caseFactory)
}
/**
@@ -96,7 +96,7 @@
*/
@Test
fun first_layout() {
- benchmarkRule.benchmarkFirstLayout(caseFactory)
+ benchmarkRule.benchmarkFirstLayoutFast(caseFactory)
}
/**
@@ -105,7 +105,7 @@
*/
@Test
fun first_draw() {
- benchmarkRule.benchmarkFirstDraw(caseFactory)
+ benchmarkRule.benchmarkFirstDrawFast(caseFactory)
}
/**
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerTestCase.kt
index 93caa92..e94b226 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerTestCase.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerTestCase.kt
@@ -20,13 +20,13 @@
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.sp
-class SelectionContainerTestCase(private val childrenCount: Int) : LayeredComposeTestCase {
+class SelectionContainerTestCase(private val childrenCount: Int) : ComposeTestCase {
@Composable
- override fun MeasuredContent() {
+ override fun Content() {
SelectionContainer {
Column {
repeat(childrenCount) {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListHeadersTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListHeadersTest.kt
index 7a2051f..276a499 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListHeadersTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListHeadersTest.kt
@@ -20,8 +20,8 @@
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.width
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
@@ -159,7 +159,7 @@
}
rule.onNodeWithTag(LazyListTag)
- .scrollBy(y = 102.dp, density = rule.density)
+ .scrollBy(y = 105.dp, density = rule.density)
rule.onNodeWithTag(firstHeaderTag)
.assertDoesNotExist()
@@ -286,7 +286,7 @@
}
rule.onNodeWithTag(LazyListTag)
- .scrollBy(x = 102.dp, density = rule.density)
+ .scrollBy(x = 105.dp, density = rule.density)
rule.onNodeWithTag(firstHeaderTag)
.assertDoesNotExist()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
index f319f730..41f9358 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
@@ -56,11 +56,13 @@
val rule = createComposeRule()
private var itemSize: Dp = Dp.Infinity
+ private var smallPaddingSize: Dp = Dp.Infinity
@Before
fun before() {
with(rule.density) {
itemSize = 50.toDp()
+ smallPaddingSize = 12.toDp()
}
}
@@ -68,7 +70,6 @@
fun column_contentPaddingIsApplied() {
lateinit var state: LazyListState
val containerSize = itemSize * 2
- val smallPaddingSize = itemSize / 4
val largePaddingSize = itemSize
rule.setContent {
LazyColumn(
@@ -341,7 +342,6 @@
fun row_contentPaddingIsApplied() {
lateinit var state: LazyListState
val containerSize = itemSize * 2
- val smallPaddingSize = itemSize / 4
val largePaddingSize = itemSize
rule.setContent {
LazyRow(
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt
index c7b6335..bd6fae3 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt
@@ -233,7 +233,7 @@
}
rule.onNodeWithTag(LazyListTag)
- .scrollBy(x = 102.dp, density = rule.density)
+ .scrollBy(x = 105.dp, density = rule.density)
rule.onNodeWithTag("1")
.assertDoesNotExist()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
index 676634a..d01ccfa 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldTest.kt
@@ -801,6 +801,7 @@
// reset flag
onValueChangeCalled = false
}
+ rule.waitUntil { onValueChangeCalled == false }
// set selection to same value, no change should occur
@OptIn(ExperimentalTestApi::class)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/FlingBehavior.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/FlingBehavior.kt
index fce4545..733b4f4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/FlingBehavior.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/FlingBehavior.kt
@@ -16,12 +16,15 @@
package androidx.compose.foundation.gestures
+import androidx.compose.runtime.Stable
+
/**
* Interface to specify fling behavior.
*
* When drag has ended with velocity in [scrollable], [performFling] is invoked to perform fling
* animation and update state via [ScrollScope.scrollBy]
*/
+@Stable
interface FlingBehavior {
/**
* Perform settling via fling animation with given velocity and suspend until fling has
diff --git a/compose/integration-tests/docs-snippets/src/main/AndroidManifest.xml b/compose/integration-tests/docs-snippets/src/main/AndroidManifest.xml
index 46be6d8..db5bed8 100644
--- a/compose/integration-tests/docs-snippets/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/docs-snippets/src/main/AndroidManifest.xml
@@ -14,12 +14,4 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.compose.integration.docs">
-
- <application>
- <activity
- android:name="androidx.activity.ComponentActivity"
- android:theme="@style/TestTheme" />
- </application>
-</manifest>
+<manifest package="androidx.compose.integration.docs" />
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt
index e6186af..ab2fb87 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt
@@ -274,7 +274,7 @@
private object LayoutSnippet12 {
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
- ) = Modifier.layout { measurable, constraints ->
+ ) = layout { measurable, constraints ->
// Measure the composable
val placeable = measurable.measure(constraints)
diff --git a/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
index 347b842..8b20f3e 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
@@ -116,7 +116,8 @@
* signature.
*/
private fun KmDeclarationContainer.findKmFunctionForPsiMethod(method: PsiMethod): KmFunction? {
- val expectedName = method.name
+ // Strip any mangled part of the name in case of inline classes
+ val expectedName = method.name.substringBefore("-")
val expectedSignature = ClassUtil.getAsmMethodSignature(method)
return functions.find {
diff --git a/compose/lint/common/src/main/java/androidx/compose/lint/KotlinUtils.kt b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinUtils.kt
index e2dc2d6..9ec00a4 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/KotlinUtils.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/KotlinUtils.kt
@@ -16,42 +16,15 @@
package androidx.compose.lint
-import com.intellij.psi.impl.compiled.ClsMethodImpl
-import kotlinx.metadata.Flag
-import org.jetbrains.kotlin.lexer.KtTokens.INLINE_KEYWORD
-import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtLambdaExpression
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
-import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.ULambdaExpression
-import org.jetbrains.uast.resolveToUElement
import org.jetbrains.uast.toUElement
/**
- * @return whether the resolved declaration for this call expression is an inline function
- */
-val UCallExpression.isDeclarationInline: Boolean
- get() {
- return when (val source = resolveToUElement()?.sourcePsi) {
- // Parsing a method defined in a class file
- is ClsMethodImpl -> {
- val flags = source.toKmFunction()?.flags ?: return false
- return Flag.Function.IS_INLINE(flags)
- }
- // Parsing a method defined in Kotlin source
- is KtFunction -> {
- source.hasModifier(INLINE_KEYWORD)
- }
- // Parsing another declaration (such as a property) which cannot be inline, or
- // a non-Kotlin declaration
- else -> false
- }
- }
-
-/**
* Returns a list of unreferenced parameters in [this]. If no parameters have been specified, but
* there is an implicit `it` parameter, this will return a list containing an
* [UnreferencedParameter] with `it` as the name.
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsBenchmark.kt b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsBenchmark.kt
index dec4d18..f610771 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsBenchmark.kt
+++ b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsBenchmark.kt
@@ -53,7 +53,10 @@
@Test
fun first_compose() {
- benchmarkRule.benchmarkFirstCompose(checkboxCaseFactory)
+ benchmarkRule.benchmarkFirstCompose(
+ checkboxCaseFactory,
+ assertNoPendingRecompositions = false
+ )
}
@Test
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsTestCase.kt b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsTestCase.kt
index a9526be..979074bc6 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsTestCase.kt
+++ b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/CheckboxesInRowsTestCase.kt
@@ -27,7 +27,7 @@
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.ToggleableTestCase
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -38,29 +38,24 @@
*/
class CheckboxesInRowsTestCase(
private val amountOfCheckboxes: Int
-) : LayeredComposeTestCase, ToggleableTestCase {
+) : ComposeTestCase, ToggleableTestCase {
private val states = mutableListOf<MutableState<Boolean>>()
@Composable
- override fun MeasuredContent() {
- Column {
- repeat(amountOfCheckboxes) {
- Row {
- Text(text = "Check Me!")
- CheckboxWithState(
- Modifier.weight(1f).wrapContentSize(Alignment.CenterEnd)
- )
- }
- }
- }
- }
-
- @Composable
- override fun ContentWrappers(content: () -> Unit) {
+ override fun Content() {
MaterialTheme {
Surface {
- content()
+ Column {
+ repeat(amountOfCheckboxes) {
+ Row {
+ Text(text = "Check Me!")
+ CheckboxWithState(
+ Modifier.weight(1f).wrapContentSize(Alignment.CenterEnd)
+ )
+ }
+ }
+ }
}
}
}
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/RadioGroupBenchmark.kt b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/RadioGroupBenchmark.kt
index e6fed73..4a33072 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/RadioGroupBenchmark.kt
+++ b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/RadioGroupBenchmark.kt
@@ -24,7 +24,7 @@
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.ToggleableTestCase
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.benchmarkFirstCompose
@@ -57,7 +57,7 @@
@Test
fun first_compose() {
- benchmarkRule.benchmarkFirstCompose(radioCaseFactory)
+ benchmarkRule.benchmarkFirstCompose(radioCaseFactory, assertNoPendingRecompositions = false)
}
@Test
@@ -99,7 +99,7 @@
}
}
-internal class RadioGroupTestCase : LayeredComposeTestCase, ToggleableTestCase {
+internal class RadioGroupTestCase : ComposeTestCase, ToggleableTestCase {
private val radiosCount = 10
private val options = (0 until radiosCount).toList()
@@ -110,30 +110,25 @@
}
@Composable
- override fun MeasuredContent() {
- Column {
- options.forEach { item ->
- Row(
- modifier = Modifier.selectable(
- selected = (select.value == item),
- onClick = { select.value = item }
- ),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(item.toString())
- RadioButton(
- selected = (select.value == item),
- onClick = { select.value = item }
- )
+ override fun Content() {
+ MaterialTheme {
+ Column {
+ options.forEach { item ->
+ Row(
+ modifier = Modifier.selectable(
+ selected = (select.value == item),
+ onClick = { select.value = item }
+ ),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(item.toString())
+ RadioButton(
+ selected = (select.value == item),
+ onClick = { select.value = item }
+ )
+ }
}
}
}
}
-
- @Composable
- override fun ContentWrappers(content: @Composable () -> Unit) {
- MaterialTheme {
- content()
- }
- }
}
\ No newline at end of file
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextInColumnSizeToggleTestCase.kt b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextInColumnSizeToggleTestCase.kt
index 270df6f..339de17 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextInColumnSizeToggleTestCase.kt
+++ b/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextInColumnSizeToggleTestCase.kt
@@ -22,7 +22,7 @@
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.ToggleableTestCase
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.sp
@@ -35,28 +35,23 @@
*/
class TextInColumnSizeToggleTestCase(
private val numberOfTexts: Int
-) : LayeredComposeTestCase, ToggleableTestCase {
+) : ComposeTestCase, ToggleableTestCase {
private val fontSize = mutableStateOf(20.sp)
@Composable
- override fun MeasuredContent() {
- Column {
- repeat(numberOfTexts) {
- // 32-character text to match dashboards
- Text(
- "Hello World Hello World Hello W",
- style = TextStyle(fontSize = fontSize.value)
- )
- }
- }
- }
-
- @Composable
- override fun ContentWrappers(content: @Composable () -> Unit) {
+ override fun Content() {
MaterialTheme {
Surface {
- content()
+ Column {
+ repeat(numberOfTexts) {
+ // 32-character text to match dashboards
+ Text(
+ "Hello World Hello World Hello W",
+ style = TextStyle(fontSize = fontSize.value)
+ )
+ }
+ }
}
}
}
diff --git a/compose/runtime/runtime-rxjava2/build.gradle b/compose/runtime/runtime-rxjava2/build.gradle
index 3ec8592..fedae8c 100644
--- a/compose/runtime/runtime-rxjava2/build.gradle
+++ b/compose/runtime/runtime-rxjava2/build.gradle
@@ -35,6 +35,7 @@
api(RX_JAVA)
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(ANDROIDX_TEST_RUNNER)
androidTestImplementation(JUNIT)
androidTestImplementation(TRUTH)
diff --git a/compose/runtime/runtime-rxjava3/build.gradle b/compose/runtime/runtime-rxjava3/build.gradle
index 949a855..c56f6dc 100644
--- a/compose/runtime/runtime-rxjava3/build.gradle
+++ b/compose/runtime/runtime-rxjava3/build.gradle
@@ -35,6 +35,7 @@
api(RX_JAVA3)
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(ANDROIDX_TEST_RUNNER)
androidTestImplementation(JUNIT)
androidTestImplementation(TRUTH)
diff --git a/compose/runtime/runtime-saveable/build.gradle b/compose/runtime/runtime-saveable/build.gradle
index 2f53274..eaf9e66 100644
--- a/compose/runtime/runtime-saveable/build.gradle
+++ b/compose/runtime/runtime-saveable/build.gradle
@@ -47,6 +47,7 @@
androidTestImplementation project(':compose:ui:ui')
androidTestImplementation project(":compose:ui:ui-test-junit4")
+ androidTestImplementation project(":compose:test-utils")
androidTestImplementation "androidx.fragment:fragment:1.3.0"
androidTestImplementation project(":activity:activity-compose")
androidTestImplementation(ANDROIDX_TEST_UIAUTOMATOR)
@@ -94,6 +95,7 @@
androidAndroidTest.dependencies {
implementation project(':compose:ui:ui')
implementation project(":compose:ui:ui-test-junit4")
+ implementation project(":compose:test-utils")
implementation "androidx.fragment:fragment:1.3.0"
implementation project(":activity:activity-compose")
implementation(ANDROIDX_TEST_UIAUTOMATOR)
diff --git a/compose/runtime/runtime/integration-tests/build.gradle b/compose/runtime/runtime/integration-tests/build.gradle
index 78faeb9..7d12b90 100644
--- a/compose/runtime/runtime/integration-tests/build.gradle
+++ b/compose/runtime/runtime/integration-tests/build.gradle
@@ -35,6 +35,7 @@
androidTestImplementation(project(":compose:ui:ui"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(project(":activity:activity-compose"))
androidTestImplementation(JUNIT)
@@ -90,6 +91,7 @@
androidAndroidTest.dependencies {
implementation(project(":compose:ui:ui"))
implementation(project(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:test-utils"))
implementation(project(":activity:activity-compose"))
implementation(ANDROIDX_TEST_EXT_JUNIT)
implementation(ANDROIDX_TEST_RULES)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index d4ba000..2570c64 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -22,6 +22,7 @@
import androidx.compose.runtime.collection.IdentityArraySet
import androidx.compose.runtime.collection.IdentityScopeMap
+import androidx.compose.runtime.snapshots.currentSnapshot
import androidx.compose.runtime.snapshots.fastForEach
import androidx.compose.runtime.snapshots.fastMap
import androidx.compose.runtime.snapshots.fastToSet
@@ -954,6 +955,7 @@
private var providersInvalid = false
private val providersInvalidStack = IntStack()
private var childrenComposing: Int = 0
+ private var snapshot = currentSnapshot()
private val invalidateStack = Stack<RecomposeScopeImpl>()
@@ -2477,11 +2479,13 @@
val scope = RecomposeScopeImpl(composition as CompositionImpl)
invalidateStack.push(scope)
updateValue(scope)
+ scope.start(snapshot.id)
} else {
val invalidation = invalidations.removeLocation(reader.parent)
val scope = reader.next() as RecomposeScopeImpl
scope.requiresRecompose = invalidation != null
invalidateStack.push(scope)
+ scope.start(snapshot.id)
}
}
@@ -2498,6 +2502,9 @@
val scope = if (invalidateStack.isNotEmpty()) invalidateStack.pop()
else null
scope?.requiresRecompose = false
+ scope?.end(snapshot.id)?.let {
+ record { _, _, _ -> it(composition) }
+ }
val result = if (scope != null && (scope.used || collectParameterInformation)) {
if (scope.anchor == null) {
scope.anchor = if (inserting) {
@@ -2547,6 +2554,7 @@
check(!isComposing) { "Reentrant composition is not supported" }
trace("Compose:recompose") {
invalidations.clear()
+ snapshot = currentSnapshot()
invalidationsRequested.forEach { scope ->
val location = scope.anchor?.location ?: return
invalidations.add(Invalidation(scope, location))
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index d6672f2..153ba94 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -527,6 +527,7 @@
composer.currentRecomposeScope?.let {
it.used = true
observations.add(value, it)
+ it.recordRead(value)
}
}
}
@@ -617,6 +618,10 @@
return if (isComposing) InvalidationResult.DEFERRED else InvalidationResult.SCHEDULED
}
+ internal fun removeObservation(instance: Any, scope: RecomposeScopeImpl) {
+ observations.remove(instance, scope)
+ }
+
/**
* This takes ownership of the invalidations. Invalidations
*/
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
index 6b14571..031000f 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
@@ -16,6 +16,8 @@
package androidx.compose.runtime
+import androidx.compose.runtime.collection.IdentityArrayIntMap
+
/**
* Represents a recomposable scope or section of the composition hierarchy. Can be used to
* manually invalidate the scope to schedule it for recomposition.
@@ -117,4 +119,53 @@
* and implements [ScopeUpdateScope].
*/
override fun updateScope(block: (Composer, Int) -> Unit) { this.block = block }
-}
\ No newline at end of file
+
+ private var currentToken = 0
+ private var trackedInstances: IdentityArrayIntMap? = null
+
+ /**
+ * Called when composition start composing into this scope. The [token] is a value that is
+ * unique everytime this is called. This is currently the snapshot id but that shouldn't be
+ * relied on.
+ */
+ fun start(token: Int) { currentToken = token }
+
+ /**
+ * Track instances that were read in scope.
+ */
+ fun recordRead(instance: Any) {
+ (trackedInstances ?: IdentityArrayIntMap().also { trackedInstances = it })
+ .add(instance, currentToken)
+ }
+
+ /**
+ * Called when composition is completed for this scope. The [token] is the same token passed
+ * in the previous call to [start]. If [end] returns a non-null value the lambda returned
+ * will be called during [ControlledComposition.applyChanges].
+ */
+ fun end(token: Int): ((Composition) -> Unit)? {
+ return trackedInstances?.let { instances ->
+ // If any value previous observed was not read in this current composition
+ // schedule the value to be removed from the observe scope and removed from the
+ // observations tracked by the composition.
+ // [used] is false if the scope was skipped. If the scope was skipped we should
+ // leave the observations unmodified.
+ if (
+ used && instances.any { _, instanceToken -> instanceToken != token }
+ ) { composition ->
+ if (
+ currentToken == token && instances == trackedInstances &&
+ composition is CompositionImpl
+ ) {
+ instances.removeValueIf { instance, instanceToken ->
+ (instanceToken != token).also { remove ->
+ if (remove)
+ composition.removeObservation(instance, this)
+ }
+ }
+ if (instances.size == 0) trackedInstances = null
+ }
+ } else null
+ }
+ }
+}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IdentityArrayIntMap.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IdentityArrayIntMap.kt
new file mode 100644
index 0000000..420e107
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/collection/IdentityArrayIntMap.kt
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.collection
+
+import androidx.compose.runtime.identityHashCode
+import kotlin.contracts.ExperimentalContracts
+
+@OptIn(ExperimentalContracts::class)
+internal class IdentityArrayIntMap {
+ @PublishedApi
+ internal var size = 0
+
+ @PublishedApi
+ internal var keys: Array<Any?> = arrayOfNulls(4)
+
+ @PublishedApi
+ internal var values: IntArray = IntArray(4)
+
+ operator fun get(key: Any): Int {
+ val index = find(key)
+ return if (index >= 0) values[index] else error("Key not found")
+ }
+ /**
+ * Add [value] to the set and return `true` if it was added or `false` if it already existed.
+ */
+ fun add(key: Any, value: Int) {
+ val index: Int
+ if (size > 0) {
+ index = find(key)
+ if (index >= 0) {
+ values[index] = value
+ return
+ }
+ } else {
+ index = -1
+ }
+
+ val insertIndex = -(index + 1)
+
+ if (size == keys.size) {
+ val newKeys = arrayOfNulls<Any>(keys.size * 2)
+ val newValues = IntArray(keys.size * 2)
+ keys.copyInto(
+ destination = newKeys,
+ destinationOffset = insertIndex + 1,
+ startIndex = insertIndex,
+ endIndex = size
+ )
+ values.copyInto(
+ destination = newValues,
+ destinationOffset = insertIndex + 1,
+ startIndex = insertIndex,
+ endIndex = size
+ )
+ keys.copyInto(
+ destination = newKeys,
+ endIndex = insertIndex
+ )
+ values.copyInto(
+ destination = newValues,
+ endIndex = insertIndex
+ )
+ keys = newKeys
+ values = newValues
+ } else {
+ keys.copyInto(
+ destination = keys,
+ destinationOffset = insertIndex + 1,
+ startIndex = insertIndex,
+ endIndex = size
+ )
+ values.copyInto(
+ destination = values,
+ destinationOffset = insertIndex + 1,
+ startIndex = insertIndex,
+ endIndex = size
+ )
+ }
+ keys[insertIndex] = key
+ values[insertIndex] = value
+ size++
+ }
+
+ /**
+ * Remove [key] from the map.
+ */
+ fun remove(key: Any): Boolean {
+ val index = find(key)
+ if (index >= 0) {
+ if (index < size - 1) {
+ keys.copyInto(
+ destination = keys,
+ destinationOffset = index,
+ startIndex = index + 1,
+ endIndex = size
+ )
+ values.copyInto(
+ destination = values,
+ destinationOffset = index,
+ startIndex = index + 1,
+ endIndex = size
+ )
+ }
+ size--
+ keys[size] = null
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Removes all values that match [predicate].
+ */
+ inline fun removeValueIf(predicate: (Any, Int) -> Boolean) {
+ var destinationIndex = 0
+ for (i in 0 until size) {
+ @Suppress("UNCHECKED_CAST")
+ val key = keys[i] as Any
+ val value = values[i]
+ if (!predicate(key, value)) {
+ if (destinationIndex != i) {
+ keys[destinationIndex] = key
+ values[destinationIndex] = value
+ }
+ destinationIndex++
+ }
+ }
+ for (i in destinationIndex until size) {
+ keys[i] = null
+ }
+ size = destinationIndex
+ }
+
+ inline fun any(predicate: (Any, Int) -> Boolean): Boolean {
+ for (i in 0 until size) {
+ if (predicate(keys[i] as Any, values[i])) return true
+ }
+ return false
+ }
+
+ /**
+ * Returns the index of [key] in the set or the negative index - 1 of the location where
+ * it would have been if it had been in the set.
+ */
+ private fun find(key: Any?): Int {
+ var low = 0
+ var high = size - 1
+ val valueIdentity = identityHashCode(key)
+
+ while (low <= high) {
+ val mid = (low + high).ushr(1)
+ val midVal = keys[mid]
+ val comparison = identityHashCode(midVal) - valueIdentity
+ when {
+ comparison < 0 -> low = mid + 1
+ comparison > 0 -> high = mid - 1
+ midVal === key -> return mid
+ else -> return findExactIndex(mid, key, valueIdentity)
+ }
+ }
+ return -(low + 1)
+ }
+
+ /**
+ * When multiple items share the same [identityHashCode], then we must find the specific
+ * index of the target item. This method assumes that [midIndex] has already been checked
+ * for an exact match for [value], but will look at nearby values to find the exact item index.
+ * If no match is found, the negative index - 1 of the position in which it would be will
+ * be returned, which is always after the last item with the same [identityHashCode].
+ */
+ private fun findExactIndex(midIndex: Int, value: Any?, valueHash: Int): Int {
+ // hunt down first
+ for (i in midIndex - 1 downTo 0) {
+ val v = keys[i]
+ if (v === value) {
+ return i
+ }
+ if (identityHashCode(v) != valueHash) {
+ break // we've gone too far
+ }
+ }
+
+ for (i in midIndex + 1 until size) {
+ val v = keys[i]
+ if (v === value) {
+ return i
+ }
+ if (identityHashCode(v) != valueHash) {
+ // We've gone too far. We should insert here.
+ return -(i + 1)
+ }
+ }
+
+ // We should insert at the end
+ return -(size + 1)
+ }
+}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt
index 0416373..8c5502f 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSet.kt
@@ -310,15 +310,15 @@
base += 32
b = b shr 32
}
- if (bits and 0xFFFF == 0L) {
+ if (b and 0xFFFF == 0L) {
base += 16
b = b shr 16
}
- if (bits and 0xFF == 0L) {
+ if (b and 0xFF == 0L) {
base += 8
b = b shr 8
}
- if (bits and 0xF == 0L) {
+ if (b and 0xF == 0L) {
base += 4
b = b shr 4
}
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
index 9ca8a64..8f86427 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -45,6 +45,7 @@
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runBlockingTest
+import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@@ -2479,6 +2480,42 @@
expectNoChanges()
}
+ @Suppress("UnrememberedMutableState")
+ @Composable
+ fun Indirect(iteration: Int, states: MutableList<MutableState<Int>>) {
+ val state = mutableStateOf(Random.nextInt())
+ states.add(state)
+ Text("$iteration state = ${state.value}")
+ }
+
+ @Composable
+ fun ComposeIndirect(iteration: State<Int>, states: MutableList<MutableState<Int>>) {
+ Text("Iteration ${iteration.value}")
+ Indirect(iteration.value, states)
+ }
+
+ @Test // Regression b/182822837
+ fun testObservationScopes_IndirectInvalidate() = compositionTest {
+ val states = mutableListOf<MutableState<Int>>()
+ val iteration = mutableStateOf(0)
+
+ compose {
+ ComposeIndirect(iteration, states)
+ }
+
+ fun nextIteration() = iteration.value++
+ fun invalidateLast() = states.last().value++
+ fun invalidateFirst() = states.first().value++
+
+ repeat(10) {
+ nextIteration()
+ expectChanges()
+ }
+
+ invalidateFirst()
+ expectNoChanges()
+ }
+
@OptIn(ComposeCompilerApi::class)
@Test
fun testApplierBeginEndCallbacks() = compositionTest {
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/collection/IdentityArrayIntMapTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/collection/IdentityArrayIntMapTests.kt
new file mode 100644
index 0000000..03a3e88
--- /dev/null
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/collection/IdentityArrayIntMapTests.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class IdentityArrayIntMapTests {
+
+ @Test
+ fun emptyConstruction() {
+ val m = IdentityArrayIntMap()
+ assertEquals(0, m.size)
+ }
+
+ @Test
+ fun canAddValues() {
+ val map = IdentityArrayIntMap()
+ val keys = Array<Any>(100) { Any() }
+ for (i in keys.indices) {
+ map.add(keys[i], i)
+ }
+ for (i in keys.indices) {
+ assertEquals(i, map[keys[i]])
+ }
+ map.removeValueIf { key, value ->
+ assertEquals(keys[value], key)
+ false
+ }
+ }
+
+ @Test
+ fun canRemoveValues() {
+ val map = IdentityArrayIntMap()
+ val keys = Array<Any>(100) { Any() }
+ for (i in keys.indices) {
+ map.add(keys[i], i)
+ }
+ for (i in keys.indices step 2) {
+ map.remove(keys[i])
+ }
+ assertEquals(50, map.size)
+ map.removeValueIf { key, value ->
+ assertEquals(keys[value], key)
+ assertTrue(value % 2 == 1)
+ false
+ }
+ }
+
+ @Test
+ fun canRemoveIfValues() {
+ val map = IdentityArrayIntMap()
+ val keys = Array<Any>(100) { Any() }
+ for (i in keys.indices) {
+ map.add(keys[i], i)
+ }
+ map.removeValueIf { _, value -> value % 2 == 0 }
+ assertEquals(50, map.size)
+ }
+
+ @Test
+ fun canReplaceValues() {
+ val map = IdentityArrayIntMap()
+ val keys = Array<Any>(100) { Any() }
+ for (i in keys.indices) {
+ map.add(keys[i], i)
+ }
+
+ for (i in keys.indices) {
+ map.add(keys[i], i + 100)
+ }
+
+ assertEquals(100, map.size)
+ for (i in keys.indices) {
+ assertEquals(i + 100, map[keys[i]])
+ }
+ }
+
+ @Test
+ fun anyFindsCorrectValue() {
+ val map = IdentityArrayIntMap()
+ val keys = Array<Any>(100) { Any() }
+ for (i in keys.indices) {
+ map.add(keys[i], i)
+ }
+ assertTrue(map.any { _, value -> value == 20 })
+ assertFalse(map.any { _, value -> value > 100 })
+ }
+}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSetTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSetTests.kt
index 3b8999a..848bcb2 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSetTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotIdSetTests.kt
@@ -269,6 +269,18 @@
test(1024)
}
+ @Test // Regression b/182822837
+ fun shouldReportTheCorrectLowest() {
+ fun test(number: Int) {
+ val set = SnapshotIdSet.EMPTY.set(number)
+ assertEquals(number, set.lowest(-1))
+ }
+
+ repeat(64) {
+ test(it)
+ }
+ }
+
@Test // Regression: b/147836978
fun shouldValidWhenSetIsLarge() {
val data = """
diff --git a/compose/test-utils/src/androidMain/AndroidManifest.xml b/compose/test-utils/src/androidMain/AndroidManifest.xml
index 5e81de1..33068f3 100644
--- a/compose/test-utils/src/androidMain/AndroidManifest.xml
+++ b/compose/test-utils/src/androidMain/AndroidManifest.xml
@@ -14,4 +14,11 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.compose.testutils"/>
+ package="androidx.compose.testutils">
+ <application>
+ <activity
+ android:name="androidx.activity.ComponentActivity"
+ android:theme="@style/TestTheme"
+ android:exported="true" />
+ </application>
+</manifest>
diff --git a/compose/ui/ui-test/src/androidMain/res/values/styles.xml b/compose/test-utils/src/androidMain/res/values/styles.xml
similarity index 100%
rename from compose/ui/ui-test/src/androidMain/res/values/styles.xml
rename to compose/test-utils/src/androidMain/res/values/styles.xml
diff --git a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeTestCase.kt b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeTestCase.kt
index f45d284..ed01647 100644
--- a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeTestCase.kt
+++ b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/ComposeTestCase.kt
@@ -71,7 +71,5 @@
* The lifecycle rules for this method are same as for [Content]
*/
@Composable
- fun ContentWrappers(content: @Composable () -> Unit) {
- content()
- }
+ fun ContentWrappers(content: @Composable () -> Unit)
}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/benchmark/src/main/java/androidx/compose/ui/graphics/benchmark/ImageVectorTestCase.kt b/compose/ui/ui-graphics/benchmark/src/main/java/androidx/compose/ui/graphics/benchmark/ImageVectorTestCase.kt
index 5b89155..e7e5175 100644
--- a/compose/ui/ui-graphics/benchmark/src/main/java/androidx/compose/ui/graphics/benchmark/ImageVectorTestCase.kt
+++ b/compose/ui/ui-graphics/benchmark/src/main/java/androidx/compose/ui/graphics/benchmark/ImageVectorTestCase.kt
@@ -29,7 +29,7 @@
import androidx.compose.ui.graphics.vector.PathData
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.foundation.layout.size
-import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ComposeTestCase
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
@@ -40,21 +40,16 @@
* Subclasses are responsible for providing the vector asset, so we can test and benchmark different
* methods of loading / creating this asset.
*/
-sealed class ImageVectorTestCase : LayeredComposeTestCase {
+sealed class ImageVectorTestCase : ComposeTestCase {
@Composable
- override fun MeasuredContent() {
- Box(
- Modifier.testTag(testTag)
- .size(24.dp)
- .paint(getPainter())
- )
- }
-
- @Composable
- override fun ContentWrappers(content: @Composable () -> Unit) {
+ override fun Content() {
Box {
- content()
+ Box(
+ Modifier.testTag(testTag)
+ .size(24.dp)
+ .paint(getPainter())
+ )
}
}
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
index 1797d44..4e01e22 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
@@ -21,6 +21,7 @@
import androidx.compose.lint.Names
import androidx.compose.lint.inheritsFrom
import androidx.compose.lint.isComposable
+import androidx.compose.lint.toKmFunction
import androidx.compose.ui.lint.ModifierDeclarationDetector.Companion.ComposableModifierFactory
import androidx.compose.ui.lint.ModifierDeclarationDetector.Companion.ModifierFactoryReturnType
import com.android.tools.lint.client.api.UElementHandler
@@ -35,6 +36,8 @@
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiType
+import com.intellij.psi.impl.compiled.ClsMethodImpl
+import kotlinx.metadata.KmClassifier
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtDeclarationWithBody
import org.jetbrains.kotlin.psi.KtFunction
@@ -43,9 +46,15 @@
import org.jetbrains.kotlin.psi.KtPropertyAccessor
import org.jetbrains.kotlin.psi.KtUserType
import org.jetbrains.kotlin.psi.psiUtil.containingClass
+import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UThisExpression
+import org.jetbrains.uast.UTypeReferenceExpression
+import org.jetbrains.uast.getContainingUClass
+import org.jetbrains.uast.resolveToUElement
import org.jetbrains.uast.toUElement
import org.jetbrains.uast.tryResolve
+import org.jetbrains.uast.visitor.AbstractUastVisitor
import java.util.EnumSet
/**
@@ -56,6 +65,8 @@
* chaining
* - Modifier factory functions should not be marked as @Composable, and should use `composed`
* instead
+ * - Modifier factory functions should reference the receiver parameter inside their body to make
+ * sure they don't drop old Modifiers in the chain
*/
class ModifierDeclarationDetector : Detector(), SourceCodeScanner {
override fun getApplicableUastTypes() = listOf(UMethod::class.java)
@@ -133,6 +144,25 @@
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
)
)
+
+ val ModifierFactoryUnreferencedReceiver = Issue.create(
+ "ModifierFactoryUnreferencedReceiver",
+ "Modifier factory functions must use the receiver Modifier instance",
+ "Modifier factory functions are fluently chained to construct a chain of " +
+ "Modifier objects that will be applied to a layout. As a result, each factory " +
+ "function *must* use the receiver `Modifier` parameter, to ensure that the " +
+ "function is returning a chain that includes previous items in the chain. Make " +
+ "sure the returned chain either explicitly includes `this`, such as " +
+ "`return this.then(MyModifier)` or implicitly by returning a chain that starts " +
+ "with an implicit call to another factory function, such as " +
+ "`return myModifier()`, where `myModifier` is defined as " +
+ "`fun Modifier.myModifier(): Modifier`.",
+ Category.CORRECTNESS, 3, Severity.ERROR,
+ Implementation(
+ ModifierDeclarationDetector::class.java,
+ EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+ )
+ )
}
}
@@ -238,11 +268,84 @@
.autoFix()
.build()
)
+ } else {
+ // Ignore interface / abstract methods with no body
+ if (uastBody != null) {
+ ensureReceiverIsReferenced(context)
+ }
}
}
}
/**
+ * See [ModifierDeclarationDetector.ModifierFactoryUnreferencedReceiver]
+ */
+private fun UMethod.ensureReceiverIsReferenced(context: JavaContext) {
+ var isReceiverReferenced = false
+ accept(object : AbstractUastVisitor() {
+ /**
+ * If there is no receiver on the call, but the call has a Modifier receiver
+ * type, then the call is implicitly using the Modifier receiver
+ * TODO: consider checking for nested receivers, in case the implicit
+ * receiver is an inner scope, and not the outer Modifier receiver
+ */
+ override fun visitCallExpression(node: UCallExpression): Boolean {
+ // We account for a receiver of `this` in `visitThisExpression`
+ if (node.receiver == null) {
+ val declaration = node.resolveToUElement()
+ // If the declaration is a member of `Modifier` (such as `then`)
+ if (declaration?.getContainingUClass()
+ ?.qualifiedName == Names.Ui.Modifier.javaFqn
+ ) {
+ isReceiverReferenced = true
+ // Otherwise if the declaration is an extension of `Modifier`
+ } else {
+ // Whether the declaration itself has a Modifier receiver - UAST might think the
+ // receiver on the node is different if it is inside another scope.
+ val hasModifierReceiver = when (val source = declaration?.sourcePsi) {
+ // Parsing a method defined in a class file
+ is ClsMethodImpl -> {
+ val receiverClassifier = source.toKmFunction()
+ ?.receiverParameterType?.classifier
+ receiverClassifier == KmClassifier.Class(Names.Ui.Modifier.kmClassName)
+ }
+ // Parsing a method defined in Kotlin source
+ is KtFunction -> {
+ val receiver = source.receiverTypeReference
+ (receiver.toUElement() as? UTypeReferenceExpression)
+ ?.getQualifiedName() == Names.Ui.Modifier.javaFqn
+ }
+ else -> false
+ }
+ if (hasModifierReceiver) {
+ isReceiverReferenced = true
+ }
+ }
+ }
+ return isReceiverReferenced
+ }
+
+ /**
+ * If `this` is explicitly referenced, no error.
+ * TODO: consider checking for nested receivers, in case `this` refers to an
+ * inner scope, and not the outer Modifier receiver
+ */
+ override fun visitThisExpression(node: UThisExpression): Boolean {
+ isReceiverReferenced = true
+ return isReceiverReferenced
+ }
+ })
+ if (!isReceiverReferenced) {
+ context.report(
+ ModifierDeclarationDetector.ModifierFactoryUnreferencedReceiver,
+ this,
+ context.getNameLocation(this),
+ "Modifier factory functions must use the receiver Modifier instance"
+ )
+ }
+}
+
+/**
* @see [ModifierDeclarationDetector.ModifierFactoryReturnType]
*/
private fun UMethod.checkReturnType(context: JavaContext, returnType: PsiType) {
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt
index 181977f..1d834e7 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/UiIssueRegistry.kt
@@ -30,6 +30,7 @@
ModifierDeclarationDetector.ComposableModifierFactory,
ModifierDeclarationDetector.ModifierFactoryExtensionFunction,
ModifierDeclarationDetector.ModifierFactoryReturnType,
+ ModifierDeclarationDetector.ModifierFactoryUnreferencedReceiver,
ModifierParameterDetector.ModifierParameter
)
}
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
index 33aac1f..e3be716 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
@@ -40,6 +40,7 @@
ModifierDeclarationDetector.ComposableModifierFactory,
ModifierDeclarationDetector.ModifierFactoryExtensionFunction,
ModifierDeclarationDetector.ModifierFactoryReturnType,
+ ModifierDeclarationDetector.ModifierFactoryUnreferencedReceiver
)
@Test
@@ -54,7 +55,7 @@
object TestModifier : Modifier.Element
fun Modifier.fooModifier(): Modifier.Element {
- return TestModifier
+ return this.then(TestModifier)
}
"""
),
@@ -91,14 +92,14 @@
object TestModifier : Modifier.Element
val Modifier.fooModifier get(): Modifier.Element {
- return TestModifier
+ return this.then(TestModifier)
}
val Modifier.fooModifier2: Modifier.Element get() {
- return TestModifier
+ return this.then(TestModifier)
}
- val Modifier.fooModifier3: Modifier.Element get() = TestModifier
+ val Modifier.fooModifier3: Modifier.Element get() = this.then(TestModifier)
"""
),
kotlin(Stubs.Modifier)
@@ -113,7 +114,7 @@
val Modifier.fooModifier2: Modifier.Element get() {
~~~
src/androidx/compose/ui/foo/TestModifier.kt:16: Warning: Modifier factory functions should have a return type of Modifier [ModifierFactoryReturnType]
- val Modifier.fooModifier3: Modifier.Element get() = TestModifier
+ val Modifier.fooModifier3: Modifier.Element get() = this.then(TestModifier)
~~~
0 errors, 3 warnings
"""
@@ -130,8 +131,8 @@
+ val Modifier.fooModifier2: Modifier get() {
Fix for src/androidx/compose/ui/foo/TestModifier.kt line 16: Change return type to Modifier:
@@ -16 +16
-- val Modifier.fooModifier3: Modifier.Element get() = TestModifier
-+ val Modifier.fooModifier3: Modifier get() = TestModifier
+- val Modifier.fooModifier3: Modifier.Element get() = this.then(TestModifier)
++ val Modifier.fooModifier3: Modifier get() = this.then(TestModifier)
"""
)
}
@@ -158,7 +159,10 @@
src/androidx/compose/ui/foo/TestModifier.kt:8: Warning: Modifier factory functions should have a return type of Modifier [ModifierFactoryReturnType]
fun Modifier.fooModifier() = TestModifier
~~~~~~~~~~~
-0 errors, 1 warnings
+src/androidx/compose/ui/foo/TestModifier.kt:8: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+ fun Modifier.fooModifier() = TestModifier
+ ~~~~~~~~~~~
+1 errors, 1 warnings
"""
)
.expectFixDiffs(
@@ -193,7 +197,10 @@
src/androidx/compose/ui/foo/TestModifier.kt:8: Warning: Modifier factory functions should have a return type of Modifier [ModifierFactoryReturnType]
val Modifier.fooModifier get() = TestModifier
~~~
-0 errors, 1 warnings
+src/androidx/compose/ui/foo/TestModifier.kt:8: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+ val Modifier.fooModifier get() = TestModifier
+ ~~~
+1 errors, 1 warnings
"""
)
.expectFixDiffs(
@@ -218,7 +225,7 @@
object TestModifier : Modifier.Element
fun Modifier.fooModifier(): TestModifier {
- return TestModifier
+ return this.then(TestModifier)
}
"""
),
@@ -330,15 +337,15 @@
object TestModifier : Modifier.Element
fun fooModifier(): Modifier {
- return TestModifier
+ return this.then(TestModifier)
}
val fooModifier get(): Modifier {
- return TestModifier
+ return this.then(TestModifier)
}
val fooModifier2: Modifier get() {
- return TestModifier
+ return this.then(TestModifier)
}
val fooModifier3: Modifier get() = TestModifier
@@ -398,18 +405,18 @@
object TestModifier : Modifier.Element
fun TestModifier.fooModifier(): Modifier {
- return TestModifier
+ return this.then(TestModifier)
}
val TestModifier.fooModifier get(): Modifier {
- return TestModifier
+ return this.then(TestModifier)
}
val TestModifier.fooModifier2: Modifier get() {
- return TestModifier
+ return this.then(TestModifier)
}
- val TestModifier.fooModifier3: Modifier get() = TestModifier
+ val TestModifier.fooModifier3: Modifier get() = this.then(TestModifier)
"""
),
kotlin(Stubs.Modifier)
@@ -427,7 +434,7 @@
val TestModifier.fooModifier2: Modifier get() {
~~~
src/androidx/compose/ui/foo/TestModifier.kt:20: Warning: Modifier factory functions should be extensions on Modifier [ModifierFactoryExtensionFunction]
- val TestModifier.fooModifier3: Modifier get() = TestModifier
+ val TestModifier.fooModifier3: Modifier get() = this.then(TestModifier)
~~~
0 errors, 4 warnings
"""
@@ -448,8 +455,8 @@
+ val Modifier.fooModifier2: Modifier get() {
Fix for src/androidx/compose/ui/foo/TestModifier.kt line 20: Change receiver to Modifier:
@@ -20 +20
-- val TestModifier.fooModifier3: Modifier get() = TestModifier
-+ val Modifier.fooModifier3: Modifier get() = TestModifier
+- val TestModifier.fooModifier3: Modifier get() = this.then(TestModifier)
++ val Modifier.fooModifier3: Modifier get() = this.then(TestModifier)
"""
)
}
@@ -472,20 +479,22 @@
@Composable
fun Modifier.fooModifier1(): Modifier {
val value = someComposableCall(3)
- return TestModifier(value)
+ return this.then(TestModifier(value))
}
@Composable
- fun Modifier.fooModifier2(): Modifier = TestModifier(someComposableCall(3))
+ fun Modifier.fooModifier2(): Modifier =
+ this.then(TestModifier(someComposableCall(3)))
@get:Composable
val Modifier.fooModifier3: Modifier get() {
val value = someComposableCall(3)
- return TestModifier(value)
+ return this.then(TestModifier(value))
}
@get:Composable
- val Modifier.fooModifier4: Modifier get() = TestModifier(someComposableCall(3))
+ val Modifier.fooModifier4: Modifier get() =
+ this.then(TestModifier(someComposableCall(3)))
"""
),
kotlin(Stubs.Modifier),
@@ -498,13 +507,13 @@
fun Modifier.fooModifier1(): Modifier {
~~~~~~~~~~~~
src/androidx/compose/ui/foo/TestModifier.kt:19: Warning: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
- fun Modifier.fooModifier2(): Modifier = TestModifier(someComposableCall(3))
+ fun Modifier.fooModifier2(): Modifier =
~~~~~~~~~~~~
-src/androidx/compose/ui/foo/TestModifier.kt:22: Warning: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
+src/androidx/compose/ui/foo/TestModifier.kt:23: Warning: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
val Modifier.fooModifier3: Modifier get() {
~~~
-src/androidx/compose/ui/foo/TestModifier.kt:28: Warning: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
- val Modifier.fooModifier4: Modifier get() = TestModifier(someComposableCall(3))
+src/androidx/compose/ui/foo/TestModifier.kt:29: Warning: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
+ val Modifier.fooModifier4: Modifier get() =
~~~
0 errors, 4 warnings
"""
@@ -517,26 +526,105 @@
- fun Modifier.fooModifier1(): Modifier {
+ fun Modifier.fooModifier1(): Modifier = composed {
@@ -15 +14
-- return TestModifier(value)
-+ TestModifier(value)
+- return this.then(TestModifier(value))
++ this.then(TestModifier(value))
Fix for src/androidx/compose/ui/foo/TestModifier.kt line 19: Replace @Composable with composed call:
@@ -18 +18
- @Composable
-- fun Modifier.fooModifier2(): Modifier = TestModifier(someComposableCall(3))
-+ fun Modifier.fooModifier2(): Modifier = composed { TestModifier(someComposableCall(3)) }
-Fix for src/androidx/compose/ui/foo/TestModifier.kt line 22: Replace @Composable with composed call:
-@@ -21 +21
+@@ -20 +19
+- this.then(TestModifier(someComposableCall(3)))
++ composed { this.then(TestModifier(someComposableCall(3))) }
+Fix for src/androidx/compose/ui/foo/TestModifier.kt line 23: Replace @Composable with composed call:
+@@ -22 +22
- @get:Composable
- val Modifier.fooModifier3: Modifier get() {
+ val Modifier.fooModifier3: Modifier get() = composed {
-@@ -24 +23
-- return TestModifier(value)
-+ TestModifier(value)
-Fix for src/androidx/compose/ui/foo/TestModifier.kt line 28: Replace @Composable with composed call:
-@@ -27 +27
+@@ -25 +24
+- return this.then(TestModifier(value))
++ this.then(TestModifier(value))
+Fix for src/androidx/compose/ui/foo/TestModifier.kt line 29: Replace @Composable with composed call:
+@@ -28 +28
- @get:Composable
-- val Modifier.fooModifier4: Modifier get() = TestModifier(someComposableCall(3))
-+ val Modifier.fooModifier4: Modifier get() = composed { TestModifier(someComposableCall(3)) }
+@@ -30 +29
+- this.then(TestModifier(someComposableCall(3)))
++ composed { this.then(TestModifier(someComposableCall(3))) }
+ """
+ )
+ }
+
+ @Test
+ fun unreferencedReceiver() {
+ lint().files(
+ kotlin(
+ """
+ package androidx.compose.ui.foo
+
+ import androidx.compose.ui.*
+
+ object TestModifier : Modifier.Element
+
+ // Modifier factory without a receiver - since this has no receiver it should
+ // trigger an error if this is returned inside another factory function
+ fun testModifier(): Modifier = TestModifier
+
+ interface FooInterface {
+ fun Modifier.fooModifier(): Modifier {
+ return TestModifier
+ }
+ }
+
+ fun Modifier.fooModifier(): Modifier {
+ return TestModifier
+ }
+
+ fun Modifier.fooModifier2(): Modifier {
+ return testModifier()
+ }
+
+ fun Modifier.fooModifier3(): Modifier = TestModifier
+
+ fun Modifier.fooModifier4(): Modifier = testModifier()
+
+ fun Modifier.fooModifier5(): Modifier {
+ return Modifier.then(TestModifier)
+ }
+
+ fun Modifier.fooModifier6(): Modifier {
+ return Modifier.fooModifier()
+ }
+ """
+ ),
+ kotlin(Stubs.Modifier),
+ kotlin(Stubs.Composable)
+ )
+ .run()
+ .expect(
+ """
+src/androidx/compose/ui/foo/TestModifier.kt:10: Warning: Modifier factory functions should be extensions on Modifier [ModifierFactoryExtensionFunction]
+ fun testModifier(): Modifier = TestModifier
+ ~~~~~~~~~~~~
+src/androidx/compose/ui/foo/TestModifier.kt:13: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+ fun Modifier.fooModifier(): Modifier {
+ ~~~~~~~~~~~
+src/androidx/compose/ui/foo/TestModifier.kt:18: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+ fun Modifier.fooModifier(): Modifier {
+ ~~~~~~~~~~~
+src/androidx/compose/ui/foo/TestModifier.kt:22: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+ fun Modifier.fooModifier2(): Modifier {
+ ~~~~~~~~~~~~
+src/androidx/compose/ui/foo/TestModifier.kt:26: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+ fun Modifier.fooModifier3(): Modifier = TestModifier
+ ~~~~~~~~~~~~
+src/androidx/compose/ui/foo/TestModifier.kt:28: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+ fun Modifier.fooModifier4(): Modifier = testModifier()
+ ~~~~~~~~~~~~
+src/androidx/compose/ui/foo/TestModifier.kt:30: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+ fun Modifier.fooModifier5(): Modifier {
+ ~~~~~~~~~~~~
+src/androidx/compose/ui/foo/TestModifier.kt:34: Error: Modifier factory functions must use the receiver Modifier instance [ModifierFactoryUnreferencedReceiver]
+ fun Modifier.fooModifier6(): Modifier {
+ ~~~~~~~~~~~~
+7 errors, 1 warnings
"""
)
}
@@ -553,7 +641,15 @@
object TestModifier : Modifier.Element
fun Modifier.fooModifier(): Modifier {
- return TestModifier
+ return this.then(TestModifier)
+ }
+
+ fun Modifier.fooModifier2(): Modifier {
+ return then(TestModifier)
+ }
+
+ fun Modifier.fooModifier3(): Modifier {
+ return fooModifier()
}
"""
),
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/AndroidManifest.xml b/compose/ui/ui-test-junit4/src/androidAndroidTest/AndroidManifest.xml
index 1318d0a..ebf98af 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/AndroidManifest.xml
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/AndroidManifest.xml
@@ -17,8 +17,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="androidx.compose.ui.test.junit4">
<application>
- <activity android:name="androidx.activity.ComponentActivity"
- android:theme="@style/TestTheme" />
<activity android:name="androidx.compose.ui.test.junit4.CustomActivity"
android:theme="@android:style/Theme.Material.NoActionBar.Fullscreen" />
<activity android:name="androidx.compose.ui.test.junit4.MultipleActivitiesFindTest$Activity1" />
diff --git a/compose/ui/ui-test-junit4/src/androidMain/AndroidManifest.xml b/compose/ui/ui-test-junit4/src/androidMain/AndroidManifest.xml
index 87c86b4..deb7139 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/AndroidManifest.xml
+++ b/compose/ui/ui-test-junit4/src/androidMain/AndroidManifest.xml
@@ -13,12 +13,4 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.compose.ui.test.junit4">
-
- <application>
- <activity
- android:name="androidx.activity.ComponentActivity"
- android:theme="@style/TestTheme" />
- </application>
-</manifest>
+<manifest package="androidx.compose.ui.test.junit4" />
diff --git a/compose/ui/ui-test-junit4/src/androidMain/res/values/styles.xml b/compose/ui/ui-test-junit4/src/androidMain/res/values/styles.xml
deleted file mode 100644
index 43e2d55..0000000
--- a/compose/ui/ui-test-junit4/src/androidMain/res/values/styles.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<!--
- Copyright 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<resources>
- <style name="TestTheme" parent="@android:style/Theme.Material.Light.NoActionBar">
- <item name="android:windowActionBar">false</item>
- <item name="android:windowAnimationStyle">@null</item>
- <item name="android:windowContentOverlay">@null</item>
- <item name="android:windowFullscreen">true</item>
- <item name="android:windowNoTitle">true</item>
- </style>
-</resources>
\ No newline at end of file
diff --git a/compose/ui/ui-test-manifest/src/main/AndroidManifest.xml b/compose/ui/ui-test-manifest/src/main/AndroidManifest.xml
index 833d681..c694ed4 100644
--- a/compose/ui/ui-test-manifest/src/main/AndroidManifest.xml
+++ b/compose/ui/ui-test-manifest/src/main/AndroidManifest.xml
@@ -17,6 +17,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="androidx.compose.ui.test.manifest">
<application>
- <activity android:name="androidx.activity.ComponentActivity" />
+ <activity android:name="androidx.activity.ComponentActivity" android:exported="true" />
</application>
</manifest>
diff --git a/compose/ui/ui-test/src/androidAndroidTest/AndroidManifest.xml b/compose/ui/ui-test/src/androidAndroidTest/AndroidManifest.xml
index ac695d6..40c2ea8 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/AndroidManifest.xml
+++ b/compose/ui/ui-test/src/androidAndroidTest/AndroidManifest.xml
@@ -17,8 +17,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="androidx.compose.ui.test">
<application>
- <activity android:name="androidx.activity.ComponentActivity"
- android:theme="@style/TestTheme" />
<activity android:name="androidx.compose.ui.test.ActivityWithActionBar" />
<activity android:name="androidx.compose.ui.test.ClickCounterActivity" />
<activity android:name="androidx.compose.ui.test.EmptyActivity" />
diff --git a/compose/ui/ui-test/src/androidMain/AndroidManifest.xml b/compose/ui/ui-test/src/androidMain/AndroidManifest.xml
index f5a2402..0d3bc8e 100644
--- a/compose/ui/ui-test/src/androidMain/AndroidManifest.xml
+++ b/compose/ui/ui-test/src/androidMain/AndroidManifest.xml
@@ -13,12 +13,4 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.compose.ui.test">
-
- <application>
- <activity
- android:name="androidx.activity.ComponentActivity"
- android:theme="@style/TestTheme" />
- </application>
-</manifest>
+<manifest package="androidx.compose.ui.test" />
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/AndroidManifest.xml b/compose/ui/ui-tooling-data/src/androidTest/AndroidManifest.xml
similarity index 86%
rename from compose/ui/ui-tooling-data/src/androidTest/java/AndroidManifest.xml
rename to compose/ui/ui-tooling-data/src/androidTest/AndroidManifest.xml
index 09f8b59..b2770d0 100644
--- a/compose/ui/ui-tooling-data/src/androidTest/java/AndroidManifest.xml
+++ b/compose/ui/ui-tooling-data/src/androidTest/AndroidManifest.xml
@@ -15,7 +15,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="androidx.compose.ui.tooling.data.test">
-
- <application/>
-
+ <application>
+ <activity android:name="androidx.compose.ui.tooling.data.TestActivity" />
+ </application>
</manifest>
\ No newline at end of file
diff --git a/compose/ui/ui-tooling-data/src/main/AndroidManifest.xml b/compose/ui/ui-tooling-data/src/main/AndroidManifest.xml
index db78381..95bc4c1 100644
--- a/compose/ui/ui-tooling-data/src/main/AndroidManifest.xml
+++ b/compose/ui/ui-tooling-data/src/main/AndroidManifest.xml
@@ -13,11 +13,4 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.compose.ui.tooling.data">
-
- <application>
- <activity android:name=".TestActivity" />
- </application>
-
-</manifest>
\ No newline at end of file
+<manifest package="androidx.compose.ui.tooling.data" />
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index ed238dd..9e636db 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -50,6 +50,7 @@
androidTestImplementation(ANDROIDX_TEST_RULES)
androidTestImplementation(project(":compose:foundation:foundation-layout"))
androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(TRUTH)
androidTestImplementation(KOTLIN_REFLECT)
androidTestImplementation(project(":ui:ui-animation-tooling-internal"))
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
index 5eddcf7..aef6f62 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
@@ -130,7 +130,12 @@
*/
// This deprecated-error function shadows the varargs overload so that the varargs version
// is not used without key parameters.
-@Suppress("DeprecatedCallableAddReplaceWith", "UNUSED_PARAMETER", "unused")
+@Suppress(
+ "DeprecatedCallableAddReplaceWith",
+ "UNUSED_PARAMETER",
+ "unused",
+ "ModifierFactoryUnreferencedReceiver"
+)
@Deprecated(PointerInputModifierNoParamError, level = DeprecationLevel.ERROR)
fun Modifier.pointerInput(
block: suspend PointerInputScope.() -> Unit
diff --git a/lifecycle/lifecycle-viewmodel-compose/build.gradle b/lifecycle/lifecycle-viewmodel-compose/build.gradle
index 1d0082c..2bdbb1c 100644
--- a/lifecycle/lifecycle-viewmodel-compose/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/build.gradle
@@ -39,6 +39,7 @@
implementation(KOTLIN_STDLIB)
androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
+ androidTestImplementation projectOrArtifact(":compose:test-utils")
androidTestImplementation(ANDROIDX_TEST_RULES)
androidTestImplementation(ANDROIDX_TEST_RUNNER)
androidTestImplementation(JUNIT)
diff --git a/room/compiler-processing-testing/build.gradle b/room/compiler-processing-testing/build.gradle
index e310d6e..41b9d08 100644
--- a/room/compiler-processing-testing/build.gradle
+++ b/room/compiler-processing-testing/build.gradle
@@ -74,7 +74,8 @@
// in the source.
tasks.named("compileTestKotlin", KotlinCompile.class).configure {
it.kotlinOptions {
- freeCompilerArgs += ["-Xopt-in=androidx.room.compiler.processing.ExperimentalProcessingApi"]
+ freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn",
+ "-Xopt-in=androidx.room.compiler.processing.ExperimentalProcessingApi"]
}
}
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
index d4115d2..e1c5fd5 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
@@ -28,6 +28,7 @@
import com.google.common.truth.Truth.assertWithMessage
import com.google.devtools.ksp.processing.SymbolProcessor
import com.tschuchort.compiletesting.KotlinCompilation
+import com.tschuchort.compiletesting.kspArgs
import com.tschuchort.compiletesting.symbolProcessors
import java.io.ByteArrayOutputStream
import java.io.File
@@ -79,12 +80,14 @@
fun runProcessorTestWithoutKsp(
sources: List<Source> = emptyList(),
classpath: List<File> = emptyList(),
+ options: Map<String, String> = emptyMap(),
handler: (XTestInvocation) -> Unit
) {
runTests(
params = TestCompilationParameters(
sources = sources,
classpath = classpath,
+ options = options,
handlers = listOf(handler)
),
JavacCompilationTestRunner,
@@ -111,8 +114,14 @@
fun runProcessorTest(
sources: List<Source> = emptyList(),
classpath: List<File> = emptyList(),
+ options: Map<String, String> = emptyMap(),
handler: (XTestInvocation) -> Unit
-) = runProcessorTest(sources = sources, classpath = classpath, handlers = listOf(handler))
+) = runProcessorTest(
+ sources = sources,
+ classpath = classpath,
+ options = options,
+ handlers = listOf(handler)
+)
/**
* Runs the step created by [createProcessingStep] with ksp and one of javac or kapt, depending
@@ -131,12 +140,14 @@
fun runProcessorTest(
sources: List<Source> = emptyList(),
classpath: List<File> = emptyList(),
+ options: Map<String, String> = emptyMap(),
createProcessingStep: () -> XProcessingStep,
onCompilationResult: (CompilationResultSubject) -> Unit
) {
runProcessorTest(
sources = sources,
- classpath = classpath
+ classpath = classpath,
+ options = options
) { invocation ->
val step = createProcessingStep()
val elements =
@@ -161,6 +172,7 @@
fun runProcessorTest(
sources: List<Source> = emptyList(),
classpath: List<File> = emptyList(),
+ options: Map<String, String> = emptyMap(),
handlers: List<(XTestInvocation) -> Unit>
) {
val javaApRunner = if (sources.any { it is Source.KotlinSource }) {
@@ -172,6 +184,7 @@
params = TestCompilationParameters(
sources = sources,
classpath = classpath,
+ options = options,
handlers = handlers
),
javaApRunner,
@@ -188,10 +201,12 @@
fun runJavaProcessorTest(
sources: List<Source>,
classpath: List<File> = emptyList(),
+ options: Map<String, String> = emptyMap(),
handler: (XTestInvocation) -> Unit
) = runJavaProcessorTest(
sources = sources,
classpath = classpath,
+ options = options,
handlers = listOf(handler)
)
@@ -202,12 +217,14 @@
fun runJavaProcessorTest(
sources: List<Source>,
classpath: List<File> = emptyList(),
+ options: Map<String, String> = emptyMap(),
handlers: List<(XTestInvocation) -> Unit>
) {
runTests(
params = TestCompilationParameters(
sources = sources,
classpath = classpath,
+ options = options,
handlers = handlers
),
JavacCompilationTestRunner
@@ -221,10 +238,12 @@
fun runKaptTest(
sources: List<Source>,
classpath: List<File> = emptyList(),
+ options: Map<String, String> = emptyMap(),
handler: (XTestInvocation) -> Unit
) = runKaptTest(
sources = sources,
classpath = classpath,
+ options = options,
handlers = listOf(handler)
)
@@ -235,12 +254,14 @@
fun runKaptTest(
sources: List<Source>,
classpath: List<File> = emptyList(),
+ options: Map<String, String> = emptyMap(),
handlers: List<(XTestInvocation) -> Unit>
) {
runTests(
params = TestCompilationParameters(
sources = sources,
classpath = classpath,
+ options = options,
handlers = handlers
),
KaptCompilationTestRunner
@@ -254,10 +275,12 @@
fun runKspTest(
sources: List<Source>,
classpath: List<File> = emptyList(),
+ options: Map<String, String> = emptyMap(),
handler: (XTestInvocation) -> Unit
) = runKspTest(
sources = sources,
classpath = classpath,
+ options = options,
handlers = listOf(handler)
)
@@ -268,12 +291,14 @@
fun runKspTest(
sources: List<Source>,
classpath: List<File> = emptyList(),
+ options: Map<String, String> = emptyMap(),
handlers: List<(XTestInvocation) -> Unit>
) {
runTests(
params = TestCompilationParameters(
sources = sources,
classpath = classpath,
+ options = options,
handlers = handlers
),
KspCompilationTestRunner
@@ -284,11 +309,13 @@
* Compiles the given set of sources into a temporary folder and returns the output classes
* directory.
* @param sources The list of source files to compile
+ * @param options The annotation processor arguments
* @param annotationProcessors The list of Java annotation processors to run with compilation
* @param symbolProcessors The list of Kotlin symbol processors to run with compilation
*/
fun compileFiles(
sources: List<Source>,
+ options: Map<String, String> = emptyMap(),
annotationProcessors: List<Processor> = emptyList(),
symbolProcessors: List<SymbolProcessor> = emptyList()
): File {
@@ -297,6 +324,12 @@
sources = sources,
outputStream = outputStream
)
+ if (annotationProcessors.isNotEmpty()) {
+ compilation.kaptArgs.putAll(options)
+ }
+ if (symbolProcessors.isNotEmpty()) {
+ compilation.kspArgs.putAll(options)
+ }
compilation.annotationProcessors = annotationProcessors
compilation.symbolProcessors = symbolProcessors
val result = compilation.compile()
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt
index ba50be6..844bb20 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt
@@ -39,5 +39,6 @@
internal data class TestCompilationParameters(
val sources: List<Source> = emptyList(),
val classpath: List<File> = emptyList(),
+ val options: Map<String, String> = emptyMap(),
val handlers: List<(XTestInvocation) -> Unit>
)
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt
index 9097b02..d3bd3a8 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt
@@ -48,10 +48,14 @@
} else {
params.sources
}
+
+ val optionsArg = params.options.entries.map {
+ "-A${it.key}=${it.value}"
+ }
val compiler = Compiler
.javac()
.withProcessors(syntheticJavacProcessor)
- .withOptions("-Xlint")
+ .withOptions(optionsArg + "-Xlint")
.let {
if (params.classpath.isNotEmpty()) {
it.withClasspath(params.classpath)
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt
index 6e1f860..651d73b 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt
@@ -41,6 +41,7 @@
outputStream = outputStream,
classpaths = params.classpath
)
+ compilation.kaptArgs.putAll(params.options)
compilation.annotationProcessors = listOf(syntheticJavacProcessor)
val result = compilation.compile()
return KotlinCompileTestingCompilationResult(
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
index 3ebaa0b..78308ba 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
@@ -25,6 +25,7 @@
import androidx.room.compiler.processing.util.Source
import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.SourceFile
+import com.tschuchort.compiletesting.kspArgs
import com.tschuchort.compiletesting.kspSourcesDir
import com.tschuchort.compiletesting.symbolProcessors
import java.io.ByteArrayOutputStream
@@ -57,6 +58,7 @@
outputStream = combinedOutputStream,
classpaths = params.classpath
)
+ kspCompilation.kspArgs.putAll(params.options)
kspCompilation.symbolProcessors = listOf(syntheticKspProcessor)
kspCompilation.compile()
// ignore KSP result for now because KSP stops compilation, which might create false
diff --git a/room/compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt b/room/compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt
index 7dcc46f..6ba44be 100644
--- a/room/compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt
+++ b/room/compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt
@@ -16,6 +16,7 @@
package androidx.room.compiler.processing.util
+import androidx.room.compiler.processing.ExperimentalProcessingApi
import com.google.common.truth.Truth.assertThat
import com.squareup.javapoet.CodeBlock
import com.squareup.javapoet.JavaFile
@@ -23,6 +24,7 @@
import org.junit.Test
import javax.tools.Diagnostic
+@OptIn(ExperimentalProcessingApi::class)
class TestRunnerTest {
@Test
fun generatedBadCode_expected() = generatedBadCode(assertFailure = true)
@@ -30,6 +32,19 @@
@Test(expected = AssertionError::class)
fun generatedBadCode_unexpected() = generatedBadCode(assertFailure = false)
+ @Test
+ fun options() {
+ val testOptions = mapOf(
+ "a" to "b",
+ "c" to "d"
+ )
+ runProcessorTest(
+ options = testOptions
+ ) {
+ assertThat(it.processingEnv.options).containsAtLeastEntriesIn(testOptions)
+ }
+ }
+
private fun generatedBadCode(assertFailure: Boolean) {
runProcessorTest {
if (it.processingEnv.findTypeElement("foo.Foo") == null) {
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index d22d0d2..386c3ec 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -721,6 +721,11 @@
) = "The partial entity $partialEntityName is missing the primary key fields " +
"(${primaryKeyNames.joinToString()}) needed to perform an UPDATE."
+ fun noColumnsInPartialEntity(
+ partialEntityName: String
+ ) = "The partial entity $partialEntityName does not have any columns that can be used to " +
+ "perform the query."
+
fun cannotFindPreparedQueryResultAdapter(
returnType: String,
type: QueryType
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
index b6bddc9..59adf60 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
@@ -143,6 +143,15 @@
ProcessorErrors.INVALID_RELATION_IN_PARTIAL_ENTITY
)
}
+
+ if (pojo.fields.isEmpty()) {
+ context.logger.e(
+ executableElement,
+ ProcessorErrors.noColumnsInPartialEntity(
+ partialEntityName = pojo.typeName.toString()
+ )
+ )
+ }
onValidatePartialEntity(targetEntity, pojo)
}
}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
index f753d89..e22402f 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
@@ -21,17 +21,15 @@
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.compiler.processing.XTypeElement
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.ext.CommonTypeNames
import androidx.room.ext.RxJava2TypeNames
import androidx.room.ext.RxJava3TypeNames
import androidx.room.solver.shortcut.result.InsertMethodAdapter
-import androidx.room.testing.TestInvocation
-import androidx.room.testing.TestProcessor
+import androidx.room.testing.context
import androidx.room.vo.InsertionMethod
-import com.google.common.truth.Truth.assertAbout
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
import com.squareup.javapoet.ArrayTypeName
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.ParameterizedTypeName
@@ -43,8 +41,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import toJFO
-import javax.tools.JavaFileObject
+import toSources
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
@RunWith(JUnit4::class)
@@ -70,14 +67,17 @@
@Insert
abstract public void foo();
"""
- ) { insertion, _ ->
+ ) { insertion, invocation ->
assertThat(insertion.name, `is`("foo"))
assertThat(insertion.parameters.size, `is`(0))
assertThat(insertion.returnType.typeName, `is`(TypeName.VOID))
assertThat(insertion.entities.size, `is`(0))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.INSERTION_DOES_NOT_HAVE_ANY_PARAMETERS_TO_INSERT
- )
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.INSERTION_DOES_NOT_HAVE_ANY_PARAMETERS_TO_INSERT
+ )
+ }
+ }
}
@Test
@@ -99,7 +99,7 @@
`is`(ClassName.get("foo.bar", "User") as TypeName)
)
assertThat(insertion.returnType.typeName, `is`(TypeName.LONG))
- }.compilesWithoutError()
+ }
}
@Test
@@ -109,13 +109,16 @@
@Insert
abstract public void foo(NotAnEntity notValid);
"""
- ) { insertion, _ ->
+ ) { insertion, invocation ->
assertThat(insertion.name, `is`("foo"))
assertThat(insertion.parameters.size, `is`(1))
assertThat(insertion.entities.size, `is`(0))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER
- )
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER
+ )
+ }
+ }
}
@Test
@@ -138,7 +141,7 @@
assertThat(insertion.entities["u2"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
assertThat(insertion.parameters.map { it.name }, `is`(listOf("u1", "u2")))
assertThat(insertion.returnType.typeName, `is`(TypeName.VOID))
- }.compilesWithoutError()
+ }
}
@Test
@@ -173,7 +176,7 @@
) as TypeName
)
)
- }.compilesWithoutError()
+ }
}
@Test
@@ -196,7 +199,7 @@
assertThat(insertion.entities.size, `is`(1))
assertThat(insertion.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
assertThat(insertion.returnType.typeName, `is`(TypeName.VOID))
- }.compilesWithoutError()
+ }
}
@Test
@@ -222,7 +225,7 @@
assertThat(insertion.entities.size, `is`(1))
assertThat(insertion.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
assertThat(insertion.returnType.typeName, `is`(TypeName.VOID))
- }.compilesWithoutError()
+ }
}
@Test
@@ -248,7 +251,7 @@
assertThat(insertion.entities.size, `is`(1))
assertThat(insertion.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
assertThat(insertion.returnType.typeName, `is`(TypeName.VOID))
- }.compilesWithoutError()
+ }
}
@Test
@@ -274,7 +277,7 @@
assertThat(insertion.entities.size, `is`(1))
assertThat(insertion.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
assertThat(insertion.returnType.typeName, `is`(TypeName.VOID))
- }.compilesWithoutError()
+ }
}
@Test
@@ -301,7 +304,7 @@
assertThat(insertion.entities.size, `is`(1))
assertThat(insertion.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
assertThat(insertion.returnType.typeName, `is`(TypeName.VOID))
- }.compilesWithoutError()
+ }
}
@Test
@@ -326,7 +329,7 @@
assertThat(insertion.entities.size, `is`(2))
assertThat(insertion.entities["u1"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
assertThat(insertion.entities["b1"]?.pojo?.typeName, `is`(BOOK_TYPE_NAME))
- }.compilesWithoutError()
+ }
}
@Test
@@ -338,7 +341,7 @@
"""
) { insertion, _ ->
assertThat(insertion.onConflict, `is`(OnConflictStrategy.ABORT))
- }.compilesWithoutError()
+ }
}
@Test
@@ -348,8 +351,13 @@
@Insert(onConflict = -1)
abstract public void foo(User user);
"""
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_ON_CONFLICT_VALUE)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.INVALID_ON_CONFLICT_VALUE
+ )
+ }
+ }
}
@Test
@@ -368,7 +376,7 @@
"""
) { insertion, _ ->
assertThat(insertion.onConflict, `is`(pair.second))
- }.compilesWithoutError()
+ }
}
}
@@ -388,11 +396,14 @@
@Insert
abstract public $type foo(User user);
"""
- ) { insertion, _ ->
+ ) { insertion, invocation ->
assertThat(insertion.methodBinder.adapter, `is`(nullValue()))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
- )
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
+ )
+ }
+ }
}
}
@@ -410,11 +421,14 @@
@Insert
abstract public $type foo(User user);
"""
- ) { insertion, _ ->
+ ) { insertion, invocation ->
assertThat(insertion.methodBinder.adapter, `is`(nullValue()))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
- )
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
+ )
+ }
+ }
}
}
@@ -431,11 +445,14 @@
@Insert
abstract public $type foo(User... user);
"""
- ) { insertion, _ ->
+ ) { insertion, invocation ->
assertThat(insertion.methodBinder.adapter, `is`(nullValue()))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
- )
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
+ )
+ }
+ }
}
}
@@ -452,11 +469,14 @@
@Insert
abstract public $type foo(User user1, User user2);
"""
- ) { insertion, _ ->
+ ) { insertion, invocation ->
assertThat(insertion.methodBinder.adapter, `is`(nullValue()))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
- )
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.CANNOT_FIND_INSERT_RESULT_ADAPTER
+ )
+ }
+ }
}
}
@@ -526,13 +546,15 @@
"""
) { insertion, _ ->
assertThat(insertion.methodBinder.adapter, `is`(notNullValue()))
- }.compilesWithoutError()
+ }
}
}
@Test
fun targetEntitySingle() {
- val usernameJfo = """
+ val usernameSource = Source.java(
+ "foo.bar.Username",
+ """
package foo.bar;
import androidx.room.*;
@@ -542,13 +564,14 @@
@ColumnInfo(name = "ageColumn")
int age;
}
- """.toJFO("foo.bar.Username")
+ """
+ )
singleInsertMethod(
"""
@Insert(entity = User.class)
abstract public long foo(Username username);
- """,
- additionalJFOs = listOf(usernameJfo)
+ """,
+ additionalSources = listOf(usernameSource)
) { insertion, _ ->
assertThat(insertion.name, `is`("foo"))
assertThat(insertion.parameters.size, `is`(1))
@@ -559,7 +582,7 @@
assertThat(insertion.entities["username"]?.isPartialEntity, `is`(true))
assertThat(insertion.entities["username"]?.entityTypeName, `is`(USER_TYPE_NAME))
assertThat(insertion.entities["username"]?.pojo?.typeName, `is`(USERNAME_TYPE_NAME))
- }.compilesWithoutError()
+ }
}
@Test
@@ -568,14 +591,16 @@
"""
@Insert(entity = User.class)
abstract public long foo(User user);
- """
+ """
) { _, _ ->
- }.compilesWithoutError()
+ }
}
@Test
fun targetEntityTwo() {
- val usernameJfo = """
+ val usernameSource = Source.java(
+ "foo.bar.Username",
+ """
package foo.bar;
import androidx.room.*;
@@ -585,20 +610,23 @@
@ColumnInfo(name = "ageColumn")
int age;
}
- """.toJFO("foo.bar.Username")
+ """
+ )
singleInsertMethod(
"""
@Insert(entity = User.class)
abstract public void foo(Username usernameA, Username usernameB);
- """,
- additionalJFOs = listOf(usernameJfo)
+ """,
+ additionalSources = listOf(usernameSource)
) { _, _ ->
- }.compilesWithoutError()
+ }
}
@Test
fun targetEntityMissingRequiredColumn() {
- val usernameJfo = """
+ val usernameSource = Source.java(
+ "foo.bar.Username",
+ """
package foo.bar;
import androidx.room.*;
@@ -606,25 +634,31 @@
int uid;
String name;
}
- """.toJFO("foo.bar.Username")
+ """
+ )
singleInsertMethod(
"""
@Insert(entity = User.class)
abstract public void foo(Username username);
- """,
- additionalJFOs = listOf(usernameJfo)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.missingRequiredColumnsInPartialEntity(
- partialEntityName = USERNAME_TYPE_NAME.toString(),
- missingColumnNames = listOf("ageColumn")
- )
- )
+ """,
+ additionalSources = listOf(usernameSource)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.missingRequiredColumnsInPartialEntity(
+ partialEntityName = USERNAME_TYPE_NAME.toString(),
+ missingColumnNames = listOf("ageColumn")
+ )
+ )
+ }
+ }
}
@Test
fun targetEntityColumnDefaultValue() {
- val petNameJfo = """
+ val petNameSource = Source.java(
+ "foo.bar.PetName",
+ """
package foo.bar;
import androidx.room.*;
@@ -632,8 +666,11 @@
@ColumnInfo(name = "name")
String string;
}
- """.toJFO("foo.bar.PetName")
- val petJfo = """
+ """
+ )
+ val petSource = Source.java(
+ "foo.bar.Pet",
+ """
package foo.bar;
import androidx.room.*;
@@ -645,20 +682,23 @@
@ColumnInfo(defaultValue = "0")
int age;
}
- """.toJFO("foo.bar.Pet")
+ """
+ )
singleInsertMethod(
"""
@Insert(entity = Pet.class)
abstract public long foo(PetName petName);
- """,
- additionalJFOs = listOf(petNameJfo, petJfo)
+ """,
+ additionalSources = listOf(petNameSource, petSource)
) { _, _ ->
- }.compilesWithoutError()
+ }
}
@Test
fun targetEntityMissingPrimaryKey() {
- val petNameJfo = """
+ val petNameSource = Source.java(
+ "foo.bar.PetName",
+ """
package foo.bar;
import androidx.room.*;
@@ -666,8 +706,11 @@
@ColumnInfo(name = "name")
String string;
}
- """.toJFO("foo.bar.PetName")
- val petJfo = """
+ """
+ )
+ val petSource = Source.java(
+ "foo.bar.Pet",
+ """
package foo.bar;
import androidx.room.*;
@@ -677,25 +720,31 @@
int petId;
String name;
}
- """.toJFO("foo.bar.Pet")
+ """
+ )
singleInsertMethod(
"""
@Insert(entity = Pet.class)
abstract public long foo(PetName petName);
- """,
- additionalJFOs = listOf(petNameJfo, petJfo)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.missingPrimaryKeysInPartialEntityForInsert(
- partialEntityName = "foo.bar.PetName",
- primaryKeyNames = listOf("petId")
- )
- )
+ """,
+ additionalSources = listOf(petNameSource, petSource)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.missingPrimaryKeysInPartialEntityForInsert(
+ partialEntityName = "foo.bar.PetName",
+ primaryKeyNames = listOf("petId")
+ )
+ )
+ }
+ }
}
@Test
fun targetEntityAutoGeneratedPrimaryKey() {
- val petNameJfo = """
+ val petNameSource = Source.java(
+ "foo.bar.PetName",
+ """
package foo.bar;
import androidx.room.*;
@@ -703,8 +752,11 @@
@ColumnInfo(name = "name")
String string;
}
- """.toJFO("foo.bar.PetName")
- val petJfo = """
+ """
+ )
+ val petSource = Source.java(
+ "foo.bar.Pet",
+ """
package foo.bar;
import androidx.room.*;
@@ -714,20 +766,23 @@
int petId;
String name;
}
- """.toJFO("foo.bar.Pet")
+ """
+ )
singleInsertMethod(
"""
@Insert(entity = Pet.class)
abstract public long foo(PetName petName);
- """,
- additionalJFOs = listOf(petNameJfo, petJfo)
+ """,
+ additionalSources = listOf(petNameSource, petSource)
) { _, _ ->
- }.compilesWithoutError()
+ }
}
@Test
fun targetEntityExtraColumn() {
- val usernameJfo = """
+ val usernameSource = Source.java(
+ "foo.bar.Username",
+ """
package foo.bar;
import androidx.room.*;
@@ -736,22 +791,28 @@
String name;
long extraField;
}
- """.toJFO("foo.bar.Username")
+ """
+ )
singleInsertMethod(
"""
@Insert(entity = User.class)
abstract public long foo(Username username);
- """,
- additionalJFOs = listOf(usernameJfo)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.cannotFindAsEntityField("foo.bar.User")
- )
+ """,
+ additionalSources = listOf(usernameSource)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.cannotFindAsEntityField("foo.bar.User")
+ )
+ }
+ }
}
@Test
fun targetEntityExtraColumnIgnored() {
- val usernameJfo = """
+ val usernameSource = Source.java(
+ "foo.bar.Username",
+ """
package foo.bar;
import androidx.room.*;
@@ -763,20 +824,23 @@
@Ignore
long extraField;
}
- """.toJFO("foo.bar.Username")
+ """
+ )
singleInsertMethod(
"""
@Insert(entity = User.class)
abstract public long foo(Username username);
- """,
- additionalJFOs = listOf(usernameJfo)
+ """,
+ additionalSources = listOf(usernameSource)
) { _, _ ->
- }.compilesWithoutError()
+ }
}
@Test
fun targetEntityWithEmbedded() {
- val usernameJfo = """
+ val usernameSource = Source.java(
+ "foo.bar.Username",
+ """
package foo.bar;
import androidx.room.*;
@@ -787,8 +851,11 @@
@ColumnInfo(name = "ageColumn")
int age;
}
- """.toJFO("foo.bar.Username")
- val fullnameJfo = """
+ """
+ )
+ val fullnameSource = Source.java(
+ "foo.bar.Fullname",
+ """
package foo.bar;
import androidx.room.*;
@@ -797,20 +864,23 @@
String firstName;
String lastName;
}
- """.toJFO("foo.bar.Fullname")
+ """
+ )
singleInsertMethod(
"""
@Insert(entity = User.class)
abstract public long foo(Username username);
- """,
- additionalJFOs = listOf(usernameJfo, fullnameJfo)
+ """,
+ additionalSources = listOf(usernameSource, fullnameSource)
) { _, _ ->
- }.compilesWithoutError()
+ }
}
@Test
fun targetEntityWithRelation() {
- val userPetsJfo = """
+ val userPetsSource = Source.java(
+ "foo.bar.UserPets",
+ """
package foo.bar;
import androidx.room.*;
import java.util.List;
@@ -820,8 +890,11 @@
@Relation(parentColumn = "uid", entityColumn = "ownerId")
List<Pet> pets;
}
- """.toJFO("foo.bar.UserPets")
- val petJfo = """
+ """
+ )
+ val petSource = Source.java(
+ "foo.bar.Pet",
+ """
package foo.bar;
import androidx.room.*;
@@ -831,59 +904,59 @@
int petId;
int ownerId;
}
- """.toJFO("foo.bar.Pet")
+ """
+ )
singleInsertMethod(
"""
@Insert(entity = User.class)
abstract public long foo(UserPets userPets);
""",
- additionalJFOs = listOf(userPetsJfo, petJfo)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_RELATION_IN_PARTIAL_ENTITY)
+ additionalSources = listOf(userPetsSource, petSource)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.INVALID_RELATION_IN_PARTIAL_ENTITY
+ )
+ }
+ }
}
fun singleInsertMethod(
vararg input: String,
- additionalJFOs: List<JavaFileObject> = emptyList(),
- handler: (InsertionMethod, TestInvocation) -> Unit
- ): CompileTester {
- return assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(
- listOf(
- JavaFileObjects.forSourceString(
- "foo.bar.MyClass",
- DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
- ),
- COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY, COMMON.RX2_COMPLETABLE,
- COMMON.RX2_MAYBE, COMMON.RX2_SINGLE, COMMON.RX3_COMPLETABLE,
- COMMON.RX3_MAYBE, COMMON.RX3_SINGLE
- ) + additionalJFOs
+ additionalSources: List<Source> = emptyList(),
+ handler: (InsertionMethod, XTestInvocation) -> Unit
+ ) {
+ val inputSource = Source.java(
+ "foo.bar.MyClass",
+ DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
+ )
+ val commonSources = listOf(
+ COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY, COMMON.RX2_COMPLETABLE,
+ COMMON.RX2_MAYBE, COMMON.RX2_SINGLE, COMMON.RX3_COMPLETABLE,
+ COMMON.RX3_MAYBE, COMMON.RX3_SINGLE
+ ).toSources()
+
+ runProcessorTest(
+ sources = commonSources + additionalSources + inputSource
+ ) { invocation ->
+ val (owner, methods) = invocation.roundEnv
+ .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
+ .filterIsInstance<XTypeElement>()
+ .map {
+ Pair(
+ it,
+ it.getAllMethods().filter {
+ it.hasAnnotation(Insert::class)
+ }
+ )
+ }.first { it.second.isNotEmpty() }
+ val processor = InsertionMethodProcessor(
+ baseContext = invocation.context,
+ containing = owner.type,
+ executableElement = methods.first()
)
- .processedWith(
- TestProcessor.builder()
- .forAnnotations(Insert::class, Dao::class)
- .nextRunHandler { invocation ->
- val (owner, methods) = invocation.roundEnv
- .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
- .filterIsInstance<XTypeElement>()
- .map {
- Pair(
- it,
- it.getAllMethods().filter {
- it.hasAnnotation(Insert::class)
- }
- )
- }.first { it.second.isNotEmpty() }
- val processor = InsertionMethodProcessor(
- baseContext = invocation.context,
- containing = owner.type,
- executableElement = methods.first()
- )
- val processed = processor.process()
- handler(processed, invocation)
- true
- }
- .build()
- )
+ val processed = processor.process()
+ handler(processed, invocation)
+ }
}
}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
index 00b3b2b..001e186 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
@@ -17,32 +17,25 @@
package androidx.room.processor
import COMMON
-import androidx.room.ColumnInfo
import androidx.room.Dao
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import androidx.room.Query
import androidx.room.RawQuery
-import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.compiler.processing.XTypeElement
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.ext.PagingTypeNames
import androidx.room.ext.SupportDbTypeNames
import androidx.room.processor.ProcessorErrors.RAW_QUERY_STRING_PARAMETER_REMOVED
-import androidx.room.testing.TestInvocation
-import androidx.room.testing.TestProcessor
import androidx.room.testing.context
import androidx.room.vo.RawQueryMethod
import androidx.sqlite.db.SupportSQLiteQuery
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
import com.squareup.javapoet.ArrayTypeName
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.TypeName
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
+import toSources
class RawQueryMethodProcessorTest {
@Test
@@ -67,7 +60,7 @@
query.returnType.typeName,
`is`(ArrayTypeName.of(TypeName.INT) as TypeName)
)
- }.compilesWithoutError()
+ }
}
@Test
@@ -77,8 +70,11 @@
@RawQuery
abstract public int[] foo(String query);
"""
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(RAW_QUERY_STRING_PARAMETER_REMOVED)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(RAW_QUERY_STRING_PARAMETER_REMOVED)
+ }
+ }
}
@Test
@@ -101,7 +97,7 @@
)
assertThat(query.observedTableNames.size, `is`(1))
assertThat(query.observedTableNames, `is`(setOf("User")))
- }.compilesWithoutError()
+ }
}
@Test
@@ -111,7 +107,7 @@
@RawQuery(observedEntities = {})
abstract public LiveData<User> foo(SupportSQLiteQuery query);
"""
- ) { query, _ ->
+ ) { query, invocation ->
assertThat(query.name, `is`("foo"))
assertThat(
query.runtimeQueryParam,
@@ -123,8 +119,12 @@
)
)
assertThat(query.observedTableNames, `is`(emptySet()))
- }.failsToCompile()
- .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE
+ )
+ }
+ }
}
@Test
@@ -134,10 +134,13 @@
@RawQuery
abstract public ${PagingTypeNames.DATA_SOURCE_FACTORY}<Integer, User> getOne();
"""
- ) { _, _ ->
- // do nothing
- }.failsToCompile()
- .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE
+ )
+ }
+ }
}
@Test
@@ -147,10 +150,13 @@
@RawQuery
abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE}<User> getOne();
"""
- ) { _, _ ->
- // do nothing
- }.failsToCompile()
- .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE
+ )
+ }
+ }
}
@Test
@@ -163,7 +169,7 @@
"""
) { _, _ ->
// do nothing
- }.compilesWithoutError()
+ }
}
@Test
@@ -192,7 +198,7 @@
)
assertThat(query.returnType.typeName, `is`(pojo))
assertThat(query.observedTableNames, `is`(emptySet()))
- }.compilesWithoutError()
+ }
}
@Test
@@ -202,10 +208,13 @@
@RawQuery
abstract public void foo(SupportSQLiteQuery query);
"""
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RAW_QUERY_BAD_RETURN_TYPE
- )
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.RAW_QUERY_BAD_RETURN_TYPE
+ )
+ }
+ }
}
interface RawQuerySuspendUnitDao {
@@ -237,10 +246,13 @@
@RawQuery
abstract public int[] foo();
"""
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RAW_QUERY_BAD_PARAMS
- )
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.RAW_QUERY_BAD_PARAMS
+ )
+ }
+ }
}
@Test
@@ -251,10 +263,11 @@
abstract public int[] foo(SupportSQLiteQuery query,
SupportSQLiteQuery query2);
"""
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RAW_QUERY_BAD_PARAMS
- )
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(ProcessorErrors.RAW_QUERY_BAD_PARAMS)
+ }
+ }
}
@Test
@@ -264,10 +277,13 @@
@RawQuery
abstract public int[] foo(SupportSQLiteQuery... query);
"""
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.RAW_QUERY_BAD_PARAMS
- )
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.RAW_QUERY_BAD_PARAMS
+ )
+ }
+ }
}
@Test
@@ -277,10 +293,13 @@
@RawQuery(observedEntities = {${COMMON.NOT_AN_ENTITY_TYPE_NAME}.class})
abstract public int[] foo(SupportSQLiteQuery query);
"""
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.rawQueryBadEntity(COMMON.NOT_AN_ENTITY_TYPE_NAME)
- )
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.rawQueryBadEntity(COMMON.NOT_AN_ENTITY_TYPE_NAME)
+ )
+ }
+ }
}
@Test
@@ -300,7 +319,7 @@
"""
) { method, _ ->
assertThat(method.observedTableNames, `is`(setOf("User")))
- }.compilesWithoutError()
+ }
}
@Test
@@ -317,56 +336,46 @@
"""
) { method, _ ->
assertThat(method.observedTableNames, `is`(setOf("User")))
- }.compilesWithoutError()
+ }
}
private fun singleQueryMethod(
vararg input: String,
- handler: (RawQueryMethod, TestInvocation) -> Unit
- ): CompileTester {
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(
- listOf(
- JavaFileObjects.forSourceString(
- "foo.bar.MyClass",
- DAO_PREFIX +
- input.joinToString("\n") +
- DAO_SUFFIX
- ),
- COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER,
- COMMON.DATA_SOURCE_FACTORY, COMMON.POSITIONAL_DATA_SOURCE,
- COMMON.NOT_AN_ENTITY
- )
- )
- .processedWith(
- TestProcessor.builder()
- .forAnnotations(
- Query::class, Dao::class, ColumnInfo::class,
- Entity::class, PrimaryKey::class, RawQuery::class
+ handler: (RawQueryMethod, XTestInvocation) -> Unit
+ ) {
+ val inputSource = Source.java(
+ "foo.bar.MyClass",
+ DAO_PREFIX +
+ input.joinToString("\n") +
+ DAO_SUFFIX
+ )
+ val commonSources = listOf(
+ COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER,
+ COMMON.DATA_SOURCE_FACTORY, COMMON.POSITIONAL_DATA_SOURCE,
+ COMMON.NOT_AN_ENTITY
+ ).toSources()
+ runProcessorTest(
+ sources = commonSources + inputSource
+ ) { invocation ->
+ val (owner, methods) = invocation.roundEnv
+ .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
+ .filterIsInstance<XTypeElement>()
+ .map {
+ Pair(
+ it,
+ it.getAllMethods().filter {
+ it.hasAnnotation(RawQuery::class)
+ }
)
- .nextRunHandler { invocation ->
- val (owner, methods) = invocation.roundEnv
- .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
- .filterIsInstance<XTypeElement>()
- .map {
- Pair(
- it,
- it.getAllMethods().filter {
- it.hasAnnotation(RawQuery::class)
- }
- )
- }.first { it.second.isNotEmpty() }
- val parser = RawQueryMethodProcessor(
- baseContext = invocation.context,
- containing = owner.type,
- executableElement = methods.first()
- )
- val parsedQuery = parser.process()
- handler(parsedQuery, invocation)
- true
- }
- .build()
+ }.first { it.second.isNotEmpty() }
+ val parser = RawQueryMethodProcessor(
+ baseContext = invocation.context,
+ containing = owner.type,
+ executableElement = methods.first()
)
+ val parsedQuery = parser.process()
+ handler(parsedQuery, invocation)
+ }
}
companion object {
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/RemoveUnusedColumnsTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/RemoveUnusedColumnsTest.kt
index d030b1b..76bcdd6 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/RemoveUnusedColumnsTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/RemoveUnusedColumnsTest.kt
@@ -17,47 +17,51 @@
package androidx.room.processor
import COMMON
+import androidx.room.DatabaseProcessingStep
import androidx.room.RewriteQueriesToDropUnusedColumns
-import androidx.room.RoomProcessor
-import com.google.common.truth.Truth.assertAbout
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
+import androidx.room.compiler.processing.util.CompilationResultSubject
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.runProcessorTest
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import javax.tools.JavaFileObject
-import javax.tools.StandardLocation
@RunWith(JUnit4::class)
class RemoveUnusedColumnsTest {
@Test
fun noAnnotationGivesWarning() {
- compile()
- .withWarningCount(1)
- .withWarningContaining("The query returns some columns [uid, ageColumn]")
+ compile { result ->
+ result.hasWarningContaining("The query returns some columns [uid, ageColumn]")
+ result.hasWarningCount(1)
+ }
}
@Test
fun annotateMethod() {
compile(
annotateMethod = true
- ).withWarningCount(0)
+ ) { result ->
+ result.hasNoWarnings()
+ }
}
@Test
fun annotateDao() {
compile(
annotateDao = true
- ).withWarningCount(0)
+ ) { result ->
+ result.hasNoWarnings()
+ }
}
@Test
fun annotateDb() {
compile(
annotateDb = true
- ).withWarningCount(0)
+ ) { result ->
+ result.hasNoWarnings()
+ }
}
@Test
@@ -65,8 +69,10 @@
compile(
annotateDb = true,
enableExpandProjection = true
- ).withWarningCount(1)
- .withWarningContaining(ProcessorErrors.EXPAND_PROJECTION_ALONG_WITH_REMOVE_UNUSED)
+ ) { result ->
+ result.hasWarningContaining(ProcessorErrors.EXPAND_PROJECTION_ALONG_WITH_REMOVE_UNUSED)
+ result.hasWarningCount(1)
+ }
}
@Test
@@ -74,8 +80,10 @@
compile(
annotateMethod = true,
enableExpandProjection = true
- ).withWarningCount(1)
- .withWarningContaining(ProcessorErrors.EXPAND_PROJECTION_ALONG_WITH_REMOVE_UNUSED)
+ ) { result ->
+ result.hasWarningContaining(ProcessorErrors.EXPAND_PROJECTION_ALONG_WITH_REMOVE_UNUSED)
+ result.hasWarningCount(1)
+ }
}
@Test
@@ -83,41 +91,44 @@
compile(
annotateDao = true,
enableExpandProjection = true
- ).withWarningCount(1)
- .withWarningContaining(ProcessorErrors.EXPAND_PROJECTION_ALONG_WITH_REMOVE_UNUSED)
+ ) { result ->
+ result.hasWarningContaining(
+ ProcessorErrors.EXPAND_PROJECTION_ALONG_WITH_REMOVE_UNUSED
+ )
+ result.hasWarningCount(1)
+ }
}
private fun compile(
annotateDb: Boolean = false,
annotateDao: Boolean = false,
annotateMethod: Boolean = false,
- enableExpandProjection: Boolean = false
- ): CompileTester.SuccessfulCompilationClause {
- val jfos = dao(
+ enableExpandProjection: Boolean = false,
+ validate: (CompilationResultSubject) -> Unit,
+ ) {
+ val sources = dao(
annotateDao = annotateDao,
annotateDb = annotateDb,
annotateMethod = annotateMethod
- ) + COMMON.USER
- return assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(jfos)
- .withCompilerOptions("-Xlint:-processing") // remove unclaimed annotation warnings
- .also {
- if (enableExpandProjection) {
- it.withCompilerOptions("-Aroom.expandProjection=true")
- }
- }
- .processedWith(RoomProcessor())
- .compilesWithoutError()
- .also {
- it.and()
- .generatesFileNamed(
- StandardLocation.CLASS_OUTPUT, "foo.bar", "MyDao_Impl.class"
- )
- .and()
- .generatesFileNamed(
- StandardLocation.CLASS_OUTPUT, "foo.bar", "MyDb_Impl.class"
- )
- }
+ ) + Source.fromJavaFileObject(COMMON.USER)
+
+ runProcessorTest(
+ sources = sources,
+ createProcessingStep = {
+ DatabaseProcessingStep()
+ },
+ options = mapOf(
+ "room.expandProjection" to enableExpandProjection.toString()
+ )
+ ) { result ->
+ validate(result)
+ result.generatedSourceFileWithPath(
+ "foo/bar/MyDao_Impl.java"
+ )
+ result.generatedSourceFileWithPath(
+ "foo/bar/MyDb_Impl.java"
+ )
+ }
}
companion object {
@@ -125,14 +136,14 @@
annotateDb: Boolean,
annotateDao: Boolean,
annotateMethod: Boolean
- ): List<JavaFileObject> {
+ ): List<Source> {
fun annotationText(enabled: Boolean) = if (enabled) {
"@${RewriteQueriesToDropUnusedColumns::class.java.canonicalName}"
} else {
""
}
- val pojo = JavaFileObjects.forSourceString(
+ val pojo = Source.java(
"foo.bar.Pojo",
"""
package foo.bar;
@@ -142,7 +153,7 @@
}
""".trimIndent()
)
- val dao = JavaFileObjects.forSourceString(
+ val dao = Source.java(
"foo.bar.MyDao",
"""
package foo.bar;
@@ -156,7 +167,7 @@
}
""".trimIndent()
)
- val db = JavaFileObjects.forSourceString(
+ val db = Source.java(
"foo.bar.MyDb",
"""
package foo.bar;
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
index 4621ccb..a105248 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
@@ -18,20 +18,18 @@
import COMMON
import androidx.room.Dao
+import androidx.room.compiler.processing.XMethodElement
+import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XTypeElement
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.ext.CommonTypeNames
import androidx.room.ext.GuavaUtilConcurrentTypeNames
import androidx.room.ext.RxJava2TypeNames
import androidx.room.ext.RxJava3TypeNames
-import androidx.room.compiler.processing.XMethodElement
-import androidx.room.compiler.processing.XType
-import androidx.room.compiler.processing.XTypeElement
-import androidx.room.testing.TestInvocation
-import androidx.room.testing.TestProcessor
+import androidx.room.testing.context
import androidx.room.vo.ShortcutMethod
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
import com.squareup.javapoet.ArrayTypeName
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.ParameterizedTypeName
@@ -39,8 +37,7 @@
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
-import toJFO
-import javax.tools.JavaFileObject
+import toSources
import kotlin.reflect.KClass
/**
@@ -70,10 +67,13 @@
@${annotation.java.canonicalName}
abstract public void foo();
"""
- ) { shortcut, _ ->
+ ) { shortcut, invocation ->
assertThat(shortcut.name, `is`("foo"))
assertThat(shortcut.parameters.size, `is`(0))
- }.failsToCompile().withErrorContaining(noParamsError())
+ invocation.assertCompilationResult {
+ hasErrorContaining(noParamsError())
+ }
+ }
}
abstract fun noParamsError(): String
@@ -94,7 +94,7 @@
assertThat(shortcut.entities.size, `is`(1))
assertThat(shortcut.entities["user"]?.isPartialEntity, `is`(false))
assertThat(shortcut.entities["user"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
- }.compilesWithoutError()
+ }
}
@Test
@@ -104,13 +104,16 @@
@${annotation.java.canonicalName}
abstract public void foo(NotAnEntity notValid);
"""
- ) { shortcut, _ ->
+ ) { shortcut, invocation ->
assertThat(shortcut.name, `is`("foo"))
assertThat(shortcut.parameters.size, `is`(1))
assertThat(shortcut.entities.size, `is`(0))
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER
- )
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER
+ )
+ }
+ }
}
@Test
@@ -135,7 +138,7 @@
shortcut.parameters.map { it.name },
`is`(listOf("u1", "u2"))
)
- }.compilesWithoutError()
+ }
}
@Test
@@ -171,7 +174,7 @@
assertThat(param.pojoType?.typeName, `is`(USER_TYPE_NAME))
assertThat(shortcut.entities.size, `is`(1))
assertThat(shortcut.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
- }.compilesWithoutError()
+ }
}
}
@@ -194,7 +197,7 @@
)
assertThat(shortcut.entities.size, `is`(1))
assertThat(shortcut.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
- }.compilesWithoutError()
+ }
}
@Test
@@ -219,7 +222,7 @@
)
assertThat(shortcut.entities.size, `is`(1))
assertThat(shortcut.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
- }.compilesWithoutError()
+ }
}
@Test
@@ -244,7 +247,7 @@
)
assertThat(shortcut.entities.size, `is`(1))
assertThat(shortcut.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
- }.compilesWithoutError()
+ }
}
@Test
@@ -270,7 +273,7 @@
)
assertThat(shortcut.entities.size, `is`(1))
assertThat(shortcut.entities["users"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
- }.compilesWithoutError()
+ }
}
@Test
@@ -306,7 +309,7 @@
assertThat(shortcut.entities.size, `is`(2))
assertThat(shortcut.entities["u1"]?.pojo?.typeName, `is`(USER_TYPE_NAME))
assertThat(shortcut.entities["b1"]?.pojo?.typeName, `is`(BOOK_TYPE_NAME))
- }.compilesWithoutError()
+ }
}
}
@@ -331,14 +334,19 @@
@${annotation.java.canonicalName}
abstract public $type foo(User user);
"""
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(invalidReturnTypeError())
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(invalidReturnTypeError())
+ }
+ }
}
}
@Test
fun targetEntity() {
- val usernameJfo = """
+ val usernameSource = Source.java(
+ "foo.bar.Username",
+ """
package foo.bar;
import androidx.room.*;
@@ -346,13 +354,14 @@
int uid;
String name;
}
- """.toJFO("foo.bar.Username")
+ """
+ )
singleShortcutMethod(
"""
@${annotation.java.canonicalName}(entity = User.class)
abstract public int foo(Username username);
""",
- additionalJFOs = listOf(usernameJfo)
+ additionalSources = listOf(usernameSource)
) { shortcut, _ ->
assertThat(shortcut.name, `is`("foo"))
assertThat(shortcut.parameters.size, `is`(1))
@@ -363,7 +372,7 @@
assertThat(shortcut.entities["username"]?.isPartialEntity, `is`(true))
assertThat(shortcut.entities["username"]?.entityTypeName, `is`(USER_TYPE_NAME))
assertThat(shortcut.entities["username"]?.pojo?.typeName, `is`(USERNAME_TYPE_NAME))
- }.compilesWithoutError()
+ }
}
@Test
@@ -374,12 +383,14 @@
abstract public int foo(User user);
"""
) { _, _ ->
- }.compilesWithoutError()
+ }
}
@Test
fun targetEntityExtraColumn() {
- val usernameJfo = """
+ val usernameSource = Source.java(
+ "foo.bar.Username",
+ """
package foo.bar;
import androidx.room.*;
@@ -388,22 +399,28 @@
String name;
long extraField;
}
- """.toJFO("foo.bar.Username")
+ """
+ )
singleShortcutMethod(
"""
@${annotation.java.canonicalName}(entity = User.class)
abstract public int foo(Username username);
""",
- additionalJFOs = listOf(usernameJfo)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.cannotFindAsEntityField("foo.bar.User")
- )
+ additionalSources = listOf(usernameSource)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.cannotFindAsEntityField("foo.bar.User")
+ )
+ }
+ }
}
@Test
fun targetEntityExtraColumnIgnored() {
- val usernameJfo = """
+ val usernameSource = Source.java(
+ "foo.bar.Username",
+ """
package foo.bar;
import androidx.room.*;
@@ -413,20 +430,23 @@
@Ignore
long extraField;
}
- """.toJFO("foo.bar.Username")
+ """
+ )
singleShortcutMethod(
"""
@${annotation.java.canonicalName}(entity = User.class)
abstract public int foo(Username username);
""",
- additionalJFOs = listOf(usernameJfo)
+ additionalSources = listOf(usernameSource)
) { _, _ ->
- }.compilesWithoutError()
+ }
}
@Test
fun targetEntityWithEmbedded() {
- val usernameJfo = """
+ val usernameSource = Source.java(
+ "foo.bar.Username",
+ """
package foo.bar;
import androidx.room.*;
@@ -435,8 +455,11 @@
@Embedded
Fullname name;
}
- """.toJFO("foo.bar.Username")
- val fullnameJfo = """
+ """
+ )
+ val fullnameSource = Source.java(
+ "foo.bar.Fullname",
+ """
package foo.bar;
import androidx.room.*;
@@ -445,20 +468,23 @@
String firstName;
String lastName;
}
- """.toJFO("foo.bar.Fullname")
+ """
+ )
singleShortcutMethod(
"""
@${annotation.java.canonicalName}(entity = User.class)
abstract public int foo(Username username);
""",
- additionalJFOs = listOf(usernameJfo, fullnameJfo)
+ additionalSources = listOf(usernameSource, fullnameSource)
) { _, _ ->
- }.compilesWithoutError()
+ }
}
@Test
fun targetEntityWithRelation() {
- val userPetsJfo = """
+ val userPetsSource = Source.java(
+ "foo.bar.UserPets",
+ """
package foo.bar;
import androidx.room.*;
import java.util.List;
@@ -468,8 +494,11 @@
@Relation(parentColumn = "uid", entityColumn = "ownerId")
List<Pet> pets;
}
- """.toJFO("foo.bar.UserPets")
- val petJfo = """
+ """
+ )
+ val petSource = Source.java(
+ "foo.bar.Pet",
+ """
package foo.bar;
import androidx.room.*;
@@ -479,15 +508,19 @@
int petId;
int ownerId;
}
- """.toJFO("foo.bar.Pet")
+ """
+ )
singleShortcutMethod(
"""
@${annotation.java.canonicalName}(entity = User.class)
abstract public int foo(UserPets userPets);
""",
- additionalJFOs = listOf(userPetsJfo, petJfo)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_RELATION_IN_PARTIAL_ENTITY)
+ additionalSources = listOf(userPetsSource, petSource)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(ProcessorErrors.INVALID_RELATION_IN_PARTIAL_ENTITY)
+ }
+ }
}
@Test
@@ -496,13 +529,52 @@
"""
@${annotation.java.canonicalName}(entity = User.class)
abstract public int foo(long x);
- """
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.shortcutMethodArgumentMustBeAClass(
- TypeName.LONG
- )
+ """,
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ if (invocation.isKsp) {
+ hasErrorContaining(
+ ProcessorErrors.noColumnsInPartialEntity(
+ "java.lang.Long"
+ )
+ )
+ } else {
+ // javac has a different error for primitives.
+ hasErrorContaining(
+ ProcessorErrors.shortcutMethodArgumentMustBeAClass(
+ TypeName.LONG
+ )
+ )
+ }
+ }
+ }
+ }
+
+ @Test
+ fun targetEntity_emptyClassParameter() {
+ val emptyClass = Source.java(
+ "foo.bar.EmptyClass",
+ """
+ package foo.bar;
+ public class EmptyClass {}
+ """.trimIndent()
)
+
+ singleShortcutMethod(
+ """
+ @${annotation.java.canonicalName}(entity = User.class)
+ abstract public int foo(EmptyClass x);
+ """,
+ additionalSources = listOf(emptyClass)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.noColumnsInPartialEntity(
+ "foo.bar.EmptyClass"
+ )
+ )
+ }
+ }
}
abstract fun invalidReturnTypeError(): String
@@ -515,47 +587,39 @@
fun singleShortcutMethod(
vararg input: String,
- additionalJFOs: List<JavaFileObject> = emptyList(),
- handler: (T, TestInvocation) -> Unit
- ):
- CompileTester {
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(
- listOf(
- JavaFileObjects.forSourceString(
- "foo.bar.MyClass",
- DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
- ),
- COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY, COMMON.RX2_COMPLETABLE,
- COMMON.RX2_MAYBE, COMMON.RX2_SINGLE, COMMON.RX3_COMPLETABLE,
- COMMON.RX3_MAYBE, COMMON.RX3_SINGLE, COMMON.LISTENABLE_FUTURE,
- COMMON.GUAVA_ROOM
- ) + additionalJFOs
- )
- .processedWith(
- TestProcessor.builder()
- .forAnnotations(annotation, Dao::class)
- .nextRunHandler { invocation ->
- val (owner, methods) = invocation.roundEnv
- .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
- .filterIsInstance<XTypeElement>()
- .map {
- Pair(
- it,
- it.getAllMethods().filter {
- it.hasAnnotation(annotation)
- }
- )
- }.first { it.second.isNotEmpty() }
- val processed = process(
- baseContext = invocation.context,
- containing = owner.type,
- executableElement = methods.first()
- )
- handler(processed, invocation)
- true
+ additionalSources: List<Source> = emptyList(),
+ handler: (T, XTestInvocation) -> Unit
+ ) {
+ val inputSource = Source.java(
+ "foo.bar.MyClass",
+ DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
+ )
+ val commonSources = listOf(
+ COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY, COMMON.RX2_COMPLETABLE,
+ COMMON.RX2_MAYBE, COMMON.RX2_SINGLE, COMMON.RX3_COMPLETABLE,
+ COMMON.RX3_MAYBE, COMMON.RX3_SINGLE, COMMON.LISTENABLE_FUTURE,
+ COMMON.GUAVA_ROOM
+ ).toSources()
+ runProcessorTest(
+ sources = commonSources + additionalSources + inputSource
+ ) { invocation ->
+ val (owner, methods) = invocation.roundEnv
+ .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
+ .filterIsInstance<XTypeElement>()
+ .map {
+ Pair(
+ it,
+ it.getAllMethods().filter {
+ it.hasAnnotation(annotation)
}
- .build()
- )
+ )
+ }.first { it.second.isNotEmpty() }
+ val processed = process(
+ baseContext = invocation.context,
+ containing = owner.type,
+ executableElement = methods.first()
+ )
+ handler(processed, invocation)
}
+ }
}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/UpdateMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/UpdateMethodProcessorTest.kt
index 88def3e..e2a264a 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/UpdateMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/UpdateMethodProcessorTest.kt
@@ -19,6 +19,7 @@
import androidx.room.Update
import androidx.room.compiler.processing.XMethodElement
import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.util.Source
import androidx.room.processor.ProcessorErrors.CANNOT_FIND_UPDATE_RESULT_ADAPTER
import androidx.room.processor.ProcessorErrors.UPDATE_MISSING_PARAMS
import androidx.room.vo.UpdateMethod
@@ -27,7 +28,6 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import toJFO
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
@RunWith(JUnit4::class)
@@ -53,7 +53,7 @@
"""
) { shortcut, _ ->
assertThat(shortcut.onConflictStrategy, `is`(OnConflictStrategy.REPLACE))
- }.compilesWithoutError()
+ }
}
@Test
@@ -63,32 +63,41 @@
@Update(onConflict = -1)
abstract public void foo(User user);
"""
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(ProcessorErrors.INVALID_ON_CONFLICT_VALUE)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(ProcessorErrors.INVALID_ON_CONFLICT_VALUE)
+ }
+ }
}
@Test
fun targetEntityMissingPrimaryKey() {
- val usernameJfo = """
+ val usernameSource = Source.java(
+ "foo.bar.Username",
+ """
package foo.bar;
import androidx.room.*;
public class Username {
String name;
}
- """.toJFO("foo.bar.Username")
+ """
+ )
singleShortcutMethod(
"""
@Update(entity = User.class)
abstract public int foo(Username username);
""",
- additionalJFOs = listOf(usernameJfo)
- ) { _, _ ->
- }.failsToCompile().withErrorContaining(
- ProcessorErrors.missingPrimaryKeysInPartialEntityForUpdate(
- partialEntityName = "foo.bar.Username",
- primaryKeyNames = listOf("uid")
- )
- )
+ additionalSources = listOf(usernameSource)
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.missingPrimaryKeysInPartialEntityForUpdate(
+ partialEntityName = "foo.bar.Username",
+ primaryKeyNames = listOf("uid")
+ )
+ )
+ }
+ }
}
}
diff --git a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt
index d2c8270..72cc697 100644
--- a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt
+++ b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt
@@ -69,6 +69,7 @@
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
+import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
@@ -394,6 +395,11 @@
)
}
+ @After
+ public fun tearDown() {
+ WatchFace.clearAllEditorDelegates()
+ }
+
@Test
public fun watchFaceComponentName() {
val scenario = createOnWatchFaceEditingTestActivity(emptyList(), emptyList())
@@ -1042,7 +1048,7 @@
}
@Test
- public fun closeEditorSessionBeforeWatchFaceDelegateCreated() {
+ public fun closeEditorSessionBeforeInitCompleted() {
val session: ActivityScenario<OnWatchFaceEditingTestActivity> = ActivityScenario.launch(
WatchFaceEditorContract().createIntent(
ApplicationProvider.getApplicationContext<Context>(),
@@ -1060,9 +1066,9 @@
}
)
- session.onActivity { activity ->
+ session.onActivity {
// This shouldn't throw an exception.
- activity.editorSession.close()
+ EditorService.globalEditorService.closeEditor()
}
}
}
diff --git a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
index 7e2dffe4..ac587a4 100644
--- a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
+++ b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
@@ -549,7 +549,7 @@
editorDelegate.onDestroy()
}
// Revert any changes to the UserStyle if needed.
- if (!commitChangesOnClose) {
+ if (!commitChangesOnClose && this::previousWatchFaceUserStyle.isInitialized) {
userStyle = previousWatchFaceUserStyle
}
}
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index 7a3ec19..928f1a8 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -238,6 +238,7 @@
public final class WatchFace {
ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer, optional androidx.wear.watchface.ComplicationsManager complicationsManager);
ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @UiThread @VisibleForTesting public static void clearAllEditorDelegates();
method public androidx.wear.watchface.style.CurrentUserStyleRepository getCurrentUserStyleRepository();
method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @UiThread public static kotlinx.coroutines.CompletableDeferred<androidx.wear.watchface.WatchFace.EditorDelegate> getOrCreateEditorDelegate(android.content.ComponentName componentName);
@@ -255,6 +256,7 @@
}
public static final class WatchFace.Companion {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @UiThread @VisibleForTesting public void clearAllEditorDelegates();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @UiThread public kotlinx.coroutines.CompletableDeferred<androidx.wear.watchface.WatchFace.EditorDelegate> getOrCreateEditorDelegate(android.content.ComponentName componentName);
method public boolean isLegacyWatchFaceOverlayStyleSupported();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @UiThread public void registerEditorDelegate(android.content.ComponentName componentName, androidx.wear.watchface.WatchFace.EditorDelegate editorDelegate);
diff --git a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
index afdb21f..f00e321 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
@@ -19,8 +19,14 @@
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.graphics.Canvas
import android.graphics.Color
+import android.graphics.Rect
+import android.icu.util.Calendar
+import android.os.Handler
+import android.os.Looper
import android.support.wearable.watchface.SharedMemoryImage
+import android.view.SurfaceHolder
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -28,8 +34,14 @@
import androidx.test.screenshot.assertAgainstGolden
import androidx.wear.complications.data.PlainComplicationText
import androidx.wear.complications.data.ShortTextComplicationData
+import androidx.wear.watchface.CanvasType
import androidx.wear.watchface.DrawMode
import androidx.wear.watchface.RenderParameters
+import androidx.wear.watchface.Renderer
+import androidx.wear.watchface.WatchFace
+import androidx.wear.watchface.WatchFaceService
+import androidx.wear.watchface.WatchFaceType
+import androidx.wear.watchface.WatchState
import androidx.wear.watchface.control.IHeadlessWatchFace
import androidx.wear.watchface.control.IWatchFaceControlService
import androidx.wear.watchface.control.WatchFaceControlService
@@ -43,18 +55,50 @@
import androidx.wear.watchface.samples.EXAMPLE_OPENGL_COMPLICATION_ID
import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
import androidx.wear.watchface.samples.ExampleOpenGLWatchFaceService
+import androidx.wear.watchface.style.CurrentUserStyleRepository
+import androidx.wear.watchface.style.UserStyleSchema
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.android.asCoroutineDispatcher
+import kotlinx.coroutines.withContext
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-private const val API_VERSION = 3
+// This service constructs a WatchFace with a task that's posted on the UI thread.
+internal class AsyncInitWithUiThreadTaskWatchFace : WatchFaceService() {
+ private val mainThreadCoroutineScope = CoroutineScope(
+ Handler(Looper.getMainLooper()).asCoroutineDispatcher()
+ )
+
+ override suspend fun createWatchFace(
+ surfaceHolder: SurfaceHolder,
+ watchState: WatchState
+ ): WatchFace = withContext(mainThreadCoroutineScope.coroutineContext) {
+ val currentUserStyleRepository =
+ CurrentUserStyleRepository(UserStyleSchema(emptyList()))
+ WatchFace(
+ WatchFaceType.DIGITAL,
+ CurrentUserStyleRepository(UserStyleSchema(emptyList())),
+ object : Renderer.CanvasRenderer(
+ surfaceHolder,
+ currentUserStyleRepository,
+ watchState,
+ CanvasType.SOFTWARE,
+ 16
+ ) {
+ override fun render(canvas: Canvas, bounds: Rect, calendar: Calendar) {}
+ }
+ )
+ }
+}
@RunWith(AndroidJUnit4::class)
@MediumTest
-class WatchFaceControlServiceTest {
+public class WatchFaceControlServiceTest {
@get:Rule
- val screenshotRule = AndroidXScreenshotTestRule("wear/wear-watchface")
+ internal val screenshotRule = AndroidXScreenshotTestRule("wear/wear-watchface")
private fun createInstance(width: Int, height: Int): IHeadlessWatchFace {
val instanceService = IWatchFaceControlService.Stub.asInterface(
@@ -109,7 +153,7 @@
}
@Test
- fun createHeadlessWatchFaceInstance() {
+ public fun createHeadlessWatchFaceInstance() {
val instance = createInstance(100, 100)
val bitmap = SharedMemoryImage.ashmemReadImageBundle(
instance.renderWatchFaceToBitmap(
@@ -152,7 +196,7 @@
}
@Test
- fun createHeadlessOpenglWatchFaceInstance() {
+ public fun createHeadlessOpenglWatchFaceInstance() {
val instance = createOpenGlInstance(400, 400)
val bitmap = SharedMemoryImage.ashmemReadImageBundle(
instance.renderWatchFaceToBitmap(
@@ -186,7 +230,7 @@
}
@Test
- fun testCommandTakeComplicationScreenShot() {
+ public fun testCommandTakeComplicationScreenShot() {
val instance = createInstance(400, 400)
val bitmap = SharedMemoryImage.ashmemReadImageBundle(
instance.renderComplicationToBitmap(
@@ -215,4 +259,34 @@
instance.release()
}
+
+ @Test
+ public fun asyncInitWithUiThreadTaskWatchFace() {
+ val instanceService = IWatchFaceControlService.Stub.asInterface(
+ WatchFaceControlService().apply {
+ setContext(ApplicationProvider.getApplicationContext<Context>())
+ }.onBind(
+ Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE)
+ )
+ )
+ // This shouldn't hang.
+ val headlessInstance = instanceService.createHeadlessWatchFaceInstance(
+ HeadlessWatchFaceInstanceParams(
+ ComponentName(
+ ApplicationProvider.getApplicationContext<Context>(),
+ AsyncInitWithUiThreadTaskWatchFace::class.java
+ ),
+ DeviceConfig(
+ false,
+ false,
+ 0,
+ 0
+ ),
+ 100,
+ 100
+ )
+ )
+
+ assertThat(headlessInstance.userStyleSchema.mSchema).isEmpty()
+ }
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index e541719..6bdb080 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -164,6 +164,15 @@
componentNameToEditorDelegate.remove(componentName)
}
+ /** @hide */
+ @JvmStatic
+ @UiThread
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @VisibleForTesting
+ public fun clearAllEditorDelegates() {
+ componentNameToEditorDelegate.clear()
+ }
+
/**
* For use by on watch face editors.
* @hide
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
index d0974e9..4be3d02 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
@@ -33,8 +33,9 @@
import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
import androidx.wear.watchface.editor.EditorService
-import androidx.wear.watchface.runOnHandlerWithTracing
+import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
import java.io.FileDescriptor
import java.io.PrintWriter
@@ -111,13 +112,19 @@
override fun createHeadlessWatchFaceInstance(
params: HeadlessWatchFaceInstanceParams
- ): IHeadlessWatchFace? = uiThreadHandler.runOnHandlerWithTracing(
+ ): IHeadlessWatchFace? = TraceEvent(
"IWatchFaceInstanceServiceStub.createHeadlessWatchFaceInstance"
- ) {
+ ).use {
val engine = createHeadlessEngine(params.watchFaceName, context)
engine?.let {
// This is serviced on a background thread so it should be fine to block.
- runBlocking { it.createHeadlessInstance(params) }
+ runBlocking {
+ // However the WatchFaceService.createWatchFace method needs to be run on a UI
+ // thread.
+ withContext(uiThreadHandler.asCoroutineDispatcher().immediate) {
+ it.createHeadlessInstance(params)
+ }
+ }
}
}