Merge "Move `generateAllEnumerations` to testutils-common" into androidx-main
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
index c25e29b..9d817be 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
@@ -73,6 +73,8 @@
static final String DB_NAME_1 = "";
static final String DB_NAME_2 = "testDb2";
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+
private AppSearchSession mDb1;
private AppSearchSession mDb2;
@@ -84,8 +86,6 @@
@Before
public void setUp() throws Exception {
- Context context = ApplicationProvider.getApplicationContext();
-
mDb1 = createSearchSession(DB_NAME_1).get();
mDb2 = createSearchSession(DB_NAME_2).get();
@@ -585,7 +585,7 @@
new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()).get();
assertThat(failResult2.isSuccess()).isFalse();
assertThat(failResult2.getFailures().get("email2").getErrorMessage())
- .isEqualTo("Schema type config 'androidx.appsearch.test$" + DB_NAME_1
+ .isEqualTo("Schema type config '" + mContext.getPackageName() + "$" + DB_NAME_1
+ "/builtin:Email' not found");
}
@@ -657,7 +657,7 @@
new PutDocumentsRequest.Builder().addGenericDocuments(email3).build()).get();
assertThat(failResult2.isSuccess()).isFalse();
assertThat(failResult2.getFailures().get("email3").getErrorMessage())
- .isEqualTo("Schema type config 'androidx.appsearch.test$" + DB_NAME_1
+ .isEqualTo("Schema type config '" + mContext.getPackageName() + "$" + DB_NAME_1
+ "/builtin:Email' not found");
// Make sure email in database 2 still present.
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
index 4c639a9..1e03d4e 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
@@ -752,7 +752,7 @@
AppSearchException ase = (AppSearchException) exception.getCause();
assertThat(ase.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
assertThat(ase).hasMessageThat().contains(
- "androidx.appsearch.test does not have access to report system usage");
+ mContext.getPackageName() + " does not have access to report system usage");
}
@Test
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
index 17698b8..6784803 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
@@ -111,9 +111,11 @@
* @throws UnsupportedOperationException if this feature is not available on this
* AppSearch implementation.
*/
+ // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER)
+ // @exportToFramework:endStrip()
void addObserver(
@NonNull String observedPackage,
@NonNull ObserverSpec spec,
@@ -137,9 +139,11 @@
* @throws UnsupportedOperationException if this feature is not available on this
* AppSearch implementation.
*/
+ // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.GLOBAL_SEARCH_SESSION_ADD_REMOVE_OBSERVER)
+ // @exportToFramework:endStrip()
void removeObserver(
@NonNull String observedPackage, @NonNull AppSearchObserverCallback observer);
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
index b9d71ad..34e14e2 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
@@ -433,9 +433,11 @@
* false.
* <!--@exportToFramework:else()-->
*/
+ // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)
+ // @exportToFramework:endStrip()
@NonNull
public MatchRange getSubmatchRange() {
checkSubmatchSupported();
@@ -461,9 +463,11 @@
* false.
* <!--@exportToFramework:else()-->
*/
+ // @exportToFramework:startStrip()
@RequiresFeature(
enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)
+ // @exportToFramework:endStrip()
@NonNull
public CharSequence getSubmatch() {
checkSubmatchSupported();
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/SurfaceOrientedMeteringPointFactoryTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/SurfaceOrientedMeteringPointFactoryTest.java
index 474ac38..4576db2 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/SurfaceOrientedMeteringPointFactoryTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/SurfaceOrientedMeteringPointFactoryTest.java
@@ -22,6 +22,7 @@
import android.content.Context;
import android.util.Rational;
+import android.util.Size;
import androidx.camera.core.AspectRatio;
import androidx.camera.core.CameraSelector;
@@ -114,7 +115,6 @@
assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK));
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
- .setTargetAspectRatio(AspectRatio.RATIO_4_3)
.setTargetName("ImageAnalysis")
.build();
CameraSelector cameraSelector =
@@ -123,10 +123,13 @@
CameraUseCaseAdapter camera = CameraUtil.createCameraAndAttachUseCase(mContext,
cameraSelector, imageAnalysis);
+ Size surfaceResolution = imageAnalysis.getAttachedSurfaceResolution();
+
SurfaceOrientedMeteringPointFactory factory = new SurfaceOrientedMeteringPointFactory(
WIDTH, HEIGHT, imageAnalysis);
MeteringPoint point = factory.createPoint(0f, 0f);
- assertThat(point.getSurfaceAspectRatio()).isEqualTo(new Rational(4, 3));
+ assertThat(point.getSurfaceAspectRatio()).isEqualTo(
+ new Rational(surfaceResolution.getWidth(), surfaceResolution.getHeight()));
InstrumentationRegistry.getInstrumentation().runOnMainSync(() ->
//TODO: The removeUseCases() call might be removed after clarifying the
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.java
index ed5d6dd..fe65573 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.java
@@ -16,12 +16,10 @@
package androidx.camera.camera2.internal;
-import static androidx.camera.camera2.internal.Camera2CameraImpl.StateCallback.CameraReopenMonitor.REOPEN_LIMIT_MS;
-import static androidx.camera.camera2.internal.Camera2CameraImpl.StateCallback.REOPEN_DELAY_MS;
-
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assume.assumeFalse;
+import static org.mockito.Mockito.mock;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
@@ -70,6 +68,12 @@
@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 21)
public final class Camera2CameraImplCameraReopenTest {
+ private static final int REOPEN_DELAY_MS =
+ Camera2CameraImpl.StateCallback.CameraReopenMonitor.REOPEN_DELAY_MS;
+ private static final int REOPEN_LIMIT_MS =
+ Camera2CameraImpl.StateCallback.CameraReopenMonitor.REOPEN_LIMIT_MS;
+ private static final int ACTIVE_REOPEN_DELAY_BASE_MS =
+ Camera2CameraImpl.StateCallback.CameraReopenMonitor.ACTIVE_REOPEN_DELAY_BASE_MS;
private static final int WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS = 2_000;
@@ -128,7 +132,8 @@
TimeUnit.MILLISECONDS)).isTrue();
// Wait for half the max reopen attempts to occur
- final int maxReopenAttempts = (int) Math.ceil(REOPEN_LIMIT_MS / (double) REOPEN_DELAY_MS);
+ final int maxReopenAttempts =
+ (int) Math.ceil(REOPEN_LIMIT_MS / (double) REOPEN_DELAY_MS);
assertThat(cameraOpenSemaphore.tryAcquire(maxReopenAttempts / 2, REOPEN_LIMIT_MS,
TimeUnit.MILLISECONDS)).isTrue();
@@ -221,6 +226,100 @@
awaitCameraOpen();
}
+ @Test
+ public void activeResuming_openCameraAfterMultipleReopening_whenCameraUnavailable()
+ throws Exception {
+ // Set up the camera
+ final FailCameraOpenCameraManagerImpl cameraManagerImpl =
+ new FailCameraOpenCameraManagerImpl();
+ setUpCamera(cameraManagerImpl);
+
+ // Set up camera open attempt listener
+ final Semaphore cameraOpenSemaphore = new Semaphore(0);
+ cameraManagerImpl.setOnCameraOpenAttemptListener(cameraOpenSemaphore::release);
+
+ // Enable active reopening which will open the camera even when camera is unavailable.
+ mCamera2CameraImpl.setActiveResumingMode(true);
+ // Try opening the camera. This will fail and trigger reopening attempts
+ mCamera2CameraImpl.open();
+ // make camera unavailable.
+ mCamera2CameraImpl.getCameraAvailability().onCameraUnavailable(mCameraId);
+ assertThat(cameraOpenSemaphore.tryAcquire(1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS,
+ TimeUnit.MILLISECONDS)).isTrue();
+
+ // Wait for half the max reopen attempts to occur
+ final int maxReopenAttempts =
+ (int) Math.ceil(ACTIVE_REOPEN_DELAY_BASE_MS / (double) 10000);
+ assertThat(cameraOpenSemaphore.tryAcquire(maxReopenAttempts / 2, 10000,
+ TimeUnit.MILLISECONDS)).isTrue();
+
+ // Allow camera opening to succeed
+ cameraManagerImpl.setShouldFailCameraOpen(false);
+
+ // Verify the camera opens
+ awaitCameraOpen();
+ }
+
+ @Test
+ public void activeResuming_reopenCamera_whenCameraInterrupted() throws Exception {
+ // Set up the camera
+ final FailCameraOpenCameraManagerImpl cameraManagerImpl =
+ new FailCameraOpenCameraManagerImpl();
+ setUpCamera(cameraManagerImpl);
+
+ // Set up camera open attempt listener
+ final Semaphore cameraOpenSemaphore = new Semaphore(0);
+ cameraManagerImpl.setOnCameraOpenAttemptListener(cameraOpenSemaphore::release);
+
+ // Open camera will succeed.
+ cameraManagerImpl.setShouldFailCameraOpen(false);
+ mCamera2CameraImpl.open();
+ assertThat(cameraOpenSemaphore.tryAcquire(1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS,
+ TimeUnit.MILLISECONDS)).isTrue();
+
+ // Emulate camera is interrupted by other client.
+ mCamera2CameraImpl.getCameraAvailability().onCameraUnavailable(mCameraId);
+ CameraDevice cameraDevice = mock(CameraDevice.class);
+ cameraManagerImpl.getDeviceStateCallback().onDisconnected(cameraDevice);
+ cameraManagerImpl.getDeviceStateCallback().onClosed(cameraDevice);
+
+ // Enable active resuming which will open the camera even when camera is unavailable.
+ mCamera2CameraImpl.setActiveResumingMode(true);
+
+ // Verify the camera opens
+ awaitCameraOpen();
+ }
+
+ @Test
+ public void activeResuming_doNotReopenCamera_whenCameraDeviceError() throws Exception {
+ // Set up the camera
+ final FailCameraOpenCameraManagerImpl cameraManagerImpl =
+ new FailCameraOpenCameraManagerImpl();
+ setUpCamera(cameraManagerImpl);
+
+ // Set up camera open attempt listener
+ final Semaphore cameraOpenSemaphore = new Semaphore(0);
+ cameraManagerImpl.setOnCameraOpenAttemptListener(cameraOpenSemaphore::release);
+
+ // Open camera will succeed.
+ cameraManagerImpl.setShouldFailCameraOpen(false);
+ mCamera2CameraImpl.open();
+ assertThat(cameraOpenSemaphore.tryAcquire(1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS,
+ TimeUnit.MILLISECONDS)).isTrue();
+
+ mCamera2CameraImpl.setActiveResumingMode(true);
+ // make camera unavailable.
+ mCamera2CameraImpl.getCameraAvailability().onCameraUnavailable(mCameraId);
+ CameraDevice cameraDevice = mock(CameraDevice.class);
+ cameraManagerImpl.getDeviceStateCallback().onError(cameraDevice,
+ CameraDevice.StateCallback.ERROR_CAMERA_DEVICE);
+ cameraManagerImpl.getDeviceStateCallback().onClosed(cameraDevice);
+
+ // 2nd camera open should not happen
+ assertThat(cameraOpenSemaphore.tryAcquire(1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS,
+ TimeUnit.MILLISECONDS)).isFalse();
+ }
+
private void setUpCamera(@NonNull final FailCameraOpenCameraManagerImpl cameraManagerImpl)
throws CameraAccessExceptionCompat, CameraUnavailableException {
// Build camera manager wrapper
@@ -288,6 +387,8 @@
private OnCameraOpenAttemptListener mOnCameraOpenAttemptListener;
@GuardedBy("mLock")
private boolean mShouldFailCameraOpen = true;
+ @GuardedBy("mLock")
+ private CameraDevice.StateCallback mDeviceStateCallback;
FailCameraOpenCameraManagerImpl() {
mForwardCameraManagerCompatImpl = CameraManagerCompatImpl.from(
@@ -326,10 +427,18 @@
return mForwardCameraManagerCompatImpl.getCameraManager();
}
+ @NonNull
+ public CameraDevice.StateCallback getDeviceStateCallback() {
+ synchronized (mLock) {
+ return mDeviceStateCallback;
+ }
+ }
+
@Override
public void openCamera(@NonNull String cameraId, @NonNull Executor executor,
@NonNull CameraDevice.StateCallback callback) throws CameraAccessExceptionCompat {
synchronized (mLock) {
+ mDeviceStateCallback = callback;
if (mOnCameraOpenAttemptListener != null) {
mOnCameraOpenAttemptListener.onCameraOpenAttempt();
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index 82758a7..4d1eb81 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
@@ -182,6 +182,7 @@
@GuardedBy("mLock")
@Nullable
private SessionProcessor mSessionProcessor;
+ boolean mIsActiveResumingMode = false;
/**
* Constructor for a camera.
@@ -263,7 +264,7 @@
switch (mState) {
case INITIALIZED:
case PENDING_OPEN:
- tryForceOpenCameraDevice();
+ tryForceOpenCameraDevice(/*fromScheduledCameraReopen*/false);
break;
case CLOSING:
setState(InternalState.REOPENING);
@@ -951,10 +952,14 @@
* Attempts to force open the camera device, which may result in stealing it from a lower
* priority client. This should only happen if another client doesn't close the camera when
* it should, e.g. when its process is moved to the background.
+ *
+ * @param fromScheduledCameraReopen True if the attempt to open the camera originated from a
+ * {@linkplain StateCallback.ScheduledReopen scheduled
+ * reopen of the camera}. False otherwise.
*/
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ExecutedBy("mExecutor")
- void tryForceOpenCameraDevice() {
+ void tryForceOpenCameraDevice(boolean fromScheduledCameraReopen) {
debugLog("Attempting to force open the camera.");
final boolean shouldTryOpenCamera = mCameraStateRegistry.tryOpenCamera(this);
if (!shouldTryOpenCamera) {
@@ -962,12 +967,12 @@
setState(InternalState.PENDING_OPEN);
return;
}
- openCameraDevice(false);
+ openCameraDevice(fromScheduledCameraReopen);
}
/**
- * Attempts to open the camera device. Unlike {@link #tryForceOpenCameraDevice()}, this method
- * does not steal the camera away from other clients.
+ * Attempts to open the camera device. Unlike {@link #tryForceOpenCameraDevice(boolean)},
+ * this method does not steal the camera away from other clients.
*
* @param fromScheduledCameraReopen True if the attempt to open the camera originated from a
* {@linkplain StateCallback.ScheduledReopen scheduled
@@ -987,6 +992,23 @@
openCameraDevice(fromScheduledCameraReopen);
}
+ @Override
+ public void setActiveResumingMode(boolean enabled) {
+ mExecutor.execute(() -> {
+ // Enables/Disables active resuming mode which will reopen the camera regardless of the
+ // availability when camera is interrupted.
+ mIsActiveResumingMode = enabled;
+
+ // If camera is interrupted currently, force open the camera right now regardless of the
+ // camera availability.
+ if (enabled && (mState == InternalState.PENDING_OPEN
+ || mState == InternalState.REOPENING)) {
+ tryForceOpenCameraDevice(/*fromScheduledCameraReopen*/false);
+ }
+ });
+ }
+
+
/**
* Opens the camera device.
*
@@ -1469,11 +1491,6 @@
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
final class StateCallback extends CameraDevice.StateCallback {
-
- // Delay long enough to guarantee the app could have been backgrounded.
- // See ProcessLifecycleProvider for where this delay comes from.
- static final int REOPEN_DELAY_MS = 700;
-
@CameraExecutor
private final Executor mExecutor;
private final ScheduledExecutorService mScheduler;
@@ -1495,6 +1512,7 @@
debugLog("CameraDevice.onOpened()");
mCameraDevice = cameraDevice;
mCameraDeviceError = ERROR_NONE;
+ resetReopenMonitor();
switch (mState) {
case CLOSING:
case RELEASING:
@@ -1654,15 +1672,16 @@
if (mCameraReopenMonitor.canScheduleCameraReopen()) {
mScheduledReopenRunnable = new ScheduledReopen(mExecutor);
- debugLog("Attempting camera re-open in " + REOPEN_DELAY_MS + "ms: "
- + mScheduledReopenRunnable);
+ debugLog("Attempting camera re-open in "
+ + mCameraReopenMonitor.getReopenDelayMs() + "ms: "
+ + mScheduledReopenRunnable + " activeResuming = " + mIsActiveResumingMode);
mScheduledReopenHandle = mScheduler.schedule(mScheduledReopenRunnable,
- REOPEN_DELAY_MS, TimeUnit.MILLISECONDS);
+ mCameraReopenMonitor.getReopenDelayMs(), TimeUnit.MILLISECONDS);
} else {
// TODO(b/174685338): Report camera opening error to the user
Logger.e(TAG,
"Camera reopening attempted for "
- + CameraReopenMonitor.REOPEN_LIMIT_MS
+ + mCameraReopenMonitor.getReopenLimitMs()
+ "ms without success.");
// Set the state to PENDING_OPEN, so that an attempt to reopen the camera is made if
@@ -1737,30 +1756,86 @@
// this is still the scheduled reopen.
if (!mCancelled) {
Preconditions.checkState(mState == InternalState.REOPENING);
- tryOpenCameraDevice(/*fromScheduledCameraReopen=*/true);
+ if (shouldActiveResume()) {
+ // Ignore the camera availability when in active resuming mode.
+ tryForceOpenCameraDevice(/*fromScheduledCameraReopen*/true);
+ } else {
+ tryOpenCameraDevice(/*fromScheduledCameraReopen=*/true);
+ }
}
});
}
}
- /** Keeps track of camera reopen attempts in order to limit them. */
+ /**
+ * Enables active resume only when camera is stolen by other apps.
+ */
+ boolean shouldActiveResume() {
+ return mIsActiveResumingMode && (mCameraDeviceError != ERROR_CAMERA_DEVICE
+ && mCameraDeviceError != ERROR_MAX_CAMERAS_IN_USE);
+ }
+
+ /**
+ * Keeps track of camera reopen attempts in order to limit them.
+ *
+ * When in active resuming mode, it will periodically retry opening the camera regardless
+ * of the camera availability.
+ * Elapsed time <= 2 minutes -> retry once per 1 second.
+ * Elapsed time 2 to 5 minutes -> retry once per 2 seconds.
+ * Elapsed time > 5 minutes -> retry once per 4 seconds.
+ * Retry will stop after 30 minutes.
+ *
+ * When not in active resuming mode, it will reopen in every 700ms within the 10 seconds
+ * limit. However, if the camera is unavailable the retry will stop immediately until it
+ * becomes available.
+ */
class CameraReopenMonitor {
+ // Delay long enough to guarantee the app could have been backgrounded.
+ // See ProcessLifecycleOwner for where this delay comes from.
+ static final int REOPEN_DELAY_MS = 700;
// Time limit since the first camera reopen attempt after which reopening the camera
// should no longer be attempted.
static final int REOPEN_LIMIT_MS = 10_000;
+ static final int ACTIVE_REOPEN_DELAY_BASE_MS = 1000;
+ static final int ACTIVE_REOPEN_LIMIT_MS = 30 * 60 * 1000; // 30 minutes
static final int INVALID_TIME = -1;
private long mFirstReopenTime = INVALID_TIME;
- boolean canScheduleCameraReopen() {
- final long now = SystemClock.uptimeMillis();
+ int getReopenDelayMs() {
+ if (!shouldActiveResume()) {
+ return REOPEN_DELAY_MS;
+ } else {
+ long elapsedTime = getElapsedTime();
+ if (elapsedTime <= 2 * 60 * 1000) { // <= 2 minutes
+ return ACTIVE_REOPEN_DELAY_BASE_MS;
+ } else if (elapsedTime <= 5 * 60 * 1000) { // <= 5 minutes
+ return ACTIVE_REOPEN_DELAY_BASE_MS * 2;
+ } else { // > 5 minutes
+ return ACTIVE_REOPEN_DELAY_BASE_MS * 4;
+ }
+ }
+ }
+ int getReopenLimitMs() {
+ if (!shouldActiveResume()) {
+ return REOPEN_LIMIT_MS;
+ } else {
+ return ACTIVE_REOPEN_LIMIT_MS;
+ }
+ }
+
+ long getElapsedTime() {
+ final long now = SystemClock.uptimeMillis();
// If it's the first attempt to reopen the camera
if (mFirstReopenTime == INVALID_TIME) {
mFirstReopenTime = now;
- return true;
}
- final boolean hasReachedLimit = now - mFirstReopenTime >= REOPEN_LIMIT_MS;
+ return now - mFirstReopenTime;
+ }
+
+ boolean canScheduleCameraReopen() {
+ final boolean hasReachedLimit = getElapsedTime() >= getReopenLimitMs();
// If the limit has been reached, prevent further attempts to reopen the camera,
// and reset [firstReopenTime].
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java
index 5987245..25ebd61 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java
@@ -120,6 +120,17 @@
void close();
/**
+ * When in active resuming mode, it will actively retry opening the camera periodically to
+ * resume regardless of the camera availability if the camera is interrupted in
+ * OPEN/OPENING/PENDING_OPEN state.
+ *
+ * When not in active resuming mode, it will retry opening camera only when camera
+ * becomes available.
+ */
+ default void setActiveResumingMode(boolean enabled) {
+ }
+
+ /**
* Release the camera.
*
* <p>Once the camera is released it is permanently closed. A new instance must be created to
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index bdc3f0f8..88dc4db2 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -320,6 +320,19 @@
}
}
+
+ /**
+ * When in active resuming mode, it will actively retry opening the camera periodically to
+ * resume regardless of the camera availability if the camera is interrupted in
+ * OPEN/OPENING/PENDING_OPEN state.
+ *
+ * When not in actively resuming mode, it will retry opening camera only when camera
+ * becomes available.
+ */
+ public void setActiveResumingMode(boolean enabled) {
+ mCameraInternal.setActiveResumingMode(enabled);
+ }
+
/**
* Detach the UseCases from the {@link CameraInternal} so that the UseCases stop receiving data.
*
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCamera.java b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCamera.java
index 1c86571..f3f56b7 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCamera.java
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCamera.java
@@ -16,6 +16,8 @@
package androidx.camera.lifecycle;
+import android.os.Build;
+
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -105,6 +107,23 @@
}
}
+ @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+ public void onResume(LifecycleOwner lifecycleOwner) {
+ // ActiveResumingMode is required for Multi-window which is supported since Android 7(N).
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ mCameraUseCaseAdapter.setActiveResumingMode(true);
+ }
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+ public void onPause(LifecycleOwner lifecycleOwner) {
+ // ActiveResumingMode is required for Multi-window which is supported since Android 7(N).
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ mCameraUseCaseAdapter.setActiveResumingMode(false);
+ }
+ }
+
+
/**
* Suspend the camera so that it ignore lifecycle events.
*
diff --git a/car/app/app-samples/navigation/common/build.gradle b/car/app/app-samples/navigation/common/build.gradle
index 75374b4..997d94a 100644
--- a/car/app/app-samples/navigation/common/build.gradle
+++ b/car/app/app-samples/navigation/common/build.gradle
@@ -28,7 +28,7 @@
dependencies {
implementation(project(":car:app:app"))
- implementation 'androidx.core:core:1.5.0-alpha01'
+ implementation 'androidx.core:core:1.7.0'
implementation project(path: ':annotation:annotation-experimental')
// There is an implicit compile-only dep due to :annotation-experimental
// Build will complain without this manual declaration.
diff --git a/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/NavigationSession.java b/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/NavigationSession.java
index 75a4d23..14dcab9 100644
--- a/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/NavigationSession.java
+++ b/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/NavigationSession.java
@@ -25,10 +25,8 @@
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.location.Location;
-import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
-import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
@@ -49,6 +47,7 @@
import androidx.car.app.sample.navigation.common.model.Instruction;
import androidx.car.app.sample.navigation.common.nav.NavigationService;
import androidx.core.graphics.drawable.IconCompat;
+import androidx.core.location.LocationListenerCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
@@ -106,27 +105,8 @@
};
// A listener to periodically update the surface with the location coordinates
- LocationListener mLocationListener =
- new LocationListener() {
- @Override
- public void onLocationChanged(Location location) {
- mNavigationCarSurface.updateLocationString(getLocationString(location));
- }
-
- /** @deprecated This callback will never be invoked on Android Q and above. */
- @Override
- @Deprecated
- public void onStatusChanged(String provider, int status, Bundle extras) {
- }
-
- @Override
- public void onProviderEnabled(String provider) {
- }
-
- @Override
- public void onProviderDisabled(String provider) {
- }
- };
+ LocationListenerCompat mLocationListener =
+ location -> mNavigationCarSurface.updateLocationString(getLocationString(location));
// Monitors the state of the connection to the Navigation service.
final ServiceConnection mServiceConnection =
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PlaceListTemplateBrowseDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PlaceListTemplateBrowseDemoScreen.java
index 784ca36..cac11e3 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PlaceListTemplateBrowseDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PlaceListTemplateBrowseDemoScreen.java
@@ -21,7 +21,6 @@
import android.content.pm.PackageManager;
import android.location.Location;
-import android.location.LocationListener;
import android.location.LocationManager;
import android.os.HandlerThread;
@@ -37,6 +36,7 @@
import androidx.car.app.model.PlaceListMapTemplate;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
+import androidx.core.location.LocationListenerCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
@@ -50,7 +50,7 @@
private static final int LOCATION_UPDATE_MIN_INTERVAL_MILLIS = 1000;
private static final int LOCATION_UPDATE_MIN_DISTANCE_METER = 1;
- final LocationListener mLocationListener;
+ final LocationListenerCompat mLocationListener;
final HandlerThread mLocationUpdateHandlerThread;
@Nullable
diff --git a/car/app/app/src/main/java/androidx/car/app/AppManager.java b/car/app/app/src/main/java/androidx/car/app/AppManager.java
index 7f69c27..9c2561e 100644
--- a/car/app/app/src/main/java/androidx/car/app/AppManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/AppManager.java
@@ -25,7 +25,6 @@
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
-import android.location.LocationListener;
import android.location.LocationManager;
import android.os.HandlerThread;
import android.os.Looper;
@@ -37,6 +36,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.car.app.managers.Manager;
import androidx.car.app.utils.RemoteUtils;
+import androidx.core.location.LocationListenerCompat;
import androidx.lifecycle.Lifecycle;
/** Manages the communication between the app and the host. */
@@ -54,13 +54,12 @@
private final Lifecycle mLifecycle;
/**
- * {@link LocationListener} for getting location updates within the app and sends them over to
- * the car host.
+ * Listener for getting location updates within the app and sends them over to the car host.
*
* <p>This should only be enabled when the car host explicitly calls {@code IAppManager
* .startLocationUpdates}.
*/
- private final LocationListener mLocationListener;
+ private final LocationListenerCompat mLocationListener;
@VisibleForTesting
final HandlerThread mLocationUpdateHandlerThread;
@@ -230,14 +229,12 @@
};
mLocationUpdateHandlerThread = new HandlerThread("LocationUpdateThread");
- mLocationListener = location -> {
- mHostDispatcher.dispatch(
- CarContext.APP_SERVICE,
- "sendLocation", (IAppHost host) -> {
- host.sendLocation(location);
- return null;
- }
- );
- };
+ mLocationListener = location -> mHostDispatcher.dispatch(
+ CarContext.APP_SERVICE,
+ "sendLocation", (IAppHost host) -> {
+ host.sendLocation(location);
+ return null;
+ }
+ );
}
}
diff --git a/car/app/app/src/main/java/androidx/car/app/CarContext.java b/car/app/app/src/main/java/androidx/car/app/CarContext.java
index dea577a..23a392c 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarContext.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarContext.java
@@ -291,7 +291,8 @@
* @param intent the {@link Intent} to send to the target application
* @throws SecurityException if the app attempts to start a different app explicitly or
* does not have permissions for the requested action
- * @throws HostException if the remote call fails
+ * @throws HostException if the remote call fails. For example, if the intent cannot be
+ * handled by the car host.
* @throws NullPointerException if {@code intent} is {@code null}
*/
public void startCarApp(@NonNull Intent intent) {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
index 58e5c02..90b4b3b 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
@@ -69,6 +69,7 @@
open val liveLiteralsV2Enabled get() = false
open val generateFunctionKeyMetaClasses get() = false
open val sourceInformationEnabled get() = true
+ open val intrinsicRememberEnabled get() = false
open val decoysEnabled get() = false
open val metricsDestination: String? get() = null
@@ -77,7 +78,7 @@
liveLiteralsV2Enabled,
generateFunctionKeyMetaClasses,
sourceInformationEnabled,
- intrinsicRememberEnabled = true,
+ intrinsicRememberEnabled,
decoysEnabled,
metricsDestination
)
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
index becc6b8..e9f9486 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
@@ -184,8 +184,7 @@
}
) { }
fun Foo() {
- Bar(
- )
+ Bar()
}
"""
)
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
index 9953eff..e9a6612 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
@@ -1453,8 +1453,7 @@
a = 0
}
if (%default and 0b0010 !== 0) {
- b = Foo(
- )
+ b = Foo()
%dirty = %dirty and 0b01110000.inv()
}
} else {
@@ -1521,8 +1520,7 @@
%composer.startDefaults()
if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
if (%default and 0b0001 !== 0) {
- b = Foo(
- )
+ b = Foo()
%dirty = %dirty and 0b01110000.inv()
}
} else {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationRegressionTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationRegressionTests.kt
new file mode 100644
index 0000000..bb18ad9
--- /dev/null
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationRegressionTests.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.compiler.plugins.kotlin
+
+import org.junit.Test
+
+/**
+ * This test merely ensures that code gen changes are evaluated against potentially
+ * breaking Android Studio compose debugger integration, see change id
+ * I63ce10791fc3795a568f5f09ca6a24e801f5e3da
+ *
+ * The Android Studio debugger searches for `ComposableSingletons` classes by name.
+ * Any changes to the naming scheme have to be reflected in the Android Studio code.
+ */
+class LambdaMemoizationRegressionTests : ComposeIrTransformTest() {
+ @Test
+ fun testNestedComposableSingletonsClass() = verifyComposeIrTransform(
+ """
+ import androidx.compose.runtime.Composable
+
+ class A {
+ val x = @Composable {}
+ }
+ """,
+ """
+ @StabilityInferred(parameters = 0)
+ class A {
+ val x: Function2<Composer, Int, Unit> = ComposableSingletons%TestKt.lambda-1
+ static val %stable: Int = 0
+ }
+ internal object ComposableSingletons%TestKt {
+ val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
+ sourceInformation(%composer, "C:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ Unit
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testNestedComposableSingletonsClass2() = verifyComposeIrTransform(
+ """
+ import androidx.compose.runtime.Composable
+
+ class A {
+ class B {
+ val x = @Composable {}
+ }
+ }
+ """,
+ """
+ @StabilityInferred(parameters = 0)
+ class A {
+ @StabilityInferred(parameters = 0)
+ class B {
+ val x: Function2<Composer, Int, Unit> = ComposableSingletons%TestKt.lambda-1
+ static val %stable: Int = 0
+ }
+ static val %stable: Int = 0
+ }
+ internal object ComposableSingletons%TestKt {
+ val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
+ sourceInformation(%composer, "C:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ Unit
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testJvmNameComposableSingletons() = verifyComposeIrTransform(
+ """
+ @file:JvmName("A")
+ import androidx.compose.runtime.Composable
+
+ val x = @Composable {}
+ """,
+ """
+ val x: Function2<Composer, Int, Unit> = ComposableSingletons%TestKt.lambda-1
+ internal object ComposableSingletons%TestKt {
+ val lambda-1: Function2<Composer, Int, Unit> = composableLambdaInstance(<>, false) { %composer: Composer?, %changed: Int ->
+ sourceInformation(%composer, "C:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ Unit
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ }
+ }
+ """
+ )
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
index 77ca5c9..e2deb3c 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
@@ -19,7 +19,6 @@
import org.junit.Test
class LambdaMemoizationTransformTests : ComposeIrTransformTest() {
-
@Test
fun testCapturedThisFromFieldInitializer(): Unit = verifyComposeIrTransform(
"""
@@ -184,7 +183,7 @@
{
Local().something(2)
}
- }, %composer, 0)
+ }, %composer, 0b1110 and %changed or 0b01110000 and %changed)
%composer.endReplaceableGroup()
}
""",
@@ -220,7 +219,7 @@
{
Foo(1)
}
- }, %composer, 0)
+ }, %composer, 0b1110 and %changed)
%composer.endReplaceableGroup()
}
""",
@@ -654,7 +653,7 @@
{
println("Captures a" + a)
}
- }, %composer, 0), %composer, 0)
+ }, %composer, 0b1110 and %dirty), %composer, 0)
} else {
%composer.skipToGroupEnd()
}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
index b6b2d95..aeab392 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
@@ -21,6 +21,9 @@
import org.junit.Test
class RememberIntrinsicTransformTests : ComposeIrTransformTest() {
+ override val intrinsicRememberEnabled: Boolean
+ get() = true
+
private fun comparisonPropagation(
@Language("kotlin")
unchecked: String,
@@ -792,7 +795,7 @@
}
if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
val a = InlineInt(123)
- val foo = %composer.cache(%dirty and 0b1110 === 0b0100 or false) {
+ val foo = %composer.cache(%dirty and 0b1110 === 0b0100) {
val tmp0_return = Foo()
tmp0_return
}
@@ -918,6 +921,139 @@
"""
)
+ @Test
+ fun testRememberMemoizedLambda(): Unit = comparisonPropagation(
+ "",
+ """
+ @Composable
+ fun Test(a: Int) {
+ used { a }
+ }
+ """,
+ """
+ @Composable
+ fun Test(a: Int, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test):Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ used(%composer.cache(%dirty and 0b1110 === 0b0100) {
+ {
+ a
+ }
+ }
+ )
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(a, %composer, %changed or 0b0001)
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testRememberFunctionReference(): Unit = comparisonPropagation(
+ """
+ fun effect(): Int = 0
+ """,
+ """
+ @Composable
+ fun Test(a: Int) {
+ used(remember(a, ::effect))
+ }
+ """,
+ """
+ @Composable
+ fun Test(a: Int, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test):Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ used(%composer.cache(%dirty and 0b1110 === 0b0100, effect))
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(a, %composer, %changed or 0b0001)
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testRememberAdaptedFunctionReference(): Unit = comparisonPropagation(
+ """
+ fun effect(a: Int = 0): Int = a
+ """,
+ """
+ @Composable
+ fun Test(a: Int) {
+ used(remember(a, ::effect))
+ }
+ """,
+ """
+ @Composable
+ fun Test(a: Int, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test):Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ used(%composer.cache(%dirty and 0b1110 === 0b0100, {
+ effect()
+ }
+ ))
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(a, %composer, %changed or 0b0001)
+ }
+ }
+ """
+ )
+ @Test
+ fun testRememberPropertyReference(): Unit = comparisonPropagation(
+ """
+ class A(val value: Int)
+ """.trimIndent(),
+ """
+ @Composable
+ fun Test(a: A) {
+ used(remember(a, a::value))
+ }
+ """,
+ """
+ @Composable
+ fun Test(a: A, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test):Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ used(%composer.cache(%dirty and 0b1110 === 0b0100, a::value))
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(a, %composer, %changed or 0b0001)
+ }
+ }
+ """
+ )
+
@Ignore("This test must pass before intrinsic remember can be turned on")
fun xtestOptimizationFailsIfDefaultsGroupIsUsed(): Unit = comparisonPropagation(
"""
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt
index 1b21477..4bae40f 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt
@@ -21,7 +21,7 @@
)
}
- fun doTest(text: String, expectPass: Boolean) {
+ private fun doTest(text: String, expectPass: Boolean) {
val disposable = TestDisposable()
val classPath = createClasspath()
val configuration = newConfiguration()
@@ -53,11 +53,11 @@
class ExpectedFailureException(message: String) : Exception(message)
- fun check(expectedText: String) {
+ private fun check(expectedText: String) {
doTest(expectedText, true)
}
- fun checkFail(expectedText: String) {
+ private fun checkFail(expectedText: String) {
doTest(expectedText, false)
}
@@ -114,6 +114,47 @@
"""
)
+ fun testCinNoinlineNCLambdaArg() = check(
+ """
+ import androidx.compose.runtime.*
+ @Composable fun C() { }
+ <!NOTHING_TO_INLINE!>inline<!> fun NoinlineNC(noinline lambda: () -> Unit) { lambda() }
+ @Composable fun C3() {
+ NoinlineNC {
+ <!COMPOSABLE_INVOCATION!>C<!>()
+ }
+ }
+ """
+ )
+
+ fun testCinCrossinlineNCLambdaArg() = check(
+ """
+ import androidx.compose.runtime.*
+ @Composable fun C() { }
+ inline fun CrossinlineNC(crossinline lambda: () -> Unit) { lambda() }
+ @Composable fun C3() {
+ CrossinlineNC {
+ <!COMPOSABLE_INVOCATION!>C<!>()
+ }
+ }
+ """
+ )
+
+ fun testCinNestedInlinedNCLambdaArg() = check(
+ """
+ import androidx.compose.runtime.*
+ @Composable fun C() { }
+ inline fun InlineNC(lambda: () -> Unit) { lambda() }
+ @Composable fun C3() {
+ InlineNC {
+ InlineNC {
+ C()
+ }
+ }
+ }
+ """
+ )
+
fun testCinLambdaArgOfNC() = check(
"""
import androidx.compose.runtime.*
@@ -178,6 +219,75 @@
"""
)
+ fun testCfromComposableFunInterface() = check(
+ """
+ import androidx.compose.runtime.Composable
+
+ fun interface A { @Composable fun f() }
+ @Composable fun B() { A { B() } }
+ """
+ )
+
+ fun testCfromAnnotatedComposableFunInterface() = check(
+ """
+ import androidx.compose.runtime.Composable
+
+ fun interface A { @Composable fun f() }
+ @Composable fun B() {
+ val f = @Composable { B() }
+ A(f)
+ }
+ """
+ )
+
+ fun testCfromComposableFunInterfaceArgument() = check(
+ """
+ import androidx.compose.runtime.Composable
+
+ fun interface A { @Composable fun f() }
+
+ @Composable fun B(a: (A) -> Unit) { a { B(a) } }
+ """
+ )
+
+ fun testCfromComposableTypeAliasFunInterface() = check(
+ """
+ import androidx.compose.runtime.Composable
+
+ fun interface A { @Composable fun f() }
+ typealias B = A
+
+ @Composable fun C() { A { C() } }
+ """
+ )
+
+ fun testCfromNonComposableFunInterface() = check(
+ """
+ import androidx.compose.runtime.Composable
+
+ fun interface A { fun f() }
+ @Composable fun B() {
+ A {
+ <!COMPOSABLE_INVOCATION!>B<!>()
+ }
+ }
+ """
+ )
+
+ fun testCfromNonComposableFunInterfaceArgument() = check(
+ """
+ import androidx.compose.runtime.Composable
+
+ fun interface A { fun f() }
+
+ @Composable fun B(a: (A) -> Unit) {
+ a {
+ <!COMPOSABLE_INVOCATION!>B<!>(a)
+ }
+ }
+ """
+ )
+
fun testPreventedCaptureOnInlineLambda() = check(
"""
import androidx.compose.runtime.*
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableCallChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableCallChecker.kt
index b21473c..2f3183b 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableCallChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableCallChecker.kt
@@ -18,6 +18,7 @@
import androidx.compose.compiler.plugins.kotlin.analysis.ComposeWritableSlices
import com.intellij.psi.PsiElement
+import org.jetbrains.kotlin.builtins.isBuiltinFunctionalType
import org.jetbrains.kotlin.container.StorageComponentContainer
import org.jetbrains.kotlin.container.useInstance
import org.jetbrains.kotlin.descriptors.CallableDescriptor
@@ -28,6 +29,7 @@
import org.jetbrains.kotlin.descriptors.PropertyGetterDescriptor
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
import org.jetbrains.kotlin.descriptors.impl.LocalVariableDescriptor
+import org.jetbrains.kotlin.descriptors.synthetic.FunctionInterfaceConstructorDescriptor
import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor
import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi
import org.jetbrains.kotlin.platform.TargetPlatform
@@ -51,13 +53,11 @@
import org.jetbrains.kotlin.resolve.calls.checkers.AdditionalTypeChecker
import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker
import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext
+import org.jetbrains.kotlin.resolve.calls.context.CallPosition
import org.jetbrains.kotlin.resolve.calls.context.ResolutionContext
import org.jetbrains.kotlin.resolve.calls.model.ArgumentMatch
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.calls.model.VariableAsFunctionResolvedCall
-import org.jetbrains.kotlin.resolve.inline.InlineUtil.canBeInlineArgument
-import org.jetbrains.kotlin.resolve.inline.InlineUtil.isInline
-import org.jetbrains.kotlin.resolve.inline.InlineUtil.isInlineParameter
import org.jetbrains.kotlin.resolve.inline.InlineUtil.isInlinedArgument
import org.jetbrains.kotlin.resolve.sam.getSingleAbstractMethodOrNull
import org.jetbrains.kotlin.types.KotlinType
@@ -80,7 +80,7 @@
container.useInstance(this)
}
- fun checkInlineLambdaCall(
+ private fun checkInlineLambdaCall(
resolvedCall: ResolvedCall<*>,
reportOn: PsiElement,
context: CallCheckerContext
@@ -166,23 +166,14 @@
)
return
}
- val argTypeDescriptor = arg
- ?.type
- ?.constructor
- ?.declarationDescriptor as? ClassDescriptor
- if (argTypeDescriptor != null) {
- val sam = getSingleAbstractMethodOrNull(argTypeDescriptor)
- if (sam != null && sam.hasComposableAnnotation()) {
- return
- }
- }
// TODO(lmr): in future, we should check for CALLS_IN_PLACE contract
- val inlined = arg != null &&
- canBeInlineArgument(node.functionLiteral) &&
- isInline(arg.containingDeclaration) &&
- isInlineParameter(arg)
- if (!inlined) {
+ val isInlined = isInlinedArgument(
+ node.functionLiteral,
+ bindingContext,
+ true
+ )
+ if (!isInlined) {
illegalCall(context, reportOn)
return
} else {
@@ -332,7 +323,7 @@
if (expectedType === TypeUtils.NO_EXPECTED_TYPE) return
if (expectedType === TypeUtils.UNIT_EXPECTED_TYPE) return
if (expectedType.isAnyOrNullableAny()) return
- val expectedComposable = expectedType.hasComposableAnnotation()
+ val expectedComposable = c.hasComposableExpectedType(expression)
if (expression is KtLambdaExpression) {
val descriptor = bindingContext[BindingContext.FUNCTION, expression.functionLiteral]
?: return
@@ -406,8 +397,7 @@
if (this is VariableAsFunctionResolvedCall) {
return false
}
- val candidateDescriptor = candidateDescriptor
- return when (candidateDescriptor) {
+ return when (val candidateDescriptor = candidateDescriptor) {
is ValueParameterDescriptor -> false
is LocalVariableDescriptor -> false
is PropertyDescriptor -> {
@@ -498,10 +488,7 @@
// this lambda was marked as inferred to be composable
return true
}
- // TODO(lmr): i'm not sure that this is actually needed at this point, since this should have
- // been covered by the TypeResolutionInterceptorExtension
- val arg = getArgumentDescriptor(functionLiteral, bindingContext) ?: return false
- return arg.type.hasComposableAnnotation()
+ return false
}
// the body of this function can have composable calls in it, even if it itself is not
@@ -517,15 +504,89 @@
] == true
}
-internal fun getArgumentDescriptor(
- argument: KtFunction,
- bindingContext: BindingContext
-): ValueParameterDescriptor? {
- val call = KtPsiUtil.getParentCallIfPresent(argument) ?: return null
- val resolvedCall = call.getResolvedCall(bindingContext) ?: return null
- val valueArgument = resolvedCall.call.getValueArgumentForExpression(argument) ?: return null
- val mapping = resolvedCall.getArgumentMapping(valueArgument) as? ArgumentMatch ?: return null
- return mapping.valueParameter
+// The resolution context usually contains a call position, which records
+// the ResolvedCall and ValueParameterDescriptor for the call that we are
+// currently resolving. However, it is possible to end up in the
+// [ComposableCallChecker] or [ComposeTypeResolutionInterceptorExtension]
+// before the frontend computes the call position (e.g., when intercepting
+// function literal descriptors).
+//
+// In this case, the function below falls back to looking at the parse tree
+// for `expression`, to determine whether we are resolving a value argument.
+private fun ResolutionContext<*>.getValueArgumentPosition(
+ expression: KtExpression
+): CallPosition.ValueArgumentPosition? =
+ when (val position = callPosition) {
+ is CallPosition.ValueArgumentPosition ->
+ position
+
+ is CallPosition.Unknown ->
+ getValueArgumentPositionFromPsi(expression, trace.bindingContext)
+
+ else ->
+ null
+ }
+
+private fun getValueArgumentPositionFromPsi(
+ expression: KtExpression,
+ context: BindingContext,
+): CallPosition.ValueArgumentPosition? {
+ val resolvedCall = KtPsiUtil
+ .getParentCallIfPresent(expression)
+ .getResolvedCall(context)
+ ?: return null
+
+ val valueArgument = resolvedCall.call.getValueArgumentForExpression(expression)
+ ?: return null
+
+ val argumentMatch = resolvedCall.getArgumentMapping(valueArgument) as? ArgumentMatch
+ ?: return null
+
+ return CallPosition.ValueArgumentPosition(
+ resolvedCall,
+ argumentMatch.valueParameter,
+ valueArgument
+ )
+}
+
+private fun getArgumentDescriptor(
+ expression: KtExpression,
+ context: BindingContext
+): ValueParameterDescriptor? =
+ getValueArgumentPositionFromPsi(expression, context)?.valueParameter
+
+internal fun ResolutionContext<*>.hasComposableExpectedType(expression: KtExpression): Boolean {
+ if (expectedType.hasComposableAnnotation())
+ return true
+
+ // The Kotlin frontend discards all annotations when computing function
+ // types for fun interfaces. As a workaround we retrieve the fun interface
+ // from the current value argument position and check the annotations on the
+ // underlying method.
+ if (expectedType.isSpecialType || !expectedType.isBuiltinFunctionalType)
+ return false
+
+ val position = getValueArgumentPosition(expression)
+ ?: return false
+
+ // There are two kinds of SAM conversions in Kotlin.
+ //
+ // - Explicit SAM conversion by calling a synthetic fun interface constructor,
+ // i.e., `A { ... }` or `A(f)` for a fun interface `A`.
+ // - Implicit SAM conversion by calling a function which expects a fun interface
+ // in a value parameter.
+ //
+ // For explicit SAM conversion we check for the presence of a synthetic call,
+ // otherwise we check the type of the value parameter descriptor.
+ val callDescriptor = position.resolvedCall.resultingDescriptor.original
+ val samDescriptor = if (callDescriptor is FunctionInterfaceConstructorDescriptor) {
+ callDescriptor.baseDescriptorForSynthetic
+ } else {
+ position.valueParameter.type.constructor.declarationDescriptor as? ClassDescriptor
+ ?: return false
+ }
+
+ return getSingleAbstractMethodOrNull(samDescriptor)?.hasComposableAnnotation() == true
}
fun List<KtAnnotationEntry>.hasComposableAnnotation(bindingContext: BindingContext): Boolean {
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeTypeResolutionInterceptorExtension.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeTypeResolutionInterceptorExtension.kt
index e9efd60..cc6f27f 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeTypeResolutionInterceptorExtension.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeTypeResolutionInterceptorExtension.kt
@@ -18,14 +18,12 @@
import androidx.compose.compiler.plugins.kotlin.analysis.ComposeWritableSlices
import androidx.compose.compiler.plugins.kotlin.analysis.ComposeWritableSlices.INFERRED_COMPOSABLE_DESCRIPTOR
-import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.impl.AnonymousFunctionDescriptor
import org.jetbrains.kotlin.extensions.internal.TypeResolutionInterceptorExtension
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtLambdaExpression
import org.jetbrains.kotlin.psi.psiUtil.getAnnotationEntries
import org.jetbrains.kotlin.resolve.descriptorUtil.module
-import org.jetbrains.kotlin.resolve.sam.getSingleAbstractMethodOrNull
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils
import org.jetbrains.kotlin.types.expressions.ExpressionTypingContext
@@ -41,31 +39,20 @@
expression: KtLambdaExpression,
context: ExpressionTypingContext,
descriptor: AnonymousFunctionDescriptor
- ): AnonymousFunctionDescriptor {
- if (descriptor.isSuspend) return descriptor
+ ): AnonymousFunctionDescriptor =
if (
- context.expectedType.hasComposableAnnotation() &&
- !descriptor.hasComposableAnnotation()
+ !descriptor.isSuspend &&
+ !descriptor.hasComposableAnnotation() &&
+ context.hasComposableExpectedType(expression)
) {
// If the expected type has an @Composable annotation then the literal function
// expression should infer a an @Composable annotation
- context.trace.record(INFERRED_COMPOSABLE_DESCRIPTOR, descriptor, true)
- return descriptor.annotateAsComposable(context.scope.ownerDescriptor.module)
- }
- val arg = getArgumentDescriptor(expression.functionLiteral, context.trace.bindingContext)
-
- val argTypeDescriptor = arg
- ?.type
- ?.constructor
- ?.declarationDescriptor as? ClassDescriptor
- if (argTypeDescriptor != null) {
- val sam = getSingleAbstractMethodOrNull(argTypeDescriptor)
- if (sam != null && sam.hasComposableAnnotation()) {
- context.trace.record(INFERRED_COMPOSABLE_DESCRIPTOR, descriptor, true)
+ descriptor.annotateAsComposable(context.scope.ownerDescriptor.module).also {
+ context.trace.record(INFERRED_COMPOSABLE_DESCRIPTOR, it, true)
}
+ } else {
+ descriptor
}
- return descriptor
- }
override fun interceptType(
element: KtElement,
@@ -75,27 +62,9 @@
if (resultType === TypeUtils.NO_EXPECTED_TYPE) return resultType
if (element !is KtLambdaExpression) return resultType
- val arg = getArgumentDescriptor(element.functionLiteral, context.trace.bindingContext)
-
- val argTypeDescriptor = arg
- ?.type
- ?.constructor
- ?.declarationDescriptor as? ClassDescriptor
- if (argTypeDescriptor != null) {
- val sam = getSingleAbstractMethodOrNull(argTypeDescriptor)
- if (sam != null && sam.hasComposableAnnotation()) {
- context.trace.record(
- ComposeWritableSlices.INFERRED_COMPOSABLE_LITERAL,
- element,
- true
- )
- return resultType.makeComposable(context.scope.ownerDescriptor.module)
- }
- }
-
if (
element.getAnnotationEntries().hasComposableAnnotation(context.trace.bindingContext) ||
- context.expectedType.hasComposableAnnotation()
+ context.hasComposableExpectedType(element)
) {
context.trace.record(ComposeWritableSlices.INFERRED_COMPOSABLE_LITERAL, element, true)
return resultType.makeComposable(context.scope.ownerDescriptor.module)
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index 909607f..42e0f75 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -2770,7 +2770,7 @@
private fun visitRememberCall(expression: IrCall): IrExpression {
val inputArgs = mutableListOf<IrExpression>()
var hasSpreadArgs = false
- var calculationArg: IrFunctionExpression? = null
+ var calculationArg: IrExpression? = null
for (i in 0 until expression.valueArgumentsCount) {
val param = expression.symbol.owner.valueParameters[i]
val arg = expression.getValueArgument(i)
@@ -2781,7 +2781,7 @@
break
when {
- param.name.identifier == "calculation" && arg is IrFunctionExpression -> {
+ param.name.identifier == "calculation" -> {
calculationArg = arg
}
arg is IrVararg -> {
@@ -2820,12 +2820,10 @@
encounteredComposableCall(withGroups = false)
- val invalidExpr = if (inputArgs.isEmpty())
- irConst(false)
- else
- inputArgs
- .map { irChangedOrInferredChanged(it) }
- .reduce { acc, changed -> irBooleanOr(acc, changed) }
+ val invalidExpr = inputArgs
+ .mapNotNull(::irChangedOrInferredChanged)
+ .reduceOrNull { acc, changed -> irBooleanOr(acc, changed) }
+ ?: irConst(false)
return irCache(
expression.startOffset,
@@ -2836,12 +2834,12 @@
)
}
- private fun irChangedOrInferredChanged(arg: IrExpression): IrExpression {
+ private fun irChangedOrInferredChanged(arg: IrExpression): IrExpression? {
val meta = paramMetaOf(arg, isProvided = true)
val param = meta.maskParam
return when {
- meta.isStatic -> irConst(false)
+ meta.isStatic -> null
meta.isCertain &&
meta.stability.knownStable() &&
param is IrChangedBitMaskVariable -> {
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/FunctionReferenceBuilder.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/FunctionReferenceBuilder.kt
index 0eb6bb1..145a7dc 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/FunctionReferenceBuilder.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/FunctionReferenceBuilder.kt
@@ -118,7 +118,6 @@
returnType = callee.returnType
isSuspend = callee.isSuspend
}.apply {
- annotations += superMethod.owner.annotations
overriddenSymbols += superMethod
dispatchReceiverParameter = parentAsClass.thisReceiver!!.copyTo(this)
createLambdaInvokeMethod()
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
index bfe53c8..3b7b143 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
@@ -414,22 +414,7 @@
return
}
- val dispatchReceiver = expression.dispatchReceiver
- val extensionReceiver = expression.extensionReceiver
- val dispatchIsSpecial = dispatchReceiver.let {
- it is IrGetValue && it.symbol.owner.name.isSpecial
- }
- val extensionIsSpecial = extensionReceiver.let {
- it is IrGetValue && it.symbol.owner.name.isSpecial
- }
-
- if (dispatchReceiver != null && !dispatchIsSpecial) {
- dispatchReceiver.print()
- print(".")
- } else if (extensionReceiver != null && !extensionIsSpecial) {
- extensionReceiver.print()
- print(".")
- }
+ expression.printExplicitReceiver(".")
val prop = (function as? IrSimpleFunction)?.correspondingPropertySymbol?.owner
@@ -454,6 +439,25 @@
}
}
+ private fun IrMemberAccessExpression<*>.printExplicitReceiver(suffix: String? = null) {
+ val dispatchReceiver = dispatchReceiver
+ val extensionReceiver = extensionReceiver
+ val dispatchIsSpecial = dispatchReceiver.let {
+ it is IrGetValue && it.symbol.owner.name.isSpecial
+ }
+ val extensionIsSpecial = extensionReceiver.let {
+ it is IrGetValue && it.symbol.owner.name.isSpecial
+ }
+
+ if (dispatchReceiver != null && !dispatchIsSpecial) {
+ dispatchReceiver.print()
+ suffix?.let(::print)
+ } else if (extensionReceiver != null && !extensionIsSpecial) {
+ extensionReceiver.print()
+ suffix?.let(::print)
+ }
+ }
+
private fun IrFunctionAccessExpression.printArgumentList(
forceParameterNames: Boolean = false,
forceSingleLine: Boolean = false
@@ -481,7 +485,7 @@
useParameterNames = true
}
}
- val multiline = useParameterNames && !forceSingleLine
+ val multiline = arguments.isNotEmpty() && useParameterNames && !forceSingleLine
if (arguments.isNotEmpty() || trailingLambda == null) {
print("(")
if (multiline) {
@@ -751,7 +755,10 @@
override fun visitReturn(expression: IrReturn) {
val value = expression.value
// only print the return statement directly if it is not a lambda
- if (expression.returnTargetSymbol.descriptor.name.asString() != "<anonymous>") {
+ val returnTarget = expression.returnTargetSymbol.owner
+ if (returnTarget !is IrFunction ||
+ returnTarget.name.asString() != "<anonymous>" &&
+ returnTarget.origin != IrDeclarationOrigin.ADAPTER_FOR_CALLABLE_REFERENCE) {
print("return ")
}
if (expression.type.isUnit() || value.type.isUnit()) {
@@ -780,7 +787,7 @@
lhs.print()
print("--")
}
- IrStatementOrigin.LAMBDA -> {
+ IrStatementOrigin.LAMBDA, IrStatementOrigin.ADAPTED_FUNCTION_REFERENCE -> {
val function = expression.statements[0] as IrFunction
function.printAsLambda()
}
@@ -1188,22 +1195,7 @@
override fun visitFunctionReference(expression: IrFunctionReference) {
val function = expression.symbol.owner
- val dispatchReceiver = expression.dispatchReceiver
- val extensionReceiver = expression.extensionReceiver
- val dispatchIsSpecial = dispatchReceiver.let {
- it is IrGetValue && it.symbol.owner.name.isSpecial
- }
- val extensionIsSpecial = extensionReceiver.let {
- it is IrGetValue && it.symbol.owner.name.isSpecial
- }
-
- if (dispatchReceiver != null && !dispatchIsSpecial) {
- dispatchReceiver.print()
- print("::")
- } else if (extensionReceiver != null && !extensionIsSpecial) {
- extensionReceiver.print()
- print("::")
- }
+ expression.printExplicitReceiver("::")
val prop = (function as? IrSimpleFunction)?.correspondingPropertySymbol?.owner
@@ -1242,7 +1234,10 @@
}
override fun visitPropertyReference(expression: IrPropertyReference) {
- print("<<PROPREF>>")
+ val property = expression.symbol.owner
+ expression.printExplicitReceiver()
+ print("::")
+ print(property.name)
}
override fun visitSpreadElement(spread: IrSpreadElement) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/CircularProgressIndicatorTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/CircularProgressIndicatorTokens.kt
new file mode 100644
index 0000000..4f418ce
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/CircularProgressIndicatorTokens.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object CircularProgressIndicatorTokens {
+ val ActiveIndicatorColor = ColorSchemeKey.Primary
+ val ActiveIndicatorWidth = 4.0.dp
+ val FourColorActiveIndicatorFourColor = ColorSchemeKey.TertiaryContainer
+ val FourColorActiveIndicatorOneColor = ColorSchemeKey.Primary
+ val FourColorActiveIndicatorThreeColor = ColorSchemeKey.Tertiary
+ val FourColorActiveIndicatorTwoColor = ColorSchemeKey.PrimaryContainer
+ val Size = 48.0.dp
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LinearProgressIndicatorTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LinearProgressIndicatorTokens.kt
new file mode 100644
index 0000000..d34e1c9
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LinearProgressIndicatorTokens.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object LinearProgressIndicatorTokens {
+ val ActiveIndicatorColor = ColorSchemeKey.Primary
+ val ActiveIndicatorHeight = 4.0.dp
+ val FourColorActiveIndicatorFourColor = ColorSchemeKey.TertiaryContainer
+ val FourColorActiveIndicatorOneColor = ColorSchemeKey.Primary
+ val FourColorActiveIndicatorThreeColor = ColorSchemeKey.Tertiary
+ val FourColorActiveIndicatorTwoColor = ColorSchemeKey.PrimaryContainer
+ val TrackColor = ColorSchemeKey.SurfaceVariant
+ val TrackHeight = 4.0.dp
+}
diff --git a/compose/ui/ui-tooling-data/build.gradle b/compose/ui/ui-tooling-data/build.gradle
index 1ef1e2f..5bea127 100644
--- a/compose/ui/ui-tooling-data/build.gradle
+++ b/compose/ui/ui-tooling-data/build.gradle
@@ -31,7 +31,7 @@
api "androidx.annotation:annotation:1.1.0"
- api("androidx.compose.runtime:runtime:1.1.0-rc01")
+ api(project(":compose:runtime:runtime"))
api(project(":compose:ui:ui"))
androidTestImplementation project(":compose:ui:ui-test-junit4")
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index bc1d1e6..d9a8068 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -51,7 +51,7 @@
// any dependency on this module.
compileOnly(project(":compose:ui:ui-android-stubs"))
- implementation(project(":compose:runtime:runtime"))
+ implementation("androidx.compose.runtime:runtime:1.1.0-rc01")
implementation(project(":compose:ui:ui-util"))
implementation(libs.kotlinStdlib)
implementation("androidx.autofill:autofill:1.0.0")
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt
index 8b2d35d..b2de30d 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt
@@ -199,7 +199,7 @@
val comp = input.value.composition
val text = input.value.text
- val range = TextRange(beginIndex, endIndex)
+ val range = TextRange(beginIndex, endIndex.coerceAtMost(text.length))
if (comp == null) {
val res = text.substring(range)
return AttributedString(res).iterator
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java
index 1f5a7e0..3e98566 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java
@@ -26,6 +26,8 @@
import android.content.Context;
import android.content.Intent;
+import androidx.appsearch.app.ShortcutAdapter;
+import androidx.appsearch.builtintypes.Timer;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.google.shortcuts.builders.CapabilityBuilder;
import androidx.core.google.shortcuts.builders.ParameterBuilder;
@@ -213,6 +215,39 @@
@Test
@SmallTest
+ public void onShortcutUpdated_entityShortcut_savesToAppIndex() throws Exception {
+ ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+
+ Timer timer = new Timer.Builder(ShortcutAdapter.DEFAULT_NAMESPACE, "timer_id")
+ .setCreationTimestampMillis(1000)
+ .build();
+ ShortcutInfoCompat timerShortcut =
+ ShortcutAdapter.createShortcutBuilderFromDocument(mContext, timer)
+ .build();
+
+ mShortcutInfoChangeListener.onShortcutUpdated(Collections.singletonList(timerShortcut));
+
+ verify(mFirebaseAppIndex, only()).update(indexableCaptor.capture());
+ List<Indexable> allValues = indexableCaptor.getAllValues();
+
+ Indexable expected = new Indexable.Builder("Timer")
+ .setMetadata(new Indexable.Metadata.Builder().setScore(0))
+ .setUrl(ShortcutUtils.getIndexableUrl(mContext, "timer_id"))
+ .put("_namespace", ShortcutAdapter.DEFAULT_NAMESPACE)
+ .put("_ttlMillis", 0)
+ .put("_creationTimestampMillis", 1000)
+ .put("id", "timer_id")
+ .put("length", 0)
+ .put("name", ShortcutAdapter.DEFAULT_NAMESPACE)
+ .put("remainingTime", 0)
+ .put("timerStatus", "Unknown")
+ .put("vibrate", false)
+ .build();
+ assertThat(allValues).containsExactly(expected);
+ }
+
+ @Test
+ @SmallTest
public void onShortcutAdded_savesToAppIndex() throws Exception {
ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
index d8ad9a1..a312b72 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
@@ -29,11 +29,15 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.ShortcutAdapter;
import androidx.core.content.pm.ShortcutInfoChangeListener;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.google.shortcuts.builders.CapabilityBuilder;
import androidx.core.google.shortcuts.builders.ParameterBuilder;
import androidx.core.google.shortcuts.builders.ShortcutBuilder;
+import androidx.core.google.shortcuts.converters.AppSearchDocumentConverter;
+import androidx.core.google.shortcuts.converters.AppSearchDocumentConverterFactory;
import androidx.core.google.shortcuts.utils.EntityUriUtils;
import androidx.core.google.shortcuts.utils.ShortcutUtils;
import androidx.core.graphics.drawable.IconCompat;
@@ -93,14 +97,30 @@
// shortcuts will be indexed under their respective schema type, and capability-instance
// shortcuts will be indexed in the general shortcut corpus.
for (ShortcutInfoCompat shortcut : shortcuts) {
- ShortcutBuilder shortcutBuilder = buildShortcutIndexable(shortcut);
+ GenericDocument entity = null;
+ // ShortcutAdapter is only available for Lollipop and above.
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ entity = ShortcutAdapter.extractDocument(shortcut);
+ }
- // Capability-instance shortcuts may contain references to entity shortcuts. If
- // that's the case, report usage for those entity shortcuts.
- // TODO (b/207161241): use capability binding api directly from shortcut once it's
- // available.
- maybeReportEntityUsage(shortcutBuilder);
- indexables.add(shortcutBuilder.build());
+ if (entity == null) {
+ // API level < Lollipop, or Shortcut might be a capability-instance shortcut.
+ ShortcutBuilder shortcutBuilder = buildShortcutIndexable(shortcut);
+
+ // Capability-instance shortcuts may contain references to entity shortcuts. If
+ // that's the case, report usage for those entity shortcuts.
+ // TODO (b/207161241): use capability binding api directly from shortcut once it's
+ // available.
+ maybeReportEntityUsage(shortcutBuilder);
+ indexables.add(shortcutBuilder.build());
+ } else {
+ // Shortcut is an entity shortcut.
+ AppSearchDocumentConverter converter =
+ AppSearchDocumentConverterFactory.getConverter(entity.getSchemaType());
+ Indexable.Builder entityIndexableBuilder =
+ converter.convertGenericDocument(mContext, entity);
+ indexables.add(entityIndexableBuilder.build());
+ }
}
mFirebaseAppIndex.update(indexables.toArray(new Indexable[0]));
}
diff --git a/development/importMaven/build.gradle.kts b/development/importMaven/build.gradle.kts
index 5e20af1..f0b65e7 100644
--- a/development/importMaven/build.gradle.kts
+++ b/development/importMaven/build.gradle.kts
@@ -80,8 +80,8 @@
google()
gradlePluginPortal()
- val allowBintray: String? = findProperty("allowBintray") as String?
- if (allowBintray != null) {
+ val allowJetbrainsDev: String? = findProperty("allowJetbrainsDev") as String?
+ if (allowJetbrainsDev != null) {
maven {
url = uri("https://maven.pkg.jetbrains.space/kotlin/p/dokka/dev/")
metadataSources {
@@ -89,28 +89,6 @@
}
}
maven {
- url = uri("https://dl.bintray.com/kotlin/kotlin-dev/")
- metadataSources {
- artifact()
- }
- }
- maven {
- url = uri("https://dl.bintray.com/kotlin/kotlin-eap/")
- metadataSources {
- artifact()
- }
- }
- maven {
- url = uri("https://dl.bintray.com/kotlin/kotlinx/")
- metadataSources {
- artifact()
- }
- }
- }
-
- val allowJetbrainsDev: String? = findProperty("allowJetbrainsDev") as String?
- if (allowJetbrainsDev != null) {
- maven {
url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev")
metadataSources {
artifact()
@@ -124,7 +102,6 @@
}
}
-
listOf("macos", "macos-x86_64", "linux", "linux-x86_64").forEach { platstring ->
ivy {
setUrl("https://download.jetbrains.com/kotlin/native/builds/releases")
diff --git a/development/importMaven/import_maven_artifacts.py b/development/importMaven/import_maven_artifacts.py
index 50e8261..bbc9cc3 100755
--- a/development/importMaven/import_maven_artifacts.py
+++ b/development/importMaven/import_maven_artifacts.py
@@ -28,11 +28,6 @@
to use for metalava prebuilt fetching.
'''
-ALLOW_BINTRAY_HELP = '''
- Whether or not to allow artifacts to be fetched from bintray in addition to jcenter, mavenCentral, etc
- E.g. https://dl.bintray.com/kotlin/kotlin-dev/ and https://dl.bintray.com/kotlin/kotlinx/
-'''
-
ALLOW_JETBRAINS_DEV_HELP = '''
Whether or not to allow artifacts to be fetched from Jetbrains' dev repository
E.g. https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev
@@ -51,8 +46,6 @@
required=True, dest='name')
parser.add_argument('-mb', '--metalava-build-id', help=METALAVA_BUILD_ID_HELP,
required=False, dest='metalava_build_id')
- parser.add_argument('-ab', '--allow-bintray', help=ALLOW_BINTRAY_HELP,
- required=False, action='store_true')
parser.add_argument('-ajd', '--allow-jetbrains-dev', help=ALLOW_JETBRAINS_DEV_HELP,
required=False, action='store_true')
parse_result = parser.parse_args()
@@ -65,8 +58,6 @@
metalava_build_id = parse_result.metalava_build_id
if (metalava_build_id):
command = command + ' -PmetalavaBuildId=%s' % (metalava_build_id)
- if (parse_result.allow_bintray):
- command = command + ' -PallowBintray'
if (parse_result.allow_jetbrains_dev):
command = command + ' -PallowJetbrainsDev'
diff --git a/external/libyuv/build.gradle b/external/libyuv/build.gradle
index 3c2d124..32f07d2 100644
--- a/external/libyuv/build.gradle
+++ b/external/libyuv/build.gradle
@@ -67,39 +67,47 @@
description = "libyuv is an open source project that includes YUV scaling and conversion functionality."
}
-// Entire block is to workaround b/203448887
+// this block exists to debug flakes from b/206464070
afterEvaluate {
- tasks.named("bundleDebugLocalLintAar").configure {
- it.dependsOn("prefabDebugConfigurePackage")
- }
- tasks.named("bundleReleaseAar").configure {
- it.dependsOn("prefabReleaseConfigurePackage")
- }
- tasks.named("reportLibraryMetrics").configure {
- it.dependsOn("prefabReleaseConfigurePackage")
- }
+ def buildDir = project.buildDir
tasks.named("prefabReleaseConfigurePackage").configure {
- def releaseAbiJson = project.file("$buildDir/intermediates/prefab_package/release/prefab/modules/yuv/libs/android.armeabi-v7a/abi.json")
- def releasePrefabJson = project.file("$buildDir/intermediates/prefab_package/release/prefab/prefab.json")
it.doLast {
- if (!releaseAbiJson.exists()) {
- throw new GradleException("$releaseAbiJson does not exist")
+ def abis = ["android.arm64-v8a", "android.armeabi-v7a", "android.x86", "android.x86_64"]
+ abis.each { abi ->
+ def releaseAbiJson = new File("$buildDir/intermediates/prefab_package/release/prefab/modules/yuv/libs/${abi}/abi.json")
+ if (!releaseAbiJson.exists()) {
+ throw new GradleException("$releaseAbiJson does not exist")
+ }
}
+ def releasePrefabJson = new File("$buildDir/intermediates/prefab_package/release/prefab/prefab.json")
if (!releasePrefabJson.exists()) {
throw new GradleException("$releasePrefabJson does not exist")
}
+
+ def releaseConvertArgb = new File("$buildDir/intermediates/prefab_package/release/prefab/modules/yuv/include/libyuv/convert_argb.h")
+ if (!releaseConvertArgb.exists()) {
+ throw new GradleException("$releaseConvertArgb does not exist")
+ }
}
}
tasks.named("prefabDebugConfigurePackage").configure {
- def debugAbiJson = project.file("$buildDir/intermediates/prefab_package/debug/prefab/modules/yuv/libs/android.armeabi-v7a/abi.json")
- def debugPrefabJson = project.file("$buildDir/intermediates/prefab_package/debug/prefab/prefab.json")
it.doLast {
- if (!debugAbiJson.exists()) {
- throw new GradleException("$debugAbiJson does not exist")
+ def abis = ["android.arm64-v8a", "android.armeabi-v7a", "android.x86", "android.x86_64"]
+ abis.each { abi ->
+ def debugAbiJson = new File("$buildDir/intermediates/prefab_package/debug/prefab/modules/yuv/libs/${abi}/abi.json")
+ if (!debugAbiJson.exists()) {
+ throw new GradleException("$debugAbiJson does not exist")
+ }
}
+ def debugPrefabJson = new File("$buildDir/intermediates/prefab_package/debug/prefab/prefab.json")
if (!debugPrefabJson.exists()) {
throw new GradleException("$debugPrefabJson does not exist")
}
+
+ def debugConvertArgb = new File("$buildDir/intermediates/prefab_package/debug/prefab/modules/yuv/include/libyuv/convert_argb.h")
+ if (!debugConvertArgb.exists()) {
+ throw new GradleException("$debugConvertArgb does not exist")
+ }
}
}
}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
index 778b197..ada24a1 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -464,9 +464,14 @@
internal data class AppWidgetId(val appWidgetId: Int) : GlanceId
// Extract the sizes from the bundle
-internal fun Bundle.extractAllSizes(minSize: () -> DpSize): List<DpSize> =
- getParcelableArrayList<SizeF>(AppWidgetManager.OPTION_APPWIDGET_SIZES)
- ?.map { DpSize(it.width.dp, it.height.dp) } ?: estimateSizes(minSize)
+internal fun Bundle.extractAllSizes(minSize: () -> DpSize): List<DpSize> {
+ val sizes = getParcelableArrayList<SizeF>(AppWidgetManager.OPTION_APPWIDGET_SIZES)
+ return if (sizes.isNullOrEmpty()) {
+ estimateSizes(minSize)
+ } else {
+ sizes.map { DpSize(it.width.dp, it.height.dp) }
+ }
+}
// If the list of sizes is not available, estimate it from the min/max width and height.
// We can assume that the min width and max height correspond to the portrait mode and the max
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt
index 628de2b..210187d 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt
@@ -22,6 +22,7 @@
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
+import android.util.SizeF
import android.widget.FrameLayout
import android.widget.TextView
import androidx.compose.runtime.Composable
@@ -430,6 +431,27 @@
.isEqualTo(DpSize(180.dp, 180.dp))
}
+ // Testing on pre-S and post-S to test both when OPTION_APPWIDGET_SIZES is present or not.
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.Q, Build.VERSION_CODES.S])
+ fun extractAllSizes_shouldExtractSizesWhenPresent() {
+ val bundle = optionsBundleOf(listOf(DpSize(140.dp, 110.dp), DpSize(100.dp, 150.dp)))
+ assertThat(bundle.extractAllSizes { DpSize.Zero }).containsExactly(
+ DpSize(140.dp, 110.dp),
+ DpSize(100.dp, 150.dp)
+ )
+ }
+
+ @Test
+ fun extractAllSizes_emptyAppWidgetSizes_shouldExtractFromMinMax() {
+ val bundle = optionsBundleOf(listOf(DpSize(140.dp, 110.dp), DpSize(100.dp, 150.dp)))
+ bundle.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, ArrayList<SizeF>())
+ assertThat(bundle.extractAllSizes { DpSize.Zero }).containsExactly(
+ DpSize(140.dp, 110.dp),
+ DpSize(100.dp, 150.dp)
+ )
+ }
+
private fun optionsBundleOf(sizes: List<DpSize>): Bundle {
require(sizes.isNotEmpty()) { "There must be at least one size" }
val (minSize, maxSize) = sizes.fold(sizes[0] to sizes[0]) { acc, s ->
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c5b17de..1246430 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -148,7 +148,7 @@
robolectric = { module = "org.robolectric:robolectric", version = "4.7" }
rxjava2 = { module = "io.reactivex.rxjava2:rxjava", version = "2.2.9" }
rxjava3 = { module = "io.reactivex.rxjava3:rxjava", version = "3.0.0" }
-shadow = { module = "gradle.plugin.com.github.johnrengelman:shadow", version = "7.1.0" }
+shadow = { module = "gradle.plugin.com.github.johnrengelman:shadow", version = "7.1.1" }
skiko = { module = "org.jetbrains.skiko:skiko-jvm", version.ref = "skiko" }
skikoCommon = { module = "org.jetbrains.skiko:skiko", version.ref = "skiko" }
skikoMacOsArm64 = { module = "org.jetbrains.skiko:skiko-jvm-runtime-macos-arm64", version.ref = "skiko" }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
index 4155174..b773e09 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
@@ -155,14 +155,7 @@
Variance.CONTRAVARIANT -> WildcardTypeName.supertypeOf(resolveTypeName())
Variance.COVARIANT -> WildcardTypeName.subtypeOf(resolveTypeName())
Variance.STAR -> {
- // for star projected types, JavaPoet uses the name from the declaration if
- // * is not given explicitly
- if (type == null) {
- // explicit *
- WildcardTypeName.subtypeOf(TypeName.OBJECT)
- } else {
- WildcardTypeName.subtypeOf(type.typeName(resolver, typeArgumentTypeLookup))
- }
+ WildcardTypeName.subtypeOf(TypeName.OBJECT)
}
else -> {
if (hasJvmWildcardAnnotation()) {
@@ -187,7 +180,7 @@
resolver: Resolver,
typeArgumentTypeLookup: TypeArgumentTypeLookup
): TypeName {
- return if (this.arguments.isNotEmpty()) {
+ return if (this.arguments.isNotEmpty() && !isRaw()) {
val args: Array<TypeName> = this.arguments
.map { typeArg ->
typeArg.typeName(
@@ -331,4 +324,11 @@
}
val parent = parent ?: return false
return parent.hasSuppressWildcardsAnnotationInHierarchy()
+}
+
+internal fun KSType.isRaw(): Boolean {
+ // yes this is gross but KSP itself seems to be doing it as well
+ // https://github.com/google/ksp/blob/main/compiler-plugin/
+ // src/main/kotlin/com/google/devtools/ksp/symbol/impl/kotlin/KSTypeImpl.kt#L85
+ return toString().startsWith("raw ")
}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
index 9807eef..263b75f 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
@@ -24,7 +24,6 @@
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSTypeArgument
import com.google.devtools.ksp.symbol.KSTypeParameter
-import com.google.devtools.ksp.symbol.KSTypeReference
import com.google.devtools.ksp.symbol.Modifier
import com.google.devtools.ksp.symbol.Variance
@@ -51,7 +50,7 @@
ksType: KSType,
wildcardMode: WildcardMode,
declarationType: KSType?
- ): KSType = ksType.inheritVariance(declarationType, wildcardMode)
+ ): KSType = ksType.inheritVariance(declarationType, wildcardMode, ReferenceStack())
/**
* Update the variance of the arguments of this type based on the types declaration.
@@ -61,62 +60,68 @@
private fun KSType.inheritVariance(
declarationType: KSType?,
wildcardMode: WildcardMode,
+ referenceStack: ReferenceStack
): KSType {
if (arguments.isEmpty()) return this
- // arrays don't inherit variance unless it is in an inherited method
- if (this.declaration.qualifiedName?.asString() == KOTLIN_ARRAY_Q_NAME &&
- declarationType == null
- ) return this
-
- // if we have type arguments but the declarationType doesn't, we should consider it like
- // star projection.
- // This happens when a given List<X> overrides T. In this case, we need to force X's
- // wildcards
- val starProject = declarationType != null && declarationType.arguments.isEmpty()
-
- // need to swap arguments with the variance from declaration
- val newArguments = arguments.mapIndexed { index, typeArg ->
- val param = declaration.typeParameters.getOrNull(index)
- val declarationArg = declarationType?.arguments?.getOrNull(index)
- val argWildcardMode = if (starProject) {
- WildcardMode.FORCED
- } else {
- wildcardMode
+ return referenceStack.withReference(this) {
+ // arrays don't inherit variance unless it is in an inherited method
+ if (this.declaration.qualifiedName?.asString() == KOTLIN_ARRAY_Q_NAME &&
+ declarationType == null
+ ) {
+ return@withReference this
}
- typeArg.inheritVariance(declarationArg, argWildcardMode, param)
- }
- return this.replace(newArguments)
- }
- private fun KSTypeReference.inheritVariance(
- declarationType: KSTypeReference?,
- wildcardMode: WildcardMode
- ): KSTypeReference {
- return resolve()
- .inheritVariance(declarationType = declarationType?.resolve(), wildcardMode)
- .createTypeReference()
+ // if we have type arguments but the declarationType doesn't, we should consider it like
+ // star projection.
+ // This happens when a given List<X> overrides T. In this case, we need to force X's
+ // wildcards
+ val starProject = declarationType != null && declarationType.arguments.isEmpty()
+
+ // need to swap arguments with the variance from declaration
+ val newArguments = arguments.mapIndexed { index, typeArg ->
+ val param = declaration.typeParameters.getOrNull(index)
+ val declarationArg = declarationType?.arguments?.getOrNull(index)
+ val argWildcardMode = if (starProject) {
+ WildcardMode.FORCED
+ } else {
+ wildcardMode
+ }
+ typeArg.inheritVariance(declarationArg, argWildcardMode, param, referenceStack)
+ }
+ this.replace(newArguments)
+ }
}
private fun KSTypeArgument.inheritVariance(
declarationType: KSTypeArgument?,
wildcardMode: WildcardMode,
- param: KSTypeParameter?
+ param: KSTypeParameter?,
+ referenceStack: ReferenceStack
): KSTypeArgument {
if (param == null) {
return this
}
val myTypeRef = type ?: return this
+ val myType = myTypeRef.resolve()
+
+ if (referenceStack.contains(myType)) {
+ // self referencing type
+ return this
+ }
if (variance != Variance.INVARIANT) {
return resolver.getTypeArgument(
- typeRef = myTypeRef.inheritVariance(declarationType?.type, wildcardMode),
+ typeRef = myType.inheritVariance(
+ declarationType?.type?.resolve(),
+ wildcardMode,
+ referenceStack
+ ).createTypeReference(),
variance = variance
)
}
// Now we need to guess from this type. If the type is final, it does not inherit unless
// the parameter is CONTRAVARIANT (`in`).
- val myType = myTypeRef.resolve()
val shouldInherit = when {
hasJvmWildcardAnnotation() -> {
// we actually don't need to check for wildcard annotation here as the TypeName
@@ -151,12 +156,17 @@
} ?: param.variance
return if (shouldInherit) {
resolver.getTypeArgument(
- typeRef = myTypeRef.inheritVariance(declarationType?.type, wildcardMode),
+ typeRef = myType.inheritVariance(
+ declarationType?.type?.resolve(),
+ wildcardMode,
+ referenceStack
+ ).createTypeReference(),
variance = newVariance
)
} else {
resolver.getTypeArgument(
- typeRef = myTypeRef.inheritVariance(null, wildcardMode),
+ typeRef = myType.inheritVariance(null, wildcardMode, referenceStack)
+ .createTypeReference(),
variance = variance
)
}
@@ -179,3 +189,27 @@
SUPPRESSED
}
}
+
+/**
+ * Inheriting variance for self referencing types (e.g. Foo<T : Foo>) could go into an infinite
+ * loop. To avoid that issue, every time we visit a type, we keep it in the reference stack and
+ * if a type argument resolves to it, it will stop recursion.
+ */
+private class ReferenceStack {
+ @Suppress("PropertyName")
+ val _queue = ArrayDeque<KSType>()
+
+ fun contains(ksType: KSType) = _queue.contains(ksType)
+
+ inline fun <T> withReference(
+ ksType: KSType,
+ crossinline block: () -> T
+ ): T {
+ return try {
+ _queue.addLast(ksType)
+ block()
+ } finally {
+ _queue.removeLast()
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
index 741f59d..299efe5 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
@@ -408,15 +408,17 @@
val src = Source.kotlin(
"Foo.kt",
"""
- class SelfReferencing<T : SelfReferencing<T>>
+ class SelfReferencing<T : SelfReferencing<T>> {
+ fun method(sr: SelfReferencing<*>) { TODO() }
+ }
""".trimIndent()
)
runProcessorTest(
sources = listOf(src)
) { invocation ->
- val elm = invocation.processingEnv.requireTypeElement("SelfReferencing")
- val typeName = elm.type.typeName
- assertThat(typeName.dumpToString(5)).isEqualTo(
+ val typeElement = invocation.processingEnv.requireTypeElement("SelfReferencing")
+ val parameter = typeElement.getMethodByJvmName("method").parameters.single()
+ assertThat(typeElement.type.typeName.dumpToString(5)).isEqualTo(
"""
SelfReferencing<T>
| T
@@ -426,6 +428,12 @@
| > | > | T
""".trimIndent()
)
+ assertThat(parameter.type.typeName.dumpToString(5)).isEqualTo(
+ """
+ SelfReferencing<?>
+ | ?
+ """.trimIndent()
+ )
}
}
@@ -435,15 +443,16 @@
"SelfReferencing",
"""
class SelfReferencing<T extends SelfReferencing<T>> {
+ static void method(SelfReferencing sr) {}
}
""".trimIndent()
)
runProcessorTest(
sources = listOf(src)
) { invocation ->
- val elm = invocation.processingEnv.requireTypeElement("SelfReferencing")
- val dump = elm.type.typeName.dumpToString(5)
- assertThat(dump).isEqualTo(
+ val typeElement = invocation.processingEnv.requireTypeElement("SelfReferencing")
+ val parameter = typeElement.getMethodByJvmName("method").parameters.single()
+ assertThat(typeElement.type.typeName.dumpToString(5)).isEqualTo(
"""
SelfReferencing<T>
| T
@@ -453,6 +462,11 @@
| > | > | T
""".trimIndent()
)
+ assertThat(parameter.type.typeName.dumpToString(5)).isEqualTo(
+ """
+ SelfReferencing
+ """.trimIndent()
+ )
}
}
@@ -461,15 +475,19 @@
val src = Source.kotlin(
"Foo.kt",
"""
- open class Node<TX : Node<TX, RX>, RX : Node<RX, TX>> {
- }
+ open class Node<TX : Node<TX, RX>, RX : Node<RX, TX>> {
+ fun allStar(node : Node<*, *>) { TODO() }
+ fun secondStar(node : Node<TX, *>) { TODO() }
+ fun firstStar(node : Node<*, RX>) { TODO() }
+ fun noStar(node : Node<TX, RX>) { TODO() }
+ }
""".trimIndent()
)
runProcessorTest(
sources = listOf(src)
) { invocation ->
- val nodeElm = invocation.processingEnv.requireType("Node")
- val nodeTypeName = nodeElm.typeName
+ val nodeElm = invocation.processingEnv.requireTypeElement("Node")
+ val nodeTypeName = nodeElm.type.typeName
assertThat(nodeTypeName.dumpToString(5)).isEqualTo(
"""
Node<TX, RX>
@@ -495,6 +513,69 @@
| > | > | RX
""".trimIndent()
)
+ val paramTypeNames = nodeElm.getDeclaredMethods().associate {
+ it.name to it.parameters.single().type.typeName.dumpToString(5)
+ }
+ assertThat(paramTypeNames).containsExactlyEntriesIn(
+ mapOf(
+ "allStar" to """
+ Node<?, ?>
+ | ?
+ | ?
+ """.trimIndent(),
+ "firstStar" to """
+ Node<?, RX>
+ | ?
+ | RX
+ | > Node<RX, TX>
+ | > | RX
+ | > | > Node<RX, TX>
+ | > | > | RX
+ | > | > | TX
+ | > | TX
+ | > | > Node<TX, RX>
+ | > | > | TX
+ | > | > | RX
+ """.trimIndent(),
+ "secondStar" to """
+ Node<TX, ?>
+ | TX
+ | > Node<TX, RX>
+ | > | TX
+ | > | > Node<TX, RX>
+ | > | > | TX
+ | > | > | RX
+ | > | RX
+ | > | > Node<RX, TX>
+ | > | > | RX
+ | > | > | TX
+ | ?
+ """.trimIndent(),
+ "noStar" to """
+ Node<TX, RX>
+ | TX
+ | > Node<TX, RX>
+ | > | TX
+ | > | > Node<TX, RX>
+ | > | > | TX
+ | > | > | RX
+ | > | RX
+ | > | > Node<RX, TX>
+ | > | > | RX
+ | > | > | TX
+ | RX
+ | > Node<RX, TX>
+ | > | RX
+ | > | > Node<RX, TX>
+ | > | > | RX
+ | > | > | TX
+ | > | TX
+ | > | > Node<TX, RX>
+ | > | > | TX
+ | > | > | RX
+ """.trimIndent()
+ )
+ )
}
}
@@ -600,6 +681,92 @@
}
/**
+ * Repro for b/208207043
+ */
+ @Test
+ fun selfReferencing_starProjectedJava() {
+ val src = Source.kotlin(
+ "StyleBuilder.kt",
+ """
+ class StyleApplier<X, Y>
+ class StyleBuilder<out B : StyleBuilder<B, A>, out A : StyleApplier<*, *>>
+ class KotlinSubject {
+ fun subject_1(builder: StyleBuilder<*, *>) {
+ }
+ }
+ """.trimIndent()
+ )
+ val javaSource = Source.java(
+ "JavaSubject",
+ """
+ public class JavaSubject {
+ static void subject_1(StyleBuilder<?, ?> builder) {
+ }
+ static void subject_2(StyleBuilder builder) {
+ }
+ }
+ """.trimIndent()
+ )
+ runProcessorTest(
+ sources = listOf(src, javaSource)
+ ) { invocation ->
+ val styleApplier = invocation.processingEnv.requireType("StyleApplier")
+ val styleBuilder = invocation.processingEnv.requireType("StyleBuilder")
+ assertThat(styleApplier.typeName.dumpToString(5)).isEqualTo(
+ """
+ StyleApplier<X, Y>
+ | X
+ | Y
+ """.trimIndent()
+ )
+ // we don't match what kapt generates here so this test is kept here to acknowledge the
+ // skew. Otoh, definition of B extending a type that has a type parameter that
+ // extends B is very very weird in practice :).
+ val bArgSignature = if (invocation.isKsp) {
+ "StyleBuilder<B, A>"
+ } else {
+ "StyleBuilder<? extends B, ? extends A>"
+ }
+ assertThat(styleBuilder.typeName.dumpToString(2)).isEqualTo(
+ """
+ StyleBuilder<B, A>
+ | B
+ | > $bArgSignature
+ | A
+ | > StyleApplier<?, ?>
+ """.trimIndent()
+ )
+
+ val javaSubject = invocation.processingEnv.requireTypeElement("JavaSubject")
+ val kotlinSubject = invocation.processingEnv.requireTypeElement("KotlinSubject")
+ // detect raw java types properly to be consistent
+ assertThat(
+ javaSubject.getMethodByJvmName("subject_2").parameters.single()
+ .type.typeName.dumpToString(5)
+ ).isEqualTo("StyleBuilder")
+
+ val javaTypeName = javaSubject.getMethodByJvmName("subject_1").parameters.single()
+ .type.typeName.dumpToString(5)
+ val kotlinTypeName = kotlinSubject.getMethodByJvmName("subject_1").parameters.single()
+ .type.typeName.dumpToString(5)
+ assertThat(javaTypeName).isEqualTo(
+ """
+ StyleBuilder<?, ?>
+ | ?
+ | ?
+ """.trimIndent()
+ )
+ assertThat(kotlinTypeName).isEqualTo(
+ """
+ StyleBuilder<?, ?>
+ | ?
+ | ?
+ """.trimIndent()
+ )
+ }
+ }
+
+ /**
* Reproduces the first bug in b/204415667
*/
@Test
@@ -638,10 +805,10 @@
invocation.processingEnv.requireType("foo.bar.Baz").let {
val superTypes = it.superTypes
assertThat(superTypes).hasSize(2)
- val superClass = superTypes.first {
- type -> type.rawType.toString() == "foo.bar.AbstractClass" }
- val superInterface = superTypes.first {
- type -> type.rawType.toString() == "foo.bar.MyInterface" }
+ val superClass =
+ superTypes.first { type -> type.rawType.toString() == "foo.bar.AbstractClass" }
+ val superInterface =
+ superTypes.first { type -> type.rawType.toString() == "foo.bar.MyInterface" }
assertThat(superClass.typeArguments).hasSize(1)
assertThat(superClass.typeArguments[0].typeName)
.isEqualTo(ClassName.get("java.lang", "String"))
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSTypeExtTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSTypeExtTest.kt
index 8aa640d..ad531fd 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSTypeExtTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSTypeExtTest.kt
@@ -131,11 +131,7 @@
.that(
typeName
).isEqualTo(
- // kotlin does not have raw types hence it becomes List<Object>
- ParameterizedTypeName.get(
- List::class.java,
- Object::class.java
- )
+ ClassName.get(List::class.java)
)
val nestedTypeName = subject.propertyType("nested").typeName(invocation.kspResolver)
assertWithMessage(subject.qualifiedName!!.asString())
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
index 43002a4..3539d9a 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
@@ -181,6 +181,7 @@
suppressWildcards = true
),
createOverrideSubjects(pkg),
+ createSelfReferencingType(pkg),
createOverridesWithGenericArguments(pkg),
createKotlinOverridesJavaSubjects(pkg),
createKotlinOverridesJavaSubjectsWithGenerics(pkg),
@@ -225,6 +226,34 @@
)
}
+ private fun createSelfReferencingType(pkg: String): TestInput {
+ return TestInput(
+ sources = listOf(
+ Source.java("$pkg.SelfReferencingJava", """
+ package $pkg;
+ public interface SelfReferencingJava<T extends SelfReferencingJava<T>> {
+ void method1(SelfReferencingJava sr);
+ void method2(SelfReferencingJava<?> sr);
+ }
+ """.trimIndent()),
+ Source.java("$pkg.SubSelfReferencingJava", """
+ package $pkg;
+ public interface SubSelfReferencingJava extends SelfReferencingJava<SubSelfReferencingJava> {}
+ """.trimIndent()),
+ Source.kotlin("SelfReferencing.kt",
+ """
+ package $pkg
+ interface SelfReferencingKotlin<T : SelfReferencingKotlin<T>> {
+ fun method1(sr: SelfReferencingKotlin<*>)
+ }
+ interface SubSelfReferencingKotlin : SelfReferencingKotlin<SubSelfReferencingKotlin>
+ """.trimIndent()),
+ ),
+ subjects = listOf("SubSelfReferencingJava", "SelfReferencingJava",
+ "SelfReferencingKotlin", "SubSelfReferencingKotlin")
+ )
+ }
+
/**
* Subjects with variance and star projections.
*/
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/NullAwareTypeConverterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/NullAwareTypeConverterStore.kt
index a8dc8dc..453ffd0 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/NullAwareTypeConverterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/NullAwareTypeConverterStore.kt
@@ -52,6 +52,9 @@
*/
private val knownColumnTypes: List<XType>
) : TypeConverterStore {
+ private val knownColumnTypeNames = knownColumnTypes.map {
+ it.typeName
+ }
override val typeConverters = if (context.processingEnv.backend == Backend.KSP) {
val processedConverters = typeConverters.toMutableList()
// create copies for converters that receive non-null values
@@ -154,6 +157,11 @@
return null
}
+ private fun isColumnType(type: XType): Boolean {
+ // compare using type names to handle both null and non-null.
+ return knownColumnTypeNames.contains(type.typeName)
+ }
+
private fun findConverterIntoStatementInternal(
input: XType,
columnTypes: List<XType>
@@ -162,7 +170,8 @@
val queue = TypeConverterQueue(
sourceType = input,
// each converter is keyed on which type they will take us to
- keyType = TypeConverter::to
+ keyType = TypeConverter::to,
+ isKnownColumnType = this::isColumnType
)
while (true) {
@@ -178,7 +187,8 @@
columnTypes.forEach { columnType ->
if (columnType.isAssignableFrom(current.type)) {
queue.maybeEnqueue(
- current.appendConverter(
+ prevEntry = current,
+ converter = current.appendConverter(
UpCastTypeConverter(
upCastFrom = current.type,
upCastTo = columnType
@@ -188,7 +198,10 @@
}
}
getAllTypeConvertersFrom(current.type).forEach {
- queue.maybeEnqueue(current.appendConverter(it))
+ queue.maybeEnqueue(
+ prevEntry = current,
+ converter = current.appendConverter(it)
+ )
}
}
return null
@@ -237,7 +250,8 @@
sourceType = output,
// each converter is keyed on which type they receive as we are doing pathfinding
// reverse here
- keyType = TypeConverter::from
+ keyType = TypeConverter::from,
+ isKnownColumnType = this::isColumnType
)
while (true) {
@@ -253,7 +267,8 @@
columnTypes.forEach { columnType ->
if (current.type.isAssignableFrom(columnType)) {
queue.maybeEnqueue(
- current.prependConverter(
+ prevEntry = current,
+ converter = current.prependConverter(
UpCastTypeConverter(
upCastFrom = columnType,
upCastTo = current.type
@@ -263,7 +278,10 @@
}
}
getAllTypeConvertersTo(current.type).forEach {
- queue.maybeEnqueue(current.prependConverter(it))
+ queue.maybeEnqueue(
+ prevEntry = current,
+ converter = current.prependConverter(it)
+ )
}
}
return null
@@ -348,6 +366,7 @@
*/
private class TypeConverterQueue(
sourceType: XType,
+ val isKnownColumnType: (XType) -> Boolean,
val keyType: TypeConverter.() -> XType
) {
// using insertion order as the tie breaker for reproducible builds.
@@ -361,7 +380,8 @@
val typeConverterEntry = TypeConverterEntry(
tieBreakerPriority = insertionOrder++,
type = sourceType,
- converter = null
+ converter = null,
+ convertsBetweenDbAndNonDbType = false
)
cheapestEntry[sourceType] = typeConverterEntry
queue.add(typeConverterEntry)
@@ -384,14 +404,31 @@
* or visited with a more expensive converter.
*/
fun maybeEnqueue(
+ prevEntry: TypeConverterEntry,
converter: TypeConverter
): Boolean {
val keyType = converter.keyType()
+ val convertsBetweenDbAndNonDbType =
+ isKnownColumnType(converter.from) != isKnownColumnType(converter.to)
+ if (prevEntry.convertsBetweenDbAndNonDbType) {
+ // if previous entry converted from db type to user type (or vice versa), the new
+ // converter must also be converting between db type and non-db type.
+ if (!convertsBetweenDbAndNonDbType) {
+ // prev entry already visited a column type, we cannot add any converters that
+ // will visit a non-column type
+ return false
+ }
+ }
val existing = cheapestEntry[keyType]
if (existing == null ||
(existing.converter != null && existing.converter.cost > converter.cost)
) {
- val entry = TypeConverterEntry(insertionOrder++, keyType, converter)
+ val entry = TypeConverterEntry(
+ tieBreakerPriority = insertionOrder++,
+ type = keyType,
+ converter = converter,
+ convertsBetweenDbAndNonDbType = convertsBetweenDbAndNonDbType
+ )
cheapestEntry[keyType] = entry
queue.add(entry)
return true
@@ -404,7 +441,16 @@
// when costs are equal, tieBreakerPriority is used
val tieBreakerPriority: Int,
val type: XType,
- val converter: TypeConverter?
+ val converter: TypeConverter?,
+ /**
+ * If true, this entry converts between a column type and a non column type. Once a
+ * converter entry converts between a column type and user type, it can never go back. This
+ * is to ensure that we don't find converters between unrelated user types just because they
+ * both convert to the same database type.
+ * For instance, both TypeA and TypeB might be convertible to `String` to be persisted, yet,
+ * this doesn't mean TypeA can be converted into TypeB.
+ */
+ val convertsBetweenDbAndNonDbType: Boolean
) : Comparable<TypeConverterEntry> {
override fun compareTo(other: TypeConverterEntry): Int {
if (converter == null) {
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/NullabilityAwareTypeConverterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/NullabilityAwareTypeConverterStoreTest.kt
index 654bb51..3cc910d 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/NullabilityAwareTypeConverterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/NullabilityAwareTypeConverterStoreTest.kt
@@ -22,6 +22,7 @@
import androidx.room.compiler.processing.util.XTestInvocation
import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
import androidx.room.compiler.processing.util.compiler.compile
+import androidx.room.compiler.processing.util.runKspTest
import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.processor.Context.BooleanProcessorOptions.USE_NULL_AWARE_CONVERTER
import androidx.room.processor.CustomConverterProcessor
@@ -123,7 +124,8 @@
MyClass? to String!: nullableMyClassToNullableString / checkNotNull(String?)
String! to MyClass!: (String! as String?) / nullableStringToNullableMyClass / checkNotNull(MyClass?)
MyClass! to String!: (MyClass! as MyClass?) / nullableMyClassToNullableString / checkNotNull(String?)
- """.trimIndent())
+ """.trimIndent()
+ )
}
@Test
@@ -144,7 +146,8 @@
MyClass? to Cursor: nullableMyClassToNullableString
Cursor to MyClass!: nullableStringToNullableMyClass / checkNotNull(MyClass?)
MyClass! to Cursor: (MyClass! as MyClass?) / nullableMyClassToNullableString
- """.trimIndent())
+ """.trimIndent()
+ )
}
@Test
@@ -671,6 +674,118 @@
}
/**
+ * Repro for b/206961709
+ * Often times, user will provide type converters that convert user type to database type.
+ * This does not mean that two types that can be converted into db types can be converted into
+ * each-other. e.g. if you can serialize TypeA and TypeB to String, it doesn't mean you can
+ * convert TypeA to TypeB.
+ */
+ @Test
+ fun dontAssumeUserTypesCanBeConvertedIntoEachOther() {
+ val converters = Source.kotlin(
+ "Converters.kt",
+ """
+ import androidx.room.*
+ class TypeA
+ class TypeB
+ object MyConverters {
+ @TypeConverter
+ fun nullableStringToTypeA(input: String?): TypeA { TODO() }
+ @TypeConverter
+ fun nullableTypeAToString(input: TypeA): String { TODO() }
+ @TypeConverter
+ fun nullableTypeBToNullableString(input: TypeB?): String? { TODO() }
+ @TypeConverter
+ fun nullableStringToNullableTypeB(input: String?): TypeB? { TODO() }
+ }
+ """.trimIndent()
+ )
+ runKspTest(
+ sources = listOf(converters),
+ options = mapOf(
+ USE_NULL_AWARE_CONVERTER.argName to "true"
+ )
+ ) { invocation ->
+ val store = invocation.createStore("MyConverters")
+ val aType = invocation.processingEnv.requireType("TypeA")
+ val bType = invocation.processingEnv.requireType("TypeB")
+ val stringType = invocation.processingEnv.requireType("java.lang.String")
+ assertThat(
+ store.findTypeConverter(
+ aType,
+ bType
+ )?.toSignature()
+ ).isNull()
+ assertThat(
+ store.findTypeConverter(
+ bType,
+ aType
+ )?.toSignature()
+ ).isNull()
+ assertThat(
+ store.findTypeConverter(
+ input = bType.makeNonNullable(),
+ output = stringType
+ )?.toSignature()
+ ).isEqualTo(
+ """
+ (TypeB! as TypeB?) / nullableTypeBToNullableString / checkNotNull(String?)
+ """.trimIndent()
+ )
+ }
+ }
+
+ @Test // 3P provided test case from https://issuetracker.google.com/issues/206961709#comment4
+ fun dontAssumeTypesCanBeConvertedUserCase() {
+ val source = Source.kotlin(
+ "Foo.kt", """
+ import androidx.room.*
+ import java.time.Instant
+ enum class Awesomeness {
+ AWESOME,
+ SUPER_DUPER_AWESOME,
+ }
+ @TypeConverters(
+ TimeConverter::class,
+ AwesomenessConverter::class,
+ )
+ class TimeConverter {
+ @TypeConverter
+ fun instantToValue(value: Instant?): String? { TODO() }
+
+ @TypeConverter
+ fun valueToInstant(value: String?): Instant? { TODO() }
+ }
+
+ class AwesomenessConverter {
+ @TypeConverter
+ fun awesomenessToValue(value: Awesomeness): String { TODO() }
+
+ @TypeConverter
+ fun valueToAwesomeness(value: String?): Awesomeness { TODO() }
+ }
+ """.trimIndent()
+ )
+ runKspTest(
+ sources = listOf(source)
+ ) { invocation ->
+ val store = invocation.createStore(
+ "TimeConverter", "AwesomenessConverter"
+ )
+ val instantType = invocation.processingEnv.requireType("java.time.Instant")
+ val stringType = invocation.processingEnv.requireType("java.lang.String")
+ assertThat(
+ store.findTypeConverter(
+ input = instantType,
+ output = stringType
+ )?.toSignature()
+ ).isEqualTo(
+ "(Instant! as Instant?) / instantToValue / checkNotNull(String?)"
+ )
+ }
+ }
+
+ /**
* Collect results for conversion from String to our type
*/
private fun collectStringConversionResults(
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/Signatures.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/Signatures.kt
index 4ddee69..4b6fbf2 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/Signatures.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/Signatures.kt
@@ -33,7 +33,7 @@
}
fun XType.toSignature() =
- "$typeName${nullability.toSignature()}".substringAfter("java.lang.")
+ "$typeName${nullability.toSignature()}".substringAfterLast(".")
fun TypeConverter.toSignature(): String {
return when (this) {
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index d6779a2..28b5b05 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -38,6 +38,7 @@
implementation(libs.kotlinStdlib)
implementation("androidx.compose.foundation:foundation-layout:1.1.0-rc01")
+ implementation(project(":profileinstaller:profileinstaller"))
testImplementation(libs.testRules)
testImplementation(libs.testRunner)
diff --git a/wear/compose/compose-foundation/src/androidMain/baseline-prof.txt b/wear/compose/compose-foundation/src/androidMain/baseline-prof.txt
new file mode 100644
index 0000000..e65b9e1
--- /dev/null
+++ b/wear/compose/compose-foundation/src/androidMain/baseline-prof.txt
@@ -0,0 +1,18 @@
+SPLandroidx/wear/compose/foundation/AnchorType;->**(**)**
+HSPLandroidx/wear/compose/foundation/ArcPaddingPx;->**(**)**
+Landroidx/wear/compose/foundation/ArcPaddingValues;
+SPLandroidx/wear/compose/foundation/ArcPaddingValuesImpl;->**(**)**
+HSPLandroidx/wear/compose/foundation/BasicCurvedTextKt**->**(**)**
+SPLandroidx/wear/compose/foundation/CurvedMeasuredChild;->**(**)**
+HSPLandroidx/wear/compose/foundation/CurvedRowKt**->**(**)**
+SPLandroidx/wear/compose/foundation/CurvedRowParentData;->**(**)**
+Landroidx/wear/compose/foundation/CurvedRowScope;
+SPLandroidx/wear/compose/foundation/CurvedRowScopeInstance;->**(**)**
+HSPLandroidx/wear/compose/foundation/CurvedTextDelegate;->**(**)**
+SPLandroidx/wear/compose/foundation/CurvedTextModifier;->**(**)**
+HSPLandroidx/wear/compose/foundation/CurvedTextStyle;->**(**)**
+SPLandroidx/wear/compose/foundation/CurvedTextStyleKt**->**(**)**
+HSPLandroidx/wear/compose/foundation/MeasuredChild;->**(**)**
+Landroidx/wear/compose/foundation/NormalMeasuredChild;
+SPLandroidx/wear/compose/foundation/RadialAlignment;->**(**)**
+Landroidx/wear/compose/foundation/RadialAlignmentImpl;
diff --git a/wear/compose/compose-material/build.gradle b/wear/compose/compose-material/build.gradle
index 96963ae..9676951 100644
--- a/wear/compose/compose-material/build.gradle
+++ b/wear/compose/compose-material/build.gradle
@@ -42,6 +42,7 @@
implementation("androidx.compose.material:material-ripple:1.1.0-rc01")
implementation("androidx.compose.ui:ui-util:1.1.0-rc01")
implementation(project(":wear:compose:compose-foundation"))
+ implementation(project(":profileinstaller:profileinstaller"))
androidTestImplementation(project(":compose:ui:ui-test"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
diff --git a/wear/compose/compose-material/src/androidMain/baseline-prof.txt b/wear/compose/compose-material/src/androidMain/baseline-prof.txt
new file mode 100644
index 0000000..dc96f70
--- /dev/null
+++ b/wear/compose/compose-material/src/androidMain/baseline-prof.txt
@@ -0,0 +1,103 @@
+HSPLandroidx/wear/compose/material/BrushPainter;->**(**)**
+Landroidx/wear/compose/material/ButtonColors;
+HSPLandroidx/wear/compose/material/ButtonDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/ButtonKt**->**(**)**
+HSPLandroidx/wear/compose/material/CardDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/CardKt**->**(**)**
+Landroidx/wear/compose/material/ChipColors;
+HSPLandroidx/wear/compose/material/ChipDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/ChipKt**->**(**)**
+HSPLandroidx/wear/compose/material/Colors;->**(**)**
+HSPLandroidx/wear/compose/material/ColorsKt**->**(**)**
+SPLandroidx/wear/compose/material/CombinedPaddingValues;->**(**)**
+SPLandroidx/wear/compose/material/ComposableSingletons;->**(**)**
+HSPLandroidx/wear/compose/material/ContentAlpha;->**(**)**
+HSPLandroidx/wear/compose/material/ContentAlphaKt**->**(**)**
+HSPLandroidx/wear/compose/material/ContentColorKt**->**(**)**
+HSPLandroidx/wear/compose/material/CurvedTextKt**->**(**)**
+HSPLandroidx/wear/compose/material/DefaultButtonColors;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultChipColors;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultInlineSliderColors;->**(**)**
+HPLandroidx/wear/compose/material/DefaultScalingLazyListItemInfo;->**(**)**
+HPLandroidx/wear/compose/material/DefaultScalingLazyListLayoutInfo;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultScalingParams;->**(**)**
+SPLandroidx/wear/compose/material/DefaultTimeSource;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultTimeSourceKt**->**(**)**
+SPLandroidx/wear/compose/material/DefaultToggleButtonColors;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultToggleChipColors;->**(**)**
+PLandroidx/wear/compose/material/DialogDefaults;->**(**)**
+HPLandroidx/wear/compose/material/DialogKt**->**(**)**
+Landroidx/wear/compose/material/EmptyScalingLazyListLayoutInfo;
+HSPLandroidx/wear/compose/material/FortyFiveDegreeLinearGradient;->**(**)**
+Landroidx/wear/compose/material/FractionPositionIndicatorState;
+Landroidx/wear/compose/material/FractionalThreshold;
+HSPLandroidx/wear/compose/material/IconKt**->**(**)**
+SPLandroidx/wear/compose/material/ImageResources;->**(**)**
+Landroidx/wear/compose/material/ImageWithScrimPainter;
+Landroidx/wear/compose/material/InlineSliderColors;
+HSPLandroidx/wear/compose/material/InlineSliderDefaults;->**(**)**
+Landroidx/wear/compose/material/LazyColumnStateAdapter;
+HSPLandroidx/wear/compose/material/ListHeaderKt**->**(**)**
+HSPLandroidx/wear/compose/material/MaterialRippleTheme;->**(**)**
+SPLandroidx/wear/compose/material/MaterialTextSelectionColorsKt**->**(**)**
+HSPLandroidx/wear/compose/material/MaterialTheme;->**(**)**
+SPLandroidx/wear/compose/material/MaterialThemeKt**->**(**)**
+SPLandroidx/wear/compose/material/PickerDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/PickerKt**->**(**)**
+Landroidx/wear/compose/material/PickerScope;
+SPLandroidx/wear/compose/material/PickerScopeImpl;->**(**)**
+SPLandroidx/wear/compose/material/PickerState;->**(**)**
+HSPLandroidx/wear/compose/material/PositionIndicatorKt**->**(**)**
+Landroidx/wear/compose/material/PositionIndicatorState;
+Landroidx/wear/compose/material/R;
+SPLandroidx/wear/compose/material/RangeDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/RangeDefaultsKt**->**(**)**
+SPLandroidx/wear/compose/material/RangeIcons;->**(**)**
+SPLandroidx/wear/compose/material/ResistanceConfig;->**(**)**
+HSPLandroidx/wear/compose/material/Resources_androidKt**->**(**)**
+HSPLandroidx/wear/compose/material/ScaffoldKt**->**(**)**
+HPLandroidx/wear/compose/material/ScaleAndAlpha;->**(**)**
+Landroidx/wear/compose/material/ScalingLazyColumnDefaults;
+HSPLandroidx/wear/compose/material/ScalingLazyColumnKt**->**(**)**
+HPLandroidx/wear/compose/material/ScalingLazyColumnMeasureKt**->**(**)**
+Landroidx/wear/compose/material/ScalingLazyColumnStateAdapter;
+Landroidx/wear/compose/material/ScalingLazyListItemInfo;
+Landroidx/wear/compose/material/ScalingLazyListItemScope;
+SPLandroidx/wear/compose/material/ScalingLazyListItemScopeImpl;->**(**)**
+Landroidx/wear/compose/material/ScalingLazyListLayoutInfo;
+SPLandroidx/wear/compose/material/ScalingLazyListScope;->**(**)**
+HSPLandroidx/wear/compose/material/ScalingLazyListScopeImpl;->**(**)**
+HSPLandroidx/wear/compose/material/ScalingLazyListState;->**(**)**
+Landroidx/wear/compose/material/ScalingLazyListStateKt;
+Landroidx/wear/compose/material/ScalingParams;
+HSPLandroidx/wear/compose/material/ScrollStateAdapter;->**(**)**
+SPLandroidx/wear/compose/material/Shapes;->**(**)**
+SPLandroidx/wear/compose/material/ShapesKt**->**(**)**
+HSPLandroidx/wear/compose/material/SliderKt**->**(**)**
+SPLandroidx/wear/compose/material/SqueezeMotion;->**(**)**
+SPLandroidx/wear/compose/material/StepperDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/StepperKt**->**(**)**
+SPLandroidx/wear/compose/material/SwipeDismissTarget;->**(**)**
+Landroidx/wear/compose/material/SwipeProgress;
+SPLandroidx/wear/compose/material/SwipeToDismissBoxDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/SwipeToDismissBoxKt**->**(**)**
+SPLandroidx/wear/compose/material/SwipeToDismissBoxState;->**(**)**
+SPLandroidx/wear/compose/material/SwipeableDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/SwipeableKt**->**(**)**
+HSPLandroidx/wear/compose/material/SwipeableState;->**(**)**
+HSPLandroidx/wear/compose/material/TextKt**->**(**)**
+Landroidx/wear/compose/material/ThresholdConfig;
+SPLandroidx/wear/compose/material/TimeBroadcastReceiver;->**(**)**
+Landroidx/wear/compose/material/TimeSource;
+SPLandroidx/wear/compose/material/TimeTextDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/TimeTextKt**->**(**)**
+Landroidx/wear/compose/material/ToggleButtonColors;
+HSPLandroidx/wear/compose/material/ToggleButtonDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/ToggleButtonKt**->**(**)**
+Landroidx/wear/compose/material/ToggleChipColors;
+HSPLandroidx/wear/compose/material/ToggleChipDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/ToggleChipKt**->**(**)**
+HSPLandroidx/wear/compose/material/Typography;->**(**)**
+HSPLandroidx/wear/compose/material/TypographyKt**->**(**)**
+HSPLandroidx/wear/compose/material/VignetteKt**->**(**)**
+SPLandroidx/wear/compose/material/VignettePosition;->**(**)**
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index b9cf22d..ef1ab1c 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -35,6 +35,7 @@
implementation(libs.kotlinStdlib)
implementation("androidx.navigation:navigation-compose:2.4.0-rc01")
+ implementation(project(":profileinstaller:profileinstaller"))
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
diff --git a/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt b/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt
new file mode 100644
index 0000000..a33330f
--- /dev/null
+++ b/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt
@@ -0,0 +1,4 @@
+SPLandroidx/wear/compose/navigation/NavGraphBuilderKt**->**(**)**
+SPLandroidx/wear/compose/navigation/SwipeDismissableNavHostControllerKt**->**(**)**
+HSPLandroidx/wear/compose/navigation/SwipeDismissableNavHostKt**->**(**)**
+HSPLandroidx/wear/compose/navigation/WearNavigator;->**(**)**
diff --git a/wear/compose/integration-tests/macrobenchmark-target/build.gradle b/wear/compose/integration-tests/macrobenchmark-target/build.gradle
index df9ad99..c2c870b 100644
--- a/wear/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/wear/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -43,7 +43,9 @@
implementation(project(":compose:ui:ui-tooling"))
implementation("androidx.activity:activity-compose:1.3.1")
implementation(project(":profileinstaller:profileinstaller"))
+ implementation project(path: ':wear:compose:compose-foundation')
implementation project(path: ':wear:compose:compose-material')
+ implementation project(path: ':wear:compose:compose-navigation')
}
android.defaultConfig.minSdkVersion 25
\ No newline at end of file
diff --git a/wear/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/wear/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index 76fba1c..8abaf1d 100644
--- a/wear/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/wear/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -72,6 +72,17 @@
</intent-filter>
</activity>
+ <activity
+ android:name=".BaselineActivity"
+ android:theme="@style/AppTheme"
+ android:exported="true">
+ <intent-filter>
+ <action android:name=
+ "androidx.wear.compose.integration.macrobenchmark.target.BASELINE_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
</application>
<uses-permission android:name="android.permission.WAKE_LOCK" />
diff --git a/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/BaselineActivity.kt b/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/BaselineActivity.kt
new file mode 100644
index 0000000..a149f5c
--- /dev/null
+++ b/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/BaselineActivity.kt
@@ -0,0 +1,259 @@
+/*
+ * 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.wear.compose.integration.macrobenchmark.target
+
+import androidx.activity.ComponentActivity
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.wear.compose.foundation.CurvedRow
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavHostController
+import androidx.wear.compose.foundation.ArcPaddingValues
+import androidx.wear.compose.foundation.BasicCurvedText
+import androidx.wear.compose.foundation.CurvedTextStyle
+import androidx.wear.compose.material.AlertDialog
+import androidx.wear.compose.material.AppCard
+import androidx.wear.compose.material.Button
+import androidx.wear.compose.material.Card
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.ChipDefaults
+import androidx.wear.compose.material.CompactButton
+import androidx.wear.compose.material.CompactChip
+import androidx.wear.compose.material.ConfirmationDialog
+import androidx.wear.compose.material.CurvedText
+import androidx.wear.compose.material.ExperimentalWearMaterialApi
+import androidx.wear.compose.material.InlineSlider
+import androidx.wear.compose.material.ListHeader
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.Picker
+import androidx.wear.compose.material.PositionIndicator
+import androidx.wear.compose.material.Scaffold
+import androidx.wear.compose.material.SplitToggleChip
+import androidx.wear.compose.material.Stepper
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.TimeText
+import androidx.wear.compose.material.TitleCard
+import androidx.wear.compose.material.ToggleButton
+import androidx.wear.compose.material.ToggleChip
+import androidx.wear.compose.material.Vignette
+import androidx.wear.compose.material.VignettePosition
+import androidx.wear.compose.material.rememberPickerState
+import androidx.wear.compose.navigation.SwipeDismissableNavHost
+import androidx.wear.compose.navigation.composable
+import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
+
+private val ALERT_DIALOG = "alert-dialog"
+private val CONFIRMATION_DIALOG = "confirmation-dialog"
+private val STEPPER = "stepper"
+private val SWIPE_DISMISS = "swipe-dismiss"
+
+class BaselineActivity : ComponentActivity() {
+
+ @OptIn(ExperimentalWearMaterialApi::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ val navController = rememberSwipeDismissableNavController()
+ val scrollState = rememberScrollState()
+
+ MaterialTheme {
+ Scaffold(
+ timeText = { TimeText() },
+ positionIndicator = { PositionIndicator(scrollState = scrollState) },
+ vignette = {
+ Vignette(vignettePosition = VignettePosition.TopAndBottom)
+ },
+ ) {
+ SwipeDismissableNavHost(
+ navController = navController,
+ startDestination = "start",
+ modifier = Modifier
+ .background(MaterialTheme.colors.background)
+ .semantics { contentDescription = SWIPE_DISMISS }
+ ) {
+ composable("start") {
+ Box {
+ CurvedTexts()
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(state = scrollState)
+ .padding(vertical = 16.dp)
+ .semantics { contentDescription = CONTENT_DESCRIPTION },
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Dialogs(navController)
+ Steppers(navController)
+ Buttons()
+ Cards()
+ Chips()
+ Sliders()
+ Pickers()
+ }
+ }
+ }
+ composable(ALERT_DIALOG) {
+ AlertDialog(
+ title = { Text("Alert") },
+ negativeButton = {},
+ positiveButton = {},
+ )
+ }
+ composable(CONFIRMATION_DIALOG) {
+ ConfirmationDialog(
+ onTimeout = { navController.popBackStack() },
+ content = { Text("Confirmation") },
+ )
+ }
+ composable(STEPPER) {
+ var value by remember { mutableStateOf(2f) }
+ Stepper(
+ value = value,
+ onValueChange = { value = it },
+ valueRange = 1f..4f,
+ steps = 7
+ ) { Text("Value: $value") }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun Buttons() {
+ ListHeader { Text("Buttons") }
+ Button(onClick = {}) { Text("Button") }
+ CompactButton(onClick = {}) { Text("CompactButton") }
+ ToggleButton(checked = true, onCheckedChange = {}) { Text("ToggleButton") }
+}
+
+@Composable
+fun Cards() {
+ ListHeader { Text("Cards") }
+ Card(onClick = {}) { Text("Card") }
+ AppCard(onClick = {}, appName = {}, time = {}, title = {}) { Text("AppCard") }
+ TitleCard(onClick = {}, title = {}) { Text("TitleCard") }
+}
+
+@Composable
+fun Chips() {
+ ListHeader { Text("Chips") }
+ Chip(onClick = {}, colors = ChipDefaults.primaryChipColors()) { Text("Chip") }
+ CompactChip(onClick = {}, label = { Text("CompactChip") })
+ ToggleChip(true, onCheckedChange = {}, label = { Text("ToggleChip") })
+ SplitToggleChip(
+ checked = true,
+ onCheckedChange = {},
+ label = { Text("SplitToggleChip") },
+ onClick = {})
+}
+
+@Composable
+fun CurvedTexts() {
+ CurvedRow(anchor = 235f) {
+ BasicCurvedText(
+ "Basic",
+ CurvedTextStyle(
+ fontSize = 16.sp,
+ color = Color.White,
+ background = MaterialTheme.colors.background
+ ),
+ contentArcPadding = ArcPaddingValues(2.dp)
+ )
+ }
+ CurvedRow(anchor = 310f) {
+ CurvedText(text = "Curved")
+ }
+}
+
+@Composable
+fun Dialogs(navController: NavHostController) {
+ ListHeader { Text("Dialogs") }
+ CompactChip(
+ onClick = { navController.navigate(ALERT_DIALOG) },
+ colors = ChipDefaults.primaryChipColors(),
+ label = { Text(ALERT_DIALOG) },
+ modifier = Modifier.semantics { contentDescription = ALERT_DIALOG },
+
+ )
+ CompactChip(
+ onClick = { navController.navigate(CONFIRMATION_DIALOG) },
+ colors = ChipDefaults.primaryChipColors(),
+ label = { Text(CONFIRMATION_DIALOG) },
+ modifier = Modifier.semantics { contentDescription = CONFIRMATION_DIALOG },
+ )
+}
+
+@Composable
+fun Pickers() {
+ ListHeader { Text("Pickers") }
+ val items = listOf("One", "Two", "Three", "Four", "Five")
+ Picker(
+ items.size,
+ state = rememberPickerState(),
+ option = { Text(items[it]) },
+ modifier = Modifier.size(100.dp, 100.dp),
+ )
+}
+
+@Composable
+fun Sliders() {
+ ListHeader { Text("Sliders") }
+ var value by remember { mutableStateOf(4.5f) }
+ InlineSlider(
+ value = value,
+ onValueChange = { value = it },
+ valueRange = 3f..6f,
+ steps = 5,
+ segmented = false
+ )
+}
+
+@Composable
+fun Steppers(navController: NavHostController) {
+ ListHeader { Text("Steppers") }
+ CompactChip(
+ onClick = { navController.navigate(STEPPER) },
+ colors = ChipDefaults.primaryChipColors(),
+ label = { Text(STEPPER) },
+ modifier = Modifier.semantics { contentDescription = STEPPER },
+ )
+}
diff --git a/wear/compose/integration-tests/macrobenchmark/build.gradle b/wear/compose/integration-tests/macrobenchmark/build.gradle
index c4e3c35..a37be4c 100644
--- a/wear/compose/integration-tests/macrobenchmark/build.gradle
+++ b/wear/compose/integration-tests/macrobenchmark/build.gradle
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
/*
* Copyright 2021 The Android Open Source Project
*
@@ -37,6 +39,13 @@
androidTestImplementation(libs.testUiautomator)
}
+// Allow usage of Kotlin's @OptIn.
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += [ '-Xopt-in=kotlin.RequiresOptIn' ]
+ }
+}
+
// Define a task dependency so the app is installed before we run macro benchmarks.
tasks.getByPath(":wear:compose:integration-tests:macrobenchmark:connectedCheck")
.dependsOn(tasks.getByPath(":wear:compose:integration-tests:macrobenchmark-target:installRelease"))
diff --git a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/BaselineProfile.kt b/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/BaselineProfile.kt
new file mode 100644
index 0000000..3789840
--- /dev/null
+++ b/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/BaselineProfile.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.wear.compose.integration.macrobenchmark.test
+
+import android.content.Intent
+import android.graphics.Point
+import androidx.benchmark.macro.ExperimentalBaselineProfilesApi
+import androidx.benchmark.macro.junit4.BaselineProfileRule
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.testutils.createCompilationParams
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runners.Parameterized
+
+// This test generates a baseline profile rules file that can be parsed to produce the
+// baseline-prof.txt files for the Wear Compose libraries.
+// 1) Build and install debug build of androidx.wear.compose.integration.macrobenchmark-target
+// onto a device (not minified, because we need non-obsfuscated method/class names)
+// 2) Run this test on the device - search for Benchmark in logcat to discover the location
+// of the generated file
+// e.g. /sdcard/Android/media/androidx.wear.compose.integration.macrobenchmark.test/
+// additional_test_output/BaselineProfile_profile-baseline-prof.txt
+// 3) Copy the generated file to your workspace:
+// adb pull <path-to-file-from-step-2 e.g. xxx/BaselineProfile_profile-baseline-prof.txt>
+// <workspace path e.g. xxx/frameworks/support/wear/compose/>
+// 4) Build profileparser:
+// If necessary, include it in settings.gradle:
+// includeProject(":wear:compose:integration-tests:profileparser",
+// "wear/compose/integration-tests/profileparser",
+// [BuildType.MAIN])
+// ./gradlew :wear:compose:integration-tests:profileparser:assemble
+// 5) Run profileparser for each of wear.compose.material, wear.compose.foundation and
+// wear.compose.navigation. From <workspace>/frameworks/support:
+// java -jar
+// ../../out/androidx/wear/compose/integration-tests/profileparser/build/libs/profileparser-all.jar
+// <input-generated-file eg ./wear/compose/BaselineProfile_profile-baseline-prof.txt>
+// <library-name e.g. androidx/wear/compose/material>
+// <output-file eg ./wear/compose/compose-material/src/androidMain/baseline-prof.txt>
+@LargeTest
+@SdkSuppress(minSdkVersion = 29)
+@OptIn(ExperimentalBaselineProfilesApi::class)
+class BaselineProfile {
+
+ @get:Rule
+ val baselineRule = BaselineProfileRule()
+
+ private lateinit var device: UiDevice
+ private val ALERT_DIALOG = "alert-dialog"
+ private val CONFIRMATION_DIALOG = "confirmation-dialog"
+ private val STEPPER = "stepper"
+
+ @Before
+ fun setUp() {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ device = UiDevice.getInstance(instrumentation)
+ }
+
+ @Test
+ fun profile() {
+ baselineRule.collectBaselineProfile(
+ packageName = PACKAGE_NAME,
+ setupBlock = {
+ val intent = Intent()
+ intent.action = ACTION
+ startActivityAndWait(intent)
+ },
+ profileBlock = {
+ testDestination(ALERT_DIALOG)
+ testDestination(CONFIRMATION_DIALOG)
+ testDestination(STEPPER)
+
+ // Scroll down to view remaining UI elements
+ // Setting a gesture margin is important otherwise gesture nav is triggered.
+ val list = device.findObject(By.desc(CONTENT_DESCRIPTION))
+ list.setGestureMargin(device.displayWidth / 5)
+ repeat(25) {
+ list.drag(Point(list.visibleCenter.x, list.visibleCenter.y / 3))
+ device.waitForIdle()
+ }
+ }
+ )
+ }
+
+ private fun testDestination(name: String) {
+ device.findObject(By.desc(name)).click()
+ device.waitForIdle()
+ device.pressBack()
+ device.waitForIdle()
+ }
+
+ companion object {
+ private const val PACKAGE_NAME = "androidx.wear.compose.integration.macrobenchmark.target"
+ private const val ACTION =
+ "androidx.wear.compose.integration.macrobenchmark.target.BASELINE_ACTIVITY"
+
+ @Parameterized.Parameters(name = "compilation={0}")
+ @JvmStatic
+ fun parameters() = createCompilationParams()
+ }
+}
\ No newline at end of file
diff --git a/wear/compose/integration-tests/profileparser/build.gradle.kts b/wear/compose/integration-tests/profileparser/build.gradle.kts
new file mode 100644
index 0000000..f6cd114
--- /dev/null
+++ b/wear/compose/integration-tests/profileparser/build.gradle.kts
@@ -0,0 +1,24 @@
+plugins {
+ application
+ kotlin("jvm")
+ // Use ShadowJar plugin to build fat jar.
+ id("com.github.johnrengelman.shadow")
+}
+
+application {
+ mainClass.set("androidx.wear.compose.integration.profileparser.ProfileParser")
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation(kotlin("stdlib"))
+}
+
+tasks.withType<Jar> {
+ manifest {
+ attributes["Main-Class"] = "androidx.wear.compose.integration.profileparser.ProfileParser"
+ }
+}
diff --git a/wear/compose/integration-tests/profileparser/src/main/AndroidManifest.xml b/wear/compose/integration-tests/profileparser/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..84bd075
--- /dev/null
+++ b/wear/compose/integration-tests/profileparser/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.wear.compose.integration.profileparser">
+
+ <application android:label="profileparser" />
+
+</manifest>
\ No newline at end of file
diff --git a/wear/compose/integration-tests/profileparser/src/main/java/androidx/wear/compose/integration/profileparser/ProfileParser.kt b/wear/compose/integration-tests/profileparser/src/main/java/androidx/wear/compose/integration/profileparser/ProfileParser.kt
new file mode 100644
index 0000000..37464eb
--- /dev/null
+++ b/wear/compose/integration-tests/profileparser/src/main/java/androidx/wear/compose/integration/profileparser/ProfileParser.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.wear.compose.integration.profileparser
+
+import java.io.File
+
+class ProfileParser {
+ companion object {
+ @JvmStatic
+ fun main(args: Array<String>) {
+ if (args.size < 3) {
+ println("Usage: profileparser <input file> <target e.g. wear/compose/material> " +
+ "<output file>")
+ return
+ }
+
+ println("Input: ${args[0]}")
+ println("Parse: ${args[1]}")
+ println("Output: ${args[2]}")
+
+ val input = File(args[0])
+ val output = File(args[2]).printWriter()
+
+ val lines = input.useLines {
+ it
+ .toList()
+ .map { it.truncatedAt(';') }
+ .map { it.truncatedAt('$') }
+ .filter { it.contains(args[1]) }
+ .fold(mutableMapOf()) {
+ acc: MutableMap<String, MutableList<String>>, item: String ->
+ // Collect unique keys based on the androidx/xxxx part of the name
+ // and accumulate a list of flags as the map value e.g. HSPL, L, SPL
+ val idx = item.indexOf("androidx")
+ val key = item.substring(idx)
+ val flags = item.substring(0, idx)
+ acc.getOrPut(key) { mutableListOf() }.add(flags)
+ acc
+ }
+ .map { (key, flags) ->
+ val flag = "HSPL".filter { c -> flags.any { flag -> flag.contains(c) } }
+ flag + key
+ }
+ .map {
+ // Tag on wild cards.
+ if (it.startsWith("L")) {
+ it + ";"
+ } else if (it.endsWith("Kt")) {
+ it + "**->**(**)**"
+ } else {
+ it + ";->**(**)**"
+ }
+ }
+ .sortedBy { it.substring(it.indexOf("androidx")) }
+ }
+ output.use { out ->
+ lines.forEach {
+ out.println(it)
+ }
+ }
+ println("Success!")
+ }
+
+ private fun String.truncatedAt(c: Char): String {
+ val idx = this.indexOf(c)
+ return if (idx == -1) this else this.substring(0, idx)
+ }
+ }
+}
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessClientTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessClientTest.java
index bc4ffb3..c31bb07 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessClientTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessClientTest.java
@@ -23,7 +23,6 @@
import androidx.annotation.Nullable;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
import org.junit.After;
@@ -37,7 +36,6 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
-@FlakyTest(bugId = 204197604)
@LargeTest
@RunWith(AndroidJUnit4.class)
public class WebViewRenderProcessClientTest {
@@ -116,7 +114,14 @@
WebkitUtils.onMainThreadSync(() -> {
WebView webView = mWebViewOnUiThread.getWebViewOnCurrentThread();
webView.evaluateJavascript("blocker.block();", null);
- blocker.waitForBlocked();
+ });
+ // Wait on the test instrumentation thread not the main thread. Blocking the main thread
+ // may block other async calls such as initializing the GPU service channel that happens on
+ // the UI thread and has to finish before the renderer can execute any javascript,
+ // see https://crbug.com/1269552.
+ blocker.waitForBlocked();
+ WebkitUtils.onMainThreadSync(() -> {
+ WebView webView = mWebViewOnUiThread.getWebViewOnCurrentThread();
// Sending an input event that does not get acknowledged will cause
// the unresponsive renderer event to fire.
webView.dispatchKeyEvent(
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/DatabaseTest.java b/work/work-runtime/src/androidTest/java/androidx/work/DatabaseTest.java
index cdbd3e2..e7d6c4a 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/DatabaseTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/DatabaseTest.java
@@ -21,27 +21,34 @@
import androidx.work.impl.model.WorkName;
import androidx.work.impl.model.WorkTag;
+import com.google.common.truth.Truth;
+
import org.junit.After;
import org.junit.Before;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
/**
* An abstract class for getting an in-memory instance of the {@link WorkDatabase}.
*/
public abstract class DatabaseTest extends WorkManagerTest {
protected WorkDatabase mDatabase;
+ private final ExecutorService mQueryExecutor = Executors.newCachedThreadPool();
@Before
public void initializeDb() {
mDatabase = WorkDatabase.create(
ApplicationProvider.getApplicationContext(),
- Executors.newCachedThreadPool(),
+ mQueryExecutor,
true);
}
@After
- public void closeDb() {
+ public void closeDb() throws InterruptedException {
+ mQueryExecutor.shutdown();
+ Truth.assertThat(mQueryExecutor.awaitTermination(3, TimeUnit.SECONDS)).isTrue();
mDatabase.close();
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/ProcessorTests.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/ProcessorTests.kt
index b3d0f4f..10e1b9a 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/ProcessorTests.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/ProcessorTests.kt
@@ -29,21 +29,29 @@
import androidx.work.impl.utils.SerialExecutor
import androidx.work.impl.utils.taskexecutor.TaskExecutor
import androidx.work.worker.StopLatchWorker
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
+import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class ProcessorTests : DatabaseTest() {
lateinit var scheduler: Scheduler
lateinit var factory: WorkerFactory
- lateinit var workerMap: MutableMap<String, ListenableWorker?>
+ val lastCreatedWorker = MutableStateFlow<ListenableWorker?>(null)
lateinit var processor: Processor
lateinit var defaultExecutor: ExecutorService
lateinit var backgroundExecutor: ExecutorService
@@ -52,7 +60,9 @@
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>().applicationContext
- defaultExecutor = Executors.newSingleThreadExecutor()
+ // first worker will take over the first thread with its doWork
+ // second worker will execute on the second thread
+ defaultExecutor = Executors.newFixedThreadPool(2)
backgroundExecutor = Executors.newSingleThreadExecutor()
serialExecutor = SerialExecutor(backgroundExecutor)
val taskExecutor = object : TaskExecutor {
@@ -68,20 +78,19 @@
return serialExecutor
}
}
- workerMap = mutableMapOf()
factory = object : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
- ): ListenableWorker? {
+ ): ListenableWorker {
val worker = getDefaultWorkerFactory()
.createWorkerWithDefaultFallback(
appContext,
workerClassName,
workerParameters
- )
- workerMap[workerParameters.id.toString()] = worker
+ )!!
+ lastCreatedWorker.value = worker
return worker
}
}
@@ -108,19 +117,37 @@
}
processor.addExecutionListener(listener)
processor.startWork(request.workSpec.id)
- processor.stopWork(request.workSpec.id)
+
+ val firstWorker = runBlocking { lastCreatedWorker.filterNotNull().first() }
+ val blockedThread = Executors.newSingleThreadExecutor()
+ blockedThread.execute {
+ // gonna stall for 10 seconds
+ processor.stopWork(request.workSpec.id)
+ }
+ assertTrue((firstWorker as StopLatchWorker).awaitOnStopCall())
+ // onStop call results in onExecuted. It happens on "main thread", which is instant
+ // in this case.
+ assertTrue(listenerCalled)
+ processor.removeExecutionListener(listener)
+ listenerCalled = false
+ val executionFinished = CountDownLatch(1)
+ processor.addExecutionListener { _, _ -> executionFinished.countDown() }
// This would have previously failed trying to acquire a lock
processor.startWork(request.workSpec.id)
- (workerMap[request.workSpec.id] as? StopLatchWorker)?.countDown()
- while (serialExecutor.hasPendingTasks()) {
- // Wait until we are done
- }
- assertEquals(listenerCalled, true)
+ val secondWorker =
+ runBlocking { lastCreatedWorker.filterNotNull().filter { it != firstWorker }.first() }
+ (secondWorker as StopLatchWorker).countDown()
+ assertTrue(executionFinished.await(3, TimeUnit.SECONDS))
+ firstWorker.countDown()
+ blockedThread.shutdown()
+ assertTrue(blockedThread.awaitTermination(3, TimeUnit.SECONDS))
}
@After
fun tearDown() {
defaultExecutor.shutdownNow()
backgroundExecutor.shutdownNow()
+ assertTrue(defaultExecutor.awaitTermination(3, TimeUnit.SECONDS))
+ assertTrue(backgroundExecutor.awaitTermination(3, TimeUnit.SECONDS))
}
}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/worker/StopLatchWorker.kt b/work/work-runtime/src/androidTest/java/androidx/work/worker/StopLatchWorker.kt
index 2805997..8a7b87b 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/worker/StopLatchWorker.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/worker/StopLatchWorker.kt
@@ -25,20 +25,24 @@
class StopLatchWorker(context: Context, parameters: WorkerParameters) :
Worker(context, parameters) {
- private val latch = CountDownLatch(1)
+ private val stopLatch = CountDownLatch(1)
+ private val onStopLatch = CountDownLatch(1)
override fun doWork(): Result {
- while (latch.count > 0) {
+ while (stopLatch.count > 0) {
// do nothing.
}
return Result.success()
}
+ fun awaitOnStopCall() = onStopLatch.await(5000, TimeUnit.SECONDS)
+
fun countDown() {
- latch.countDown()
+ stopLatch.countDown()
}
override fun onStopped() {
- latch.await(10, TimeUnit.SECONDS)
+ onStopLatch.countDown()
+ stopLatch.await(5000, TimeUnit.SECONDS)
}
}