Merge changes I5a8e18d4,I066fb9db into androidx-main
* changes:
Move corruption test to the new infra
Move update data cancelation unblocks other process test to the new infra
diff --git a/activity/activity/api/current.txt b/activity/activity/api/current.txt
index c241ac8..f55a193 100644
--- a/activity/activity/api/current.txt
+++ b/activity/activity/api/current.txt
@@ -3,7 +3,7 @@
public final class BackEventCompat {
ctor @RequiresApi(34) public BackEventCompat(android.window.BackEvent backEvent);
- ctor @VisibleForTesting public BackEventCompat(float touchX, float touchY, float progress, int swipeEdge);
+ ctor @VisibleForTesting public BackEventCompat(float touchX, float touchY, @FloatRange(from=0.0, to=1.0) float progress, int swipeEdge);
method public float getProgress();
method public int getSwipeEdge();
method public float getTouchX();
diff --git a/activity/activity/api/restricted_current.txt b/activity/activity/api/restricted_current.txt
index 9d7924a..0348b24 100644
--- a/activity/activity/api/restricted_current.txt
+++ b/activity/activity/api/restricted_current.txt
@@ -3,7 +3,7 @@
public final class BackEventCompat {
ctor @RequiresApi(34) public BackEventCompat(android.window.BackEvent backEvent);
- ctor @VisibleForTesting public BackEventCompat(float touchX, float touchY, float progress, int swipeEdge);
+ ctor @VisibleForTesting public BackEventCompat(float touchX, float touchY, @FloatRange(from=0.0, to=1.0) float progress, int swipeEdge);
method public float getProgress();
method public int getSwipeEdge();
method public float getTouchX();
diff --git a/activity/activity/src/main/java/androidx/activity/BackEventCompat.kt b/activity/activity/src/main/java/androidx/activity/BackEventCompat.kt
index b850269..a53c63d 100644
--- a/activity/activity/src/main/java/androidx/activity/BackEventCompat.kt
+++ b/activity/activity/src/main/java/androidx/activity/BackEventCompat.kt
@@ -19,6 +19,7 @@
import android.os.Build
import android.window.BackEvent
import androidx.annotation.DoNotInline
+import androidx.annotation.FloatRange
import androidx.annotation.IntDef
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
@@ -29,16 +30,19 @@
*/
class BackEventCompat @VisibleForTesting constructor(
/**
- * Absolute X location of the touch point of this event.
+ * Absolute X location of the touch point of this event in the coordinate space of the view that
+ * * received this back event.
*/
val touchX: Float,
/**
- * Absolute Y location of the touch point of this event.
+ * Absolute Y location of the touch point of this event in the coordinate space of the view that
+ * received this back event.
*/
val touchY: Float,
/**
* Value between 0 and 1 on how far along the back gesture is.
*/
+ @FloatRange(from = 0.0, to = 1.0)
val progress: Float,
/**
* Indicates which edge the swipe starts from.
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/SideEffects.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/SideEffects.kt
index 60e765f..5d16f73 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/SideEffects.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/SideEffects.kt
@@ -50,8 +50,10 @@
// https://source.corp.google.com/piper///depot/google3/java/com/google/android/libraries/swpower/fixture/DisableModule.java
internal val DEFAULT_PACKAGES_TO_DISABLE = listOf(
"com.android.chrome",
+ "com.android.dialer",
"com.android.phone",
"com.android.ramdump",
+ "com.android.server.telecom",
"com.android.vending",
"com.google.android.apps.docs",
"com.google.android.apps.gcs",
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
index 2ce63cd..5bde073 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
@@ -216,6 +216,8 @@
)
// Turn on method tracing
scope.launchWithMethodTracing = true
+ // Force Method Tracing
+ scope.methodTracingForTests = true
// Launch first activity, and validate it is displayed
scope.startActivityAndWait(ConfigurableActivity.createIntent("InitialText"))
assertTrue(device.hasObject(By.text("InitialText")))
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index df00a1a..217cf5d 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -290,13 +290,14 @@
metrics.forEach {
it.stop()
}
- if (launchWithMethodTracing) {
+ if (launchWithMethodTracing && scope.isMethodTracing) {
val (label, tracePath) = scope.stopMethodTracing()
val resultFile = Profiler.ResultFile(
label = label,
absolutePath = tracePath
)
resultFiles += resultFile
+ scope.isMethodTracing = false
}
}
}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
index 12c949c..9b141c6 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
@@ -22,6 +22,7 @@
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
import androidx.benchmark.DeviceInfo
import androidx.benchmark.Outputs
import androidx.benchmark.Shell
@@ -62,6 +63,18 @@
internal var launchWithMethodTracing: Boolean = false
/**
+ * Only use this for testing. This forces `--start-profiler` without the check for process
+ * live ness.
+ */
+ @VisibleForTesting
+ internal var methodTracingForTests: Boolean = false
+
+ /**
+ * This is `true` iff method tracing is currently active.
+ */
+ internal var isMethodTracing: Boolean = false
+
+ /**
* Current Macrobenchmark measurement iteration, or null if measurement is not yet enabled.
*
* Non-measurement iterations can occur due to warmup a [CompilationMode], or prior to the first
@@ -133,9 +146,16 @@
getFrameStats().map { it.uniqueName }
}
val preLaunchTimestampNs = System.nanoTime()
- val profileArgs = if (launchWithMethodTracing) {
+ // Only use --start-profiler is the package is not alive. Otherwise re-use the existing
+ // profiling session.
+ val profileArgs =
+ if (launchWithMethodTracing && (methodTracingForTests || !Shell.isPackageAlive(
+ packageName
+ ))
+ ) {
+ isMethodTracing = true
val tracePath = methodTracePath(packageName, iteration ?: 0)
- "--start-profiler \"$tracePath\""
+ "--start-profiler \"$tracePath\" --streaming"
} else {
""
}
diff --git a/browser/browser/api/current.txt b/browser/browser/api/current.txt
index 2d7adf9d6..28f6ded 100644
--- a/browser/browser/api/current.txt
+++ b/browser/browser/api/current.txt
@@ -104,6 +104,7 @@
method public static androidx.browser.customtabs.CustomTabColorSchemeParams getColorSchemeParams(android.content.Intent, int);
method @Dimension(unit=androidx.annotation.Dimension.PX) public static int getInitialActivityHeightPx(android.content.Intent);
method public static int getMaxToolbarItems();
+ method public android.app.PendingIntent? getSecondaryToolbarSwipeUpGesture(android.content.Intent);
method @Dimension(unit=androidx.annotation.Dimension.DP) public static int getToolbarCornerRadiusDp(android.content.Intent);
method public static java.util.Locale? getTranslateLocale(android.content.Intent);
method public static boolean isBackgroundInteractionEnabled(android.content.Intent);
@@ -145,6 +146,7 @@
field public static final String EXTRA_REMOTEVIEWS_PENDINGINTENT = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_PENDINGINTENT";
field public static final String EXTRA_REMOTEVIEWS_VIEW_IDS = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_VIEW_IDS";
field public static final String EXTRA_SECONDARY_TOOLBAR_COLOR = "android.support.customtabs.extra.SECONDARY_TOOLBAR_COLOR";
+ field public static final String EXTRA_SECONDARY_TOOLBAR_SWIPE_UP_GESTURE = "androidx.browser.customtabs.extra.SECONDARY_TOOLBAR_SWIPE_UP_GESTURE";
field public static final String EXTRA_SEND_TO_EXTERNAL_DEFAULT_HANDLER = "android.support.customtabs.extra.SEND_TO_EXTERNAL_HANDLER";
field public static final String EXTRA_SESSION = "android.support.customtabs.extra.SESSION";
field public static final String EXTRA_SHARE_STATE = "androidx.browser.customtabs.extra.SHARE_STATE";
@@ -196,6 +198,7 @@
method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setNavigationBarColor(@ColorInt int);
method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setNavigationBarDividerColor(@ColorInt int);
method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarColor(@ColorInt int);
+ method public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarSwipeUpGesture(android.app.PendingIntent?);
method public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarViews(android.widget.RemoteViews, int[]?, android.app.PendingIntent?);
method public androidx.browser.customtabs.CustomTabsIntent.Builder setSendToExternalDefaultHandlerEnabled(boolean);
method public androidx.browser.customtabs.CustomTabsIntent.Builder setSession(androidx.browser.customtabs.CustomTabsSession);
@@ -266,6 +269,7 @@
method public boolean setActionButton(android.graphics.Bitmap, String);
method @RequiresFeature(name=androidx.browser.customtabs.CustomTabsFeatures.ENGAGEMENT_SIGNALS, enforcement="androidx.browser.customtabs.CustomTabsSession#isEngagementSignalsApiAvailable") public boolean setEngagementSignalsCallback(androidx.browser.customtabs.EngagementSignalsCallback, android.os.Bundle) throws android.os.RemoteException;
method @RequiresFeature(name=androidx.browser.customtabs.CustomTabsFeatures.ENGAGEMENT_SIGNALS, enforcement="androidx.browser.customtabs.CustomTabsSession#isEngagementSignalsApiAvailable") public boolean setEngagementSignalsCallback(java.util.concurrent.Executor, androidx.browser.customtabs.EngagementSignalsCallback, android.os.Bundle) throws android.os.RemoteException;
+ method public boolean setSecondaryToolbarSwipeUpGesture(android.app.PendingIntent?);
method public boolean setSecondaryToolbarViews(android.widget.RemoteViews?, int[]?, android.app.PendingIntent?);
method @Deprecated public boolean setToolbarItem(int, android.graphics.Bitmap, String);
method public boolean validateRelationship(@androidx.browser.customtabs.CustomTabsService.Relation int, android.net.Uri, android.os.Bundle?);
diff --git a/browser/browser/api/restricted_current.txt b/browser/browser/api/restricted_current.txt
index 5b842a4..4c1585e 100644
--- a/browser/browser/api/restricted_current.txt
+++ b/browser/browser/api/restricted_current.txt
@@ -115,6 +115,7 @@
method public static androidx.browser.customtabs.CustomTabColorSchemeParams getColorSchemeParams(android.content.Intent, int);
method @Dimension(unit=androidx.annotation.Dimension.PX) public static int getInitialActivityHeightPx(android.content.Intent);
method public static int getMaxToolbarItems();
+ method public android.app.PendingIntent? getSecondaryToolbarSwipeUpGesture(android.content.Intent);
method @Dimension(unit=androidx.annotation.Dimension.DP) public static int getToolbarCornerRadiusDp(android.content.Intent);
method public static java.util.Locale? getTranslateLocale(android.content.Intent);
method public static boolean isBackgroundInteractionEnabled(android.content.Intent);
@@ -156,6 +157,7 @@
field public static final String EXTRA_REMOTEVIEWS_PENDINGINTENT = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_PENDINGINTENT";
field public static final String EXTRA_REMOTEVIEWS_VIEW_IDS = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_VIEW_IDS";
field public static final String EXTRA_SECONDARY_TOOLBAR_COLOR = "android.support.customtabs.extra.SECONDARY_TOOLBAR_COLOR";
+ field public static final String EXTRA_SECONDARY_TOOLBAR_SWIPE_UP_GESTURE = "androidx.browser.customtabs.extra.SECONDARY_TOOLBAR_SWIPE_UP_GESTURE";
field public static final String EXTRA_SEND_TO_EXTERNAL_DEFAULT_HANDLER = "android.support.customtabs.extra.SEND_TO_EXTERNAL_HANDLER";
field public static final String EXTRA_SESSION = "android.support.customtabs.extra.SESSION";
field public static final String EXTRA_SHARE_STATE = "androidx.browser.customtabs.extra.SHARE_STATE";
@@ -207,6 +209,7 @@
method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setNavigationBarColor(@ColorInt int);
method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setNavigationBarDividerColor(@ColorInt int);
method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarColor(@ColorInt int);
+ method public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarSwipeUpGesture(android.app.PendingIntent?);
method public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarViews(android.widget.RemoteViews, int[]?, android.app.PendingIntent?);
method public androidx.browser.customtabs.CustomTabsIntent.Builder setSendToExternalDefaultHandlerEnabled(boolean);
method public androidx.browser.customtabs.CustomTabsIntent.Builder setSession(androidx.browser.customtabs.CustomTabsSession);
@@ -277,6 +280,7 @@
method public boolean setActionButton(android.graphics.Bitmap, String);
method @RequiresFeature(name=androidx.browser.customtabs.CustomTabsFeatures.ENGAGEMENT_SIGNALS, enforcement="androidx.browser.customtabs.CustomTabsSession#isEngagementSignalsApiAvailable") public boolean setEngagementSignalsCallback(androidx.browser.customtabs.EngagementSignalsCallback, android.os.Bundle) throws android.os.RemoteException;
method @RequiresFeature(name=androidx.browser.customtabs.CustomTabsFeatures.ENGAGEMENT_SIGNALS, enforcement="androidx.browser.customtabs.CustomTabsSession#isEngagementSignalsApiAvailable") public boolean setEngagementSignalsCallback(java.util.concurrent.Executor, androidx.browser.customtabs.EngagementSignalsCallback, android.os.Bundle) throws android.os.RemoteException;
+ method public boolean setSecondaryToolbarSwipeUpGesture(android.app.PendingIntent?);
method public boolean setSecondaryToolbarViews(android.widget.RemoteViews?, int[]?, android.app.PendingIntent?);
method @Deprecated public boolean setToolbarItem(int, android.graphics.Bitmap, String);
method public boolean validateRelationship(@androidx.browser.customtabs.CustomTabsService.Relation int, android.net.Uri, android.os.Bundle?);
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
index 19aa937..73ece64 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
@@ -200,6 +200,13 @@
"android.support.customtabs.customaction.SHOW_ON_TOOLBAR";
/**
+ * Extra that specifies the {@link PendingIntent} to be sent when the user swipes up from
+ * the secondary (bottom) toolbar.
+ */
+ public static final String EXTRA_SECONDARY_TOOLBAR_SWIPE_UP_GESTURE =
+ "androidx.browser.customtabs.extra.SECONDARY_TOOLBAR_SWIPE_UP_GESTURE";
+
+ /**
* Don't show any title. Shows only the domain.
*/
public static final int NO_TITLE = 0;
@@ -896,6 +903,18 @@
}
/**
+ * Sets the {@link PendingIntent} to be sent when the user swipes up from
+ * the secondary (bottom) toolbar.
+ * @param pendingIntent The {@link PendingIntent} that will be sent when
+ * the user swipes up from the secondary toolbar.
+ */
+ @NonNull
+ public Builder setSecondaryToolbarSwipeUpGesture(@Nullable PendingIntent pendingIntent) {
+ mIntent.putExtra(EXTRA_SECONDARY_TOOLBAR_SWIPE_UP_GESTURE, pendingIntent);
+ return this;
+ }
+
+ /**
* Sets whether Instant Apps is enabled for this Custom Tab.
* @param enabled Whether Instant Apps should be enabled.
@@ -1451,6 +1470,17 @@
return intent.getBooleanExtra(EXTRA_SHOW_ON_TOOLBAR, false);
}
+ /**
+ * @return The {@link PendingIntent} that will be sent when the user swipes up
+ * from the secondary toolbar.
+ * @see CustomTabsIntent#EXTRA_SECONDARY_TOOLBAR_SWIPE_UP_GESTURE
+ */
+ @SuppressWarnings("deprecation")
+ @Nullable
+ public PendingIntent getSecondaryToolbarSwipeUpGesture(@NonNull Intent intent) {
+ return intent.getParcelableExtra(EXTRA_SECONDARY_TOOLBAR_SWIPE_UP_GESTURE);
+ }
+
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private static class Api21Impl {
@DoNotInline
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
index 8f4f865..76a35e9 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
@@ -166,6 +166,25 @@
}
/**
+ * Sets a {@link PendingIntent} object to be sent when the user swipes up from the secondary
+ * (bottom) toolbar.
+ *
+ * @param pendingIntent {@link PendingIntent} to send.
+ * @return Whether the update succeeded.
+ */
+ public boolean setSecondaryToolbarSwipeUpGesture(@Nullable PendingIntent pendingIntent) {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(CustomTabsIntent.EXTRA_SECONDARY_TOOLBAR_SWIPE_UP_GESTURE,
+ pendingIntent);
+ addIdToBundle(bundle);
+ try {
+ return mService.updateVisuals(mCallback, bundle);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /**
* Updates the visuals for toolbar items. Will only succeed if a custom tab created using this
* session is in the foreground in browser and the given id is valid.
*
diff --git a/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java b/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
index 89d5148..267d5a0 100644
--- a/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
+++ b/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
@@ -25,6 +25,7 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import android.app.PendingIntent;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
@@ -621,6 +622,18 @@
assertEquals(locale.toLanguageTag(), Locale.FRANCE.toLanguageTag());
}
+ @Config(minSdk = Build.VERSION_CODES.N)
+ @Test
+ public void testSecondaryToolbarSwipeUpGesture() {
+ PendingIntent pendingIntent = TestUtil.makeMockPendingIntent();
+ Intent intent = new CustomTabsIntent.Builder()
+ .setSecondaryToolbarSwipeUpGesture(pendingIntent)
+ .build()
+ .intent;
+ assertEquals(pendingIntent, intent.getParcelableExtra(
+ CustomTabsIntent.EXTRA_SECONDARY_TOOLBAR_SWIPE_UP_GESTURE));
+ }
+
private void assertNullSessionInExtras(Intent intent) {
assertTrue(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION));
assertNull(intent.getExtras().getBinder(CustomTabsIntent.EXTRA_SESSION));
diff --git a/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java b/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java
index fa0c171..79e40a0 100644
--- a/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java
+++ b/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java
@@ -51,7 +51,7 @@
}
@NonNull
- private static PendingIntent makeMockPendingIntent() {
+ public static PendingIntent makeMockPendingIntent() {
return PendingIntent.getBroadcast(mock(Context.class), 0, new Intent(), 0);
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt
index 07d5c42..a19f9a0 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt
@@ -106,10 +106,8 @@
// TODO(149103692): remove all elements of this set
val taskNamesKnownToDuplicateOutputs =
setOf(
- "kotlinSourcesJar",
- "releaseSourcesJar",
- "sourceJarRelease",
- "sourceJar",
+ // Instead of adding new elements to this set, prefer to disable unused tasks when possible
+
// The following tests intentionally have the same output of golden images
"updateGoldenDesktopTest",
"updateGoldenDebugUnitTest"
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
index e98648a..1e6ed89 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
@@ -97,6 +97,11 @@
}
}
}
+
+ val disableNames = setOf(
+ "releaseSourcesJar",
+ )
+ disableUnusedSourceJarTasks(disableNames)
}
/** Sets up a source jar task for a Java library project. */
@@ -128,6 +133,11 @@
}
}
registerSourcesVariant(sourceJar)
+
+ val disableNames = setOf(
+ "kotlinSourcesJar",
+ )
+ disableUnusedSourceJarTasks(disableNames)
}
fun Project.configureSourceJarForMultiplatform() {
@@ -161,6 +171,18 @@
task.metaInf.from(metadataFile)
}
registerMultiplatformSourcesVariant(sourceJar)
+ val disableNames = setOf(
+ "kotlinSourcesJar",
+ )
+ disableUnusedSourceJarTasks(disableNames)
+}
+
+fun Project.disableUnusedSourceJarTasks(disableNames: Set<String>) {
+ project.tasks.configureEach({ task ->
+ if (disableNames.contains(task.name)) {
+ task.enabled = false
+ }
+ })
}
internal val Project.multiplatformUsage
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
index 4d083da..8361fb3 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
@@ -248,8 +248,6 @@
builder.addCameraCaptureCallback(CaptureCallbackContainer.create(it))
}
- // TODO: Copy CameraEventCallback (used for extension)
-
// Copy extended Camera2 configurations
val extendedConfig = MutableOptionsBundle.create().apply {
camera2Config.getPhysicalCameraId()?.let { physicalCameraId ->
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt
index ab6853b..58c49d6 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfig.kt
@@ -66,7 +66,6 @@
internal val SESSION_PHYSICAL_CAMERA_ID_OPTION: Config.Option<String> = Config.Option.create(
"camera2.cameraCaptureSession.physicalCameraId", String::class.java
)
-// TODO: Porting the CameraEventCallback option constant.
/**
* Internal shared implementation details for camera 2 interop.
@@ -170,8 +169,6 @@
return config.retrieveOption(SESSION_CAPTURE_CALLBACK_OPTION, valueIfMissing)
}
- // TODO: Prepare a getter for CameraEventCallbacks
-
/**
* Returns the capture request tag.
*
@@ -275,8 +272,6 @@
return Camera2ImplConfig(OptionsBundle.from(mutableOptionsBundle))
}
}
-
- // TODO: Prepare a setter for CameraEventCallbacks, ex: setCameraEventCallback
}
internal fun CaptureRequest.Key<*>.createCaptureRequestOption(): Config.Option<Any> {
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfigTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfigTest.kt
index 2e37850..a99cf92 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfigTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/Camera2ImplConfigTest.kt
@@ -163,7 +163,4 @@
true
}
}
-
- // TODO: After porting CameraEventCallback (used for extension) to CameraUseCaseAdapter,
- // also porting canExtendWithCameraEventCallback
}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2RequestProcessorTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2RequestProcessorTest.kt
index 747aa69..fb74085 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2RequestProcessorTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2RequestProcessorTest.kt
@@ -81,7 +81,7 @@
private lateinit var cameraDeviceHolder: CameraUtil.CameraDeviceHolder
private lateinit var captureSessionRepository: CaptureSessionRepository
private lateinit var dynamicRangesCompat: DynamicRangesCompat
- private lateinit var captureSessionOpenerBuilder: SynchronizedCaptureSessionOpener.Builder
+ private lateinit var captureSessionOpenerBuilder: SynchronizedCaptureSession.OpenerBuilder
private lateinit var mainThreadExecutor: Executor
private lateinit var previewSurface: SessionProcessorSurface
private lateinit var captureSurface: SessionProcessorSurface
@@ -107,7 +107,7 @@
captureSessionRepository = CaptureSessionRepository(mainThreadExecutor)
val cameraCharacteristics = getCameraCharacteristic(CAMERA_ID)
dynamicRangesCompat = DynamicRangesCompat.fromCameraCharacteristics(cameraCharacteristics)
- captureSessionOpenerBuilder = SynchronizedCaptureSessionOpener.Builder(
+ captureSessionOpenerBuilder = SynchronizedCaptureSession.OpenerBuilder(
mainThreadExecutor,
mainThreadExecutor as ScheduledExecutorService,
handler,
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
index ba71d6e..4f491d0 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/CaptureSessionTest.java
@@ -38,14 +38,11 @@
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.content.Context;
import android.graphics.ImageFormat;
@@ -74,8 +71,6 @@
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.Camera2Config;
import androidx.camera.camera2.impl.Camera2ImplConfig;
-import androidx.camera.camera2.impl.CameraEventCallback;
-import androidx.camera.camera2.impl.CameraEventCallbacks;
import androidx.camera.camera2.internal.CaptureSession.State;
import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
@@ -93,7 +88,6 @@
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.ImmediateSurface;
-import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.Quirks;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
@@ -123,7 +117,6 @@
import org.junit.runner.RunWith;
import org.junit.runners.model.Statement;
import org.mockito.ArgumentCaptor;
-import org.mockito.InOrder;
import org.mockito.Mockito;
import java.util.ArrayList;
@@ -184,7 +177,7 @@
private CameraUtil.CameraDeviceHolder mCameraDeviceHolder;
private CaptureSessionRepository mCaptureSessionRepository;
- private SynchronizedCaptureSessionOpener.Builder mCaptureSessionOpenerBuilder;
+ private SynchronizedCaptureSession.OpenerBuilder mCaptureSessionOpenerBuilder;
private final List<CaptureSession> mCaptureSessions = new ArrayList<>();
private final List<DeferrableSurface> mDeferrableSurfaces = new ArrayList<>();
@@ -240,7 +233,7 @@
mCaptureSessionRepository = new CaptureSessionRepository(mExecutor);
- mCaptureSessionOpenerBuilder = new SynchronizedCaptureSessionOpener.Builder(mExecutor,
+ mCaptureSessionOpenerBuilder = new SynchronizedCaptureSession.OpenerBuilder(mExecutor,
mScheduledExecutor, mHandler, mCaptureSessionRepository,
new Quirks(new ArrayList<>()), DeviceQuirks.getAll());
@@ -684,14 +677,12 @@
CaptureResult captureResult =
((Camera2CameraCaptureResult) cameraCaptureResult).getCaptureResult();
- // From CameraEventCallbacks option
+ // From SessionConfig option
assertThat(captureResult.getRequest().get(CaptureRequest.CONTROL_AF_MODE)).isEqualTo(
- CaptureRequest.CONTROL_AF_MODE_MACRO);
+ CaptureRequest.CONTROL_AF_MODE_AUTO);
assertThat(captureResult.getRequest().get(
CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION)).isEqualTo(
- mTestParameters0.mEvRange.getLower());
-
- // From SessionConfig option
+ mTestParameters0.mEvRange.getUpper());
assertThat(captureResult.getRequest().get(CaptureRequest.CONTROL_AE_MODE)).isEqualTo(
CaptureRequest.CONTROL_AE_MODE_ON);
}
@@ -857,12 +848,10 @@
assertThat(captureResult.getRequest().get(CaptureRequest.CONTROL_AF_MODE)).isEqualTo(
CaptureRequest.CONTROL_AF_MODE_OFF);
- // From CameraEventCallbacks option
+ // From SessionConfig option
assertThat(captureResult.getRequest().get(
CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION)).isEqualTo(
- mTestParameters0.mEvRange.getLower());
-
- // From SessionConfig option
+ mTestParameters0.mEvRange.getUpper());
assertThat(captureResult.getRequest().get(CaptureRequest.CONTROL_AE_MODE)).isEqualTo(
CaptureRequest.CONTROL_AE_MODE_ON);
}
@@ -948,7 +937,7 @@
@Test
public void surfaceTerminationFutureIsCalledWhenSessionIsClose() throws InterruptedException {
- mCaptureSessionOpenerBuilder = new SynchronizedCaptureSessionOpener.Builder(mExecutor,
+ mCaptureSessionOpenerBuilder = new SynchronizedCaptureSession.OpenerBuilder(mExecutor,
mScheduledExecutor, mHandler, mCaptureSessionRepository,
new Quirks(Arrays.asList(new PreviewOrientationIncorrectQuirk())),
DeviceQuirks.getAll());
@@ -971,101 +960,9 @@
}
@Test
- public void cameraEventCallbackInvokedInOrder() {
- CaptureSession captureSession = createCaptureSession();
- captureSession.setSessionConfig(mTestParameters0.mSessionConfig);
-
- captureSession.open(mTestParameters0.mSessionConfig, mCameraDeviceHolder.get(),
- mCaptureSessionOpenerBuilder.build());
- InOrder inOrder = inOrder(mTestParameters0.mMockCameraEventCallback);
-
- inOrder.verify(mTestParameters0.mMockCameraEventCallback, timeout(3000)).onInitSession();
- inOrder.verify(mTestParameters0.mMockCameraEventCallback, timeout(3000)).onEnableSession();
- inOrder.verify(mTestParameters0.mMockCameraEventCallback, timeout(3000)).onRepeating();
- verify(mTestParameters0.mMockCameraEventCallback, never()).onDisableSession();
-
- verifyNoMoreInteractions(mTestParameters0.mMockCameraEventCallback);
-
- captureSession.close();
- verify(mTestParameters0.mMockCameraEventCallback, timeout(3000)).onDisableSession();
- captureSession.release(false);
- verify(mTestParameters0.mMockCameraEventCallback, timeout(3000)).onDeInitSession();
-
- verifyNoMoreInteractions(mTestParameters0.mMockCameraEventCallback);
- }
-
- @Test
- public void cameraEventCallbackInvoked_assignDifferentSessionConfig() {
- CaptureSession captureSession = createCaptureSession();
- captureSession.setSessionConfig(new SessionConfig.Builder().build());
- captureSession.open(mTestParameters0.mSessionConfig, mCameraDeviceHolder.get(),
- mCaptureSessionOpenerBuilder.build());
-
- InOrder inOrder = inOrder(mTestParameters0.mMockCameraEventCallback);
- inOrder.verify(mTestParameters0.mMockCameraEventCallback, timeout(3000)).onInitSession();
- inOrder.verify(mTestParameters0.mMockCameraEventCallback, timeout(3000)).onEnableSession();
- // Should not trigger repeating since the repeating SessionConfig is empty.
- verify(mTestParameters0.mMockCameraEventCallback, never()).onRepeating();
-
- captureSession.close();
- inOrder.verify(mTestParameters0.mMockCameraEventCallback, timeout(3000)).onDisableSession();
- captureSession.release(false);
- verify(mTestParameters0.mMockCameraEventCallback, timeout(3000)).onDeInitSession();
-
- verifyNoMoreInteractions(mTestParameters0.mMockCameraEventCallback);
- }
-
- @Test
- public void cameraEventCallback_requestKeysIssuedSuccessfully() {
- ArgumentCaptor<CameraCaptureResult> captureResultCaptor = ArgumentCaptor.forClass(
- CameraCaptureResult.class);
-
- CaptureSession captureSession = createCaptureSession();
- captureSession.setSessionConfig(mTestParameters0.mSessionConfig);
-
- // Open the capture session and verify the onEnableSession callback would be invoked
- // but onDisableSession callback not.
- captureSession.open(mTestParameters0.mSessionConfig, mCameraDeviceHolder.get(),
- mCaptureSessionOpenerBuilder.build());
-
- // Verify the request options in onEnableSession.
- verify(mTestParameters0.mTestCameraEventCallback.mEnableCallback,
- timeout(3000)).onCaptureCompleted(captureResultCaptor.capture());
- CameraCaptureResult result1 = captureResultCaptor.getValue();
- assertThat(result1).isInstanceOf(Camera2CameraCaptureResult.class);
- CaptureResult captureResult1 = ((Camera2CameraCaptureResult) result1).getCaptureResult();
- assertThat(captureResult1.getRequest().get(
- CaptureRequest.CONTROL_SCENE_MODE)).isEqualTo(
- mTestParameters0.mTestCameraEventCallback.mAvailableSceneMode);
- // The onDisableSession should not been invoked.
- verify(mTestParameters0.mTestCameraEventCallback.mDisableCallback,
- never()).onCaptureCompleted(any(CameraCaptureResult.class));
-
- reset(mTestParameters0.mTestCameraEventCallback.mEnableCallback);
- reset(mTestParameters0.mTestCameraEventCallback.mDisableCallback);
-
- // Close the capture session and verify the onDisableSession callback would be invoked
- // but onEnableSession callback not.
- captureSession.close();
-
- // Verify the request options in onDisableSession.
- verify(mTestParameters0.mTestCameraEventCallback.mDisableCallback,
- timeout(3000)).onCaptureCompleted(captureResultCaptor.capture());
- CameraCaptureResult result2 = captureResultCaptor.getValue();
- assertThat(result2).isInstanceOf(Camera2CameraCaptureResult.class);
- CaptureResult captureResult2 = ((Camera2CameraCaptureResult) result2).getCaptureResult();
- assertThat(captureResult2.getRequest().get(
- CaptureRequest.CONTROL_SCENE_MODE)).isEqualTo(
- mTestParameters0.mTestCameraEventCallback.mAvailableSceneMode);
- // The onEnableSession should not been invoked in close().
- verify(mTestParameters0.mTestCameraEventCallback.mEnableCallback,
- never()).onCaptureCompleted(any(CameraCaptureResult.class));
- }
-
- @Test
public void closingCaptureSessionClosesDeferrableSurface()
throws ExecutionException, InterruptedException {
- mCaptureSessionOpenerBuilder = new SynchronizedCaptureSessionOpener.Builder(mExecutor,
+ mCaptureSessionOpenerBuilder = new SynchronizedCaptureSession.OpenerBuilder(mExecutor,
mScheduledExecutor, mHandler, mCaptureSessionRepository,
new Quirks(Arrays.asList(new ConfigureSurfaceToSecondarySessionFailQuirk())),
DeviceQuirks.getAll());
@@ -1213,22 +1110,41 @@
public void cameraDisconnected_whenOpeningCaptureSessions_onClosedShouldBeCalled()
throws CameraAccessException, InterruptedException, ExecutionException,
TimeoutException {
+ assumeFalse("Known device issue, b/255461164", "cph1931".equalsIgnoreCase(Build.MODEL));
+
List<OutputConfigurationCompat> outputConfigList = new LinkedList<>();
outputConfigList.add(
new OutputConfigurationCompat(mTestParameters0.mImageReader.getSurface()));
- SynchronizedCaptureSessionOpener synchronizedCaptureSessionOpener =
- mCaptureSessionOpenerBuilder.build();
+ CountDownLatch endedCountDown = new CountDownLatch(1);
+ CameraCaptureSession.StateCallback testStateCallback =
+ new CameraCaptureSession.StateCallback() {
- SessionConfigurationCompat sessionConfigCompat =
- synchronizedCaptureSessionOpener.createSessionConfigurationCompat(
- SessionConfigurationCompat.SESSION_REGULAR,
- outputConfigList,
- new SynchronizedCaptureSessionStateCallbacks.Adapter(
- mTestParameters0.mSessionStateCallback));
+ @Override
+ public void onClosed(@NonNull CameraCaptureSession session) {
+ endedCountDown.countDown();
+ }
+
+ @Override
+ public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
+
+ }
+
+ @Override
+ public void onConfigureFailed(
+ @NonNull CameraCaptureSession cameraCaptureSession) {
+ endedCountDown.countDown();
+ }
+ };
+
+ SynchronizedCaptureSession.Opener opener = mCaptureSessionOpenerBuilder.build();
+ SessionConfigurationCompat sessionConfigCompat = opener.createSessionConfigurationCompat(
+ SessionConfigurationCompat.SESSION_REGULAR,
+ outputConfigList,
+ new SynchronizedCaptureSessionStateCallbacks.Adapter(testStateCallback));
// Open the CameraCaptureSession without waiting for the onConfigured() callback.
- synchronizedCaptureSessionOpener.openCaptureSession(mCameraDeviceHolder.get(),
+ opener.openCaptureSession(mCameraDeviceHolder.get(),
sessionConfigCompat, mTestParameters0.mSessionConfig.getSurfaces());
// Open the camera again to simulate the cameraDevice is disconnected
@@ -1251,11 +1167,10 @@
}
});
// Only verify the result when the camera can open successfully.
- assumeTrue(countDownLatch.await(3000, TimeUnit.MILLISECONDS));
+ assumeTrue(countDownLatch.await(3, TimeUnit.SECONDS));
// The opened CaptureSession should be closed after the CameraDevice is disconnected.
- verify(mTestParameters0.mSessionStateCallback, timeout(5000)).onClosed(
- any(CameraCaptureSession.class));
+ assumeTrue(endedCountDown.await(3, TimeUnit.SECONDS));
assertThat(mCaptureSessionRepository.getCaptureSessions().size()).isEqualTo(0);
CameraUtil.releaseCameraDevice(holder);
@@ -1266,6 +1181,8 @@
public void cameraDisconnected_captureSessionsOnClosedShouldBeCalled_repeatingStarted()
throws ExecutionException, InterruptedException, TimeoutException,
CameraAccessException {
+ assumeFalse("Known device issue, b/255461164", "cph1931".equalsIgnoreCase(Build.MODEL));
+
CaptureSession captureSession = createCaptureSession();
captureSession.setSessionConfig(mTestParameters0.mSessionConfig);
captureSession.open(mTestParameters0.mSessionConfig, mCameraDeviceHolder.get(),
@@ -1316,6 +1233,8 @@
public void cameraDisconnected_captureSessionsOnClosedShouldBeCalled_withoutRepeating()
throws CameraAccessException, InterruptedException, ExecutionException,
TimeoutException {
+ assumeFalse("Known device issue, b/255461164", "cph1931".equalsIgnoreCase(Build.MODEL));
+
// The CameraCaptureSession will call close() automatically when CameraDevice is
// disconnected, and the CameraCaptureSession should receive the onClosed() callback if
// the CameraDevice status is idling.
@@ -1367,7 +1286,7 @@
outputConfigList.add(
new OutputConfigurationCompat(mTestParameters0.mImageReader.getSurface()));
- SynchronizedCaptureSessionOpener synchronizedCaptureSessionOpener =
+ SynchronizedCaptureSession.Opener synchronizedCaptureSessionOpener =
mCaptureSessionOpenerBuilder.build();
SessionConfigurationCompat sessionConfigCompat =
@@ -1433,12 +1352,11 @@
sessionConfigBuilder.addSurface(deferrableSurface);
}
- FakeOpenerImpl fakeOpener = new FakeOpenerImpl();
- SynchronizedCaptureSessionOpener opener = new SynchronizedCaptureSessionOpener(fakeOpener);
+ FakeOpener fakeOpener = new FakeOpener();
// Don't use #createCaptureSession since FakeOpenerImpl won't create CameraCaptureSession
// so no need to be released.
CaptureSession captureSession = new CaptureSession(mDynamicRangesCompat);
- captureSession.open(sessionConfigBuilder.build(), mCameraDeviceHolder.get(), opener);
+ captureSession.open(sessionConfigBuilder.build(), mCameraDeviceHolder.get(), fakeOpener);
ArgumentCaptor<SessionConfigurationCompat> captor =
ArgumentCaptor.forClass(SessionConfigurationCompat.class);
@@ -1673,55 +1591,6 @@
}
}
- /**
- * A implementation to test {@link CameraEventCallback} on CaptureSession.f
- */
- private static class TestCameraEventCallback extends CameraEventCallback {
-
- TestCameraEventCallback(CameraCharacteristicsCompat characteristics) {
- if (characteristics != null) {
- int[] availableSceneModes =
- characteristics.get(CameraCharacteristics.CONTROL_AVAILABLE_SCENE_MODES);
- if (availableSceneModes != null && availableSceneModes.length > 0) {
- mAvailableSceneMode = availableSceneModes[0];
- } else {
- mAvailableSceneMode = CameraCharacteristics.CONTROL_SCENE_MODE_DISABLED;
- }
- } else {
- mAvailableSceneMode = CameraCharacteristics.CONTROL_SCENE_MODE_DISABLED;
- }
- }
-
- private final CameraCaptureCallback mEnableCallback = Mockito.mock(
- CameraCaptureCallback.class);
- private final CameraCaptureCallback mDisableCallback = Mockito.mock(
- CameraCaptureCallback.class);
-
- private final int mAvailableSceneMode;
-
- @Override
- public CaptureConfig onInitSession() {
- return getCaptureConfig(CaptureRequest.CONTROL_SCENE_MODE, mAvailableSceneMode, null);
- }
-
- @Override
- public CaptureConfig onEnableSession() {
- return getCaptureConfig(CaptureRequest.CONTROL_SCENE_MODE, mAvailableSceneMode,
- mEnableCallback);
- }
-
- @Override
- public CaptureConfig onRepeating() {
- return getCaptureConfig(CaptureRequest.CONTROL_SCENE_MODE, mAvailableSceneMode, null);
- }
-
- @Override
- public CaptureConfig onDisableSession() {
- return getCaptureConfig(CaptureRequest.CONTROL_SCENE_MODE, mAvailableSceneMode,
- mDisableCallback);
- }
- }
-
private static <T> CaptureConfig getCaptureConfig(CaptureRequest.Key<T> key, T effectValue,
CameraCaptureCallback callback) {
CaptureConfig.Builder captureConfigBuilder = new CaptureConfig.Builder();
@@ -1733,10 +1602,10 @@
return captureConfigBuilder.build();
}
- private static class FakeOpenerImpl implements SynchronizedCaptureSessionOpener.OpenerImpl {
+ private static class FakeOpener implements SynchronizedCaptureSession.Opener {
- final SynchronizedCaptureSessionOpener.OpenerImpl mMock = mock(
- SynchronizedCaptureSessionOpener.OpenerImpl.class);
+ final SynchronizedCaptureSession.Opener mMock = mock(
+ SynchronizedCaptureSession.Opener.class);
@NonNull
@Override
@@ -1816,10 +1685,6 @@
private final SessionConfig mSessionConfig;
private final CaptureConfig mCaptureConfig;
- private final TestCameraEventCallback mTestCameraEventCallback;
- private final CameraEventCallback mMockCameraEventCallback = Mockito.mock(
- CameraEventCallback.class);
-
private final CameraCaptureSession.StateCallback mSessionStateCallback =
Mockito.mock(CameraCaptureSession.StateCallback.class);
private final CameraCaptureCallback mSessionCameraCaptureCallback =
@@ -1864,27 +1729,13 @@
builder.addRepeatingCameraCaptureCallback(
CaptureCallbackContainer.create(mCamera2CaptureCallback));
- mTestCameraEventCallback = new TestCameraEventCallback(characteristics);
- MutableOptionsBundle testCallbackConfig = MutableOptionsBundle.create();
- testCallbackConfig.insertOption(Camera2ImplConfig.CAMERA_EVENT_CALLBACK_OPTION,
- new CameraEventCallbacks(mTestCameraEventCallback));
- builder.addImplementationOptions(testCallbackConfig);
-
- MutableOptionsBundle mockCameraEventCallbackConfig = MutableOptionsBundle.create();
- mockCameraEventCallbackConfig.insertOption(
- Camera2ImplConfig.CAMERA_EVENT_CALLBACK_OPTION,
- new CameraEventCallbacks(mMockCameraEventCallback));
- builder.addImplementationOptions(mockCameraEventCallbackConfig);
-
// Set capture request options
// ==================================================================================
// Priority | Component | AF_MODE | EV MODE | AE_MODE
// ----------------------------------------------------------------------------------
// P1 | CaptureConfig | AF_MODE_OFF | |
// ----------------------------------------------------------------------------------
- // P2 | CameraEventCallbacks | AF_MODE_MACRO | Min EV |
- // ----------------------------------------------------------------------------------
- // P3 | SessionConfig | AF_MODE_AUTO | Max EV | AE_MODE_ON
+ // P2 | SessionConfig | AF_MODE_AUTO | Max EV | AE_MODE_ON
// ==================================================================================
mEvRange = characteristics != null
@@ -1893,27 +1744,6 @@
Camera2ImplConfig.Builder camera2ConfigBuilder = new Camera2ImplConfig.Builder();
- // Add capture request options for CameraEventCallbacks
- CameraEventCallback cameraEventCallback = new CameraEventCallback() {
- @Override
- public CaptureConfig onRepeating() {
- CaptureConfig.Builder builder = new CaptureConfig.Builder();
- builder.addImplementationOptions(
- new Camera2ImplConfig.Builder()
- .setCaptureRequestOption(
- CaptureRequest.CONTROL_AF_MODE,
- CaptureRequest.CONTROL_AF_MODE_MACRO)
- .setCaptureRequestOption(
- CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION,
- mEvRange.getLower())
- .build());
- return builder.build();
- }
- };
- new Camera2ImplConfig.Extender<>(camera2ConfigBuilder)
- .setCameraEventCallback(
- new CameraEventCallbacks(cameraEventCallback));
-
// Add capture request options for SessionConfig
camera2ConfigBuilder
.setCaptureRequestOption(
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
index 6752ba2..63460d1 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
@@ -133,7 +133,7 @@
private lateinit var cameraDeviceHolder: CameraDeviceHolder
private lateinit var captureSessionRepository: CaptureSessionRepository
- private lateinit var captureSessionOpenerBuilder: SynchronizedCaptureSessionOpener.Builder
+ private lateinit var captureSessionOpenerBuilder: SynchronizedCaptureSession.OpenerBuilder
private lateinit var sessionProcessor: FakeSessionProcessor
private lateinit var executor: Executor
private lateinit var handler: Handler
@@ -160,7 +160,7 @@
val cameraId = CameraUtil.getCameraIdWithLensFacing(lensFacing)!!
camera2CameraInfo = Camera2CameraInfoImpl(cameraId, cameraManagerCompat)
captureSessionRepository = CaptureSessionRepository(executor)
- captureSessionOpenerBuilder = SynchronizedCaptureSessionOpener.Builder(
+ captureSessionOpenerBuilder = SynchronizedCaptureSession.OpenerBuilder(
executor,
executor as ScheduledExecutorService,
handler,
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2ImplConfig.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2ImplConfig.java
index 39e937f..aed6d4e 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2ImplConfig.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2ImplConfig.java
@@ -66,11 +66,6 @@
SESSION_CAPTURE_CALLBACK_OPTION =
Option.create("camera2.cameraCaptureSession.captureCallback",
CameraCaptureSession.CaptureCallback.class);
-
- @RestrictTo(Scope.LIBRARY)
- public static final Option<CameraEventCallbacks> CAMERA_EVENT_CALLBACK_OPTION =
- Option.create("camera2.cameraEvent.callback", CameraEventCallbacks.class);
-
@RestrictTo(Scope.LIBRARY)
public static final Option<Object> CAPTURE_REQUEST_TAG_OPTION = Option.create(
"camera2.captureRequest.tag", Object.class);
@@ -180,19 +175,6 @@
}
/**
- * Returns the stored CameraEventCallbacks instance.
- *
- * @param valueIfMissing The value to return if this configuration option has not been set.
- * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
- * configuration.
- */
- @Nullable
- public CameraEventCallbacks getCameraEventCallback(
- @Nullable CameraEventCallbacks valueIfMissing) {
- return getConfig().retrieveOption(CAMERA_EVENT_CALLBACK_OPTION, valueIfMissing);
- }
-
- /**
* Returns the capture request tag.
*
* @param valueIfMissing The value to return if this configuration option has not been set.
@@ -281,38 +263,4 @@
return new Camera2ImplConfig(OptionsBundle.from(mMutableOptionsBundle));
}
}
-
- /**
- * Extends a {@link ExtendableBuilder} to add Camera2 implementation options.
- *
- * @param <T> the type being built by the extendable builder.
- */
- public static final class Extender<T> {
-
- ExtendableBuilder<T> mBaseBuilder;
-
- /**
- * Creates an Extender that can be used to add Camera2 implementation options to another
- * Builder.
- *
- * @param baseBuilder The builder being extended.
- */
- public Extender(@NonNull ExtendableBuilder<T> baseBuilder) {
- mBaseBuilder = baseBuilder;
- }
-
- /**
- * Sets a CameraEventCallbacks instance.
- *
- * @param cameraEventCallbacks The CameraEventCallbacks.
- * @return The current Extender.
- */
- @NonNull
- public Extender<T> setCameraEventCallback(
- @NonNull CameraEventCallbacks cameraEventCallbacks) {
- mBaseBuilder.getMutableConfig().insertOption(CAMERA_EVENT_CALLBACK_OPTION,
- cameraEventCallbacks);
- return this;
- }
- }
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/CameraEventCallback.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/CameraEventCallback.java
deleted file mode 100644
index 1a2d805..0000000
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/CameraEventCallback.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.impl;
-
-import android.hardware.camera2.CameraCaptureSession;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.CaptureConfig;
-
-/**
- * A callback object for tracking the camera capture session event and get request data.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public abstract class CameraEventCallback {
-
- /**
- * This will be invoked before creating a {@link CameraCaptureSession} for initializing the
- * session.
- *
- * <p>The returned parameter in CaptureConfig will be passed to the camera device as part of
- * the capture session initialization via setSessionParameters(). The valid parameter is a
- * subset of the available capture request parameters.
- *
- * @return CaptureConfig The request information to customize the session.
- */
- @Nullable
- public CaptureConfig onInitSession() {
- return null;
- }
-
- /**
- * This will be invoked once after a {@link CameraCaptureSession} is created. The returned
- * parameter in CaptureConfig will be used to generate a single request to the current
- * configured camera device. The generated request would be submitted to camera before process
- * other single request.
- *
- * @return CaptureConfig The request information to customize the session.
- */
- @Nullable
- public CaptureConfig onEnableSession() {
- return null;
- }
-
- /**
- * This callback will be invoked before starting the repeating request in the
- * {@link CameraCaptureSession}. The returned CaptureConfig will be used to generate a
- * capture request, and would be used in setRepeatingRequest().
- *
- * @return CaptureConfig The request information to customize the session.
- */
- @Nullable
- public CaptureConfig onRepeating() {
- return null;
- }
-
- /**
- * This will be invoked once before the {@link CameraCaptureSession} is closed. The
- * returned parameter in CaptureConfig will be used to generate a single request to the current
- * configured camera device. The generated request would be submitted to camera before the
- * capture session was closed.
- *
- * @return CaptureConfig The request information to customize the session.
- */
- @Nullable
- public CaptureConfig onDisableSession() {
- return null;
- }
-
- /**
- * This will be invoked after the {@link CameraCaptureSession} is closed.
- */
- public void onDeInitSession() {}
-}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/CameraEventCallbacks.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/CameraEventCallbacks.java
deleted file mode 100644
index f0badc3..0000000
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/CameraEventCallbacks.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.impl;
-
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.CaptureConfig;
-import androidx.camera.core.impl.MultiValueSet;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Different implementations of {@link CameraEventCallback}.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public final class CameraEventCallbacks extends MultiValueSet<CameraEventCallback> {
-
- public CameraEventCallbacks(@NonNull CameraEventCallback... callbacks) {
- addAll(Arrays.asList(callbacks));
- }
-
- /** Returns a camera event callback which calls a list of other callbacks. */
- @NonNull
- public ComboCameraEventCallback createComboCallback() {
- return new ComboCameraEventCallback(getAllItems());
- }
-
- /** Returns a camera event callback which does nothing. */
- @NonNull
- public static CameraEventCallbacks createEmptyCallback() {
- return new CameraEventCallbacks();
- }
-
- @NonNull
- @Override
- public MultiValueSet<CameraEventCallback> clone() {
- CameraEventCallbacks ret = createEmptyCallback();
- ret.addAll(getAllItems());
- return ret;
- }
-
- /**
- * A CameraEventCallback which contains a list of CameraEventCallback and will
- * propagate received callback to the list.
- */
- public static final class ComboCameraEventCallback {
- private final List<CameraEventCallback> mCallbacks = new ArrayList<>();
-
- ComboCameraEventCallback(List<CameraEventCallback> callbacks) {
- for (CameraEventCallback callback : callbacks) {
- mCallbacks.add(callback);
- }
- }
-
- /**
- * Invokes {@link CameraEventCallback#onInitSession()} on all registered callbacks and
- * returns a {@link CaptureConfig} list that aggregates all the results for setting the
- * session parameters.
- *
- * @return a {@link List<CaptureConfig>} that contains session parameters to be configured
- * upon creating {@link android.hardware.camera2.CameraCaptureSession}
- */
- @NonNull
- public List<CaptureConfig> onInitSession() {
- List<CaptureConfig> ret = new ArrayList<>();
- for (CameraEventCallback callback : mCallbacks) {
- CaptureConfig presetCaptureStage = callback.onInitSession();
- if (presetCaptureStage != null) {
- ret.add(presetCaptureStage);
- }
- }
- return ret;
- }
-
- /**
- * Invokes {@link CameraEventCallback#onEnableSession()} on all registered callbacks and
- * returns a {@link CaptureConfig} list that aggregates all the results. The returned
- * list contains capture request parameters to be set on a single request that will be
- * triggered right after {@link android.hardware.camera2.CameraCaptureSession} is
- * configured.
- *
- * @return a {@link List<CaptureConfig>} that contains capture request parameters to be
- * set on a single request that will be triggered after
- * {@link android.hardware.camera2.CameraCaptureSession} is configured.
- */
- @NonNull
- public List<CaptureConfig> onEnableSession() {
- List<CaptureConfig> ret = new ArrayList<>();
- for (CameraEventCallback callback : mCallbacks) {
- CaptureConfig enableCaptureStage = callback.onEnableSession();
- if (enableCaptureStage != null) {
- ret.add(enableCaptureStage);
- }
- }
- return ret;
- }
-
- /**
- * Invokes {@link CameraEventCallback#onRepeating()} on all registered callbacks and
- * returns a {@link CaptureConfig} list that aggregates all the results. The returned
- * list contains capture request parameters to be set on the repeating request.
- *
- * @return a {@link List<CaptureConfig>} that contains capture request parameters to be
- * set on the repeating request.
- */
- @NonNull
- public List<CaptureConfig> onRepeating() {
- List<CaptureConfig> ret = new ArrayList<>();
- for (CameraEventCallback callback : mCallbacks) {
- CaptureConfig repeatingCaptureStage = callback.onRepeating();
- if (repeatingCaptureStage != null) {
- ret.add(repeatingCaptureStage);
- }
- }
- return ret;
- }
-
- /**
- * Invokes {@link CameraEventCallback#onDisableSession()} on all registered callbacks and
- * returns a {@link CaptureConfig} list that aggregates all the results. The returned
- * list contains capture request parameters to be set on a single request that will be
- * triggered right before {@link android.hardware.camera2.CameraCaptureSession} is closed.
- *
- * @return a {@link List<CaptureConfig>} that contains capture request parameters to be
- * set on a single request that will be triggered right before
- * {@link android.hardware.camera2.CameraCaptureSession} is closed.
- */
- @NonNull
- public List<CaptureConfig> onDisableSession() {
- List<CaptureConfig> ret = new ArrayList<>();
- for (CameraEventCallback callback : mCallbacks) {
- CaptureConfig disableCaptureStage = callback.onDisableSession();
- if (disableCaptureStage != null) {
- ret.add(disableCaptureStage);
- }
- }
- return ret;
- }
-
- /**
- * Invokes {@link CameraEventCallback#onDeInitSession()} on all registered callbacks.
- */
- public void onDeInitSession() {
- for (CameraEventCallback callback : mCallbacks) {
- callback.onDeInitSession();
- }
- }
-
- @NonNull
- public List<CameraEventCallback> getCallbacks() {
- return mCallbacks;
- }
- }
-}
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 1f4e430..c049a71 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
@@ -183,7 +183,7 @@
@NonNull
private final CaptureSessionRepository mCaptureSessionRepository;
@NonNull
- private final SynchronizedCaptureSessionOpener.Builder mCaptureSessionOpenerBuilder;
+ private final SynchronizedCaptureSession.OpenerBuilder mCaptureSessionOpenerBuilder;
private final Set<String> mNotifyStateAttachedSet = new HashSet<>();
@NonNull
@@ -257,7 +257,7 @@
DynamicRangesCompat.fromCameraCharacteristics(mCameraCharacteristicsCompat);
mCaptureSession = newCaptureSession();
- mCaptureSessionOpenerBuilder = new SynchronizedCaptureSessionOpener.Builder(mExecutor,
+ mCaptureSessionOpenerBuilder = new SynchronizedCaptureSession.OpenerBuilder(mExecutor,
mScheduledExecutorService, schedulerHandler, mCaptureSessionRepository,
cameraInfoImpl.getCameraQuirks(), DeviceQuirks.getAll());
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
index 17ca148..e64c90c 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
@@ -22,7 +22,6 @@
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.impl.Camera2ImplConfig;
-import androidx.camera.camera2.impl.CameraEventCallbacks;
import androidx.camera.camera2.internal.compat.params.OutputConfigurationCompat;
import androidx.camera.camera2.internal.compat.workaround.PreviewPixelHDRnet;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
@@ -92,8 +91,6 @@
// Copy extended Camera2 configurations
MutableOptionsBundle extendedConfig = MutableOptionsBundle.create();
- extendedConfig.insertOption(Camera2ImplConfig.CAMERA_EVENT_CALLBACK_OPTION,
- camera2Config.getCameraEventCallback(CameraEventCallbacks.createEmptyCallback()));
extendedConfig.insertOption(Camera2ImplConfig.SESSION_PHYSICAL_CAMERA_ID_OPTION,
camera2Config.getPhysicalCameraId(null));
extendedConfig.insertOption(Camera2ImplConfig.STREAM_USE_CASE_OPTION,
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
index 1030126..78f66c1 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
@@ -32,7 +32,6 @@
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.impl.Camera2ImplConfig;
-import androidx.camera.camera2.impl.CameraEventCallbacks;
import androidx.camera.camera2.internal.compat.params.DynamicRangeConversions;
import androidx.camera.camera2.internal.compat.params.DynamicRangesCompat;
import androidx.camera.camera2.internal.compat.params.InputConfigurationCompat;
@@ -45,10 +44,7 @@
import androidx.camera.core.Logger;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CaptureConfig;
-import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.DeferrableSurface;
-import androidx.camera.core.impl.MutableOptionsBundle;
-import androidx.camera.core.impl.OptionsBundle;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.FutureChain;
@@ -63,7 +59,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.concurrent.CancellationException;
/**
@@ -96,7 +91,7 @@
/** The Opener to help on creating the SynchronizedCaptureSession. */
@Nullable
@GuardedBy("mSessionLock")
- SynchronizedCaptureSessionOpener mSynchronizedCaptureSessionOpener;
+ SynchronizedCaptureSession.Opener mSessionOpener;
/** The framework camera capture session held by this session. */
@Nullable
@GuardedBy("mSessionLock")
@@ -105,15 +100,6 @@
@Nullable
@GuardedBy("mSessionLock")
SessionConfig mSessionConfig;
- /** The capture options from CameraEventCallback.onRepeating(). */
- @NonNull
- @GuardedBy("mSessionLock")
- Config mCameraEventOnRepeatingOptions = OptionsBundle.emptyBundle();
- /** The CameraEventCallbacks for this capture session. */
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
- @GuardedBy("mSessionLock")
- @NonNull
- CameraEventCallbacks mCameraEventCallbacks = CameraEventCallbacks.createEmptyCallback();
/**
* The map of DeferrableSurface to Surface. It is both for restoring the surfaces used to
* configure the current capture session and for getting the configured surface from a
@@ -215,22 +201,19 @@
@Override
public ListenableFuture<Void> open(@NonNull SessionConfig sessionConfig,
@NonNull CameraDevice cameraDevice,
- @NonNull SynchronizedCaptureSessionOpener opener) {
+ @NonNull SynchronizedCaptureSession.Opener opener) {
synchronized (mSessionLock) {
switch (mState) {
case INITIALIZED:
mState = State.GET_SURFACE;
- List<DeferrableSurface> surfaces = sessionConfig.getSurfaces();
- mConfiguredDeferrableSurfaces = new ArrayList<>(surfaces);
- mSynchronizedCaptureSessionOpener = opener;
+ mConfiguredDeferrableSurfaces = new ArrayList<>(sessionConfig.getSurfaces());
+ mSessionOpener = opener;
ListenableFuture<Void> openFuture = FutureChain.from(
- mSynchronizedCaptureSessionOpener.startWithDeferrableSurface(
- mConfiguredDeferrableSurfaces,
- TIMEOUT_GET_SURFACE_IN_MS))
- .transformAsync(
- surfaceList -> openCaptureSession(surfaceList, sessionConfig,
- cameraDevice),
- mSynchronizedCaptureSessionOpener.getExecutor());
+ mSessionOpener.startWithDeferrableSurface(
+ mConfiguredDeferrableSurfaces, TIMEOUT_GET_SURFACE_IN_MS)
+ ).transformAsync(
+ surfaces -> openCaptureSession(surfaces, sessionConfig, cameraDevice),
+ mSessionOpener.getExecutor());
Futures.addCallback(openFuture, new FutureCallback<Void>() {
@Override
@@ -242,7 +225,7 @@
public void onFailure(@NonNull Throwable t) {
synchronized (mSessionLock) {
// Stop the Opener if we get any failure during opening.
- mSynchronizedCaptureSessionOpener.stop();
+ mSessionOpener.stop();
switch (mState) {
case OPENING:
case CLOSED:
@@ -256,7 +239,7 @@
}
}
}
- }, mSynchronizedCaptureSessionOpener.getExecutor());
+ }, mSessionOpener.getExecutor());
// The cancellation of the external ListenableFuture cannot actually stop
// the open session since we can't cancel the camera2 flow. The underlying
@@ -305,23 +288,12 @@
Camera2ImplConfig camera2Config =
new Camera2ImplConfig(sessionConfig.getImplementationOptions());
- // Start check preset CaptureStage information.
- mCameraEventCallbacks = camera2Config
- .getCameraEventCallback(CameraEventCallbacks.createEmptyCallback());
- List<CaptureConfig> presetList =
- mCameraEventCallbacks.createComboCallback().onInitSession();
-
// Generate the CaptureRequest builder from repeating request since Android
// recommend use the same template type as the initial capture request. The
// tag and output targets would be ignored by default.
- CaptureConfig.Builder captureConfigBuilder =
+ CaptureConfig.Builder sessionParameterConfigBuilder =
CaptureConfig.Builder.from(sessionConfig.getRepeatingCaptureConfig());
- for (CaptureConfig config : presetList) {
- captureConfigBuilder.addImplementationOptions(
- config.getImplementationOptions());
- }
-
List<OutputConfigurationCompat> outputConfigList = new ArrayList<>();
String physicalCameraIdForAllStreams =
camera2Config.getPhysicalCameraId(null);
@@ -346,7 +318,7 @@
outputConfigList = getUniqueOutputConfigurations(outputConfigList);
SessionConfigurationCompat sessionConfigCompat =
- mSynchronizedCaptureSessionOpener.createSessionConfigurationCompat(
+ mSessionOpener.createSessionConfigurationCompat(
SessionConfigurationCompat.SESSION_REGULAR, outputConfigList,
callbacks);
@@ -360,7 +332,7 @@
try {
CaptureRequest captureRequest =
Camera2CaptureRequestBuilder.buildWithoutTarget(
- captureConfigBuilder.build(), cameraDevice);
+ sessionParameterConfigBuilder.build(), cameraDevice);
if (captureRequest != null) {
sessionConfigCompat.setSessionParameters(captureRequest);
}
@@ -368,7 +340,7 @@
return Futures.immediateFailedFuture(e);
}
- return mSynchronizedCaptureSessionOpener.openCaptureSession(cameraDevice,
+ return mSessionOpener.openCaptureSession(cameraDevice,
sessionConfigCompat, mConfiguredDeferrableSurfaces);
default:
return Futures.immediateFailedFuture(new CancellationException(
@@ -459,34 +431,19 @@
throw new IllegalStateException(
"close() should not be possible in state: " + mState);
case GET_SURFACE:
- Preconditions.checkNotNull(mSynchronizedCaptureSessionOpener, "The "
- + "Opener shouldn't null in state:" + mState);
- mSynchronizedCaptureSessionOpener.stop();
+ Preconditions.checkNotNull(mSessionOpener,
+ "The Opener shouldn't null in state:" + mState);
+ mSessionOpener.stop();
// Fall through
case INITIALIZED:
mState = State.RELEASED;
break;
case OPENED:
- // Only issue onDisableSession requests at OPENED state.
- if (mSessionConfig != null) {
- List<CaptureConfig> configList =
- mCameraEventCallbacks.createComboCallback().onDisableSession();
- if (!configList.isEmpty()) {
- try {
- issueCaptureRequests(setupConfiguredSurface(configList));
- } catch (IllegalStateException e) {
- // We couldn't issue the request before close the capture session,
- // but we should continue the close flow.
- Logger.e(TAG, "Unable to issue the request before close the "
- + "capture session", e);
- }
- }
- }
// Not break close flow. Fall through
case OPENING:
- Preconditions.checkNotNull(mSynchronizedCaptureSessionOpener, "The "
- + "Opener shouldn't null in state:" + mState);
- mSynchronizedCaptureSessionOpener.stop();
+ Preconditions.checkNotNull(mSessionOpener,
+ "The Opener shouldn't null in state:" + mState);
+ mSessionOpener.stop();
mState = State.CLOSED;
mSessionConfig = null;
@@ -527,11 +484,10 @@
}
// Fall through
case OPENING:
- mCameraEventCallbacks.createComboCallback().onDeInitSession();
mState = State.RELEASING;
- Preconditions.checkNotNull(mSynchronizedCaptureSessionOpener, "The "
- + "Opener shouldn't null in state:" + mState);
- if (mSynchronizedCaptureSessionOpener.stop()) {
+ Preconditions.checkNotNull(mSessionOpener,
+ "The Opener shouldn't null in state:" + mState);
+ if (mSessionOpener.stop()) {
// The CameraCaptureSession doesn't created finish the release flow
// directly.
finishClose();
@@ -553,9 +509,9 @@
return mReleaseFuture;
case GET_SURFACE:
- Preconditions.checkNotNull(mSynchronizedCaptureSessionOpener, "The "
- + "Opener shouldn't null in state:" + mState);
- mSynchronizedCaptureSessionOpener.stop();
+ Preconditions.checkNotNull(mSessionOpener,
+ "The Opener shouldn't null in state:" + mState);
+ mSessionOpener.stop();
// Fall through
case INITIALIZED:
mState = State.RELEASED;
@@ -668,19 +624,8 @@
try {
Logger.d(TAG, "Issuing request for session.");
-
- // The override priority for implementation options
- // P1 CameraEventCallback onRepeating options
- // P2 SessionConfig options
- CaptureConfig.Builder captureConfigBuilder = CaptureConfig.Builder.from(
- captureConfig);
-
- mCameraEventOnRepeatingOptions = mergeOptions(
- mCameraEventCallbacks.createComboCallback().onRepeating());
- captureConfigBuilder.addImplementationOptions(mCameraEventOnRepeatingOptions);
-
CaptureRequest captureRequest = Camera2CaptureRequestBuilder.build(
- captureConfigBuilder.build(), mSynchronizedCaptureSession.getDevice(),
+ captureConfig, mSynchronizedCaptureSession.getDevice(),
mConfiguredSurfaceMap);
if (captureRequest == null) {
Logger.d(TAG, "Skipping issuing empty request for session.");
@@ -775,16 +720,13 @@
// The override priority for implementation options
// P1 Single capture options
- // P2 CameraEventCallback onRepeating options
- // P3 SessionConfig options
+ // P2 SessionConfig options
if (mSessionConfig != null) {
captureConfigBuilder.addImplementationOptions(
mSessionConfig.getRepeatingCaptureConfig()
.getImplementationOptions());
}
- captureConfigBuilder.addImplementationOptions(mCameraEventOnRepeatingOptions);
-
// Need to override again since single capture options has highest priority.
captureConfigBuilder.addImplementationOptions(
captureConfig.getImplementationOptions());
@@ -930,42 +872,6 @@
return Camera2CaptureCallbacks.createComboCallback(camera2Callbacks);
}
-
- /**
- * Merges the implementation options from the input {@link CaptureConfig} list.
- *
- * <p>It will retain the first option if a conflict is detected.
- *
- * @param captureConfigList CaptureConfig list to be merged.
- * @return merged options.
- */
- @NonNull
- private static Config mergeOptions(List<CaptureConfig> captureConfigList) {
- MutableOptionsBundle options = MutableOptionsBundle.create();
- for (CaptureConfig captureConfig : captureConfigList) {
- Config newOptions = captureConfig.getImplementationOptions();
- for (Config.Option<?> option : newOptions.listOptions()) {
- @SuppressWarnings("unchecked") // Options/values are being copied directly
- Config.Option<Object> objectOpt = (Config.Option<Object>) option;
- Object newValue = newOptions.retrieveOption(objectOpt, null);
- if (options.containsOption(option)) {
- Object oldValue = options.retrieveOption(objectOpt, null);
- if (!Objects.equals(oldValue, newValue)) {
- Logger.d(TAG, "Detect conflicting option "
- + objectOpt.getId()
- + " : "
- + newValue
- + " != "
- + oldValue);
- }
- } else {
- options.insertOption(objectOpt, newValue);
- }
- }
- }
- return options;
- }
-
enum State {
/** The default state of the session before construction. */
UNINITIALIZED,
@@ -1033,16 +939,6 @@
case OPENING:
mState = State.OPENED;
mSynchronizedCaptureSession = session;
-
- // Issue capture request of enableSession if exists.
- if (mSessionConfig != null) {
- List<CaptureConfig> list =
- mCameraEventCallbacks.createComboCallback().onEnableSession();
- if (!list.isEmpty()) {
- issueBurstCaptureRequest(setupConfiguredSurface(list));
- }
- }
-
Logger.d(TAG, "Attempting to send capture request onConfigured");
issueRepeatingCaptureRequests(mSessionConfig);
issuePendingCaptureRequest();
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSessionInterface.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSessionInterface.java
index 351c8f5..4e745ce 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSessionInterface.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSessionInterface.java
@@ -62,7 +62,7 @@
@NonNull
ListenableFuture<Void> open(@NonNull SessionConfig sessionConfig,
@NonNull CameraDevice cameraDevice,
- @NonNull SynchronizedCaptureSessionOpener opener);
+ @NonNull SynchronizedCaptureSession.Opener opener);
/**
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
index 5c9eb4b..01357dc 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
@@ -142,7 +142,7 @@
@NonNull
@Override
public ListenableFuture<Void> open(@NonNull SessionConfig sessionConfig,
- @NonNull CameraDevice cameraDevice, @NonNull SynchronizedCaptureSessionOpener opener) {
+ @NonNull CameraDevice cameraDevice, @NonNull SynchronizedCaptureSession.Opener opener) {
Preconditions.checkArgument(mProcessorState == ProcessorState.UNINITIALIZED,
"Invalid state state:" + mProcessorState);
Preconditions.checkArgument(!sessionConfig.getSurfaces().isEmpty(),
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSession.java
index 2c1a74b..e5e07ac 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSession.java
@@ -20,18 +20,26 @@
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.SessionConfiguration;
import android.os.Build;
+import android.os.Handler;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.annotation.CameraExecutor;
import androidx.camera.camera2.internal.compat.CameraCaptureSessionCompat;
+import androidx.camera.camera2.internal.compat.params.OutputConfigurationCompat;
+import androidx.camera.camera2.internal.compat.params.SessionConfigurationCompat;
+import androidx.camera.core.impl.DeferrableSurface;
+import androidx.camera.core.impl.Quirks;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
/**
* The interface for accessing features in {@link CameraCaptureSession}.
@@ -47,10 +55,10 @@
* if it need to use a Executor. Most use cases should attempt to call the overloaded method
* instead.
*
- * <p>The {@link SynchronizedCaptureSessionOpener} can help to create the
+ * <p>The {@link SynchronizedCaptureSession.Opener} can help to create the
* {@link SynchronizedCaptureSession} object.
*
- * @see SynchronizedCaptureSessionOpener
+ * @see SynchronizedCaptureSession.Opener
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public interface SynchronizedCaptureSession {
@@ -363,4 +371,157 @@
}
}
+
+ /**
+ * Opener interface to open the {@link SynchronizedCaptureSession}.
+ *
+ * <p>The {@link #openCaptureSession} method can be used to open a new
+ * {@link SynchronizedCaptureSession}, and the {@link SessionConfigurationCompat} object is
+ * needed by the {@link #openCaptureSession} should be created via the
+ * {@link #createSessionConfigurationCompat}. It will send the ready-to-use
+ * {@link SynchronizedCaptureSession} to the provided listener's
+ * {@link SynchronizedCaptureSession.StateCallback#onConfigured} callback.
+ *
+ * <p>An Opener should only be used to open one SynchronizedCaptureSession. The Opener cannot be
+ * reused to open the second SynchronizedCaptureSession. The {@link #openCaptureSession} can't
+ * be called more than once in the same Opener.
+ *
+ * @see #openCaptureSession(CameraDevice, SessionConfigurationCompat, List)
+ * @see #createSessionConfigurationCompat(int, List, SynchronizedCaptureSession.StateCallback)
+ * @see SynchronizedCaptureSession.StateCallback
+ *
+ * <p>The {@link #stop} method should be invoked when the SynchronizedCaptureSession opening
+ * flow is interrupted.
+ * @see #startWithDeferrableSurface
+ * @see #stop()
+ */
+ interface Opener {
+
+ /**
+ * Opens the SynchronizedCaptureSession.
+ *
+ * <p>The behavior of this method similar to the
+ * {@link CameraDevice#createCaptureSession(SessionConfiguration)}. It will use the
+ * input cameraDevice to create the SynchronizedCaptureSession.
+ *
+ * <p>The {@link SessionConfigurationCompat} object that is needed in this method should be
+ * created via the {@link #createSessionConfigurationCompat}.
+ *
+ * <p>The use count of the input DeferrableSurfaces will be increased. It will be
+ * automatically decreased when the surface is not used by the camera. For instance, when
+ * the opened SynchronizedCaptureSession is closed completely or when the configuration of
+ * the session is failed.
+ *
+ * <p>Cancellation of the returned future is a no-op. The opening task can only be
+ * cancelled by the {@link #stop()}. The {@link #stop()} only effective when the
+ * CameraDevice#createCaptureSession() hasn't been invoked. If the {@link #stop()} is called
+ * before the CameraDevice#createCaptureSession(), it will stop the
+ * SynchronizedCaptureSession creation.
+ * Otherwise, the SynchronizedCaptureSession will be created and the
+ * {@link SynchronizedCaptureSession.StateCallback#onConfigured} or
+ * {@link SynchronizedCaptureSession.StateCallback#onConfigureFailed} callback will be
+ * invoked.
+ *
+ * @param cameraDevice the camera with which to generate the
+ * SynchronizedCaptureSession
+ * @param sessionConfigurationCompat A {@link SessionConfigurationCompat} that is created
+ * via the {@link #createSessionConfigurationCompat}.
+ * @param deferrableSurfaces the list of the DeferrableSurface that be used to
+ * configure the session.
+ * @return a ListenableFuture object which completes when the SynchronizedCaptureSession is
+ * configured.
+ * @see #createSessionConfigurationCompat
+ * @see #stop()
+ */
+ @NonNull
+ ListenableFuture<Void> openCaptureSession(@NonNull CameraDevice cameraDevice,
+ @NonNull SessionConfigurationCompat sessionConfigurationCompat,
+ @NonNull List<DeferrableSurface> deferrableSurfaces);
+
+ /**
+ * Create the SessionConfigurationCompat for {@link #openCaptureSession} used.
+ *
+ * This method will add necessary information into the created SessionConfigurationCompat
+ * instance for SynchronizedCaptureSession.
+ *
+ * @param sessionType The session type.
+ * @param outputsCompat A list of output configurations for the SynchronizedCaptureSession.
+ * @param stateCallback A state callback interface implementation.
+ */
+ @NonNull
+ SessionConfigurationCompat createSessionConfigurationCompat(int sessionType,
+ @NonNull List<OutputConfigurationCompat> outputsCompat,
+ @NonNull SynchronizedCaptureSession.StateCallback stateCallback);
+
+ /**
+ * Get the surface from the DeferrableSurfaces.
+ *
+ * <p>The {@link #startWithDeferrableSurface} method will return a Surface list that
+ * is held in the List<DeferrableSurface>. The Opener helps in maintaining the timing to
+ * close the returned DeferrableSurface list. Most use case should attempt to use the
+ * {@link #startWithDeferrableSurface} method to get the Surface for creating the
+ * SynchronizedCaptureSession.
+ *
+ * @param deferrableSurfaces The deferrable surfaces to open.
+ * @param timeout the timeout to get surfaces from the deferrable surface list.
+ * @return the Future which will contain the surface list, Cancellation of this
+ * future is a no-op. The returned Surface list can be used to create the
+ * SynchronizedCaptureSession.
+ * @see #openCaptureSession
+ * @see #stop
+ */
+ @NonNull
+ ListenableFuture<List<Surface>> startWithDeferrableSurface(
+ @NonNull List<DeferrableSurface> deferrableSurfaces, long timeout);
+
+ @NonNull
+ @CameraExecutor
+ Executor getExecutor();
+
+ /**
+ * Disable the startWithDeferrableSurface() and openCaptureSession() ability, and stop the
+ * startWithDeferrableSurface() and openCaptureSession() if
+ * CameraDevice#createCaptureSession() hasn't been invoked. Once the
+ * CameraDevice#createCaptureSession() already been invoked, the task of
+ * openCaptureSession() will keep going.
+ *
+ * @return true if the CameraCaptureSession creation has not been started yet. Otherwise
+ * return false.
+ */
+ boolean stop();
+ }
+
+ /**
+ * A builder to create new {@link SynchronizedCaptureSession.Opener}
+ */
+ class OpenerBuilder {
+
+ private final Executor mExecutor;
+ private final ScheduledExecutorService mScheduledExecutorService;
+ private final Handler mCompatHandler;
+ private final CaptureSessionRepository mCaptureSessionRepository;
+ private final Quirks mCameraQuirks;
+ private final Quirks mDeviceQuirks;
+
+ OpenerBuilder(@NonNull @CameraExecutor Executor executor,
+ @NonNull ScheduledExecutorService scheduledExecutorService,
+ @NonNull Handler compatHandler,
+ @NonNull CaptureSessionRepository captureSessionRepository,
+ @NonNull Quirks cameraQuirks,
+ @NonNull Quirks deviceQuirks) {
+ mExecutor = executor;
+ mScheduledExecutorService = scheduledExecutorService;
+ mCompatHandler = compatHandler;
+ mCaptureSessionRepository = captureSessionRepository;
+ mCameraQuirks = cameraQuirks;
+ mDeviceQuirks = deviceQuirks;
+ }
+
+ @NonNull
+ Opener build() {
+ return new SynchronizedCaptureSessionImpl(mCameraQuirks, mDeviceQuirks,
+ mCaptureSessionRepository, mExecutor, mScheduledExecutorService,
+ mCompatHandler);
+ }
+ }
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionBaseImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionBaseImpl.java
index cea447a..9ed6659 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionBaseImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionBaseImpl.java
@@ -54,17 +54,19 @@
import java.util.concurrent.ScheduledExecutorService;
/**
- * The basic implementation of {@link SynchronizedCaptureSession} to forward the feature calls
- * into the {@link CameraCaptureSession}. It will not synchronize methods with the other
- * SynchronizedCaptureSessions.
+ * The implementation of {@link SynchronizedCaptureSession} to forward the feature calls
+ * into the {@link CameraCaptureSession}.
*
- * The {@link StateCallback} to receives the state callbacks from the
- * {@link CameraCaptureSession.StateCallback} and convert the {@link CameraCaptureSession} to the
- * SynchronizedCaptureSession object.
+ * The implementation of {@link SynchronizedCaptureSession.StateCallback} and
+ * {@link SynchronizedCaptureSession.Opener} will be able to track the creation and close of the
+ * SynchronizedCaptureSession in {@link CaptureSessionRepository}.
+ * Some Quirks may be required to take some action before opening/closing other sessions, with the
+ * SynchronizedCaptureSessionBaseImpl, it would be useful when implementing the workaround of
+ * Quirks.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
class SynchronizedCaptureSessionBaseImpl extends SynchronizedCaptureSession.StateCallback implements
- SynchronizedCaptureSession, SynchronizedCaptureSessionOpener.OpenerImpl {
+ SynchronizedCaptureSession, SynchronizedCaptureSession.Opener {
private static final String TAG = "SyncCaptureSessionBase";
@@ -144,22 +146,21 @@
mCaptureSessionRepository.onCreateCaptureSession(this);
CameraDeviceCompat cameraDeviceCompat =
CameraDeviceCompat.toCameraDeviceCompat(cameraDevice, mCompatHandler);
- mOpenCaptureSessionFuture = CallbackToFutureAdapter.getFuture(
- completer -> {
- synchronized (mLock) {
- // Attempt to set all the configured deferrable surfaces is in used
- // before adding them to the session.
- holdDeferrableSurfaces(deferrableSurfaces);
+ mOpenCaptureSessionFuture = CallbackToFutureAdapter.getFuture(completer -> {
+ synchronized (mLock) {
+ // Attempt to set all the configured deferrable surfaces is in used
+ // before adding them to the session.
+ holdDeferrableSurfaces(deferrableSurfaces);
- Preconditions.checkState(mOpenCaptureSessionCompleter == null,
- "The openCaptureSessionCompleter can only set once!");
+ Preconditions.checkState(mOpenCaptureSessionCompleter == null,
+ "The openCaptureSessionCompleter can only set once!");
- mOpenCaptureSessionCompleter = completer;
- cameraDeviceCompat.createCaptureSession(sessionConfigurationCompat);
- return "openCaptureSession[session="
- + SynchronizedCaptureSessionBaseImpl.this + "]";
- }
- });
+ mOpenCaptureSessionCompleter = completer;
+ cameraDeviceCompat.createCaptureSession(sessionConfigurationCompat);
+ return "openCaptureSession[session="
+ + SynchronizedCaptureSessionBaseImpl.this + "]";
+ }
+ });
Futures.addCallback(mOpenCaptureSessionFuture, new FutureCallback<Void>() {
@Override
@@ -169,7 +170,7 @@
@Override
public void onFailure(@NonNull Throwable t) {
- SynchronizedCaptureSessionBaseImpl.this.finishClose();
+ finishClose();
mCaptureSessionRepository.onCaptureSessionConfigureFail(
SynchronizedCaptureSessionBaseImpl.this);
}
@@ -299,34 +300,31 @@
new CancellationException("Opener is disabled"));
}
- mStartingSurface = FutureChain.from(
- DeferrableSurfaces.surfaceListWithTimeout(deferrableSurfaces, false, timeout,
- getExecutor(), mScheduledExecutorService)).transformAsync(surfaces -> {
- Logger.d(TAG,
- "[" + SynchronizedCaptureSessionBaseImpl.this
- + "] getSurface...done");
- // If a Surface in configuredSurfaces is null it means the
- // Surface was not retrieved from the ListenableFuture. Only
- // handle the first failed Surface since subsequent calls to
- // CaptureSession.open() will handle the other failed Surfaces if
- // there are any.
- if (surfaces.contains(null)) {
- DeferrableSurface deferrableSurface = deferrableSurfaces.get(
- surfaces.indexOf(null));
- return Futures.immediateFailedFuture(
- new DeferrableSurface.SurfaceClosedException(
- "Surface closed", deferrableSurface));
- }
+ ListenableFuture<List<Surface>> future = DeferrableSurfaces.surfaceListWithTimeout(
+ deferrableSurfaces, false, timeout, getExecutor(), mScheduledExecutorService);
- if (surfaces.isEmpty()) {
- return Futures.immediateFailedFuture(
- new IllegalArgumentException(
- "Unable to open capture session without "
- + "surfaces"));
- }
-
- return Futures.immediateFuture(surfaces);
- }, getExecutor());
+ mStartingSurface = FutureChain.from(future).transformAsync(surfaces -> {
+ Logger.d(TAG, "[" + SynchronizedCaptureSessionBaseImpl.this + "] getSurface done "
+ + "with results: " + surfaces);
+ // If a Surface in configuredSurfaces is null it means the
+ // Surface was not retrieved from the ListenableFuture. Only
+ // handle the first failed Surface since subsequent calls to
+ // CaptureSession.open() will handle the other failed Surfaces if
+ // there are any.
+ if (surfaces.isEmpty()) {
+ return Futures.immediateFailedFuture(new IllegalArgumentException(
+ "Unable to open capture session without surfaces")
+ );
+ }
+ if (surfaces.contains(null)) {
+ return Futures.immediateFailedFuture(
+ new DeferrableSurface.SurfaceClosedException(
+ "Surface closed", deferrableSurfaces.get(surfaces.indexOf(null))
+ )
+ );
+ }
+ return Futures.immediateFuture(surfaces);
+ }, getExecutor());
return Futures.nonCancellationPropagating(mStartingSurface);
}
@@ -554,8 +552,15 @@
// the onClosed callback, we can treat this session is already in closed state.
onSessionFinished(session);
- Objects.requireNonNull(mCaptureSessionStateCallback);
- mCaptureSessionStateCallback.onClosed(session);
+ if (mCameraCaptureSessionCompat != null) {
+ // Only call onClosed() if we have the instance of CameraCaptureSession.
+ Objects.requireNonNull(mCaptureSessionStateCallback);
+ mCaptureSessionStateCallback.onClosed(session);
+ } else {
+ Logger.w(TAG, "[" + SynchronizedCaptureSessionBaseImpl.this + "] Cannot call "
+ + "onClosed() when the CameraCaptureSession is not correctly "
+ + "configured.");
+ }
}, CameraXExecutors.directExecutor());
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionImpl.java
index 10ab59b..de4b4b8c 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionImpl.java
@@ -31,33 +31,24 @@
import androidx.camera.camera2.internal.compat.params.SessionConfigurationCompat;
import androidx.camera.camera2.internal.compat.workaround.ForceCloseCaptureSession;
import androidx.camera.camera2.internal.compat.workaround.ForceCloseDeferrableSurface;
+import androidx.camera.camera2.internal.compat.workaround.SessionResetPolicy;
import androidx.camera.camera2.internal.compat.workaround.WaitForRepeatingRequestStart;
import androidx.camera.core.Logger;
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.Quirks;
+import androidx.camera.core.impl.utils.futures.FutureChain;
import androidx.camera.core.impl.utils.futures.Futures;
import com.google.common.util.concurrent.ListenableFuture;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
- * <p>The SynchronizedCaptureSessionImpl synchronizing methods between the other
- * SynchronizedCaptureSessions to fix b/135050586, b/145725334, b/144817309, b/146773463. The
- * SynchronizedCaptureSessionBaseImpl would be a non-synchronizing version.
- *
- * <p>In b/144817309, the onClosed() callback on
- * {@link android.hardware.camera2.CameraCaptureSession.StateCallback}
- * might not be invoked if the capture session is not the latest one. To align the fixed
- * framework behavior, we manually call the onClosed() when a new CameraCaptureSession is created.
- *
- * <p>The b/135050586, b/145725334 need to close the {@link DeferrableSurface} to force the
- * {@link DeferrableSurface} recreate in the new CaptureSession.
- *
- * <p>b/146773463: It needs to check all the releasing capture sessions are ready for opening
- * next capture session.
+ * The SynchronizedCaptureSessionImpl applies a few workarounds for Quirks.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
class SynchronizedCaptureSessionImpl extends SynchronizedCaptureSessionBaseImpl {
@@ -72,11 +63,13 @@
private List<DeferrableSurface> mDeferrableSurfaces;
@Nullable
@GuardedBy("mObjectLock")
- ListenableFuture<Void> mOpeningCaptureSession;
+ ListenableFuture<List<Void>> mOpenSessionBlockerFuture;
private final ForceCloseDeferrableSurface mCloseSurfaceQuirk;
private final WaitForRepeatingRequestStart mWaitForOtherSessionCompleteQuirk;
private final ForceCloseCaptureSession mForceCloseSessionQuirk;
+ private final SessionResetPolicy mSessionResetPolicy;
+ private final AtomicBoolean mClosed = new AtomicBoolean(false);
SynchronizedCaptureSessionImpl(
@NonNull Quirks cameraQuirks,
@@ -89,6 +82,7 @@
mCloseSurfaceQuirk = new ForceCloseDeferrableSurface(cameraQuirks, deviceQuirks);
mWaitForOtherSessionCompleteQuirk = new WaitForRepeatingRequestStart(cameraQuirks);
mForceCloseSessionQuirk = new ForceCloseCaptureSession(deviceQuirks);
+ mSessionResetPolicy = new SessionResetPolicy(deviceQuirks);
}
@NonNull
@@ -97,11 +91,32 @@
@NonNull SessionConfigurationCompat sessionConfigurationCompat,
@NonNull List<DeferrableSurface> deferrableSurfaces) {
synchronized (mObjectLock) {
- mOpeningCaptureSession = mWaitForOtherSessionCompleteQuirk.openCaptureSession(
- cameraDevice, sessionConfigurationCompat, deferrableSurfaces,
- mCaptureSessionRepository.getClosingCaptureSession(),
- super::openCaptureSession);
- return Futures.nonCancellationPropagating(mOpeningCaptureSession);
+ // For b/146773463: It needs to check all the releasing capture sessions are ready for
+ // opening next capture session.
+ List<SynchronizedCaptureSession>
+ closingSessions = mCaptureSessionRepository.getClosingCaptureSession();
+ List<ListenableFuture<Void>> futureList = new ArrayList<>();
+ for (SynchronizedCaptureSession session : closingSessions) {
+ futureList.add(session.getOpeningBlocker());
+ }
+ mOpenSessionBlockerFuture = Futures.successfulAsList(futureList);
+
+ return Futures.nonCancellationPropagating(
+ FutureChain.from(mOpenSessionBlockerFuture).transformAsync(v -> {
+ if (mSessionResetPolicy.needAbortCapture()) {
+ closeCreatedSession();
+ }
+ debugLog("start openCaptureSession");
+ return super.openCaptureSession(cameraDevice, sessionConfigurationCompat,
+ deferrableSurfaces);
+ }, getExecutor()));
+ }
+ }
+
+ private void closeCreatedSession() {
+ List<SynchronizedCaptureSession> sessions = mCaptureSessionRepository.getCaptureSessions();
+ for (SynchronizedCaptureSession session : sessions) {
+ session.close();
}
}
@@ -126,8 +141,10 @@
synchronized (mObjectLock) {
if (isCameraCaptureSessionOpen()) {
mCloseSurfaceQuirk.onSessionEnd(mDeferrableSurfaces);
- } else if (mOpeningCaptureSession != null) {
- mOpeningCaptureSession.cancel(true);
+ } else {
+ if (mOpenSessionBlockerFuture != null) {
+ mOpenSessionBlockerFuture.cancel(true);
+ }
}
return super.stop();
}
@@ -151,6 +168,20 @@
@Override
public void close() {
+ if (!mClosed.compareAndSet(false, true)) {
+ debugLog("close() has been called. Skip this invocation.");
+ return;
+ }
+
+ if (mSessionResetPolicy.needAbortCapture()) {
+ try {
+ debugLog("Call abortCaptures() before closing session.");
+ abortCaptures();
+ } catch (Exception e) {
+ debugLog("Exception when calling abortCaptures()" + e);
+ }
+ }
+
debugLog("Session call close()");
mWaitForOtherSessionCompleteQuirk.onSessionEnd();
mWaitForOtherSessionCompleteQuirk.getStartStreamFuture().addListener(() -> {
@@ -169,6 +200,12 @@
super.onClosed(session);
}
+ @Override
+ public void finishClose() {
+ super.finishClose();
+ mWaitForOtherSessionCompleteQuirk.onFinishClosed();
+ }
+
void debugLog(String message) {
Logger.d(TAG, "[" + SynchronizedCaptureSessionImpl.this + "] " + message);
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionOpener.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionOpener.java
deleted file mode 100644
index 7e28273..0000000
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionOpener.java
+++ /dev/null
@@ -1,237 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.internal;
-
-import android.hardware.camera2.CameraDevice;
-import android.hardware.camera2.params.SessionConfiguration;
-import android.os.Handler;
-import android.view.Surface;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.camera2.internal.annotation.CameraExecutor;
-import androidx.camera.camera2.internal.compat.params.OutputConfigurationCompat;
-import androidx.camera.camera2.internal.compat.params.SessionConfigurationCompat;
-import androidx.camera.camera2.internal.compat.workaround.ForceCloseCaptureSession;
-import androidx.camera.camera2.internal.compat.workaround.ForceCloseDeferrableSurface;
-import androidx.camera.camera2.internal.compat.workaround.WaitForRepeatingRequestStart;
-import androidx.camera.core.impl.DeferrableSurface;
-import androidx.camera.core.impl.Quirks;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ScheduledExecutorService;
-
-/**
- * The Opener to open the {@link SynchronizedCaptureSession}.
- *
- * <p>The {@link #openCaptureSession} method can be used to open a new
- * {@link SynchronizedCaptureSession}, and the {@link SessionConfigurationCompat} object that
- * needed by the {@link #openCaptureSession} should be created via the
- * {@link #createSessionConfigurationCompat}. It will send the ready-to-use
- * {@link SynchronizedCaptureSession} to the provided listener's
- * {@link SynchronizedCaptureSession.StateCallback#onConfigured} callback.
- *
- * <p>An Opener should only be used to open one SynchronizedCaptureSession. The Opener cannot be
- * reused to open the second SynchronizedCaptureSession. The {@link #openCaptureSession} can't
- * be called more than once in the same Opener.
- *
- * @see #openCaptureSession(CameraDevice, SessionConfigurationCompat, List)
- * @see #createSessionConfigurationCompat(int, List, SynchronizedCaptureSession.StateCallback)
- * @see SynchronizedCaptureSession.StateCallback
- *
- * <p>The {@link #stop} method should be invoked when the SynchronizedCaptureSession opening flow
- * is interropted.
- * @see #startWithDeferrableSurface
- * @see #stop()
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-final class SynchronizedCaptureSessionOpener {
-
- @NonNull
- private final OpenerImpl mImpl;
-
- SynchronizedCaptureSessionOpener(@NonNull OpenerImpl impl) {
- mImpl = impl;
- }
-
- /**
- * Opens the SynchronizedCaptureSession.
- *
- * <p>The behavior of this method similar to the
- * {@link CameraDevice#createCaptureSession(SessionConfiguration)}. It will use the
- * input cameraDevice to create the SynchronizedCaptureSession.
- *
- * <p>The {@link SessionConfigurationCompat} object that is needed in this method should be
- * created via the {@link #createSessionConfigurationCompat}.
- *
- * <p>The use count of the input DeferrableSurfaces will be increased. It will be
- * automatically decreased when the surface is not used by the camera. For instance, when the
- * opened SynchronizedCaptureSession is closed completely or when the configuration of the
- * session is failed.
- *
- * <p>Cancellation of the returned future is a no-op. The opening task can only be
- * cancelled by the {@link #stop()}. The {@link #stop()} only effective when the
- * CameraDevice#createCaptureSession() hasn't been invoked. If the {@link #stop()} is called
- * before the CameraDevice#createCaptureSession(), it will stop the
- * SynchronizedCaptureSession creation.
- * Otherwise, the SynchronizedCaptureSession will be created and the
- * {@link SynchronizedCaptureSession.StateCallback#onConfigured} or
- * {@link SynchronizedCaptureSession.StateCallback#onConfigureFailed} callback will be invoked.
- *
- * @param cameraDevice the camera with which to generate the
- * SynchronizedCaptureSession
- * @param sessionConfigurationCompat A {@link SessionConfigurationCompat} that is created via
- * the {@link #createSessionConfigurationCompat}.
- * @param deferrableSurfaces the list of the DeferrableSurface that be used to
- * configure the session.
- * @return a ListenableFuture object which completes when the SynchronizedCaptureSession is
- * configured.
- * @see #createSessionConfigurationCompat(int, List, SynchronizedCaptureSession.StateCallback)
- * @see #stop()
- */
- @NonNull
- ListenableFuture<Void> openCaptureSession(@NonNull CameraDevice cameraDevice,
- @NonNull SessionConfigurationCompat sessionConfigurationCompat,
- @NonNull List<DeferrableSurface> deferrableSurfaces) {
- return mImpl.openCaptureSession(cameraDevice, sessionConfigurationCompat,
- deferrableSurfaces);
- }
-
- /**
- * Create the SessionConfigurationCompat for {@link #openCaptureSession} used.
- *
- * This method will add necessary information into the created SessionConfigurationCompat
- * instance for SynchronizedCaptureSession.
- *
- * @param sessionType The session type.
- * @param outputsCompat A list of output configurations for the SynchronizedCaptureSession.
- * @param stateCallback A state callback interface implementation.
- */
- @NonNull
- SessionConfigurationCompat createSessionConfigurationCompat(int sessionType,
- @NonNull List<OutputConfigurationCompat> outputsCompat,
- @NonNull SynchronizedCaptureSession.StateCallback stateCallback) {
- return mImpl.createSessionConfigurationCompat(sessionType, outputsCompat,
- stateCallback);
- }
-
- /**
- * Get the surface from the DeferrableSurfaces.
- *
- * <p>The {@link #startWithDeferrableSurface} method will return a Surface list that
- * is held in the List<DeferrableSurface>. The Opener helps in maintaining the timing to
- * close the returned DeferrableSurface list. Most use case should attempt to use the
- * {@link #startWithDeferrableSurface} method to get the Surface for creating the
- * SynchronizedCaptureSession.
- *
- * @param deferrableSurfaces The deferrable surfaces to open.
- * @param timeout the timeout to get surfaces from the deferrable surface list.
- * @return the Future which will contain the surface list, Cancellation of this
- * future is a no-op. The returned Surface list can be used to create the
- * SynchronizedCaptureSession.
- * @see #openCaptureSession
- * @see #stop
- */
- @NonNull
- ListenableFuture<List<Surface>> startWithDeferrableSurface(
- @NonNull List<DeferrableSurface> deferrableSurfaces, long timeout) {
- return mImpl.startWithDeferrableSurface(deferrableSurfaces, timeout);
- }
-
- /**
- * Disable the startWithDeferrableSurface() and openCaptureSession() ability, and stop the
- * startWithDeferrableSurface() and openCaptureSession() if CameraDevice#createCaptureSession()
- * hasn't been invoked. Once the CameraDevice#createCaptureSession() already been invoked, the
- * task of openCaptureSession() will keep going.
- *
- * @return true if the CameraCaptureSession creation has not been started yet. Otherwise true
- * false.
- */
- boolean stop() {
- return mImpl.stop();
- }
-
- @NonNull
- @CameraExecutor
- public Executor getExecutor() {
- return mImpl.getExecutor();
- }
-
- static class Builder {
- private final Executor mExecutor;
- private final ScheduledExecutorService mScheduledExecutorService;
- private final Handler mCompatHandler;
- private final CaptureSessionRepository mCaptureSessionRepository;
- private final Quirks mCameraQuirks;
- private final Quirks mDeviceQuirks;
- private final boolean mQuirkExist;
-
- Builder(@NonNull @CameraExecutor Executor executor,
- @NonNull ScheduledExecutorService scheduledExecutorService,
- @NonNull Handler compatHandler,
- @NonNull CaptureSessionRepository captureSessionRepository,
- @NonNull Quirks cameraQuirks,
- @NonNull Quirks deviceQuirks) {
- mExecutor = executor;
- mScheduledExecutorService = scheduledExecutorService;
- mCompatHandler = compatHandler;
- mCaptureSessionRepository = captureSessionRepository;
- mCameraQuirks = cameraQuirks;
- mDeviceQuirks = deviceQuirks;
- mQuirkExist = new ForceCloseDeferrableSurface(mCameraQuirks,
- mDeviceQuirks).shouldForceClose() || new WaitForRepeatingRequestStart(
- mCameraQuirks).shouldWaitRepeatingSubmit() || new ForceCloseCaptureSession(
- mDeviceQuirks).shouldForceClose();
- }
-
- @NonNull
- SynchronizedCaptureSessionOpener build() {
- return new SynchronizedCaptureSessionOpener(
- mQuirkExist ? new SynchronizedCaptureSessionImpl(mCameraQuirks, mDeviceQuirks,
- mCaptureSessionRepository, mExecutor, mScheduledExecutorService,
- mCompatHandler)
- : new SynchronizedCaptureSessionBaseImpl(mCaptureSessionRepository,
- mExecutor, mScheduledExecutorService, mCompatHandler));
- }
- }
-
- interface OpenerImpl {
-
- @NonNull
- ListenableFuture<Void> openCaptureSession(@NonNull CameraDevice cameraDevice, @NonNull
- SessionConfigurationCompat sessionConfigurationCompat,
- @NonNull List<DeferrableSurface> deferrableSurfaces);
-
- @NonNull
- SessionConfigurationCompat createSessionConfigurationCompat(int sessionType,
- @NonNull List<OutputConfigurationCompat> outputsCompat,
- @NonNull SynchronizedCaptureSession.StateCallback stateCallback);
-
- @NonNull
- @CameraExecutor
- Executor getExecutor();
-
- @NonNull
- ListenableFuture<List<Surface>> startWithDeferrableSurface(
- @NonNull List<DeferrableSurface> deferrableSurfaces, long timeout);
-
- boolean stop();
- }
-}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
index 695c99d..f3fdd6d 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
@@ -89,6 +89,9 @@
if (InvalidVideoProfilesQuirk.load()) {
quirks.add(new InvalidVideoProfilesQuirk());
}
+ if (Preview3AThreadCrash.load()) {
+ quirks.add(new Preview3AThreadCrash());
+ }
if (SmallDisplaySizeQuirk.load()) {
quirks.add(new SmallDisplaySizeQuirk());
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/Preview3AThreadCrash.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/Preview3AThreadCrash.java
new file mode 100644
index 0000000..dedf839
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/Preview3AThreadCrash.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * Camera service crashes after submitting a request by a newly created CameraCaptureSession.
+ *
+ * <p>QuirkSummary
+ * Bug Id: 290861504
+ * Description: The camera service may crash once a newly created CameraCaptureSession submit
+ * a repeating request.
+ * Device(s): Samsung device with samsungexynos7870 hardware
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class Preview3AThreadCrash implements Quirk {
+
+ static boolean load() {
+ return "samsungexynos7870".equalsIgnoreCase(Build.HARDWARE);
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/SessionResetPolicy.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/SessionResetPolicy.java
new file mode 100644
index 0000000..f04af77
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/SessionResetPolicy.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal.compat.workaround;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.compat.quirk.Preview3AThreadCrash;
+import androidx.camera.core.impl.Quirks;
+
+/**
+ * Indicate the required actions when going to switch CameraCaptureSession.
+ *
+ * @see Preview3AThreadCrash
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class SessionResetPolicy {
+
+ private final boolean mNeedAbortCapture;
+
+ public SessionResetPolicy(@NonNull Quirks deviceQuirks) {
+ mNeedAbortCapture = deviceQuirks.contains(Preview3AThreadCrash.class);
+ }
+
+ /**
+ * @return true if it needs to call abortCapture before the CameraCaptureSession is closed.
+ */
+ public boolean needAbortCapture() {
+ return mNeedAbortCapture;
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/WaitForRepeatingRequestStart.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/WaitForRepeatingRequestStart.java
index 8da55f2..e1a91a1 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/WaitForRepeatingRequestStart.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/WaitForRepeatingRequestStart.java
@@ -18,27 +18,18 @@
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
-import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.internal.Camera2CaptureCallbacks;
-import androidx.camera.camera2.internal.SynchronizedCaptureSession;
-import androidx.camera.camera2.internal.compat.params.SessionConfigurationCompat;
import androidx.camera.camera2.internal.compat.quirk.CaptureSessionStuckQuirk;
-import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.Quirks;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.core.impl.utils.futures.FutureChain;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import com.google.common.util.concurrent.ListenableFuture;
-import java.util.ArrayList;
-import java.util.List;
-
/**
* The workaround is used to wait for the other CameraCaptureSessions to complete their in-flight
* capture sequences before opening the current session.
@@ -87,27 +78,6 @@
return Futures.nonCancellationPropagating(mStartStreamingFuture);
}
- /**
- * For b/146773463: It needs to check all the releasing capture sessions are ready for
- * opening next capture session.
- */
- @NonNull
- public ListenableFuture<Void> openCaptureSession(
- @NonNull CameraDevice cameraDevice,
- @NonNull SessionConfigurationCompat sessionConfigurationCompat,
- @NonNull List<DeferrableSurface> deferrableSurfaces,
- @NonNull List<SynchronizedCaptureSession> closingSessions,
- @NonNull OpenCaptureSession openCaptureSession) {
- List<ListenableFuture<Void>> futureList = new ArrayList<>();
- for (SynchronizedCaptureSession session : closingSessions) {
- futureList.add(session.getOpeningBlocker());
- }
-
- return FutureChain.from(Futures.successfulAsList(futureList)).transformAsync(
- v -> openCaptureSession.run(cameraDevice, sessionConfigurationCompat,
- deferrableSurfaces), CameraXExecutors.directExecutor());
- }
-
/** Hook the setSingleRepeatingRequest() to know if it has started a repeating request. */
public int setSingleRepeatingRequest(
@NonNull CaptureRequest request,
@@ -134,6 +104,13 @@
}
}
+ /**
+ * This should be called when SynchronizedCaptureSession#finishClose is called.
+ */
+ public void onFinishClosed() {
+ mStartStreamingFuture.cancel(true);
+ }
+
private final CameraCaptureSession.CaptureCallback mCaptureCallback =
new CameraCaptureSession.CaptureCallback() {
@Override
@@ -163,14 +140,4 @@
@NonNull CameraCaptureSession.CaptureCallback listener)
throws CameraAccessException;
}
-
- /** Interface to forward call of the openCaptureSession() method. */
- @FunctionalInterface
- public interface OpenCaptureSession {
- /** Run the openCaptureSession() method. */
- @NonNull
- ListenableFuture<Void> run(@NonNull CameraDevice cameraDevice,
- @NonNull SessionConfigurationCompat sessionConfigurationCompat,
- @NonNull List<DeferrableSurface> deferrableSurfaces);
- }
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/Camera2ImplConfigTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/Camera2ImplConfigTest.java
index c3d9873..bff78ef 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/Camera2ImplConfigTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/impl/Camera2ImplConfigTest.java
@@ -37,8 +37,6 @@
public class Camera2ImplConfigTest {
private static final int INVALID_TEMPLATE_TYPE = -1;
private static final int INVALID_COLOR_CORRECTION_MODE = -1;
- private static final CameraEventCallbacks CAMERA_EVENT_CALLBACKS =
- CameraEventCallbacks.createEmptyCallback();
@Test
public void emptyConfigurationDoesNotContainTemplateType() {
@@ -50,17 +48,6 @@
}
@Test
- public void canExtendWithCameraEventCallback() {
- FakeConfig.Builder builder = new FakeConfig.Builder();
-
- new Camera2ImplConfig.Extender<>(builder).setCameraEventCallback(CAMERA_EVENT_CALLBACKS);
- Camera2ImplConfig config = new Camera2ImplConfig(builder.build());
-
- assertThat(config.getCameraEventCallback(/*valueIfMissing=*/ null))
- .isSameInstanceAs(CAMERA_EVENT_CALLBACKS);
- }
-
- @Test
public void canSetAndRetrieveCaptureRequestKeys_byBuilder() {
Range<Integer> fakeRange = new Range<>(0, 30);
Camera2ImplConfig.Builder builder =
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java
index bc4a710..76be504 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpackerTest.java
@@ -19,7 +19,6 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
@@ -30,7 +29,6 @@
import androidx.annotation.OptIn;
import androidx.camera.camera2.impl.Camera2ImplConfig;
-import androidx.camera.camera2.impl.CameraEventCallbacks;
import androidx.camera.camera2.interop.Camera2Interop;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.ImageCapture;
@@ -73,16 +71,10 @@
CameraDevice.StateCallback deviceCallback = mock(CameraDevice.StateCallback.class);
CameraCaptureSession.StateCallback sessionStateCallback =
mock(CameraCaptureSession.StateCallback.class);
- CameraEventCallbacks cameraEventCallbacks = mock(CameraEventCallbacks.class);
- when(cameraEventCallbacks.clone()).thenReturn(cameraEventCallbacks);
-
new Camera2Interop.Extender<>(imageCaptureBuilder)
.setSessionCaptureCallback(captureCallback)
.setDeviceStateCallback(deviceCallback)
.setSessionStateCallback(sessionStateCallback);
- new Camera2ImplConfig.Extender<>(imageCaptureBuilder)
- .setCameraEventCallback(cameraEventCallbacks);
-
SessionConfig.Builder sessionBuilder = new SessionConfig.Builder();
mUnpacker.unpack(RESOLUTION_VGA, imageCaptureBuilder.getUseCaseConfig(), sessionBuilder);
SessionConfig sessionConfig = sessionBuilder.build();
@@ -98,10 +90,6 @@
assertThat(sessionConfig.getDeviceStateCallbacks()).containsExactly(deviceCallback);
assertThat(sessionConfig.getSessionStateCallbacks())
.containsExactly(sessionStateCallback);
- assertThat(
- new Camera2ImplConfig(
- sessionConfig.getImplementationOptions()).getCameraEventCallback(
- null)).isEqualTo(cameraEventCallbacks);
}
@Test
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionTest.java
index 03cdf81..d539e15 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SynchronizedCaptureSessionTest.java
@@ -36,6 +36,7 @@
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.ImmediateSurface;
import androidx.camera.core.impl.Quirks;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import org.junit.Before;
import org.junit.Test;
@@ -61,8 +62,8 @@
private SynchronizedCaptureSession.StateCallback mMockStateCallback;
private List<OutputConfigurationCompat> mOutputs;
private CaptureSessionRepository mCaptureSessionRepository;
- private SynchronizedCaptureSessionOpener mSynchronizedCaptureSessionOpener;
- private SynchronizedCaptureSessionOpener.Builder mCaptureSessionOpenerBuilder;
+ private SynchronizedCaptureSession.Opener mSynchronizedCaptureSessionOpener;
+ private SynchronizedCaptureSession.OpenerBuilder mCaptureSessionOpenerBuilder;
private ScheduledExecutorService mScheduledExecutorService =
Executors.newSingleThreadScheduledExecutor();
@@ -83,8 +84,8 @@
mFakeDeferrableSurfaces.add(mDeferrableSurface1);
mFakeDeferrableSurfaces.add(mDeferrableSurface2);
- mCaptureSessionOpenerBuilder = new SynchronizedCaptureSessionOpener.Builder(
- android.os.AsyncTask.SERIAL_EXECUTOR, mScheduledExecutorService,
+ mCaptureSessionOpenerBuilder = new SynchronizedCaptureSession.OpenerBuilder(
+ CameraXExecutors.directExecutor(), mScheduledExecutorService,
mock(Handler.class), mCaptureSessionRepository,
new Quirks(Arrays.asList(new PreviewOrientationIncorrectQuirk(),
new ConfigureSurfaceToSecondarySessionFailQuirk())),
@@ -122,7 +123,7 @@
CameraCaptureSession mockCaptureSession1 = mock(CameraCaptureSession.class);
SynchronizedCaptureSession.StateCallback mockStateCallback1 = mock(
SynchronizedCaptureSession.StateCallback.class);
- SynchronizedCaptureSessionOpener captureSessionUtil1 =
+ SynchronizedCaptureSession.Opener captureSessionUtil1 =
mCaptureSessionOpenerBuilder.build();
SessionConfigurationCompat sessionConfigurationCompat1 =
captureSessionUtil1.createSessionConfigurationCompat(
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageSaverTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageSaverTest.java
deleted file mode 100644
index 5037730..0000000
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageSaverTest.java
+++ /dev/null
@@ -1,502 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assume.assumeFalse;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.Manifest;
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.ImageFormat;
-import android.graphics.Rect;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Environment;
-import android.os.ParcelFileDescriptor;
-import android.provider.MediaStore;
-import android.util.Base64;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.camera.core.ImageSaver.OnImageSavedCallback;
-import androidx.camera.core.ImageSaver.SaveError;
-import androidx.exifinterface.media.ExifInterface;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.MediumTest;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.rule.GrantPermissionRule;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.ByteBuffer;
-import java.util.Objects;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Semaphore;
-
-/**
- * Instrument tests for {@link ImageSaver}.
- */
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = 21)
-public class ImageSaverTest {
-
- private static final int WIDTH = 160;
- private static final int HEIGHT = 120;
- private static final int CROP_WIDTH = 100;
- private static final int CROP_HEIGHT = 100;
- private static final int Y_PIXEL_STRIDE = 1;
- private static final int Y_ROW_STRIDE = WIDTH;
- private static final int UV_PIXEL_STRIDE = 1;
- private static final int UV_ROW_STRIDE = WIDTH / 2;
- private static final int DEFAULT_JPEG_QUALITY = 100;
- private static final String JPEG_IMAGE_DATA_BASE_64 =
- "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB"
- + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEB"
- + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAB4AKADASIA"
- + "AhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA"
- + "AAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3"
- + "ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm"
- + "p6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA"
- + "AwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx"
- + "BhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK"
- + "U1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3"
- + "uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD/AD/6"
- + "KKK/8/8AP/P/AAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii"
- + "gAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA"
- + "CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK"
- + "KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo"
- + "ooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii"
- + "gAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//9k=";
- // The image used here has a YUV_420_888 format.
-
- private static final String TAG = "ImageSaverTest";
- private static final String INVALID_DATA_PATH = "/invalid_path";
-
- private static final String TAG_TO_IGNORE = ExifInterface.TAG_COMPRESSION;
- private static final String TAG_TO_IGNORE_VALUE = "6";
- private static final String TAG_TO_COPY = ExifInterface.TAG_MAKE;
- private static final String TAG_TO_COPY_VALUE = "make";
-
- @Rule
- public GrantPermissionRule mStoragePermissionRule =
- GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE,
- Manifest.permission.READ_EXTERNAL_STORAGE);
-
- @Mock
- private final ImageProxy mMockYuvImage = mock(ImageProxy.class);
- @Mock
- private final ImageProxy.PlaneProxy mYPlane = mock(ImageProxy.PlaneProxy.class);
- @Mock
- private final ImageProxy.PlaneProxy mUPlane = mock(ImageProxy.PlaneProxy.class);
- @Mock
- private final ImageProxy.PlaneProxy mVPlane = mock(ImageProxy.PlaneProxy.class);
- private final ByteBuffer mYBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT);
- private final ByteBuffer mUBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 4);
- private final ByteBuffer mVBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 4);
- @Mock
- private final ImageProxy mMockJpegImage = mock(ImageProxy.class);
- @Mock
- private final ImageProxy.PlaneProxy mJpegDataPlane = mock(ImageProxy.PlaneProxy.class);
- private ByteBuffer mJpegDataBuffer;
-
- private final Semaphore mSemaphore = new Semaphore(0);
- private final ImageSaver.OnImageSavedCallback mMockCallback =
- mock(ImageSaver.OnImageSavedCallback.class);
- private final ImageSaver.OnImageSavedCallback mSyncCallback =
- new OnImageSavedCallback() {
- @Override
- public void onImageSaved(
- @NonNull ImageCapture.OutputFileResults outputFileResults) {
- mMockCallback.onImageSaved(outputFileResults);
- mSemaphore.release();
- }
-
- @Override
- public void onError(@NonNull SaveError saveError, @NonNull String message,
- @Nullable Throwable cause) {
- Logger.d(TAG, message, cause);
- mMockCallback.onError(saveError, message, cause);
- mSemaphore.release();
- }
- };
-
- private ExecutorService mBackgroundExecutor;
- private ContentResolver mContentResolver;
-
- @Before
- public void setup() throws IOException {
- assumeFalse("Skip for Cuttlefish.", Build.MODEL.contains("Cuttlefish"));
- createDefaultPictureFolderIfNotExist();
- mJpegDataBuffer = createJpegBufferWithExif();
- // The YUV image's behavior.
- when(mMockYuvImage.getFormat()).thenReturn(ImageFormat.YUV_420_888);
- when(mMockYuvImage.getWidth()).thenReturn(WIDTH);
- when(mMockYuvImage.getHeight()).thenReturn(HEIGHT);
-
- when(mYPlane.getBuffer()).thenReturn(mYBuffer);
- when(mYPlane.getPixelStride()).thenReturn(Y_PIXEL_STRIDE);
- when(mYPlane.getRowStride()).thenReturn(Y_ROW_STRIDE);
-
- when(mUPlane.getBuffer()).thenReturn(mUBuffer);
- when(mUPlane.getPixelStride()).thenReturn(UV_PIXEL_STRIDE);
- when(mUPlane.getRowStride()).thenReturn(UV_ROW_STRIDE);
-
- when(mVPlane.getBuffer()).thenReturn(mVBuffer);
- when(mVPlane.getPixelStride()).thenReturn(UV_PIXEL_STRIDE);
- when(mVPlane.getRowStride()).thenReturn(UV_ROW_STRIDE);
- when(mMockYuvImage.getPlanes())
- .thenReturn(new ImageProxy.PlaneProxy[]{mYPlane, mUPlane, mVPlane});
- when(mMockYuvImage.getCropRect()).thenReturn(new Rect(0, 0, CROP_WIDTH, CROP_HEIGHT));
-
- // The JPEG image's behavior
- when(mMockJpegImage.getFormat()).thenReturn(ImageFormat.JPEG);
- when(mMockJpegImage.getWidth()).thenReturn(WIDTH);
- when(mMockJpegImage.getHeight()).thenReturn(HEIGHT);
- when(mMockJpegImage.getCropRect()).thenReturn(new Rect(0, 0, CROP_WIDTH, CROP_HEIGHT));
-
- when(mJpegDataPlane.getBuffer()).thenReturn(mJpegDataBuffer);
- when(mMockJpegImage.getPlanes()).thenReturn(new ImageProxy.PlaneProxy[]{mJpegDataPlane});
-
- // Set up a background executor for callbacks
- mBackgroundExecutor = Executors.newSingleThreadExecutor();
-
- mContentResolver = ApplicationProvider.getApplicationContext().getContentResolver();
- }
-
- @After
- public void tearDown() {
- if (mBackgroundExecutor != null) {
- mBackgroundExecutor.shutdown();
- }
- }
-
- private ByteBuffer createJpegBufferWithExif() throws IOException {
- // Create a jpeg file with the test data.
- File tempFile = File.createTempFile("jpeg_with_exif", ".jpg");
- tempFile.deleteOnExit();
- try (FileOutputStream fos = new FileOutputStream(tempFile)) {
- fos.write(Base64.decode(JPEG_IMAGE_DATA_BASE_64, Base64.DEFAULT));
- }
-
- // Add exif tag to the jpeg file and save.
- ExifInterface saveExif = new ExifInterface(tempFile.toString());
- saveExif.setAttribute(TAG_TO_IGNORE, TAG_TO_IGNORE_VALUE);
- saveExif.setAttribute(TAG_TO_COPY, TAG_TO_COPY_VALUE);
- saveExif.saveAttributes();
-
- // Verify that the tags are saved correctly.
- ExifInterface verifyExif = new ExifInterface(tempFile.getPath());
- assertThat(verifyExif.getAttribute(TAG_TO_IGNORE)).isEqualTo(TAG_TO_IGNORE_VALUE);
- assertThat(verifyExif.getAttribute(TAG_TO_COPY)).isEqualTo(TAG_TO_COPY_VALUE);
-
- // Read the jpeg file and return it as a ByteBuffer.
- byte[] buffer = new byte[1024];
- try (FileInputStream in = new FileInputStream(tempFile);
- ByteArrayOutputStream out = new ByteArrayOutputStream(1024)) {
- int read;
- while (true) {
- read = in.read(buffer);
- if (read == -1) break;
- out.write(buffer, 0, read);
- }
- return ByteBuffer.wrap(out.toByteArray());
- }
- }
-
- @SuppressWarnings("deprecation")
- private void createDefaultPictureFolderIfNotExist() {
- File pictureFolder = Environment.getExternalStoragePublicDirectory(
- Environment.DIRECTORY_PICTURES);
- if (!pictureFolder.exists()) {
- pictureFolder.mkdir();
- }
- }
-
- private ImageSaver getDefaultImageSaver(ImageProxy image, File file) {
- return getDefaultImageSaver(image,
- new ImageCapture.OutputFileOptions.Builder(file).build());
- }
-
- private ImageSaver getDefaultImageSaver(@NonNull ImageProxy image) {
- ContentValues contentValues = new ContentValues();
- contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
- return getDefaultImageSaver(image,
- new ImageCapture.OutputFileOptions.Builder(mContentResolver,
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
- contentValues).build());
- }
-
- private ImageSaver getDefaultImageSaver(ImageProxy image, OutputStream outputStream) {
- return getDefaultImageSaver(image,
- new ImageCapture.OutputFileOptions.Builder(outputStream).build());
- }
-
- private ImageSaver getDefaultImageSaver(ImageProxy image,
- ImageCapture.OutputFileOptions outputFileOptions) {
- return new ImageSaver(
- image,
- outputFileOptions,
- /*orientation=*/ 0,
- DEFAULT_JPEG_QUALITY,
- mBackgroundExecutor,
- mBackgroundExecutor,
- mSyncCallback);
- }
-
- @Test
- public void savedImage_exifIsCopiedToCroppedImage() throws IOException, InterruptedException {
- // Arrange.
- File saveLocation = File.createTempFile("test", ".jpg");
- saveLocation.deleteOnExit();
-
- // Act.
- getDefaultImageSaver(mMockJpegImage, saveLocation).run();
- mSemaphore.acquire();
- verify(mMockCallback).onImageSaved(any());
-
- // Assert.
- ExifInterface exifInterface = new ExifInterface(saveLocation.getPath());
- assertThat(exifInterface.getAttribute(TAG_TO_IGNORE)).isNotEqualTo(TAG_TO_IGNORE_VALUE);
- assertThat(exifInterface.getAttribute(TAG_TO_COPY)).isEqualTo(TAG_TO_COPY_VALUE);
- }
-
- @Test
- public void canSaveYuvImage_withNonExistingFile() throws InterruptedException {
- File saveLocation = new File(ApplicationProvider.getApplicationContext().getCacheDir(),
- "test" + System.currentTimeMillis() + ".jpg");
- saveLocation.deleteOnExit();
- // make sure file does not exist
- if (saveLocation.exists()) {
- saveLocation.delete();
- }
- assertThat(!saveLocation.exists());
-
- getDefaultImageSaver(mMockYuvImage, saveLocation).run();
- mSemaphore.acquire();
-
- verify(mMockCallback).onImageSaved(any());
- }
-
- @Test
- public void canSaveYuvImage_withExistingFile() throws InterruptedException, IOException {
- File saveLocation = File.createTempFile("test", ".jpg");
- saveLocation.deleteOnExit();
- assertThat(saveLocation.exists());
-
- getDefaultImageSaver(mMockYuvImage, saveLocation).run();
- mSemaphore.acquire();
-
- verify(mMockCallback).onImageSaved(any());
- }
-
- @Test
- public void saveToUri() throws InterruptedException, FileNotFoundException {
- // Act.
- getDefaultImageSaver(mMockYuvImage).run();
- mSemaphore.acquire();
-
- // Assert.
- // Verify success callback is called.
- ArgumentCaptor<ImageCapture.OutputFileResults> outputFileResultsArgumentCaptor =
- ArgumentCaptor.forClass(ImageCapture.OutputFileResults.class);
- verify(mMockCallback).onImageSaved(outputFileResultsArgumentCaptor.capture());
-
- // Verify save location Uri is available.
- Uri saveLocationUri = outputFileResultsArgumentCaptor.getValue().getSavedUri();
- assertThat(saveLocationUri).isNotNull();
-
- // Loads image and verify width and height.
- ParcelFileDescriptor pfd = mContentResolver.openFileDescriptor(saveLocationUri, "r");
- Bitmap bitmap = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
- assertThat(bitmap.getWidth()).isEqualTo(CROP_WIDTH);
- assertThat(bitmap.getHeight()).isEqualTo(CROP_HEIGHT);
-
- // Clean up.
- mContentResolver.delete(saveLocationUri, null, null);
- }
-
- @SuppressWarnings("deprecation")
- @Test
- public void saveToUriWithEmptyCollection_onErrorCalled() throws InterruptedException {
- // Arrange.
- ContentValues contentValues = new ContentValues();
- contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
- contentValues.put(MediaStore.MediaColumns.DATA, INVALID_DATA_PATH);
- ImageSaver imageSaver = getDefaultImageSaver(mMockYuvImage,
- new ImageCapture.OutputFileOptions.Builder(mContentResolver,
- Uri.EMPTY,
- contentValues).build());
-
- // Act.
- imageSaver.run();
- mSemaphore.acquire();
-
- // Assert.
- verify(mMockCallback).onError(eq(SaveError.FILE_IO_FAILED), any(), any());
- }
-
- @Test
- public void saveToOutputStream() throws InterruptedException, IOException {
- // Arrange.
- File file = File.createTempFile("test", ".jpg");
- file.deleteOnExit();
-
- // Act.
- try (OutputStream outputStream = new FileOutputStream(file)) {
- getDefaultImageSaver(mMockYuvImage, outputStream).run();
- mSemaphore.acquire();
- }
-
- // Assert.
- verify(mMockCallback).onImageSaved(any());
- // Loads image and verify width and height.
- Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
- assertThat(bitmap.getWidth()).isEqualTo(CROP_WIDTH);
- assertThat(bitmap.getHeight()).isEqualTo(CROP_HEIGHT);
- }
-
- @Test
- public void saveToClosedOutputStream_onErrorCalled() throws InterruptedException,
- IOException {
- // Arrange.
- File file = File.createTempFile("test", ".jpg");
- file.deleteOnExit();
- OutputStream outputStream = new FileOutputStream(file);
- outputStream.close();
-
- // Act.
- getDefaultImageSaver(mMockYuvImage, outputStream).run();
- mSemaphore.acquire();
-
- // Assert.
- verify(mMockCallback).onError(eq(SaveError.FILE_IO_FAILED), anyString(),
- any(Throwable.class));
- }
-
- @Test
- public void canSaveJpegImage() throws InterruptedException, IOException {
- File saveLocation = File.createTempFile("test", ".jpg");
- saveLocation.deleteOnExit();
-
- getDefaultImageSaver(mMockJpegImage, saveLocation).run();
- mSemaphore.acquire();
-
- verify(mMockCallback).onImageSaved(any());
- }
-
- @Test
- public void saveToFile_uriIsSet() throws InterruptedException, IOException {
- // Arrange.
- File saveLocation = File.createTempFile("test", ".jpg");
- saveLocation.deleteOnExit();
-
- // Act.
- getDefaultImageSaver(mMockJpegImage, saveLocation).run();
- mSemaphore.acquire();
-
- // Assert.
- ArgumentCaptor<ImageCapture.OutputFileResults> argumentCaptor =
- ArgumentCaptor.forClass(ImageCapture.OutputFileResults.class);
- verify(mMockCallback).onImageSaved(argumentCaptor.capture());
- String savedPath = Objects.requireNonNull(
- argumentCaptor.getValue().getSavedUri()).getPath();
- assertThat(savedPath).isEqualTo(saveLocation.getPath());
- }
-
- @Test
- public void errorCallbackWillBeCalledOnInvalidPath() throws InterruptedException {
- // Invalid filename should cause error
- File saveLocation = new File("/not/a/real/path.jpg");
-
- getDefaultImageSaver(mMockJpegImage, saveLocation).run();
- mSemaphore.acquire();
-
- verify(mMockCallback).onError(eq(SaveError.FILE_IO_FAILED), anyString(),
- any(Throwable.class));
- }
-
- @Test
- public void imageIsClosedOnSuccess() throws InterruptedException, IOException {
- File saveLocation = File.createTempFile("test", ".jpg");
- saveLocation.deleteOnExit();
-
- getDefaultImageSaver(mMockJpegImage, saveLocation).run();
-
- mSemaphore.acquire();
-
- verify(mMockJpegImage).close();
- }
-
- @Test
- public void imageIsClosedOnError() throws InterruptedException {
- // Invalid filename should cause error
- File saveLocation = new File("/not/a/real/path.jpg");
-
- getDefaultImageSaver(mMockJpegImage, saveLocation).run();
- mSemaphore.acquire();
-
- verify(mMockJpegImage).close();
- }
-
- private void imageCanBeCropped(ImageProxy image) throws InterruptedException, IOException {
- File saveLocation = File.createTempFile("test", ".jpg");
- saveLocation.deleteOnExit();
-
- getDefaultImageSaver(image, saveLocation).run();
- mSemaphore.acquire();
-
- Bitmap bitmap = BitmapFactory.decodeFile(saveLocation.getPath());
- assertThat(bitmap.getWidth()).isEqualTo(CROP_WIDTH);
- assertThat(bitmap.getHeight()).isEqualTo(CROP_HEIGHT);
- }
-
- @Test
- public void jpegImageCanBeCropped() throws InterruptedException, IOException {
- imageCanBeCropped(mMockJpegImage);
- }
-
- @Test
- public void yuvImageCanBeCropped() throws InterruptedException, IOException {
- imageCanBeCropped(mMockYuvImage);
- }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Camera.java b/camera/camera-core/src/main/java/androidx/camera/core/Camera.java
index 583094a..5616fc8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Camera.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Camera.java
@@ -110,13 +110,43 @@
void setExtendedConfig(@Nullable CameraConfig cameraConfig);
/**
- * Checks whether the use cases combination is supported by the camera.
+ * Checks whether the use cases combination is supported.
*
* @param useCases to be checked whether can be supported.
- * @return whether the use cases combination is supported by the camera
+ * @return whether the use cases combination is supported by the camera.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
default boolean isUseCasesCombinationSupported(@NonNull UseCase... useCases) {
+ return isUseCasesCombinationSupported(true, useCases);
+ }
+
+ /**
+ * Checks whether the use cases combination is supported by camera framework.
+ *
+ * <p>This method verify whether the given use cases can be supported solely by the surface
+ * configurations they require. It doesn't consider the optimization done by CameraX such as
+ * {@link androidx.camera.core.streamsharing.StreamSharing}.
+ *
+ * @param useCases to be checked whether can be supported.
+ * @return whether the use cases combination is supported by the camera.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ default boolean isUseCasesCombinationSupportedByFramework(@NonNull UseCase... useCases) {
+ return isUseCasesCombinationSupported(false, useCases);
+ }
+
+ /**
+ * Checks whether the use cases combination is supported.
+ *
+ * @param withStreamSharing {@code true} if
+ * {@link androidx.camera.core.streamsharing.StreamSharing} feature is considered, otherwise
+ * {@code false}.
+ * @param useCases to be checked whether can be supported.
+ * @return whether the use cases combination is supported by the camera.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ default boolean isUseCasesCombinationSupported(boolean withStreamSharing,
+ @NonNull UseCase... useCases) {
return true;
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
deleted file mode 100644
index e1f0dbc..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
+++ /dev/null
@@ -1,384 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import android.content.ContentValues;
-import android.graphics.ImageFormat;
-import android.net.Uri;
-import android.os.Build;
-import android.provider.MediaStore;
-
-import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.utils.Exif;
-import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability;
-import androidx.camera.core.internal.utils.ImageUtil;
-import androidx.camera.core.internal.utils.ImageUtil.CodecFailedException;
-import androidx.core.util.Preconditions;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import java.util.concurrent.RejectedExecutionException;
-
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-final class ImageSaver implements Runnable {
- private static final String TAG = "ImageSaver";
-
- private static final String TEMP_FILE_PREFIX = "CameraX";
- private static final String TEMP_FILE_SUFFIX = ".tmp";
- private static final int COPY_BUFFER_SIZE = 1024;
- private static final int PENDING = 1;
- private static final int NOT_PENDING = 0;
-
- // The image that was captured
- private final ImageProxy mImage;
- // The orientation of the image
- private final int mOrientation;
- // The compression quality level of the output JPEG image
- private final int mJpegQuality;
- // The target location to save the image to.
- @NonNull
- private final ImageCapture.OutputFileOptions mOutputFileOptions;
- // The executor to call back on
- @NonNull
- private final Executor mUserCallbackExecutor;
- // The callback to call on completion
- @NonNull
- private final OnImageSavedCallback mCallback;
- // The executor to handle the I/O operations
- @NonNull
- private final Executor mSequentialIoExecutor;
-
- ImageSaver(
- @NonNull ImageProxy image,
- @NonNull ImageCapture.OutputFileOptions outputFileOptions,
- int orientation,
- @IntRange(from = 1, to = 100) int jpegQuality,
- @NonNull Executor userCallbackExecutor,
- @NonNull Executor sequentialIoExecutor,
- @NonNull OnImageSavedCallback callback) {
- mImage = image;
- mOutputFileOptions = outputFileOptions;
- mOrientation = orientation;
- mJpegQuality = jpegQuality;
- mCallback = callback;
- mUserCallbackExecutor = userCallbackExecutor;
- mSequentialIoExecutor = sequentialIoExecutor;
- }
-
- @Override
- public void run() {
- // Save the image to a temp file first. This is necessary because ExifInterface only
- // supports saving to File.
- File tempFile = saveImageToTempFile();
- if (tempFile != null) {
- // Post copying on a sequential executor. If the user provided saving destination maps
- // to a specific file on disk, accessing the file from multiple threads is not safe.
- mSequentialIoExecutor.execute(() -> copyTempFileToDestination(tempFile));
- }
- }
-
- /**
- * Saves the {@link #mImage} to a temp file.
- *
- * <p> It also crops the image and update Exif if necessary. Returns null if saving failed.
- */
- @Nullable
- private File saveImageToTempFile() {
- File tempFile;
- try {
- if (isSaveToFile()) {
- // For saving to file, write to the target folder and rename for better performance.
- // The file extensions must be the same as app provided to avoid the directory
- // access problem.
- tempFile = new File(mOutputFileOptions.getFile().getParent(),
- TEMP_FILE_PREFIX + UUID.randomUUID().toString()
- + getFileExtensionWithDot(mOutputFileOptions.getFile()));
- } else {
- tempFile = File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
- }
- } catch (IOException e) {
- postError(SaveError.FILE_IO_FAILED, "Failed to create temp file", e);
- return null;
- }
-
- SaveError saveError = null;
- String errorMessage = null;
- Throwable throwable = null;
- try (ImageProxy imageToClose = mImage;
- FileOutputStream output = new FileOutputStream(tempFile)) {
- byte[] bytes = imageToJpegByteArray(mImage, mJpegQuality);
- output.write(bytes);
-
- // Create new exif based on the original exif.
- Exif exif = Exif.createFromFile(tempFile);
- Exif.createFromImageProxy(mImage).copyToCroppedImage(exif);
-
- // Overwrite the original orientation if the quirk exists.
- if (!new ExifRotationAvailability().shouldUseExifOrientation(mImage)) {
- exif.rotate(mOrientation);
- }
-
- // Overwrite exif based on metadata.
- ImageCapture.Metadata metadata = mOutputFileOptions.getMetadata();
- if (metadata.isReversedHorizontal()) {
- exif.flipHorizontally();
- }
- if (metadata.isReversedVertical()) {
- exif.flipVertically();
- }
- if (metadata.getLocation() != null) {
- exif.attachLocation(mOutputFileOptions.getMetadata().getLocation());
- }
-
- exif.save();
- } catch (OutOfMemoryError e) {
- saveError = SaveError.UNKNOWN;
- errorMessage = "Processing failed due to low memory.";
- throwable = e;
- } catch (IOException | IllegalArgumentException e) {
- saveError = SaveError.FILE_IO_FAILED;
- errorMessage = "Failed to write temp file";
- throwable = e;
- } catch (CodecFailedException e) {
- switch (e.getFailureType()) {
- case ENCODE_FAILED:
- saveError = SaveError.ENCODE_FAILED;
- errorMessage = "Failed to encode mImage";
- break;
- case DECODE_FAILED:
- saveError = SaveError.CROP_FAILED;
- errorMessage = "Failed to crop mImage";
- break;
- case UNKNOWN:
- default:
- saveError = SaveError.UNKNOWN;
- errorMessage = "Failed to transcode mImage";
- break;
- }
- throwable = e;
- }
- if (saveError != null) {
- postError(saveError, errorMessage, throwable);
- tempFile.delete();
- return null;
- }
- return tempFile;
- }
-
- private static String getFileExtensionWithDot(File file) {
- String fileName = file.getName();
- int dotIndex = fileName.lastIndexOf('.');
- if (dotIndex >= 0) {
- return fileName.substring(dotIndex);
- } else {
- return "";
- }
- }
-
- @NonNull
- private byte[] imageToJpegByteArray(@NonNull ImageProxy image, @IntRange(from = 1,
- to = 100) int jpegQuality) throws CodecFailedException {
- boolean shouldCropImage = ImageUtil.shouldCropImage(image);
- int imageFormat = image.getFormat();
-
- if (imageFormat == ImageFormat.JPEG) {
- if (!shouldCropImage) {
- // When cropping is unnecessary, the byte array doesn't need to be decoded and
- // re-encoded again. Therefore, jpegQuality is unnecessary in this case.
- return ImageUtil.jpegImageToJpegByteArray(image);
- } else {
- return ImageUtil.jpegImageToJpegByteArray(image, image.getCropRect(), jpegQuality);
- }
- } else if (imageFormat == ImageFormat.YUV_420_888) {
- return ImageUtil.yuvImageToJpegByteArray(image, shouldCropImage ? image.getCropRect() :
- null, jpegQuality, 0 /* rotationDegrees */);
- } else {
- Logger.w(TAG, "Unrecognized image format: " + imageFormat);
- }
-
- return null;
- }
-
- /**
- * Copy the temp file to user specified destination.
- *
- * <p> The temp file will be deleted afterwards.
- */
- void copyTempFileToDestination(@NonNull File tempFile) {
- Preconditions.checkNotNull(tempFile);
- SaveError saveError = null;
- String errorMessage = null;
- Exception exception = null;
- Uri outputUri = null;
- try {
- if (isSaveToMediaStore()) {
- ContentValues values = mOutputFileOptions.getContentValues() != null
- ? new ContentValues(mOutputFileOptions.getContentValues())
- : new ContentValues();
- setContentValuePending(values, PENDING);
- outputUri = mOutputFileOptions.getContentResolver().insert(
- mOutputFileOptions.getSaveCollection(),
- values);
- if (outputUri == null) {
- saveError = SaveError.FILE_IO_FAILED;
- errorMessage = "Failed to insert URI.";
- } else {
- if (!copyTempFileToUri(tempFile, outputUri)) {
- saveError = SaveError.FILE_IO_FAILED;
- errorMessage = "Failed to save to URI.";
- }
- setUriNotPending(outputUri);
- }
- } else if (isSaveToOutputStream()) {
- copyTempFileToOutputStream(tempFile, mOutputFileOptions.getOutputStream());
- } else if (isSaveToFile()) {
- File targetFile = mOutputFileOptions.getFile();
- // Normally File#renameTo will overwrite the targetFile even if it already exists.
- // Just in case of unexpected behavior on certain platforms or devices, delete the
- // target file before renaming.
- if (targetFile.exists()) {
- targetFile.delete();
- }
- if (!tempFile.renameTo(targetFile)) {
- saveError = SaveError.FILE_IO_FAILED;
- errorMessage = "Failed to rename file.";
- }
- outputUri = Uri.fromFile(targetFile);
- }
- } catch (IOException | IllegalArgumentException | SecurityException e) {
- saveError = SaveError.FILE_IO_FAILED;
- errorMessage = "Failed to write destination file.";
- exception = e;
- } finally {
- tempFile.delete();
- }
- if (saveError != null) {
- postError(saveError, errorMessage, exception);
- } else {
- postSuccess(outputUri);
- }
- }
-
- private boolean isSaveToMediaStore() {
- return mOutputFileOptions.getSaveCollection() != null
- && mOutputFileOptions.getContentResolver() != null
- && mOutputFileOptions.getContentValues() != null;
- }
-
- private boolean isSaveToFile() {
- return mOutputFileOptions.getFile() != null;
- }
-
- private boolean isSaveToOutputStream() {
- return mOutputFileOptions.getOutputStream() != null;
- }
-
- /**
- * Removes IS_PENDING flag during the writing to {@link Uri}.
- */
- private void setUriNotPending(@NonNull Uri outputUri) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- ContentValues values = new ContentValues();
- setContentValuePending(values, NOT_PENDING);
- mOutputFileOptions.getContentResolver().update(outputUri, values, null, null);
- }
- }
-
- /** Set IS_PENDING flag to {@link ContentValues}. */
- private void setContentValuePending(@NonNull ContentValues values, int isPending) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- values.put(MediaStore.Images.Media.IS_PENDING, isPending);
- }
- }
-
- /**
- * Copies temp file to {@link Uri}.
- *
- * @return false if the {@link Uri} is not writable.
- */
- private boolean copyTempFileToUri(@NonNull File tempFile, @NonNull Uri uri) throws IOException {
- try (OutputStream outputStream =
- mOutputFileOptions.getContentResolver().openOutputStream(uri)) {
- if (outputStream == null) {
- // The URI is not writable.
- return false;
- }
- copyTempFileToOutputStream(tempFile, outputStream);
- }
- return true;
- }
-
- private void copyTempFileToOutputStream(@NonNull File tempFile,
- @NonNull OutputStream outputStream) throws IOException {
- try (InputStream in = new FileInputStream(tempFile)) {
- byte[] buf = new byte[COPY_BUFFER_SIZE];
- int len;
- while ((len = in.read(buf)) > 0) {
- outputStream.write(buf, 0, len);
- }
- }
- }
-
- private void postSuccess(@Nullable Uri outputUri) {
- try {
- mUserCallbackExecutor.execute(
- () -> mCallback.onImageSaved(new ImageCapture.OutputFileResults(outputUri)));
- } catch (RejectedExecutionException e) {
- Logger.e(TAG,
- "Application executor rejected executing OnImageSavedCallback.onImageSaved "
- + "callback. Skipping.");
- }
- }
-
- private void postError(SaveError saveError, final String message,
- @Nullable final Throwable cause) {
- try {
- mUserCallbackExecutor.execute(() -> mCallback.onError(saveError, message, cause));
- } catch (RejectedExecutionException e) {
- Logger.e(TAG, "Application executor rejected executing OnImageSavedCallback.onError "
- + "callback. Skipping.");
- }
- }
-
- /** Type of error that occurred during save */
- public enum SaveError {
- /** Failed to write to or close the file */
- FILE_IO_FAILED,
- /** Failure when attempting to encode image */
- ENCODE_FAILED,
- /** Failure when attempting to crop image */
- CROP_FAILED,
- UNKNOWN
- }
-
- public interface OnImageSavedCallback {
-
- void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults);
-
- void onError(@NonNull SaveError saveError, @NonNull String message,
- @Nullable Throwable cause);
- }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/DeferrableSurfaces.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/DeferrableSurfaces.java
index 3422289..67162d5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/DeferrableSurfaces.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/DeferrableSurfaces.java
@@ -24,6 +24,7 @@
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.core.util.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
@@ -33,8 +34,6 @@
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
@@ -51,65 +50,51 @@
* {@link DeferrableSurface} collection.
*
* @param removeNullSurfaces If true remove all Surfaces that were not retrieved.
- * @param timeout The task timeout value in milliseconds.
+ * @param timeoutMillis The task timeout value in milliseconds.
* @param executor The executor service to run the task.
* @param scheduledExecutorService The executor service to schedule the timeout event.
*/
@NonNull
public static ListenableFuture<List<Surface>> surfaceListWithTimeout(
@NonNull Collection<DeferrableSurface> deferrableSurfaces,
- boolean removeNullSurfaces, long timeout, @NonNull Executor executor,
+ boolean removeNullSurfaces, long timeoutMillis, @NonNull Executor executor,
@NonNull ScheduledExecutorService scheduledExecutorService) {
- List<ListenableFuture<Surface>> listenableFutureSurfaces = new ArrayList<>();
-
- for (DeferrableSurface deferrableSurface : deferrableSurfaces) {
- listenableFutureSurfaces.add(
- Futures.nonCancellationPropagating(deferrableSurface.getSurface()));
+ List<ListenableFuture<Surface>> list = new ArrayList<>();
+ for (DeferrableSurface surface : deferrableSurfaces) {
+ list.add(Futures.nonCancellationPropagating(surface.getSurface()));
}
+ ListenableFuture<List<Surface>> listenableFuture = Futures.makeTimeoutFuture(
+ timeoutMillis, scheduledExecutorService, Futures.successfulAsList(list)
+ );
- return CallbackToFutureAdapter.getFuture(
- completer -> {
- ListenableFuture<List<Surface>> listenableFuture = Futures.successfulAsList(
- listenableFutureSurfaces);
+ return CallbackToFutureAdapter.getFuture(completer -> {
+ // Cancel the listenableFuture if the outer task was cancelled, and the
+ // listenableFuture will cancel the scheduledFuture on its complete callback.
+ completer.addCancellationListener(() -> listenableFuture.cancel(true), executor);
- ScheduledFuture<?> scheduledFuture = scheduledExecutorService.schedule(() -> {
- executor.execute(() -> {
- if (!listenableFuture.isDone()) {
- completer.setException(
- new TimeoutException(
- "Cannot complete surfaceList within " + timeout));
- listenableFuture.cancel(true);
- }
- });
- }, timeout, TimeUnit.MILLISECONDS);
+ Futures.addCallback(listenableFuture, new FutureCallback<List<Surface>>() {
+ @Override
+ public void onSuccess(@Nullable List<Surface> result) {
+ Preconditions.checkNotNull(result);
+ List<Surface> surfaces = new ArrayList<>(result);
+ if (removeNullSurfaces) {
+ surfaces.removeAll(Collections.singleton(null));
+ }
+ completer.set(surfaces);
+ }
- // Cancel the listenableFuture if the outer task was cancelled, and the
- // listenableFuture will cancel the scheduledFuture on its complete callback.
- completer.addCancellationListener(() -> listenableFuture.cancel(true),
- executor);
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ if (t instanceof TimeoutException) {
+ completer.setException(t);
+ } else {
+ completer.set(Collections.emptyList());
+ }
+ }
+ }, executor);
- Futures.addCallback(listenableFuture,
- new FutureCallback<List<Surface>>() {
- @Override
- public void onSuccess(@Nullable List<Surface> result) {
- List<Surface> surfaces = new ArrayList<>(result);
- if (removeNullSurfaces) {
- surfaces.removeAll(Collections.singleton(null));
- }
- completer.set(surfaces);
- scheduledFuture.cancel(true);
- }
-
- @Override
- public void onFailure(@NonNull Throwable t) {
- completer.set(
- Collections.unmodifiableList(Collections.emptyList()));
- scheduledFuture.cancel(true);
- }
- }, executor);
-
- return "surfaceList";
- });
+ return "surfaceList[" + deferrableSurfaces + "]";
+ });
}
/**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/futures/Futures.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/futures/Futures.java
index c26b0b1..dde1276 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/futures/Futures.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/futures/Futures.java
@@ -35,7 +35,10 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
/**
* Utility class for generating specific implementations of {@link ListenableFuture}.
@@ -411,6 +414,30 @@
}
/**
+ * Returns a future that delegates to the supplied future but will finish early
+ * (via a TimeoutException) if the specified duration expires.
+ *
+ * @param timeoutMillis When to time out the future in milliseconds.
+ * @param scheduledExecutor The executor service to enforce the timeout.
+ * @param input The future to delegate to.
+ */
+ @NonNull
+ public static <V> ListenableFuture<V> makeTimeoutFuture(
+ long timeoutMillis,
+ @NonNull ScheduledExecutorService scheduledExecutor,
+ @NonNull ListenableFuture<V> input) {
+ return CallbackToFutureAdapter.getFuture(completer -> {
+ propagate(input, completer);
+ ScheduledFuture<?> timeoutFuture = scheduledExecutor.schedule(
+ () -> completer.setException(new TimeoutException("Future[" + input + "] is "
+ + "not done within " + timeoutMillis + " ms.")),
+ timeoutMillis, TimeUnit.MILLISECONDS);
+ input.addListener(() -> timeoutFuture.cancel(true), CameraXExecutors.directExecutor());
+ return "TimeoutFuture[" + input + "]";
+ });
+ }
+
+ /**
* Should not be instantiated.
*/
private Futures() {}
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 7d284bd..22f51ec 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
@@ -945,16 +945,22 @@
}
@Override
- public boolean isUseCasesCombinationSupported(@NonNull UseCase... useCases) {
+ public boolean isUseCasesCombinationSupported(boolean withStreamSharing,
+ @NonNull UseCase... useCases) {
+ Collection<UseCase> useCasesToVerify = Arrays.asList(useCases);
+ if (withStreamSharing) {
+ StreamSharing streamSharing = createOrReuseStreamSharing(useCasesToVerify, true);
+ useCasesToVerify = calculateCameraUseCases(useCasesToVerify, null, streamSharing);
+ }
synchronized (mLock) {
// If the UseCases exceed the resolutions then it will throw an exception
try {
- Map<UseCase, ConfigPair> configs = getConfigs(Arrays.asList(useCases),
+ Map<UseCase, ConfigPair> configs = getConfigs(useCasesToVerify,
mCameraConfig.getUseCaseConfigFactory(), mUseCaseConfigFactory);
calculateSuggestedStreamSpecs(
getCameraMode(),
mCameraInternal.getCameraInfoInternal(),
- Arrays.asList(useCases), emptyList(), configs);
+ useCasesToVerify, emptyList(), configs);
} catch (IllegalArgumentException e) {
return false;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
index e52b102..9d40fa6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
@@ -646,9 +646,11 @@
+ "provider, call SurfaceEdge#invalidate before calling "
+ "SurfaceEdge#setProvider");
checkArgument(getPrescribedSize().equals(provider.getPrescribedSize()),
- "The provider's size must match the parent");
+ String.format("The provider's size(%s) must match the parent(%s)",
+ getPrescribedSize(), provider.getPrescribedSize()));
checkArgument(getPrescribedStreamFormat() == provider.getPrescribedStreamFormat(),
- "The provider's format must match the parent");
+ String.format("The provider's format(%s) must match the parent(%s)",
+ getPrescribedStreamFormat(), provider.getPrescribedStreamFormat()));
checkState(!isClosed(), "The parent is closed. Call SurfaceEdge#invalidate() before "
+ "setting a new provider.");
mProvider = provider;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/DynamicRangeUtils.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/DynamicRangeUtils.java
new file mode 100644
index 0000000..98f25c4
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/DynamicRangeUtils.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.streamsharing;
+
+import static androidx.camera.core.DynamicRange.BIT_DEPTH_UNSPECIFIED;
+import static androidx.camera.core.DynamicRange.ENCODING_HDR_UNSPECIFIED;
+import static androidx.camera.core.DynamicRange.ENCODING_SDR;
+import static androidx.camera.core.DynamicRange.ENCODING_UNSPECIFIED;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.DynamicRange;
+import androidx.camera.core.impl.UseCaseConfig;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Utility methods for handling dynamic range.
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+public class DynamicRangeUtils {
+
+ private DynamicRangeUtils() {
+ }
+
+ /**
+ * Resolves dynamic ranges from use case configs.
+ *
+ * <p>If there is no dynamic range that satisfies all requirements, a null will be returned.
+ */
+ @Nullable
+ public static DynamicRange resolveDynamicRange(@NonNull Set<UseCaseConfig<?>> useCaseConfigs) {
+ List<DynamicRange> dynamicRanges = new ArrayList<>();
+ for (UseCaseConfig<?> useCaseConfig : useCaseConfigs) {
+ dynamicRanges.add(useCaseConfig.getDynamicRange());
+ }
+
+ return intersectDynamicRange(dynamicRanges);
+ }
+
+ /**
+ * Finds the intersection of the input dynamic ranges.
+ *
+ * <p>Returns the intersection if found, or null if no intersection.
+ */
+ @Nullable
+ private static DynamicRange intersectDynamicRange(@NonNull List<DynamicRange> dynamicRanges) {
+ if (dynamicRanges.isEmpty()) {
+ return null;
+ }
+
+ DynamicRange firstDynamicRange = dynamicRanges.get(0);
+ Integer resultEncoding = firstDynamicRange.getEncoding();
+ Integer resultBitDepth = firstDynamicRange.getBitDepth();
+ for (int i = 1; i < dynamicRanges.size(); i++) {
+ DynamicRange childDynamicRange = dynamicRanges.get(i);
+ resultEncoding = intersectDynamicRangeEncoding(resultEncoding,
+ childDynamicRange.getEncoding());
+ resultBitDepth = intersectDynamicRangeBitDepth(resultBitDepth,
+ childDynamicRange.getBitDepth());
+
+ if (resultEncoding == null || resultBitDepth == null) {
+ return null;
+ }
+ }
+
+ return new DynamicRange(resultEncoding, resultBitDepth);
+ }
+
+ @Nullable
+ private static Integer intersectDynamicRangeEncoding(@NonNull Integer encoding1,
+ @NonNull Integer encoding2) {
+ // Handle unspecified.
+ if (encoding1.equals(ENCODING_UNSPECIFIED)) {
+ return encoding2;
+ }
+ if (encoding2.equals(ENCODING_UNSPECIFIED)) {
+ return encoding1;
+ }
+
+ // Handle HDR unspecified.
+ if (encoding1.equals(ENCODING_HDR_UNSPECIFIED) && !encoding2.equals(ENCODING_SDR)) {
+ return encoding2;
+ }
+ if (encoding2.equals(ENCODING_HDR_UNSPECIFIED) && !encoding1.equals(ENCODING_SDR)) {
+ return encoding1;
+ }
+
+ return encoding1.equals(encoding2) ? encoding1 : null;
+ }
+
+ @Nullable
+ private static Integer intersectDynamicRangeBitDepth(@NonNull Integer bitDepth1,
+ @NonNull Integer bitDepth2) {
+ // Handle unspecified.
+ if (bitDepth1.equals(BIT_DEPTH_UNSPECIFIED)) {
+ return bitDepth2;
+ }
+ if (bitDepth2.equals(BIT_DEPTH_UNSPECIFIED)) {
+ return bitDepth1;
+ }
+
+ return bitDepth1.equals(bitDepth2) ? bitDepth1 : null;
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
index da98cf3..33a2a1e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCamera.java
@@ -19,11 +19,13 @@
import static androidx.camera.core.CameraEffect.PREVIEW;
import static androidx.camera.core.CameraEffect.VIDEO_CAPTURE;
import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_DYNAMIC_RANGE;
import static androidx.camera.core.impl.ImageOutputConfig.OPTION_CUSTOM_ORDERED_RESOLUTIONS;
import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
import static androidx.camera.core.impl.utils.Threads.checkMainThread;
import static androidx.camera.core.impl.utils.TransformUtils.getRotatedSize;
import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
+import static androidx.camera.core.streamsharing.DynamicRangeUtils.resolveDynamicRange;
import static androidx.camera.core.streamsharing.ResolutionUtils.getMergedResolutions;
import static androidx.core.util.Preconditions.checkState;
@@ -40,6 +42,7 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.CameraEffect;
+import androidx.camera.core.DynamicRange;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.Preview;
import androidx.camera.core.UseCase;
@@ -96,6 +99,8 @@
private final CameraCaptureCallback mParentMetadataCallback = createCameraCaptureCallback();
@NonNull
private final VirtualCameraControl mVirtualCameraControl;
+ @NonNull
+ private final VirtualCameraInfo mVirtualCameraInfo;
/**
* @param parentCamera the parent {@link CameraInternal} instance. For example, the
@@ -112,6 +117,7 @@
mChildren = children;
mVirtualCameraControl = new VirtualCameraControl(parentCamera.getCameraControlInternal(),
streamSharingControl);
+ mVirtualCameraInfo = new VirtualCameraInfo(parentCamera.getCameraInfoInternal());
// Set children state to inactive by default.
for (UseCase child : children) {
mChildrenActiveState.put(child, false);
@@ -139,6 +145,19 @@
// Merge Surface occupancy priority.
mutableConfig.insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY,
getHighestSurfacePriority(childrenConfigs));
+
+ // Merge dynamic range configs. Try to find a dynamic range that can match all child
+ // requirements, or throw an exception if no matching dynamic range.
+ // TODO: This approach works for the current code base, where only VideoCapture can be
+ // configured (Preview follows the settings, ImageCapture is fixed as SDR). When
+ // dynamic range APIs opened on other use cases, we might want a more advanced approach
+ // that allows conflicts, e.g. converting HDR stream to SDR stream.
+ DynamicRange dynamicRange = resolveDynamicRange(childrenConfigs);
+ if (dynamicRange == null) {
+ throw new IllegalArgumentException("Failed to merge child dynamic ranges, can not find"
+ + " a dynamic range that satisfies all children.");
+ }
+ mutableConfig.insertOption(OPTION_INPUT_DYNAMIC_RANGE, dynamicRange);
}
void bindChildren() {
@@ -306,9 +325,7 @@
@NonNull
@Override
public CameraInfoInternal getCameraInfoInternal() {
- // TODO(b/265818567): replace this with a virtual camera info that returns a updated sensor
- // rotation degrees based on buffer transformation applied in StreamSharing.
- return mParentCamera.getCameraInfoInternal();
+ return mVirtualCameraInfo;
}
@NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraInfo.java
new file mode 100644
index 0000000..cdd2e3e
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/VirtualCameraInfo.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.streamsharing;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.CameraInfoInternal;
+import androidx.camera.core.impl.ForwardingCameraInfo;
+
+import java.util.UUID;
+
+/**
+ * A {@link CameraInfoInternal} that returns info of the virtual camera.
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+public class VirtualCameraInfo extends ForwardingCameraInfo {
+
+ private final String mVirtualCameraId;
+
+ VirtualCameraInfo(@NonNull CameraInfoInternal cameraInfoInternal) {
+ super(cameraInfoInternal);
+ // Generate a unique ID for the virtual camera.
+ mVirtualCameraId =
+ "virtual-" + cameraInfoInternal.getCameraId() + "-" + UUID.randomUUID().toString();
+ }
+
+ /**
+ * Override the parent camera ID.
+ */
+ @NonNull
+ @Override
+ public String getCameraId() {
+ return mVirtualCameraId;
+ }
+}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
index e023a9f..c59ee46 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
@@ -224,6 +224,66 @@
)
}
+ @Test
+ fun isUseCasesCombinationSupported_returnTrueWhenSupported() {
+ // Assert
+ assertThat(adapter.isUseCasesCombinationSupported(preview, image)).isTrue()
+ }
+
+ @Test
+ fun isUseCasesCombinationSupported_returnFalseWhenNotSupported() {
+ // Arrange
+ val preview2 = Preview.Builder().build()
+ // Assert: double preview use cases should not be supported even with stream sharing.
+ assertThat(
+ adapter.isUseCasesCombinationSupported(
+ preview,
+ preview2,
+ video,
+ image
+ )
+ ).isFalse()
+ }
+
+ @Test
+ fun isUseCasesCombinationSupportedByFramework_returnTrueWhenSupported() {
+ // Assert
+ assertThat(adapter.isUseCasesCombinationSupportedByFramework(preview, image)).isTrue()
+ }
+
+ @Test
+ fun isUseCasesCombinationSupportedByFramework_returnFalseWhenNotSupported() {
+ // Assert
+ assertThat(
+ adapter.isUseCasesCombinationSupportedByFramework(
+ preview,
+ video,
+ image
+ )
+ ).isFalse()
+ }
+
+ @Test
+ fun isUseCasesCombinationSupported_withStreamSharing() {
+ // preview, video, image should not be supported if stream sharing is not enabled.
+ assertThat(
+ adapter.isUseCasesCombinationSupported( /*withStreamSharing=*/ false,
+ preview,
+ video,
+ image
+ )
+ ).isFalse()
+
+ // preview, video, image should be supported if stream sharing is enabled.
+ assertThat(
+ adapter.isUseCasesCombinationSupported( /*withStreamSharing=*/ true,
+ preview,
+ video,
+ image
+ )
+ ).isTrue()
+ }
+
@Test(expected = CameraException::class)
fun invalidUseCaseComboCantBeFixedByStreamSharing_throwsException() {
// Arrange: create a camera that only support one JPEG stream.
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
index de16acd..3b5c6ea 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/StreamSharingTest.kt
@@ -27,6 +27,7 @@
import androidx.camera.core.CameraEffect.PREVIEW
import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
+import androidx.camera.core.DynamicRange
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
import androidx.camera.core.ImageProxy
@@ -154,6 +155,41 @@
}
@Test
+ fun getParentDynamicRange_isIntersectionOfChildrenDynamicRanges() {
+ val unspecifiedChild = FakeUseCase(
+ FakeUseCaseConfig.Builder().setSurfaceOccupancyPriority(1)
+ .setDynamicRange(DynamicRange.UNSPECIFIED).useCaseConfig
+ )
+ val hdrChild = FakeUseCase(
+ FakeUseCaseConfig.Builder().setSurfaceOccupancyPriority(2)
+ .setDynamicRange(DynamicRange.HLG_10_BIT).useCaseConfig
+ )
+ streamSharing =
+ StreamSharing(camera, setOf(unspecifiedChild, hdrChild), useCaseConfigFactory)
+ assertThat(
+ streamSharing.mergeConfigs(
+ camera.cameraInfoInternal, /*extendedConfig*/null, /*cameraDefaultConfig*/null
+ ).dynamicRange
+ ).isEqualTo(DynamicRange.HLG_10_BIT)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun getParentDynamicRange_exception_whenChildrenDynamicRangesConflict() {
+ val sdrChild = FakeUseCase(
+ FakeUseCaseConfig.Builder().setSurfaceOccupancyPriority(1)
+ .setDynamicRange(DynamicRange.SDR).useCaseConfig
+ )
+ val hdrChild = FakeUseCase(
+ FakeUseCaseConfig.Builder().setSurfaceOccupancyPriority(2)
+ .setDynamicRange(DynamicRange.HLG_10_BIT).useCaseConfig
+ )
+ streamSharing = StreamSharing(camera, setOf(sdrChild, hdrChild), useCaseConfigFactory)
+ streamSharing.mergeConfigs(
+ camera.cameraInfoInternal, /*extendedConfig*/null, /*cameraDefaultConfig*/null
+ )
+ }
+
+ @Test
fun verifySupportedEffects() {
assertThat(streamSharing.isEffectTargetsSupported(PREVIEW or VIDEO_CAPTURE)).isTrue()
assertThat(
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraTest.kt
index 8fe319e..4681fbb 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/streamsharing/VirtualCameraTest.kt
@@ -116,6 +116,12 @@
}
@Test
+ fun getCameraId_returnsVirtualCameraId() {
+ assertThat(virtualCamera.cameraInfoInternal.cameraId)
+ .startsWith("virtual-" + parentCamera.cameraInfoInternal.cameraId)
+ }
+
+ @Test
fun submitStillCaptureRequests_triggersSnapshot() {
// Arrange.
virtualCamera.bindChildren()
@@ -249,7 +255,8 @@
@Test
fun virtualCameraInheritsParentProperties() {
assertThat(virtualCamera.cameraState).isEqualTo(parentCamera.cameraState)
- assertThat(virtualCamera.cameraInfo).isEqualTo(parentCamera.cameraInfo)
+ assertThat(virtualCamera.cameraInfoInternal.implementation)
+ .isEqualTo(virtualCamera.cameraInfoInternal.implementation)
}
@Test
diff --git a/camera/camera-effects/build.gradle b/camera/camera-effects/build.gradle
index 26dc06e..6c618ee 100644
--- a/camera/camera-effects/build.gradle
+++ b/camera/camera-effects/build.gradle
@@ -33,13 +33,6 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.truth)
- androidTestImplementation(project(":camera:camera-testing")) {
- // Ensure camera-testing does not pull in androidx.test dependencies
- exclude(group:"androidx.test")
- }
- androidTestImplementation(libs.kotlinStdlib)
- androidTestImplementation(libs.kotlinCoroutinesAndroid)
- androidTestImplementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
}
android {
defaultConfig {
diff --git a/camera/camera-effects/src/androidTest/java/androidx/camera/effects/opengl/GlRendererDeviceTest.kt b/camera/camera-effects/src/androidTest/java/androidx/camera/effects/opengl/GlRendererDeviceTest.kt
index 1dbd483..795082f 100644
--- a/camera/camera-effects/src/androidTest/java/androidx/camera/effects/opengl/GlRendererDeviceTest.kt
+++ b/camera/camera-effects/src/androidTest/java/androidx/camera/effects/opengl/GlRendererDeviceTest.kt
@@ -16,29 +16,11 @@
package androidx.camera.effects.opengl
-import android.graphics.Bitmap
-import android.graphics.Canvas
-import android.graphics.Color
-import android.graphics.Paint
-import android.graphics.PorterDuff
-import android.graphics.Rect
-import android.graphics.SurfaceTexture
-import android.opengl.Matrix
-import android.os.Handler
-import android.os.Looper
-import android.util.Size
-import android.view.Surface
-import androidx.camera.testing.impl.TestImageUtil.createBitmap
-import androidx.camera.testing.impl.TestImageUtil.getAverageDiff
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withTimeoutOrNull
import org.junit.After
-import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -51,159 +33,21 @@
@SdkSuppress(minSdkVersion = 21)
class GlRendererDeviceTest {
- companion object {
- private const val WIDTH = 640
- private const val HEIGHT = 480
- private const val TIMESTAMP_NS = 0L
- }
-
- private val input = createBitmap(WIDTH, HEIGHT)
- private val overlay = createOverlayBitmap()
- private val transparentOverlay = createTransparentOverlay()
-
private val glRenderer = GlRenderer()
- private lateinit var inputSurface: Surface
- private lateinit var inputTexture: SurfaceTexture
-
- private lateinit var outputSurface: Surface
- private lateinit var outputTexture: SurfaceTexture
-
- private val identityMatrix = FloatArray(16).apply {
- Matrix.setIdentityM(this, 0)
- }
@Before
fun setUp() {
glRenderer.init()
- inputTexture = SurfaceTexture(glRenderer.inputTextureId).apply {
- setDefaultBufferSize(WIDTH, HEIGHT)
- }
- inputSurface = Surface(inputTexture)
- outputTexture = SurfaceTexture(0).apply {
- setDefaultBufferSize(WIDTH, HEIGHT)
- }
- outputSurface = Surface(outputTexture)
}
@After
fun tearDown() {
glRenderer.release()
- inputTexture.release()
- inputSurface.release()
- outputTexture.release()
- outputSurface.release()
}
- @Test(expected = IllegalStateException::class)
- fun renderInputWhenUninitialized_throwsException() {
- GlRenderer().renderInputToSurface(TIMESTAMP_NS, identityMatrix, outputSurface)
- }
-
+ // TODO(b/295407763): verify the input/output of the OpenGL renderer
@Test
- fun drawInputToQueue_snapshot() = runBlocking {
- // Arrange: upload a overlay and create a texture queue.
- glRenderer.uploadOverlay(overlay)
- drawInputSurface(input)
- val queue = glRenderer.createBufferTextureIds(1, Size(WIDTH, HEIGHT))
- // Act: draw input to the queue and then to the output.
- glRenderer.renderInputToQueueTexture(queue[0])
- val bitmap = glRenderer.renderQueueTextureToBitmap(queue[0], WIDTH, HEIGHT, identityMatrix)
- // Assert: the output is the input with overlay.
- assertOverlayColor(bitmap)
- }
-
- @Test
- fun drawInputWithoutOverlay_snapshot() = runBlocking {
- // Arrange: upload a transparent overlay.
- glRenderer.uploadOverlay(transparentOverlay)
- drawInputSurface(input)
- // Act.
- val output = glRenderer.renderInputToBitmap(WIDTH, HEIGHT, identityMatrix)
- // Assert: the output is the same as the input.
- assertThat(getAverageDiff(output, input)).isEqualTo(0)
- }
-
- /**
- * Tests that the input is rendered to the output surface with the overlay.
- */
- private fun assertOverlayColor(bitmap: Bitmap) {
- // Top left quadrant is white.
- assertThat(
- getAverageDiff(
- bitmap,
- Rect(0, 0, WIDTH / 2, HEIGHT / 2),
- Color.WHITE
- )
- ).isEqualTo(0)
- assertThat(
- getAverageDiff(
- bitmap,
- Rect(WIDTH / 2, 0, WIDTH, HEIGHT / 2),
- Color.GREEN
- )
- ).isEqualTo(0)
- assertThat(
- getAverageDiff(
- bitmap,
- Rect(WIDTH / 2, HEIGHT / 2, WIDTH, HEIGHT),
- Color.YELLOW
- )
- ).isEqualTo(0)
- assertThat(
- getAverageDiff(
- bitmap,
- Rect(0, HEIGHT / 2, WIDTH / 2, HEIGHT),
- Color.BLUE
- )
- ).isEqualTo(0)
- }
-
- /**
- * Draws the bitmap to the input surface and waits for the frame to be available.
- */
- private suspend fun drawInputSurface(bitmap: Bitmap) {
- val deferredOnFrameAvailable = CompletableDeferred<Unit>()
- inputTexture.setOnFrameAvailableListener({
- deferredOnFrameAvailable.complete(Unit)
- }, Handler(Looper.getMainLooper()))
-
- // Draw bitmap to inputSurface.
- val canvas = inputSurface.lockCanvas(null)
- canvas.drawBitmap(bitmap, 0f, 0f, null)
- inputSurface.unlockCanvasAndPost(canvas)
-
- // Wait for frame available and update texture.
- withTimeoutOrNull(5_000) {
- deferredOnFrameAvailable.await()
- } ?: Assert.fail("Timed out waiting for SurfaceTexture frame available.")
- inputTexture.updateTexImage()
- }
-
- /**
- * Creates a bitmap with a white top-left quadrant.
- */
- private fun createOverlayBitmap(): Bitmap {
- val bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888)
- val centerX = (WIDTH / 2).toFloat()
- val centerY = (HEIGHT / 2).toFloat()
-
- val canvas = Canvas(bitmap)
- canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
-
- val paint = Paint()
- paint.style = Paint.Style.FILL
- paint.color = Color.WHITE
- canvas.drawRect(0f, 0f, centerX, centerY, paint)
- return bitmap
- }
-
- /**
- * Creates a transparent bitmap.
- */
- private fun createTransparentOverlay(): Bitmap {
- val bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888)
- val canvas = Canvas(bitmap)
- canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
- return bitmap
+ fun placeholder() {
+ assertThat(true).isTrue()
}
}
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlProgramCopy.java b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlProgramCopy.java
index 2336fa3..5114b56 100644
--- a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlProgramCopy.java
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlProgramCopy.java
@@ -17,7 +17,6 @@
package androidx.camera.effects.opengl;
import static androidx.camera.effects.opengl.Utils.checkGlErrorOrThrow;
-import static androidx.camera.effects.opengl.Utils.createFbo;
import static androidx.camera.effects.opengl.Utils.drawArrays;
import android.opengl.GLES11Ext;
@@ -62,7 +61,10 @@
protected void configure() {
super.configure();
// Create a FBO for attaching the output texture.
- mFbo = createFbo();
+ int[] fbos = new int[1];
+ GLES20.glGenFramebuffers(1, fbos, 0);
+ checkGlErrorOrThrow("glGenFramebuffers");
+ mFbo = fbos[0];
}
@Override
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlProgramOverlay.java b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlProgramOverlay.java
index 61a1a9f..8bea7e0 100644
--- a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlProgramOverlay.java
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlProgramOverlay.java
@@ -16,16 +16,9 @@
package androidx.camera.effects.opengl;
-import static androidx.camera.core.ImageProcessingUtil.copyByteBufferToBitmap;
import static androidx.camera.effects.opengl.Utils.checkGlErrorOrThrow;
import static androidx.camera.effects.opengl.Utils.checkLocationOrThrow;
-import static androidx.camera.effects.opengl.Utils.configureTexture2D;
-import static androidx.camera.effects.opengl.Utils.createFbo;
-import static androidx.camera.effects.opengl.Utils.createTextureId;
-import static androidx.camera.effects.opengl.Utils.drawArrays;
-import static androidx.core.util.Preconditions.checkArgument;
-import android.graphics.Bitmap;
import android.opengl.GLES20;
import android.view.Surface;
@@ -33,8 +26,6 @@
import androidx.annotation.RequiresApi;
import androidx.camera.core.Logger;
-import java.nio.ByteBuffer;
-
/**
* A GL program that copies the source while overlaying a texture on top of it.
*/
@@ -43,8 +34,6 @@
private static final String TAG = "GlProgramOverlay";
- private static final int SNAPSHOT_PIXEL_STRIDE = 4;
-
static final String TEXTURE_MATRIX = "uTexMatrix";
static final String OVERLAY_SAMPLER = "samplerOverlayTexture";
@@ -120,91 +109,7 @@
@NonNull float[] matrix, @NonNull GlContext glContext, @NonNull Surface surface,
long timestampNs) {
use();
- uploadParameters(inputTextureTarget, inputTextureId, overlayTextureId, matrix);
- try {
- glContext.drawAndSwap(surface, timestampNs);
- } catch (IllegalStateException e) {
- Logger.w(TAG, "Failed to draw the frame", e);
- }
- }
- /**
- * Draws the input texture and overlay to a Bitmap.
- *
- * @param inputTextureTarget the texture target of the input texture. This could be either
- * GLES11Ext.GL_TEXTURE_EXTERNAL_OES or GLES20.GL_TEXTURE_2D,
- * depending if copying from an external texture or a 2D texture.
- * @param inputTextureId the texture id of the input texture. This could be either an
- * external texture or a 2D texture.
- * @param overlayTextureId the texture id of the overlay texture. This must be a 2D texture.
- * @param width the width of the output bitmap.
- * @param height the height of the output bitmap.
- * @param matrix the texture transformation matrix.
- */
- @NonNull
- Bitmap snapshot(int inputTextureTarget, int inputTextureId, int overlayTextureId, int width,
- int height, @NonNull float[] matrix) {
- use();
- // Allocate buffer.
- ByteBuffer byteBuffer = ByteBuffer.allocateDirect(width * height * SNAPSHOT_PIXEL_STRIDE);
- // Take a snapshot.
- snapshot(inputTextureTarget, inputTextureId, overlayTextureId, width, height,
- matrix, byteBuffer);
- // Create a Bitmap and copy the bytes over.
- Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- byteBuffer.rewind();
- copyByteBufferToBitmap(bitmap, byteBuffer, width * SNAPSHOT_PIXEL_STRIDE);
- return bitmap;
- }
-
- /**
- * Draws the input texture and overlay to a FBO and download the bytes to the given ByteBuffer.
- */
- private void snapshot(int inputTextureTarget,
- int inputTextureId, int overlayTextureId, int width,
- int height, @NonNull float[] textureTransform, @NonNull ByteBuffer byteBuffer) {
- checkArgument(byteBuffer.capacity() == width * height * 4,
- "ByteBuffer capacity is not equal to width * height * 4.");
- checkArgument(byteBuffer.isDirect(), "ByteBuffer is not direct.");
-
- // Create a FBO as the drawing target.
- int fbo = createFbo();
- GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fbo);
- checkGlErrorOrThrow("glBindFramebuffer");
- // Create the texture behind the FBO
- int textureId = createTextureId();
- configureTexture2D(textureId);
- GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGB, width,
- height, 0, GLES20.GL_RGB, GLES20.GL_UNSIGNED_BYTE, null);
- checkGlErrorOrThrow("glTexImage2D");
- // Attach the texture to the FBO
- GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
- GLES20.GL_TEXTURE_2D, textureId, 0);
- checkGlErrorOrThrow("glFramebufferTexture2D");
-
- // Draw
- uploadParameters(inputTextureTarget, inputTextureId, overlayTextureId, textureTransform);
- drawArrays(width, height);
-
- // Download the pixels from the FBO
- GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE,
- byteBuffer);
- checkGlErrorOrThrow("glReadPixels");
-
- // Clean up
- GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
- checkGlErrorOrThrow("glBindFramebuffer");
- GLES20.glDeleteTextures(1, new int[]{textureId}, 0);
- checkGlErrorOrThrow("glDeleteTextures");
- GLES20.glDeleteFramebuffers(1, new int[]{fbo}, 0);
- checkGlErrorOrThrow("glDeleteFramebuffers");
- }
-
- /**
- * Uploads the parameters to the shader.
- */
- private void uploadParameters(int inputTextureTarget, int inputTextureId, int overlayTextureId,
- @NonNull float[] matrix) {
// Uploads the texture transformation matrix.
GLES20.glUniformMatrix4fv(mTextureMatrixLoc, 1, false, matrix, 0);
checkGlErrorOrThrow("glUniformMatrix4fv");
@@ -218,5 +123,11 @@
GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, overlayTextureId);
checkGlErrorOrThrow("glBindTexture");
+
+ try {
+ glContext.drawAndSwap(surface, timestampNs);
+ } catch (IllegalStateException e) {
+ Logger.w(TAG, "Failed to draw the frame", e);
+ }
}
}
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlRenderer.java b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlRenderer.java
index 8046e43f..50488dc 100644
--- a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlRenderer.java
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/GlRenderer.java
@@ -235,7 +235,6 @@
*/
public void renderQueueTextureToSurface(int textureId, long timestampNs,
@NonNull float[] textureTransform, @NonNull Surface surface) {
- checkGlThreadAndInitialized();
mGlProgramOverlay.draw(GLES20.GL_TEXTURE_2D, textureId, mOverlayTextureId,
textureTransform, mGlContext, surface, timestampNs);
}
@@ -246,31 +245,9 @@
* <p>The texture ID must be from the latest return value of{@link #createBufferTextureIds}.
*/
public void renderInputToQueueTexture(int textureId) {
- checkGlThreadAndInitialized();
mGlProgramCopy.draw(mInputTextureId, textureId, mQueueTextureWidth, mQueueTextureHeight);
}
- /**
- * Renders a queued texture to a Bitmap and returns.
- */
- @NonNull
- public Bitmap renderQueueTextureToBitmap(int textureId, int width, int height,
- @NonNull float[] textureTransform) {
- checkGlThreadAndInitialized();
- return mGlProgramOverlay.snapshot(GLES20.GL_TEXTURE_2D, textureId, mOverlayTextureId,
- width, height, textureTransform);
- }
-
- /**
- * Renders the input texture to a Bitmap and returns.
- */
- @NonNull
- public Bitmap renderInputToBitmap(int width, int height, @NonNull float[] textureTransform) {
- checkGlThreadAndInitialized();
- return mGlProgramOverlay.snapshot(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mInputTextureId,
- mOverlayTextureId, width, height, textureTransform);
- }
-
// --- Private methods ---
private void checkGlThreadAndInitialized() {
diff --git a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/Utils.java b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/Utils.java
index 7c28bb1..e8f2d49 100644
--- a/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/Utils.java
+++ b/camera/camera-effects/src/main/java/androidx/camera/effects/opengl/Utils.java
@@ -100,16 +100,6 @@
}
/**
- * Creates a single FBO.
- */
- static int createFbo() {
- int[] fbos = new int[1];
- GLES20.glGenFramebuffers(1, fbos, 0);
- checkGlErrorOrThrow("glGenFramebuffers");
- return fbos[0];
- }
-
- /**
* Configures the texture as a 2D texture.
*/
static void configureTexture2D(int textureId) {
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 f3f56b7..056e784 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
@@ -281,7 +281,8 @@
}
@Override
- public boolean isUseCasesCombinationSupported(@NonNull UseCase... useCases) {
- return mCameraUseCaseAdapter.isUseCasesCombinationSupported(useCases);
+ public boolean isUseCasesCombinationSupported(boolean withStreamSharing,
+ @NonNull UseCase... useCases) {
+ return mCameraUseCaseAdapter.isUseCasesCombinationSupported(withStreamSharing, useCases);
}
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfig.java
index b8f0e05..1d9801a1 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfig.java
@@ -24,10 +24,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.core.CameraSelector;
+import androidx.camera.core.DynamicRange;
import androidx.camera.core.MirrorMode;
import androidx.camera.core.UseCase;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.ImageInputConfig;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.MutableConfig;
import androidx.camera.core.impl.MutableOptionsBundle;
@@ -72,7 +74,8 @@
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public static final class Builder implements
UseCaseConfig.Builder<FakeUseCase, FakeUseCaseConfig, FakeUseCaseConfig.Builder>,
- ImageOutputConfig.Builder<FakeUseCaseConfig.Builder> {
+ ImageOutputConfig.Builder<FakeUseCaseConfig.Builder>,
+ ImageInputConfig.Builder<FakeUseCaseConfig.Builder> {
private final MutableOptionsBundle mOptionsBundle;
@@ -166,6 +169,14 @@
@Override
@NonNull
+ public Builder setDynamicRange(
+ @NonNull DynamicRange dynamicRange) {
+ getMutableConfig().insertOption(OPTION_INPUT_DYNAMIC_RANGE, dynamicRange);
+ return this;
+ }
+
+ @Override
+ @NonNull
public Builder setCaptureOptionUnpacker(
@NonNull CaptureConfig.OptionUnpacker optionUnpacker) {
getMutableConfig().insertOption(OPTION_CAPTURE_CONFIG_UNPACKER, optionUnpacker);
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt
index 4cf082c..06f118d 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt
@@ -26,7 +26,6 @@
import androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA
import androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA
import androidx.camera.core.CameraXConfig
-import androidx.camera.core.DynamicRange
import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy
import androidx.camera.testing.impl.CameraPipeConfigTestRule
import androidx.camera.testing.impl.CameraUtil
@@ -66,8 +65,6 @@
) {
private val context: Context = ApplicationProvider.getApplicationContext()
- // TODO(b/278168212): Only SDR is checked by now. Need to extend to HDR dynamic ranges.
- private val dynamicRange = DynamicRange.SDR
private val zeroRange by lazy { android.util.Range.create(0, 0) }
@get:Rule
@@ -149,10 +146,14 @@
if (!CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!)) {
return emptyList()
}
+
val cameraInfo = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector).cameraInfo
val videoCapabilities = Recorder.getVideoCapabilities(cameraInfo)
- return videoCapabilities.getSupportedQualities(dynamicRange).mapNotNull { quality ->
- videoCapabilities.getProfiles(quality, dynamicRange)
+
+ return videoCapabilities.supportedDynamicRanges.flatMap { dynamicRange ->
+ videoCapabilities.getSupportedQualities(dynamicRange).map { quality ->
+ videoCapabilities.getProfiles(quality, dynamicRange)!!
+ }
}
}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/BackupHdrProfileEncoderProfilesProviderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/BackupHdrProfileEncoderProfilesProviderTest.kt
index ba20e24..8c2fee1 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/BackupHdrProfileEncoderProfilesProviderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/BackupHdrProfileEncoderProfilesProviderTest.kt
@@ -35,6 +35,8 @@
import androidx.camera.testing.impl.CameraXUtil
import androidx.camera.testing.impl.LabTestRule
import androidx.camera.video.internal.BackupHdrProfileEncoderProfilesProvider.DEFAULT_VALIDATOR
+import androidx.camera.video.internal.compat.quirk.DeviceQuirks
+import androidx.camera.video.internal.compat.quirk.MediaCodecInfoReportIncorrectInfoQuirk
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
@@ -42,6 +44,7 @@
import com.google.common.truth.Truth.assertWithMessage
import java.util.concurrent.TimeUnit
import org.junit.After
+import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
@@ -71,21 +74,14 @@
val labTest: LabTestRule = LabTestRule()
companion object {
+
+ // Reference to the available values listed in Quality.
@JvmStatic
private val qualities = arrayOf(
- CamcorderProfile.QUALITY_LOW,
- CamcorderProfile.QUALITY_HIGH,
- CamcorderProfile.QUALITY_QCIF,
- CamcorderProfile.QUALITY_CIF,
CamcorderProfile.QUALITY_480P,
CamcorderProfile.QUALITY_720P,
CamcorderProfile.QUALITY_1080P,
- CamcorderProfile.QUALITY_QVGA,
CamcorderProfile.QUALITY_2160P,
- CamcorderProfile.QUALITY_VGA,
- CamcorderProfile.QUALITY_4KDCI,
- CamcorderProfile.QUALITY_QHD,
- CamcorderProfile.QUALITY_2K,
)
@JvmStatic
@@ -115,6 +111,10 @@
}
}
}
+
+ private fun hasMediaCodecIncorrectInfoQuirk(): Boolean {
+ return DeviceQuirks.get(MediaCodecInfoReportIncorrectInfoQuirk::class.java) != null
+ }
}
private val context: Context = ApplicationProvider.getApplicationContext()
@@ -151,6 +151,7 @@
@Test
fun defaultValidator_returnNonNull_whenProfileIsFromCamcorder() {
// Arrange.
+ assumeFalse(hasMediaCodecIncorrectInfoQuirk())
assumeTrue(baseProvider.hasProfile(quality))
val encoderProfiles = baseProvider.getAll(quality)
val baseVideoProfile = encoderProfiles!!.videoProfiles[0]
@@ -170,6 +171,7 @@
assumeTrue(cameraInfo.supportedDynamicRanges.containsAll(setOf(SDR, HLG_10_BIT)))
// Arrange.
+ assumeFalse(hasMediaCodecIncorrectInfoQuirk())
assumeTrue(baseProvider.hasProfile(quality))
val baseVideoProfilesSize = baseProvider.getAll(quality)!!.videoProfiles.size
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/BackupHdrProfileEncoderProfilesProvider.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/BackupHdrProfileEncoderProfilesProvider.java
index 0d86c40..8f28109 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/BackupHdrProfileEncoderProfilesProvider.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/BackupHdrProfileEncoderProfilesProvider.java
@@ -269,6 +269,10 @@
VideoEncoderConfig videoEncoderConfig = toVideoEncoderConfig(profile);
try {
VideoEncoderInfo videoEncoderInfo = VideoEncoderInfoImpl.from(videoEncoderConfig);
+ if (!videoEncoderInfo.isSizeSupported(profile.getWidth(), profile.getHeight())) {
+ return null;
+ }
+
int baseBitrate = videoEncoderConfig.getBitrate();
int newBitrate = videoEncoderInfo.getSupportedBitrateRange().clamp(baseBitrate);
return newBitrate == baseBitrate ? profile : modifyBitrate(profile, newBitrate);
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt
index 9633b80..cd7c3c9 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt
@@ -111,7 +111,7 @@
}
// Creates the Preview with the CameraCaptureSessionStateMonitor to monitor whether the
- // event callbacks are called.
+ // session callbacks are called.
preview = createPreviewWithSessionStateMonitor(implName, sessionStateMonitor)
withContext(Dispatchers.Main) {
@@ -133,7 +133,7 @@
@Test
@RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
fun openCloseCaptureSessionStressTest_withPreviewImageCapture(): Unit = runBlocking {
- bindUseCase_unbindAll_toCheckCameraEvent_repeatedly(preview, imageCapture)
+ bindUseCase_unbindAll_toCheckCameraSession_repeatedly(preview, imageCapture)
}
@LabTestRule.LabTestOnly
@@ -143,7 +143,7 @@
runBlocking {
val imageAnalysis = ImageAnalysis.Builder().build()
assumeTrue(camera.isUseCasesCombinationSupported(preview, imageCapture, imageAnalysis))
- bindUseCase_unbindAll_toCheckCameraEvent_repeatedly(
+ bindUseCase_unbindAll_toCheckCameraSession_repeatedly(
preview,
imageCapture,
imageAnalysis = imageAnalysis
@@ -156,7 +156,7 @@
fun openCloseCaptureSessionStressTest_withPreviewVideoCapture(): Unit =
runBlocking {
val videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
- bindUseCase_unbindAll_toCheckCameraEvent_repeatedly(
+ bindUseCase_unbindAll_toCheckCameraSession_repeatedly(
preview,
videoCapture = videoCapture
)
@@ -169,7 +169,7 @@
runBlocking {
val videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
assumeTrue(camera.isUseCasesCombinationSupported(preview, videoCapture, imageCapture))
- bindUseCase_unbindAll_toCheckCameraEvent_repeatedly(
+ bindUseCase_unbindAll_toCheckCameraSession_repeatedly(
preview,
videoCapture = videoCapture,
imageCapture = imageCapture
@@ -184,7 +184,7 @@
val videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
val imageAnalysis = ImageAnalysis.Builder().build()
assumeTrue(camera.isUseCasesCombinationSupported(preview, videoCapture, imageAnalysis))
- bindUseCase_unbindAll_toCheckCameraEvent_repeatedly(
+ bindUseCase_unbindAll_toCheckCameraSession_repeatedly(
preview,
videoCapture = videoCapture,
imageAnalysis = imageAnalysis
@@ -193,12 +193,12 @@
/**
* Repeatedly binds use cases, unbind all to check whether the capture session can be opened
- * and closed successfully by monitoring the CameraEvent callbacks.
+ * and closed successfully by monitoring the camera session callbacks.
*
* <p>This function checks the nullabilities of the input ImageCapture, VideoCapture and
* ImageAnalysis to determine whether the use cases will be bound together to run the test.
*/
- private fun bindUseCase_unbindAll_toCheckCameraEvent_repeatedly(
+ private fun bindUseCase_unbindAll_toCheckCameraSession_repeatedly(
preview: Preview,
imageCapture: ImageCapture? = null,
videoCapture: VideoCapture<Recorder>? = null,
@@ -206,7 +206,7 @@
repeatCount: Int = STRESS_TEST_OPERATION_REPEAT_COUNT
): Unit = runBlocking {
for (i in 1..repeatCount) {
- // Arrange: resets the camera event monitor
+ // Arrange: resets the camera monitor
sessionStateMonitor.reset()
withContext(Dispatchers.Main) {
@@ -268,7 +268,7 @@
}
/**
- * An implementation of CameraCaptureSession.StateCallback to monitor whether the event
+ * An implementation of CameraCaptureSession.StateCallback to monitor whether the session
* callbacks are called properly or not.
*/
private class CameraCaptureSessionStateMonitor : StateCallback() {
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCaptureSessionStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCaptureSessionStressTest.kt
index 4ee8dc0..52db202 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCaptureSessionStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCaptureSessionStressTest.kt
@@ -28,7 +28,6 @@
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
import androidx.camera.core.UseCase
-import androidx.camera.extensions.ExtensionMode
import androidx.camera.extensions.ExtensionsManager
import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil
import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
@@ -177,7 +176,7 @@
@Test
fun openCloseCaptureSessionStressTest_withPreviewImageCapture(): Unit = runBlocking {
- bindUseCase_unbindAll_toCheckCameraEvent_repeatedly(preview, imageCapture)
+ bindUseCase_unbindAll_toCheckCameraSession_repeatedly(preview, imageCapture)
}
@Test
@@ -185,7 +184,7 @@
runBlocking {
val imageAnalysis = ImageAnalysis.Builder().build()
assumeTrue(camera.isUseCasesCombinationSupported(preview, imageCapture, imageAnalysis))
- bindUseCase_unbindAll_toCheckCameraEvent_repeatedly(
+ bindUseCase_unbindAll_toCheckCameraSession_repeatedly(
preview,
imageCapture,
imageAnalysis
@@ -196,12 +195,12 @@
* Repeatedly binds use cases, unbind all to check whether the capture session can be opened
* and closed successfully by monitoring the camera session state.
*/
- private fun bindUseCase_unbindAll_toCheckCameraEvent_repeatedly(
+ private fun bindUseCase_unbindAll_toCheckCameraSession_repeatedly(
vararg useCases: UseCase,
repeatCount: Int = CameraXExtensionsTestUtil.getStressTestRepeatingCount()
): Unit = runBlocking {
for (i in 1..repeatCount) {
- // Arrange: resets the camera event monitor
+ // Arrange: resets the camera session monitor
cameraSessionMonitor.reset()
withContext(Dispatchers.Main) {
@@ -234,32 +233,10 @@
@get:Parameterized.Parameters(name = "config = {0}")
val parameters: Collection<CameraIdExtensionModePair>
get() = CameraXExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
-
- /**
- * Retrieves the default extended camera config provider id string
- */
- private fun getExtendedCameraConfigProviderId(@ExtensionMode.Mode mode: Int): String =
- when (mode) {
- ExtensionMode.BOKEH -> "EXTENSION_MODE_BOKEH"
- ExtensionMode.HDR -> "EXTENSION_MODE_HDR"
- ExtensionMode.NIGHT -> "EXTENSION_MODE_NIGHT"
- ExtensionMode.FACE_RETOUCH -> "EXTENSION_MODE_FACE_RETOUCH"
- ExtensionMode.AUTO -> "EXTENSION_MODE_AUTO"
- else -> throw IllegalArgumentException("Invalid extension mode!")
- }.let {
- return ":camera:camera-extensions-$it"
- }
-
- /**
- * Retrieves the camera event monitor extended camera config provider id string
- */
- private fun getCameraEventMonitorCameraConfigProviderId(
- @ExtensionMode.Mode mode: Int
- ): String = "${getExtendedCameraConfigProviderId(mode)}-camera-event-monitor"
}
/**
- * An implementation of CameraEventCallback to monitor whether the camera is closed or opened.
+ * To monitor whether the camera is closed or opened.
*/
private class CameraSessionMonitor {
private var sessionEnabledLatch = CountDownLatch(1)
diff --git a/camera/integration-tests/viewtestapp/build.gradle b/camera/integration-tests/viewtestapp/build.gradle
index d8b1deb..1c44751 100644
--- a/camera/integration-tests/viewtestapp/build.gradle
+++ b/camera/integration-tests/viewtestapp/build.gradle
@@ -70,6 +70,7 @@
// Outside of androidx this is resolved via constraint added to lifecycle-common,
// but it doesn't work in androidx.
implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
+ api(libs.constraintLayout)
compileOnly(libs.kotlinCompiler)
// Lifecycle and LiveData
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt
index 2b0d6f6..a593ec0 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt
@@ -77,6 +77,7 @@
private var cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
private var camera: Camera? = null
private var previewViewMode: ImplementationMode = ImplementationMode.PERFORMANCE
+ private var previewViewScaleType = PreviewView.ScaleType.FILL_CENTER;
private var activeRecording: Recording? = null
private var isUseCasesBound: Boolean = false
private var deviceOrientation: Int = -1
@@ -102,6 +103,7 @@
// Initial view objects.
previewView = findViewById(R.id.preview_view)
+ previewView.scaleType = previewViewScaleType
previewView.implementationMode = previewViewMode
exportButton = findViewById(R.id.export_button)
exportButton.setOnClickListener {
@@ -212,9 +214,10 @@
return null
}
+ @SuppressLint("RestrictedApi")
private fun isStreamSharingEnabled(): Boolean {
val isCombinationSupported =
- camera != null && camera!!.isUseCasesCombinationSupported(*useCases)
+ camera != null && camera!!.isUseCasesCombinationSupportedByFramework(*useCases)
return !isCombinationSupported && isUseCasesBound
}
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout/activity_stream_sharing.xml b/camera/integration-tests/viewtestapp/src/main/res/layout/activity_stream_sharing.xml
index d97b32a..8a2037d 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/layout/activity_stream_sharing.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout/activity_stream_sharing.xml
@@ -14,46 +14,46 @@
limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layout_camera"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical">
+ android:orientation="vertical"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
- <FrameLayout
- android:id="@+id/container"
+ <androidx.camera.view.PreviewView
+ android:id="@+id/preview_view"
android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1">
-
- <androidx.camera.view.PreviewView
- android:id="@+id/preview_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
- </FrameLayout>
+ android:layout_height="match_parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
+ android:id="@+id/controller"
android:layout_width="wrap_content"
- android:layout_height="wrap_content">
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent">
- <LinearLayout
- android:id="@+id/controller"
- android:layout_width="wrap_content"
+ <Button
+ android:id="@+id/export_button"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="horizontal">
+ android:text="@string/btn_export" />
- <Button
- android:id="@+id/export_button"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/btn_export" />
-
- <Button
- android:id="@+id/record_button"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:enabled="false"
- android:text="@string/btn_video_record" />
- </LinearLayout>
+ <Button
+ android:id="@+id/record_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:enabled="false"
+ android:text="@string/btn_video_record" />
</LinearLayout>
-</LinearLayout>
\ No newline at end of file
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
index bc21855..0053e36 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
@@ -413,7 +413,12 @@
): V = if (playTimeNanos + initialOffsetNanos > durationNanos) {
// Start velocity of the 2nd and subsequent iteration will be the velocity at the end
// of the first iteration, instead of the initial velocity.
- getVelocityFromNanos(durationNanos - initialOffsetNanos, start, startVelocity, end)
+ animation.getVelocityFromNanos(
+ playTimeNanos = durationNanos - initialOffsetNanos,
+ initialValue = start,
+ targetValue = end,
+ initialVelocity = startVelocity
+ )
} else {
startVelocity
}
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimationTest.kt b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimationTest.kt
index a632f65..a9f239f 100644
--- a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimationTest.kt
+++ b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimationTest.kt
@@ -335,6 +335,53 @@
)
}
+ @Test
+ fun testVectorizedInfiniteRepeatableSpec_velocityOnRepetitions() {
+ val repeatableSpec = VectorizedInfiniteRepeatableSpec(
+ animation = VectorizedAverageVelocitySpec(durationMillis = 1000),
+ repeatMode = RepeatMode.Restart,
+ )
+ val playTimeNanosA = 0L
+ val playTimeNanosB = 1_000L * 1_000_000 - 1
+ val playTimeNanosC = 1_000L * 1_000_000 + 1
+
+ val vectorStart = AnimationVector(0f)
+ val vectorEnd = AnimationVector(3f)
+ val vectorV0 = AnimationVector(0f)
+
+ val velocityAtA = repeatableSpec.getVelocityFromNanos(
+ playTimeNanos = playTimeNanosA,
+ initialValue = vectorStart,
+ targetValue = vectorEnd,
+ initialVelocity = vectorV0
+ )
+
+ val velocityAtB = repeatableSpec.getVelocityFromNanos(
+ playTimeNanos = playTimeNanosB,
+ initialValue = vectorStart,
+ targetValue = vectorEnd,
+ initialVelocity = vectorV0
+ )
+
+ val velocityAC = repeatableSpec.getVelocityFromNanos(
+ playTimeNanos = playTimeNanosC,
+ initialValue = vectorStart,
+ targetValue = vectorEnd,
+ initialVelocity = vectorV0
+ )
+
+ assertEquals(vectorV0, velocityAtA)
+
+ // Final velocity will be the final velocity from the average of: [0, X] = 3 pixels/second
+ // In other words: 6 pixels/second, or `vectorEnd[0] * 2f`
+ // There will be a minor difference since we are measuring one nanosecond before the end
+ assertEquals(vectorEnd[0] * 2f, velocityAtB[0], 0.01f)
+
+ // Final velocity of "B" carries over to initial velocity of "C"
+ // There will be a minor difference since we are measuring 2 nanoseconds between each other
+ assertEquals(velocityAtB[0], velocityAC[0], 0.01f)
+ }
+
private fun verifyAnimation(
anim: VectorizedAnimationSpec<AnimationVector4D>,
start: AnimationVector4D,
@@ -364,4 +411,64 @@
fixedAnim.durationMillis
)
}
+
+ /**
+ * [VectorizedDurationBasedAnimationSpec] that promises to maintain the same average velocity
+ * based on target/initial value and duration.
+ *
+ * This means that the instantaneous velocity will also depend on the initial velocity.
+ */
+ private class VectorizedAverageVelocitySpec<V : AnimationVector>(
+ override val durationMillis: Int
+ ) : VectorizedDurationBasedAnimationSpec<V> {
+ private val durationSeconds = durationMillis.toFloat() / 1_000
+ override val delayMillis: Int = 0
+
+ override fun getValueFromNanos(
+ playTimeNanos: Long,
+ initialValue: V,
+ targetValue: V,
+ initialVelocity: V
+ ): V {
+ val playTimeSeconds = (playTimeNanos / 1_000_000).toFloat() / 1_000
+ val velocity = getVelocityFromNanos(
+ playTimeNanos = playTimeNanos,
+ initialValue = initialValue,
+ targetValue = targetValue,
+ initialVelocity = initialVelocity
+ )
+ val valueVector = initialValue.newInstance()
+ for (i in 0 until velocity.size) {
+ valueVector[i] = velocity[i] * playTimeSeconds
+ }
+ return valueVector
+ }
+
+ override fun getVelocityFromNanos(
+ playTimeNanos: Long,
+ initialValue: V,
+ targetValue: V,
+ initialVelocity: V
+ ): V {
+ val playTimeSeconds = (playTimeNanos / 1_000_000).toFloat() / 1_000
+ val averageVelocity = initialVelocity.newInstance()
+ for (i in 0 until averageVelocity.size) {
+ averageVelocity[i] = (targetValue[i] - initialValue[i]) / durationSeconds
+ }
+ val finalVelocity = initialVelocity.newInstance()
+ for (i in 0 until averageVelocity.size) {
+ finalVelocity[i] = averageVelocity[i] * 2 - initialVelocity[i]
+ }
+ val velocityVector = initialVelocity.newInstance()
+
+ for (i in 0 until averageVelocity.size) {
+ velocityVector[i] = lerp(
+ start = initialVelocity[i],
+ stop = finalVelocity[i],
+ fraction = playTimeSeconds / durationSeconds
+ )
+ }
+ return velocityVector
+ }
+ }
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
index aafa9e2..5210b23 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
@@ -220,7 +220,7 @@
companion object {
fun checkCompilerVersion(configuration: CompilerConfiguration): Boolean {
try {
- val KOTLIN_VERSION_EXPECTATION = "1.9.0"
+ val KOTLIN_VERSION_EXPECTATION = "1.9.10"
KotlinCompilerVersion.getVersion()?.let { version ->
val msgCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
val suppressKotlinVersionCheck = configuration.get(
diff --git a/compose/material/material-icons-extended-outlined/build.gradle b/compose/material/material-icons-extended-outlined/build.gradle
deleted file mode 100644
index 5933def..0000000
--- a/compose/material/material-icons-extended-outlined/build.gradle
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * 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.
- */
-
-apply from: "../material-icons-extended/generate.gradle"
-
-android {
- namespace "androidx.compose.material.icons.extended"
-}
diff --git a/compose/material/material-icons-extended-rounded/build.gradle b/compose/material/material-icons-extended-rounded/build.gradle
deleted file mode 100644
index 5933def..0000000
--- a/compose/material/material-icons-extended-rounded/build.gradle
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * 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.
- */
-
-apply from: "../material-icons-extended/generate.gradle"
-
-android {
- namespace "androidx.compose.material.icons.extended"
-}
diff --git a/compose/material/material-icons-extended-sharp/build.gradle b/compose/material/material-icons-extended-sharp/build.gradle
deleted file mode 100644
index 5933def..0000000
--- a/compose/material/material-icons-extended-sharp/build.gradle
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * 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.
- */
-
-apply from: "../material-icons-extended/generate.gradle"
-
-android {
- namespace "androidx.compose.material.icons.extended"
-}
diff --git a/compose/material/material-icons-extended-twotone/build.gradle b/compose/material/material-icons-extended-twotone/build.gradle
deleted file mode 100644
index 5933def..0000000
--- a/compose/material/material-icons-extended-twotone/build.gradle
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * 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.
- */
-
-apply from: "../material-icons-extended/generate.gradle"
-
-android {
- namespace "androidx.compose.material.icons.extended"
-}
diff --git a/compose/material/material-icons-extended/README.md b/compose/material/material-icons-extended/README.md
deleted file mode 100644
index 21d3209..0000000
--- a/compose/material/material-icons-extended/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
-This project provides the Compose Material Design extended icons
-
-To keep Kotlin compilation times down, each theme is compiled in its own Gradle project and then the resulting .class files are merged back into the output of this project
-
-Hopefully we can revert this when parallel compilation is supported:
-https://youtrack.jetbrains.com/issue/KT-46085
-
-See https://issuetracker.google.com/issues/178207305 and https://issuetracker.google.com/issues/184959797 for more information
diff --git a/compose/material/material-icons-extended/build.gradle b/compose/material/material-icons-extended/build.gradle
index ddff520..e6002b6 100644
--- a/compose/material/material-icons-extended/build.gradle
+++ b/compose/material/material-icons-extended/build.gradle
@@ -33,8 +33,6 @@
android
)
-apply from: "shared-dependencies.gradle"
-
androidXMultiplatform {
android()
if (desktopEnabled) desktop()
@@ -126,22 +124,6 @@
}
}
-configurations {
- embedThemesDebug {
- attributes {
- attribute(iconExportAttr, "true")
- attribute(iconBuildTypeAttr, "debug")
- }
- }
- embedThemesRelease {
- attributes {
- attribute(iconExportAttr, "true")
- attribute(iconBuildTypeAttr, "release")
- }
- }
-
-}
-
IconGenerationTask.registerExtendedIconThemeProject(project, android)
androidx {
diff --git a/compose/material/material-icons-extended/generate.gradle b/compose/material/material-icons-extended/generate.gradle
deleted file mode 100644
index eb45afa..0000000
--- a/compose/material/material-icons-extended/generate.gradle
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// This file contains logic used for compiling the individual themes of material-icons-extended
-
-import androidx.build.AndroidXComposePlugin
-import androidx.build.Publish
-import androidx.build.RunApiTasks
-import androidx.compose.material.icons.generator.tasks.IconGenerationTask
-
-apply plugin: "AndroidXPlugin"
-apply plugin: "com.android.library"
-apply plugin: "AndroidXComposePlugin"
-
-apply from: "${buildscript.sourceFile.parentFile}/shared-dependencies.gradle"
-
-IconGenerationTask.registerExtendedIconThemeProject(project, android)
-
-dependencies.attributesSchema {
- attribute(iconExportAttr)
- attribute(iconBuildTypeAttr)
-}
-
-configurations {
- def jarsDir = "${buildDir}/intermediates/aar_main_jar"
- iconExportDebug {
- attributes {
- attribute(iconExportAttr, "true")
- attribute(iconBuildTypeAttr, "debug")
- }
- outgoing.artifact(new File("${jarsDir}/debug/classes.jar")) {
- builtBy("syncDebugLibJars")
- }
- }
- iconExportRelease {
- attributes {
- attribute(iconExportAttr, "true")
- attribute(iconBuildTypeAttr, "release")
- }
- outgoing.artifact(new File("${jarsDir}/release/classes.jar")) {
- builtBy("syncReleaseLibJars")
- }
- }
-}
-
-androidx {
- name = "Compose Material Icons Extended"
- publish = Publish.NONE // actually embedded into the main aar rather than published separately
- // This module has a large number (1000+) of generated source files and so doc generation /
- // API tracking will simply take too long
- runApiTasks = new RunApiTasks.No("A thousand generated source files")
- inceptionYear = "2020"
- description = "Compose Material Design extended icons. This module contains material icons of the corresponding theme. It is a very large dependency and should not be included directly."
-}
diff --git a/compose/material/material-icons-extended/shared-dependencies.gradle b/compose/material/material-icons-extended/shared-dependencies.gradle
deleted file mode 100644
index 2de0ef0..0000000
--- a/compose/material/material-icons-extended/shared-dependencies.gradle
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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.
- */
-
-// This file stores common dependencies that are used both by material-icons-extended and
-// by its specific theme projects (each of which compile a specific theme)
-
-import androidx.build.AndroidXComposePlugin
-import androidx.build.KmpPlatformsKt
-
-def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
-
-androidXMultiplatform {
- android()
- if (desktopEnabled) desktop()
-}
-
-kotlin {
- /*
- * When updating dependencies, make sure to make an analogous update in the
- * corresponding block above
- */
- sourceSets {
- commonMain.dependencies {
- api(project(":compose:material:material-icons-core"))
- implementation(libs.kotlinStdlibCommon)
- implementation(project(":compose:runtime:runtime"))
- }
- }
-}
-
-project.ext.iconExportAttr = Attribute.of("com.androidx.compose.material-icons-extended.Export", String)
-project.ext.iconBuildTypeAttr = Attribute.of("com.androidx.compose.material-icons-extended.BuildType", String)
diff --git a/compose/material/material/icons/README.md b/compose/material/material/icons/README.md
index fbb8a68..a561f99 100644
--- a/compose/material/material/icons/README.md
+++ b/compose/material/material/icons/README.md
@@ -6,7 +6,6 @@
1. The `generator` module, in `generator/` - this module processes and generates Kotlin source files as part of the build step of the other modules. This module is not shipped as an artifact, and caches its outputs based on the input icons (found in `generator/raw-icons`).
2. `material-icons-core` , in `core/` - this module contains _core_ icons, the set of most-commonly-used icons used by applications, including the icons that are required by Material components themselves, such as the menu icon. This module is fairly small and is depended on by `material`.
3. `material-icons-extended`, in `extended/` - this module contains every icon that is not in `material-icons-core`, and has a transitive `api` dependency on `material-icons-core`, so depending on this module will provide every single Material icon (over 5000 at the time of writing). Due to the excessive size of this module, this module should ***NOT*** be included as a direct dependency of any other library, and should only be used if Proguard / R8 is enabled.
- 4. `material-icons-extended-$theme`, in `extended/` - these modules each contain a specific theme from material-icons-extended, to facilitate compiling the icon soure files more quickly in parallel
## Icon Generation
diff --git a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/IconProcessor.kt b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/IconProcessor.kt
index 4eae40a..f7c4b66 100644
--- a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/IconProcessor.kt
+++ b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/IconProcessor.kt
@@ -50,8 +50,7 @@
class IconProcessor(
private val iconDirectories: List<File>,
private val expectedApiFile: File,
- private val generatedApiFile: File,
- private val verifyApi: Boolean = true
+ private val generatedApiFile: File
) {
/**
* @return a list of processed [Icon]s, from the provided [iconDirectories].
@@ -59,11 +58,9 @@
fun process(): List<Icon> {
val icons = loadIcons()
- if (verifyApi) {
- ensureIconsExistInAllThemes(icons)
- writeApiFile(icons, generatedApiFile)
- checkApi(expectedApiFile, generatedApiFile)
- }
+ ensureIconsExistInAllThemes(icons)
+ writeApiFile(icons, generatedApiFile)
+ checkApi(expectedApiFile, generatedApiFile)
return icons
}
diff --git a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt
index f264ab4..fe2a652 100644
--- a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt
+++ b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt
@@ -24,11 +24,9 @@
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.tasks.CacheableTask
-import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
-import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
@@ -54,23 +52,11 @@
project.rootProject.project(GeneratorProject).projectDir.resolve("raw-icons")
/**
- * Specific theme to generate icons for, or null to generate all
- */
- @Optional
- @Input
- var themeName: String? = null
-
- /**
* Specific icon directories to use in this task
*/
@Internal
fun getIconDirectories(): List<File> {
- val themeName = themeName
- if (themeName != null) {
- return listOf(allIconsDirectory.resolve(themeName))
- } else {
- return allIconsDirectory.listFiles()!!.filter { it.isDirectory }
- }
+ return allIconsDirectory.listFiles()!!.filter { it.isDirectory }
}
/**
@@ -102,12 +88,10 @@
// material-icons-core loads and verifies all of the icons from all of the themes:
// both that all icons are present in all themes, and also that no icons have been removed.
// So, when we're loading just one theme, we don't need to verify it
- val verifyApi = themeName == null
return IconProcessor(
getIconDirectories(),
expectedApiFile,
- generatedApiFile,
- verifyApi
+ generatedApiFile
).process()
}
@@ -160,7 +144,9 @@
libraryExtension: LibraryExtension
) {
libraryExtension.libraryVariants.all { variant ->
- ExtendedIconGenerationTask.register(project, variant)
+ if (variant.name == "release") {
+ ExtendedIconGenerationTask.register(project, variant)
+ }
}
// b/175401659 - disable lint as it takes a long time, and most errors should
@@ -213,16 +199,9 @@
): Pair<TaskProvider<T>, File> {
val variantName = variant?.name ?: "allVariants"
- val themeName = if (project.name.contains("material-icons-extended-")) {
- project.name.replace("material-icons-extended-", "")
- } else {
- null
- }
-
val buildDirectory = project.buildDir.resolve("generatedIcons/$variantName")
return tasks.register("$taskName${variantName.capitalize(Locale.getDefault())}", taskClass) {
- it.themeName = themeName
it.buildDirectory = buildDirectory
} to buildDirectory
}
diff --git a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconSourceTasks.kt b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconSourceTasks.kt
index 9e8c3fe..e59eb51 100644
--- a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconSourceTasks.kt
+++ b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconSourceTasks.kt
@@ -44,12 +44,7 @@
CoreIconGenerationTask::class.java,
variant
)
- // Multiplatform
- if (variant == null) {
- registerIconGenerationTask(project, task, buildDirectory)
- }
- // AGP
- else variant.registerIconGenerationTask(project, task, buildDirectory)
+ registerIconGenerationTask(project, task, buildDirectory)
}
}
}
@@ -73,36 +68,7 @@
ExtendedIconGenerationTask::class.java,
variant
)
- // Multiplatform
- if (variant == null) {
- registerIconGenerationTask(project, task, buildDirectory)
- }
- // AGP
- else variant.registerIconGenerationTask(project, task, buildDirectory)
- }
-
- /**
- * Registers the icon generation task just for source jar generation, and not for
- * compilation. This is temporarily needed since we manually parallelize compilation in
- * material-icons-extended for the AGP build. When we remove that parallelization code,
- * we can remove this too.
- */
- @JvmStatic
- @Suppress("DEPRECATION") // BaseVariant
- fun registerSourceJarOnly(
- project: Project,
- variant: com.android.build.gradle.api.BaseVariant
- ) {
- // Setup the source jar task if this is the release variant
- if (variant.name == "release") {
- val (task, buildDirectory) = project.registerGenerationTask(
- "generateExtendedIcons",
- ExtendedIconGenerationTask::class.java,
- variant
- )
- val generatedSrcMainDirectory = buildDirectory.resolve(GeneratedSrcMain)
- project.addToSourceJar(generatedSrcMainDirectory, task)
- }
+ registerIconGenerationTask(project, task, buildDirectory)
}
}
}
@@ -123,42 +89,4 @@
project.tasks.named("multiplatformSourceJar", Jar::class.java).configure {
it.from(task.map { generatedSrcMainDirectory })
}
- project.addToSourceJar(generatedSrcMainDirectory, task)
-}
-
-/**
- * Helper to register [task] as the java source generating task that outputs to [buildDirectory].
- */
-@Suppress("DEPRECATION") // BaseVariant
-private fun com.android.build.gradle.api.BaseVariant.registerIconGenerationTask(
- project: Project,
- task: TaskProvider<*>,
- buildDirectory: File
-) {
- val generatedSrcMainDirectory = buildDirectory.resolve(IconGenerationTask.GeneratedSrcMain)
- registerJavaGeneratingTask(task, generatedSrcMainDirectory)
- // Setup the source jar task if this is the release variant
- if (name == "release") {
- project.addToSourceJar(generatedSrcMainDirectory, task)
- }
-}
-
-/**
- * Adds the contents of [buildDirectory] to the source jar generated for this [Project] by [task]
- */
-// TODO: b/191485164 remove when AGP lets us get generated sources from a TestedExtension or
-// similar, then we can just add generated sources in SourceJarTaskHelper for all projects,
-// instead of needing one-off support here.
-private fun Project.addToSourceJar(buildDirectory: File, task: TaskProvider<*>) {
- afterEvaluate {
- val sourceJar = tasks.named("sourceJarRelease", Jar::class.java)
- sourceJar.configure {
- // Generating source jars requires the generation task to run first. This shouldn't
- // be needed for the MPP build because we use builtBy to set up the dependency
- // (https://github.com/gradle/gradle/issues/17250) but the path is different for AGP,
- // so we will still need this for the AGP build.
- it.dependsOn(task)
- it.from(buildDirectory)
- }
- }
}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/BadgeBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/BadgeBenchmark.kt
new file mode 100644
index 0000000..53bc830
--- /dev/null
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/BadgeBenchmark.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.benchmark
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Badge
+import androidx.compose.material3.BadgedBox
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.junit.Rule
+import org.junit.Test
+
+class BadgeBenchmark {
+ @get:Rule
+ val benchmarkRule = ComposeBenchmarkRule()
+
+ @Test
+ fun BadgeFirstPixel() { benchmarkRule.benchmarkToFirstPixel { BadgeTestCase() } }
+
+ @Test
+ fun BadgedBoxFirstPixel() { benchmarkRule.benchmarkToFirstPixel { BadgedBoxTestCase() } }
+}
+
+private class BadgedBoxTestCase : LayeredComposeTestCase() {
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ override fun MeasuredContent() {
+ BadgedBox(
+ badge = { Spacer(Modifier.size(10.dp)) }
+ ) { Spacer(Modifier.size(24.dp)) }
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme {
+ content()
+ }
+ }
+}
+
+private class BadgeTestCase : LayeredComposeTestCase() {
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ override fun MeasuredContent() { Badge() }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme {
+ content()
+ }
+ }
+}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/CardBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/CardBenchmark.kt
new file mode 100644
index 0000000..d82f319
--- /dev/null
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/CardBenchmark.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.benchmark
+
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Card
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkFirstDraw
+import androidx.compose.testutils.benchmark.benchmarkFirstLayout
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class CardBenchmark {
+
+ @get:Rule
+ val benchmarkRule = ComposeBenchmarkRule()
+
+ private val cardTestCaseFactory = { CardTestCase() }
+ private val clickableCardTestCaseFactory = { ClickableCardTestCase() }
+
+ @Ignore
+ @Test
+ fun first_compose() {
+ benchmarkRule.benchmarkFirstCompose(cardTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun first_measure() {
+ benchmarkRule.benchmarkFirstMeasure(cardTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun first_layout() {
+ benchmarkRule.benchmarkFirstLayout(cardTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun first_draw() {
+ benchmarkRule.benchmarkFirstDraw(cardTestCaseFactory)
+ }
+
+ @Test
+ fun card_firstPixel() {
+ benchmarkRule.benchmarkToFirstPixel(cardTestCaseFactory)
+ }
+
+ @Test
+ fun clickableCard_firstPixel() {
+ benchmarkRule.benchmarkToFirstPixel(clickableCardTestCaseFactory)
+ }
+}
+
+internal class CardTestCase : LayeredComposeTestCase() {
+
+ @Composable
+ override fun MeasuredContent() {
+ Card(modifier = Modifier.size(200.dp)) { }
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme {
+ content()
+ }
+ }
+}
+
+internal class ClickableCardTestCase : LayeredComposeTestCase() {
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ override fun MeasuredContent() {
+ Card(onClick = {}, modifier = Modifier.size(200.dp)) { }
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme {
+ content()
+ }
+ }
+}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/CheckboxBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/CheckboxBenchmark.kt
new file mode 100644
index 0000000..0610c98
--- /dev/null
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/CheckboxBenchmark.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.benchmark
+
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ToggleableTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkFirstDraw
+import androidx.compose.testutils.benchmark.benchmarkFirstLayout
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.compose.testutils.benchmark.toggleStateBenchmarkComposeMeasureLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class CheckboxBenchmark {
+
+ @get:Rule
+ val benchmarkRule = ComposeBenchmarkRule()
+
+ private val checkboxTestCaseFactory = { CheckboxTestCase() }
+
+ @Ignore
+ @Test
+ fun first_compose() {
+ benchmarkRule.benchmarkFirstCompose(checkboxTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun first_measure() {
+ benchmarkRule.benchmarkFirstMeasure(checkboxTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun first_layout() {
+ benchmarkRule.benchmarkFirstLayout(checkboxTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun first_draw() {
+ benchmarkRule.benchmarkFirstDraw(checkboxTestCaseFactory)
+ }
+
+ @Test
+ fun firstPixel() {
+ benchmarkRule.benchmarkToFirstPixel(checkboxTestCaseFactory)
+ }
+
+ @Test
+ fun toggle_recomposeMeasureLayout() {
+ benchmarkRule.toggleStateBenchmarkComposeMeasureLayout(
+ caseFactory = checkboxTestCaseFactory,
+ assertOneRecomposition = false
+ )
+ }
+}
+
+internal class CheckboxTestCase : LayeredComposeTestCase(), ToggleableTestCase {
+
+ private var state by mutableStateOf(false)
+
+ @Composable
+ override fun MeasuredContent() {
+ Checkbox(checked = state, onCheckedChange = null)
+ }
+
+ override fun toggleState() {
+ state = !state
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme {
+ content()
+ }
+ }
+}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/DividerBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/DividerBenchmark.kt
new file mode 100644
index 0000000..adea672
--- /dev/null
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/DividerBenchmark.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.benchmark
+
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.VerticalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkFirstDraw
+import androidx.compose.testutils.benchmark.benchmarkFirstLayout
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class DividerBenchmark {
+
+ @get:Rule
+ val benchmarkRule = ComposeBenchmarkRule()
+
+ private val horizontalDividerTestCaseFactory = { HorizontalDividerTestCase() }
+ private val verticalDividerTestCaseFactory = { VerticalDividerTestCase() }
+
+ @Ignore
+ @Test
+ fun horizontalDivider_first_compose() {
+ benchmarkRule.benchmarkFirstCompose(horizontalDividerTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun verticalDivider_first_compose() {
+ benchmarkRule.benchmarkFirstCompose(verticalDividerTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun horizontalDivider_measure() {
+ benchmarkRule.benchmarkFirstMeasure(horizontalDividerTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun verticalDivider_measure() {
+ benchmarkRule.benchmarkFirstMeasure(verticalDividerTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun horizontalDivider_layout() {
+ benchmarkRule.benchmarkFirstLayout(horizontalDividerTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun verticalDivider_layout() {
+ benchmarkRule.benchmarkFirstLayout(verticalDividerTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun horizontalDivider_draw() {
+ benchmarkRule.benchmarkFirstDraw(horizontalDividerTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun verticalDivider_draw() {
+ benchmarkRule.benchmarkFirstDraw(verticalDividerTestCaseFactory)
+ }
+
+ @Test
+ fun horizontalDivider_firstPixel() {
+ benchmarkRule.benchmarkToFirstPixel(horizontalDividerTestCaseFactory)
+ }
+
+ @Test
+ fun verticalDivider_firstPixel() {
+ benchmarkRule.benchmarkToFirstPixel(verticalDividerTestCaseFactory)
+ }
+}
+
+internal class HorizontalDividerTestCase : LayeredComposeTestCase() {
+
+ @Composable
+ override fun MeasuredContent() {
+ HorizontalDivider()
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme {
+ content()
+ }
+ }
+}
+
+internal class VerticalDividerTestCase : LayeredComposeTestCase() {
+
+ @Composable
+ override fun MeasuredContent() {
+ VerticalDivider()
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme {
+ content()
+ }
+ }
+}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonBenchmark.kt
new file mode 100644
index 0000000..40b4a89
--- /dev/null
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/FloatingActionButtonBenchmark.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.benchmark
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkFirstDraw
+import androidx.compose.testutils.benchmark.benchmarkFirstLayout
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class FloatingActionButtonBenchmark {
+
+ @get:Rule
+ val benchmarkRule = ComposeBenchmarkRule()
+
+ private val fabTestCaseFactory = { FloatingActionButtonTestCase() }
+ private val extendedFabTestCaseFactory = { ExtendedFloatingActionButtonTestCase() }
+
+ @Ignore
+ @Test
+ fun fab_first_compose() {
+ benchmarkRule.benchmarkFirstCompose(fabTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun extendedFab_first_compose() {
+ benchmarkRule.benchmarkFirstCompose(extendedFabTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun fab_measure() {
+ benchmarkRule.benchmarkFirstMeasure(fabTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun extendedFab_measure() {
+ benchmarkRule.benchmarkFirstMeasure(extendedFabTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun fab_layout() {
+ benchmarkRule.benchmarkFirstLayout(fabTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun extendedFab_layout() {
+ benchmarkRule.benchmarkFirstLayout(extendedFabTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun fab_draw() {
+ benchmarkRule.benchmarkFirstDraw(fabTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun extendedFab_draw() {
+ benchmarkRule.benchmarkFirstDraw(extendedFabTestCaseFactory)
+ }
+
+ @Test
+ fun fab_firstPixel() {
+ benchmarkRule.benchmarkToFirstPixel(fabTestCaseFactory)
+ }
+
+ @Test
+ fun extendedFab_firstPixel() {
+ benchmarkRule.benchmarkToFirstPixel(extendedFabTestCaseFactory)
+ }
+}
+
+internal class FloatingActionButtonTestCase : LayeredComposeTestCase() {
+
+ @Composable
+ override fun MeasuredContent() {
+ FloatingActionButton(onClick = { /*TODO*/ }) {
+ Box(modifier = Modifier.size(24.dp))
+ }
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme {
+ content()
+ }
+ }
+}
+
+internal class ExtendedFloatingActionButtonTestCase : LayeredComposeTestCase() {
+
+ @Composable
+ override fun MeasuredContent() {
+ ExtendedFloatingActionButton(
+ text = { Text(text = "Extended FAB") },
+ icon = {
+ Box(modifier = Modifier.size(24.dp))
+ },
+ onClick = { /*TODO*/ })
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme {
+ content()
+ }
+ }
+}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/ListItemBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/ListItemBenchmark.kt
new file mode 100644
index 0000000..7bc00ae
--- /dev/null
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/ListItemBenchmark.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.benchmark
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkFirstDraw
+import androidx.compose.testutils.benchmark.benchmarkFirstLayout
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class ListItemBenchmark {
+
+ @get:Rule
+ val benchmarkRule = ComposeBenchmarkRule()
+
+ private val listItemTestCaseFactory = { ListItemTestCase() }
+
+ @Ignore
+ @Test
+ fun first_compose() {
+ benchmarkRule.benchmarkFirstCompose(listItemTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun listItem_measure() {
+ benchmarkRule.benchmarkFirstMeasure(listItemTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun listItem_layout() {
+ benchmarkRule.benchmarkFirstLayout(listItemTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun listItem_draw() {
+ benchmarkRule.benchmarkFirstDraw(listItemTestCaseFactory)
+ }
+
+ @Test
+ fun firstPixel() {
+ benchmarkRule.benchmarkToFirstPixel(listItemTestCaseFactory)
+ }
+}
+
+internal class ListItemTestCase : LayeredComposeTestCase() {
+
+ @Composable
+ override fun MeasuredContent() {
+ ListItem(
+ headlineContent = { Text(text = "List Item") },
+ overlineContent = { Text(text = "Overline Content") },
+ supportingContent = { Text(text = "Supporting Content") },
+ leadingContent = {
+ Box(modifier = Modifier.size(24.dp))
+ },
+ trailingContent = {
+ Box(modifier = Modifier.size(24.dp))
+ }
+ )
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme {
+ content()
+ }
+ }
+}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationBarBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationBarBenchmark.kt
new file mode 100644
index 0000000..d3e6997
--- /dev/null
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationBarBenchmark.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.benchmark
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class NavigationBarBenchmark {
+ @get:Rule
+ val benchmarkRule = ComposeBenchmarkRule()
+
+ private val testCaseFactory = { NavigationBarTestCase() }
+
+ @Test
+ fun firstPixel() {
+ benchmarkRule.benchmarkFirstRenderUntilStable(testCaseFactory)
+ }
+}
+
+internal class NavigationBarTestCase : LayeredComposeTestCase() {
+ @Composable
+ override fun MeasuredContent() {
+ NavigationBar {
+ NavigationBarItem(
+ selected = true,
+ onClick = {},
+ icon = { Spacer(Modifier.size(24.dp)) },
+ )
+ }
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme {
+ content()
+ }
+ }
+}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationRailBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationRailBenchmark.kt
new file mode 100644
index 0000000..97bb1e1
--- /dev/null
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/NavigationRailBenchmark.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.benchmark
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationRail
+import androidx.compose.material3.NavigationRailItem
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class NavigationRailBenchmark {
+ @get:Rule
+ val benchmarkRule = ComposeBenchmarkRule()
+
+ private val testCaseFactory = { NavigationRailTestCase() }
+
+ @Test
+ fun firstPixel() {
+ benchmarkRule.benchmarkToFirstPixel(testCaseFactory)
+ }
+}
+
+internal class NavigationRailTestCase : LayeredComposeTestCase() {
+ @Composable
+ override fun MeasuredContent() {
+ NavigationRail {
+ NavigationRailItem(
+ selected = true,
+ onClick = {},
+ icon = { Spacer(Modifier.size(24.dp)) },
+ )
+ }
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme {
+ content()
+ }
+ }
+}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/TooltipBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/TooltipBenchmark.kt
new file mode 100644
index 0000000..7dc1a20
--- /dev/null
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/TooltipBenchmark.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.benchmark
+
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.PlainTooltip
+import androidx.compose.material3.RichTooltip
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TooltipBox
+import androidx.compose.material3.TooltipDefaults
+import androidx.compose.material3.TooltipState
+import androidx.compose.material3.rememberTooltipState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ToggleableTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.compose.testutils.benchmark.toggleStateBenchmarkComposeMeasureLayout
+import androidx.compose.ui.window.PopupPositionProvider
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import org.junit.Rule
+import org.junit.Test
+
+class TooltipBenchmark {
+ @get:Rule
+ val benchmarkRule = ComposeBenchmarkRule()
+
+ private val plainTooltipTestCaseFactory = { TooltipTestCase(TooltipType.Plain) }
+ private val richTooltipTestCaseFactory = { TooltipTestCase(TooltipType.Rich) }
+
+ @Test
+ fun plainTooltipFirstPixel() {
+ benchmarkRule.benchmarkToFirstPixel(plainTooltipTestCaseFactory)
+ }
+
+ @Test
+ fun richTooltipFirstPixel() {
+ benchmarkRule.benchmarkToFirstPixel(richTooltipTestCaseFactory)
+ }
+
+ @Test
+ fun plainTooltipVisibilityTest() {
+ benchmarkRule.toggleStateBenchmarkComposeMeasureLayout(
+ caseFactory = plainTooltipTestCaseFactory,
+ assertOneRecomposition = false
+ )
+ }
+
+ @Test
+ fun richTooltipVisibilityTest() {
+ benchmarkRule.toggleStateBenchmarkComposeMeasureLayout(
+ caseFactory = richTooltipTestCaseFactory,
+ assertOneRecomposition = false
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+private class TooltipTestCase(
+ val tooltipType: TooltipType
+) : LayeredComposeTestCase(), ToggleableTestCase {
+ private lateinit var state: TooltipState
+ private lateinit var scope: CoroutineScope
+
+ @Composable
+ override fun MeasuredContent() {
+ state = rememberTooltipState()
+ scope = rememberCoroutineScope()
+
+ val tooltip: @Composable () -> Unit
+ val positionProvider: PopupPositionProvider
+ when (tooltipType) {
+ TooltipType.Plain -> {
+ tooltip = { PlainTooltipTest() }
+ positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider()
+ }
+ TooltipType.Rich -> {
+ tooltip = { RichTooltipTest() }
+ positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider()
+ }
+ }
+
+ TooltipBox(
+ positionProvider = positionProvider,
+ tooltip = tooltip,
+ state = state
+ ) {}
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme {
+ content()
+ }
+ }
+
+ override fun toggleState() {
+ if (state.isVisible) {
+ state.dismiss()
+ } else {
+ scope.launch { state.show() }
+ }
+ }
+
+ @Composable
+ private fun PlainTooltipTest() {
+ PlainTooltip { Text("Text") }
+ }
+
+ @Composable
+ private fun RichTooltipTest() {
+ RichTooltip(
+ title = { Text("Subhead") },
+ action = {
+ TextButton(onClick = {}) {
+ Text(text = "Action")
+ }
+ }
+ ) { Text(text = "Text") }
+ }
+}
+
+private enum class TooltipType {
+ Plain, Rich
+}
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/TopAppBarBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/TopAppBarBenchmark.kt
new file mode 100644
index 0000000..4b66d90c
--- /dev/null
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/TopAppBarBenchmark.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.benchmark
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkFirstDraw
+import androidx.compose.testutils.benchmark.benchmarkFirstLayout
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.compose.ui.Modifier
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+class TopAppBarBenchmark {
+
+ @get:Rule
+ val benchmarkRule = ComposeBenchmarkRule()
+
+ private val topAppBarTestCaseFactory = { TopAppBarTestCase() }
+
+ // Picking the LargeTopAppBar to benchmark a two-row variation.
+ private val largeTopAppBarTestCaseFactory = { LargeTopAppBarTestCase() }
+
+ @Ignore
+ @Test
+ fun first_compose() {
+ benchmarkRule.benchmarkFirstCompose(topAppBarTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun first_measure() {
+ benchmarkRule.benchmarkFirstMeasure(topAppBarTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun first_layout() {
+ benchmarkRule.benchmarkFirstLayout(topAppBarTestCaseFactory)
+ }
+
+ @Ignore
+ @Test
+ fun first_draw() {
+ benchmarkRule.benchmarkFirstDraw(topAppBarTestCaseFactory)
+ }
+
+ @Test
+ fun topAppBar_firstPixel() {
+ benchmarkRule.benchmarkToFirstPixel(topAppBarTestCaseFactory)
+ }
+
+ @Test
+ fun largeTopAppBar_firstPixel() {
+ benchmarkRule.benchmarkToFirstPixel(largeTopAppBarTestCaseFactory)
+ }
+}
+
+internal class TopAppBarTestCase : LayeredComposeTestCase() {
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ override fun MeasuredContent() {
+ // Keeping it to the minimum, with just the necessary title.
+ TopAppBar(title = { Text("Hello") }, modifier = Modifier.fillMaxWidth())
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme {
+ content()
+ }
+ }
+}
+
+internal class LargeTopAppBarTestCase : LayeredComposeTestCase() {
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ override fun MeasuredContent() {
+ // Keeping it to the minimum, with just the necessary title.
+ LargeTopAppBar(title = { Text("Hello") }, modifier = Modifier.fillMaxWidth())
+ }
+
+ @Composable
+ override fun ContentWrappers(content: @Composable () -> Unit) {
+ MaterialTheme {
+ content()
+ }
+ }
+}
diff --git a/compose/material3/material3-adaptive/build.gradle b/compose/material3/material3-adaptive/build.gradle
index 5c97bb2..67f3ad9 100644
--- a/compose/material3/material3-adaptive/build.gradle
+++ b/compose/material3/material3-adaptive/build.gradle
@@ -119,3 +119,10 @@
project.rootDir.absolutePath + "/../../golden/compose/material3/material3-adaptive"
namespace "androidx.compose.material3.adaptive"
}
+
+// b/295947829 createProjectZip mustRunAfter samples createProjectZip. Remove after https://github.com/gradle/gradle/issues/24368 is resolved
+project.tasks.configureEach { task ->
+ if (task.name == "createProjectZip") {
+ task.mustRunAfter(":compose:material3:material3-adaptive:material3-adaptive-samples:createProjectZip")
+ }
+}
diff --git a/compose/material3/material3-adaptive/samples/build.gradle b/compose/material3/material3-adaptive/samples/build.gradle
new file mode 100644
index 0000000..4e99c03
--- /dev/null
+++ b/compose/material3/material3-adaptive/samples/build.gradle
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+
+ implementation(libs.kotlinStdlib)
+
+ compileOnly(project(":annotation:annotation-sampled"))
+
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:foundation:foundation-layout"))
+ implementation(project(":compose:material3:material3"))
+ implementation(project(":compose:material3:material3-adaptive"))
+ implementation(project(":compose:material3:material3-window-size-class"))
+ implementation(project(":compose:ui:ui-util"))
+ implementation("androidx.compose.ui:ui-tooling-preview:1.4.1")
+}
+
+androidx {
+ name = "Compose Material3 Adaptive Samples"
+ type = LibraryType.SAMPLES
+ inceptionYear = "2023"
+ description = "Contains the sample code for the AndroidX Compose Material Adaptive."
+}
+
+android {
+ namespace "androidx.compose.material3.adaptive.samples"
+}
diff --git a/compose/material3/material3-adaptive/samples/src/main/java/androidx/compose/material3-adaptive/samples/NavigationSuiteScaffoldSamples.kt b/compose/material3/material3-adaptive/samples/src/main/java/androidx/compose/material3-adaptive/samples/NavigationSuiteScaffoldSamples.kt
new file mode 100644
index 0000000..51ee44b
--- /dev/null
+++ b/compose/material3/material3-adaptive/samples/src/main/java/androidx/compose/material3-adaptive/samples/NavigationSuiteScaffoldSamples.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.adaptive.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.material3.adaptive.NavigationSuite
+import androidx.compose.material3.adaptive.NavigationSuiteAlignment
+import androidx.compose.material3.adaptive.NavigationSuiteDefaults
+import androidx.compose.material3.adaptive.NavigationSuiteScaffold
+import androidx.compose.material3.adaptive.NavigationSuiteType
+import androidx.compose.material3.adaptive.calculateWindowAdaptiveInfo
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun NavigationSuiteScaffoldSample() {
+ var selectedItem by remember { mutableIntStateOf(0) }
+ val navItems = listOf("Songs", "Artists", "Playlists")
+ val navSuiteType =
+ NavigationSuiteDefaults.calculateFromAdaptiveInfo(calculateWindowAdaptiveInfo())
+
+ NavigationSuiteScaffold(
+ navigationSuite = {
+ NavigationSuite {
+ navItems.forEachIndexed { index, navItem ->
+ item(
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = navItem) },
+ label = { Text(navItem) },
+ selected = selectedItem == index,
+ onClick = { selectedItem = index }
+ )
+ }
+ }
+ }
+ ) {
+ // Screen content.
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = "Current NavigationSuiteType: $navSuiteType"
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun NavigationSuiteScaffoldCustomConfigSample() {
+ var selectedItem by remember { mutableIntStateOf(0) }
+ val navItems = listOf("Songs", "Artists", "Playlists")
+ val adaptiveInfo = calculateWindowAdaptiveInfo()
+ val customNavSuiteType = with(adaptiveInfo) {
+ if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded) {
+ NavigationSuiteType.NavigationDrawer
+ } else if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) {
+ NavigationSuiteType.NavigationRail
+ } else {
+ NavigationSuiteDefaults.calculateFromAdaptiveInfo(adaptiveInfo)
+ }
+ }
+
+ // Custom configuration that shows nav rail on end of screen in small screens, and navigation
+ // drawer in large screens.
+ NavigationSuiteScaffold(
+ navigationSuite = {
+ NavigationSuite(
+ layoutType = customNavSuiteType,
+ modifier = if (customNavSuiteType == NavigationSuiteType.NavigationRail) {
+ Modifier.alignment(NavigationSuiteAlignment.EndVertical)
+ } else {
+ Modifier
+ }
+ ) {
+ navItems.forEachIndexed { index, navItem ->
+ item(
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = navItem) },
+ label = { Text(navItem) },
+ selected = selectedItem == index,
+ onClick = { selectedItem = index }
+ )
+ }
+ }
+ }
+ ) {
+ // Screen content.
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = "Current custom NavigationSuiteType: $customNavSuiteType"
+ )
+ }
+}
diff --git a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffold.kt b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffold.kt
index 9620e34..71a31c5 100644
--- a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffold.kt
+++ b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffold.kt
@@ -65,6 +65,11 @@
* The Navigation Suite Scaffold wraps the provided content and places the adequate provided
* navigation component on the screen according to the current [NavigationSuiteType].
*
+ * Example default usage:
+ * @sample androidx.compose.material3.adaptive.samples.NavigationSuiteScaffoldSample
+ * Example custom configuration usage:
+ * @sample androidx.compose.material3.adaptive.samples.NavigationSuiteScaffoldCustomConfigSample
+ *
* @param navigationSuite the navigation component to be displayed, typically [NavigationSuite]
* @param modifier the [Modifier] to be applied to the navigation suite scaffold
* @param containerColor the color used for the background of the navigation suite scaffold. Use
diff --git a/compose/material3/material3/integration-tests/material3-catalog/build.gradle b/compose/material3/material3/integration-tests/material3-catalog/build.gradle
index 0e8ccb9..c94165a 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/build.gradle
+++ b/compose/material3/material3/integration-tests/material3-catalog/build.gradle
@@ -34,6 +34,7 @@
implementation project(":compose:material:material-icons-extended")
implementation project(":compose:material3:material3")
implementation project(":compose:material3:material3:material3-samples")
+ implementation project(":compose:material3:material3-adaptive:material3-adaptive-samples")
implementation project(":datastore:datastore-preferences")
implementation project(":navigation:navigation-compose")
}
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
index 6745a59..9ab9751 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
@@ -18,6 +18,7 @@
import androidx.annotation.DrawableRes
import androidx.compose.material3.catalog.library.R
+import androidx.compose.material3.catalog.library.util.AdaptiveMaterial3SourceUrl
import androidx.compose.material3.catalog.library.util.ComponentGuidelinesUrl
import androidx.compose.material3.catalog.library.util.DocsUrl
import androidx.compose.material3.catalog.library.util.Material3SourceUrl
@@ -243,6 +244,20 @@
examples = NavigationRailExamples
)
+private val NavigationSuiteScaffold = Component(
+ id = nextId(),
+ name = "Navigation Suite Scaffold",
+ description = "The Navigation Suite Scaffold wraps the provided content and places the " +
+ "adequate provided navigation component on the screen according to the current " +
+ "NavigationSuiteType. \n\n" +
+ "Note: this sample is better experienced in a resizable emulator or foldable device.",
+ // No navigation suite scaffold icon
+ guidelinesUrl = "", // TODO: Add guidelines url when available
+ docsUrl = "", // TODO: Add docs url when available
+ sourceUrl = "$AdaptiveMaterial3SourceUrl/NavigationSuiteScaffold.kt",
+ examples = NavigationSuiteScaffoldExamples
+)
+
private val ProgressIndicators = Component(
id = nextId(),
name = "Progress indicators",
@@ -398,6 +413,7 @@
NavigationBar,
NavigationDrawer,
NavigationRail,
+ NavigationSuiteScaffold,
ProgressIndicators,
RadioButtons,
SearchBars,
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 3fba476..c6d720e 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -21,6 +21,9 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.material3.adaptive.samples.NavigationSuiteScaffoldCustomConfigSample
+import androidx.compose.material3.adaptive.samples.NavigationSuiteScaffoldSample
+import androidx.compose.material3.catalog.library.util.AdaptiveSampleSourceUrl
import androidx.compose.material3.catalog.library.util.SampleSourceUrl
import androidx.compose.material3.samples.AlertDialogSample
import androidx.compose.material3.samples.AlertDialogWithCustomContentSample
@@ -719,6 +722,23 @@
}
)
+private const val NavigationSuiteScaffoldExampleDescription = "Navigation suite scaffold examples"
+private const val NavigationSuiteScaffoldExampleSourceUrl =
+ "$AdaptiveSampleSourceUrl/NavigationSuiteScaffoldSamples.kt"
+val NavigationSuiteScaffoldExamples =
+ listOf(
+ Example(
+ name = ::NavigationSuiteScaffoldSample.name,
+ description = NavigationSuiteScaffoldExampleDescription,
+ sourceUrl = NavigationSuiteScaffoldExampleSourceUrl,
+ ) { NavigationSuiteScaffoldSample() },
+ Example(
+ name = ::NavigationSuiteScaffoldCustomConfigSample.name,
+ description = NavigationSuiteScaffoldExampleDescription,
+ sourceUrl = NavigationSuiteScaffoldExampleSourceUrl,
+ ) { NavigationSuiteScaffoldCustomConfigSample() },
+ )
+
private const val ProgressIndicatorsExampleDescription = "Progress indicators examples"
private const val ProgressIndicatorsExampleSourceUrl = "$SampleSourceUrl/" +
"ProgressIndicatorSamples.kt"
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/util/Url.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/util/Url.kt
index 8145968..0836cfe 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/util/Url.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/util/Url.kt
@@ -40,6 +40,12 @@
const val SampleSourceUrl = "https://cs.android.com/androidx/platform/frameworks/support/+/" +
"androidx-main:compose/material3/" +
"material3/samples/src/main/java/androidx/compose/material3/samples"
+const val AdaptiveMaterial3SourceUrl = "https://cs.android.com/androidx/platform/frameworks/" +
+ "support/+/androidx-main:compose/material3/" +
+ "material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive"
+const val AdaptiveSampleSourceUrl = "https://cs.android.com/androidx/platform/frameworks/" +
+ "support/+/androidx-main:compose/material3/material3-adaptive" +
+ "samples/src/main/java/androidx/compose/material3-adaptive/samples"
const val IssueUrl = "https://issuetracker.google.com/issues/new?component=742043"
const val TermsUrl = "https://policies.google.com/terms"
const val PrivacyUrl = "https://policies.google.com/privacy"
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
index 915a021..e350948 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
+++ b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
@@ -36,6 +36,7 @@
androidTestImplementation(projectOrArtifact(":compose:foundation:foundation-layout"))
androidTestImplementation(projectOrArtifact(":compose:material:material"))
androidTestImplementation(projectOrArtifact(":compose:runtime:runtime"))
+ androidTestImplementation(projectOrArtifact(":compose:runtime:runtime-saveable"))
androidTestImplementation(projectOrArtifact(":compose:ui:ui-text"))
androidTestImplementation(projectOrArtifact(":compose:ui:ui-util"))
androidTestImplementation(projectOrArtifact(":compose:test-utils"))
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
index 147cc7b..187bc8c 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
@@ -226,6 +226,30 @@
}
}
}
+
+ @UiThreadTest
+ @Test
+ fun benchmark_f_compose_Rect_1() = runBlockingTestWithFrameClock {
+ measureComposeFocused {
+ Rect()
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun benchmark_f_compose_Rect_10() = runBlockingTestWithFrameClock {
+ measureComposeFocused {
+ repeat(10) { Rect() }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun benchmark_f_compose_Rect_100() = runBlockingTestWithFrameClock {
+ measureComposeFocused {
+ repeat(100) { Rect() }
+ }
+ }
}
class ColorModel(color: Color = Color.Black) {
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt
index 9eb04b1..7334416 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt
@@ -74,6 +74,7 @@
@Composable
private fun CountGroupsAndSlots(content: @Composable () -> Unit) {
val data = currentComposer.compositionData
+ currentComposer.disableSourceInformation()
CompositionLocalProvider(LocalInspectionTables provides compositionTables, content = content)
SideEffect {
compositionTables?.let {
@@ -150,6 +151,41 @@
@ExperimentalCoroutinesApi
@ExperimentalTestApi
+ suspend fun TestScope.measureComposeFocused(block: @Composable () -> Unit) = coroutineScope {
+ val activity = activityRule.activity
+ val recomposer = Recomposer(coroutineContext)
+ val emptyView = View(activity)
+
+ try {
+ benchmarkRule.measureRepeatedSuspendable {
+ val benchmarkState = benchmarkRule.getState()
+ benchmarkState.pauseTiming()
+
+ activity.setContent(recomposer) {
+ CountGroupsAndSlots {
+ trace("Benchmark focus") {
+ benchmarkState.resumeTiming()
+ block()
+ benchmarkState.pauseTiming()
+ }
+ }
+ }
+ benchmarkState.resumeTiming()
+
+ runWithTimingDisabled {
+ activity.setContentView(emptyView)
+ testScheduler.advanceUntilIdle()
+ }
+ }
+ } finally {
+ activity.setContentView(emptyView)
+ testScheduler.advanceUntilIdle()
+ recomposer.cancel()
+ }
+ }
+
+ @ExperimentalCoroutinesApi
+ @ExperimentalTestApi
suspend fun TestScope.measureRecomposeSuspending(
block: RecomposeReceiver.() -> Unit
) = coroutineScope {
@@ -246,3 +282,12 @@
updateModelCb = block
}
}
+
+private inline fun trace(name: String, block: () -> Unit) {
+ android.os.Trace.beginSection(name)
+ try {
+ block()
+ } finally {
+ android.os.Trace.endSection()
+ }
+}
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/RememberSavableBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/RememberSavableBenchmark.kt
new file mode 100644
index 0000000..73bf219
--- /dev/null
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/RememberSavableBenchmark.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.benchmark
+
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.autoSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class RememberSaveableBenchmark : ComposeBenchmarkBase() {
+ @UiThreadTest
+ @Test
+ fun rememberSaveable_1() = runBlockingTestWithFrameClock {
+ measureComposeFocused {
+ @Suppress("UNUSED_VARIABLE")
+ val i: Int = rememberSaveable {
+ 10
+ }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun rememberSaveable_10() = runBlockingTestWithFrameClock {
+ measureComposeFocused {
+ repeat(10) {
+ @Suppress("UNUSED_VARIABLE")
+ val i: Int = rememberSaveable {
+ 10
+ }
+ }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun rememberSaveable_100() = runBlockingTestWithFrameClock {
+ measureComposeFocused {
+ repeat(100) {
+ @Suppress("UNUSED_VARIABLE")
+ val i: Int = rememberSaveable {
+ 10
+ }
+ }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun rememberSaveable_mutable_1() = runBlockingTestWithFrameClock {
+ measureComposeFocused {
+ @Suppress("UNUSED_VARIABLE")
+ val i = rememberSaveable(stateSaver = autoSaver()) {
+ mutableStateOf(10)
+ }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun rememberSaveable_mutable_10() = runBlockingTestWithFrameClock {
+ measureComposeFocused {
+ repeat(10) {
+ @Suppress("UNUSED_VARIABLE")
+ val i = rememberSaveable(stateSaver = autoSaver()) {
+ mutableStateOf(10)
+ }
+ }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun rememberSaveable_mutable_100() = runBlockingTestWithFrameClock {
+ measureComposeFocused {
+ repeat(100) {
+ @Suppress("UNUSED_VARIABLE")
+ val i = rememberSaveable(stateSaver = autoSaver()) {
+ mutableStateOf(10)
+ }
+ }
+ }
+ }
+}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
index 5c4170f..07a1d95 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
@@ -131,3 +131,7 @@
) : SnapshotContextElement
internal expect fun logError(message: String, e: Throwable)
+
+internal expect fun currentThreadId(): Long
+
+internal expect fun currentThreadName(): String
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
index 4a7724c..c346941 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
@@ -27,6 +27,8 @@
import androidx.compose.runtime.collection.fastForEach
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.composeRuntimeError
+import androidx.compose.runtime.currentThreadId
+import androidx.compose.runtime.currentThreadName
import androidx.compose.runtime.observeDerivedStateRecalculations
import androidx.compose.runtime.structuralEqualityPolicy
@@ -207,6 +209,11 @@
private var currentMap: ObservedScopeMap? = null
/**
+ * Thread id that has set the [currentMap]
+ */
+ private var currentMapThreadId = -1L
+
+ /**
* Executes [block], observing state object reads during its execution.
*
* The [scope] and [onValueChangedForScope] are associated with any values that are read so
@@ -228,21 +235,29 @@
val oldPaused = isPaused
val oldMap = currentMap
+ val oldThreadId = currentMapThreadId
- try {
- isPaused = false
- currentMap = scopeMap
-
- scopeMap.observe(scope, readObserver, block)
- } finally {
- require(currentMap === scopeMap) {
- "Inconsistent modification of observation scopes in SnapshotStateObserver. " +
+ if (oldThreadId != -1L) {
+ require(oldThreadId == currentThreadId()) {
+ "Detected multithreaded access to SnapshotStateObserver: " +
+ "previousThreadId=$oldThreadId), " +
+ "currentThread={id=${currentThreadId()}, name=${currentThreadName()}}. " +
"Note that observation on multiple threads in layout/draw is not supported. " +
"Make sure your measure/layout/draw for each Owner (AndroidComposeView) " +
"is executed on the same thread."
}
+ }
+
+ try {
+ isPaused = false
+ currentMap = scopeMap
+ currentMapThreadId = Thread.currentThread().id
+
+ scopeMap.observe(scope, readObserver, block)
+ } finally {
currentMap = oldMap
isPaused = oldPaused
+ currentMapThreadId = oldThreadId
}
}
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt
index 71576e4..63d84d7 100644
--- a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt
+++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt
@@ -132,3 +132,7 @@
snapshot.unsafeLeave(oldState)
}
}
+
+internal actual fun currentThreadId(): Long = Thread.currentThread().id
+
+internal actual fun currentThreadName(): String = Thread.currentThread().name
diff --git a/compose/ui/ui-tooling-preview/api/current.txt b/compose/ui/ui-tooling-preview/api/current.txt
index 80d0e53..4d4cc08 100644
--- a/compose/ui/ui-tooling-preview/api/current.txt
+++ b/compose/ui/ui-tooling-preview/api/current.txt
@@ -24,8 +24,18 @@
field public static final String PIXEL_3A_XL = "id:pixel_3a_xl";
field public static final String PIXEL_3_XL = "id:pixel_3_xl";
field public static final String PIXEL_4 = "id:pixel_4";
+ field public static final String PIXEL_4A = "id:pixel_4a";
field public static final String PIXEL_4_XL = "id:pixel_4_xl";
+ field public static final String PIXEL_5 = "id:pixel_5";
+ field public static final String PIXEL_6 = "id:pixel_6";
+ field public static final String PIXEL_6A = "id:pixel_6a";
+ field public static final String PIXEL_6_PRO = "id:pixel_6_pro";
+ field public static final String PIXEL_7 = "id:pixel_7";
+ field public static final String PIXEL_7A = "id:pixel_7a";
+ field public static final String PIXEL_7_PRO = "id:pixel_7_pro";
field public static final String PIXEL_C = "id:pixel_c";
+ field public static final String PIXEL_FOLD = "id:pixel_fold";
+ field public static final String PIXEL_TABLET = "id:pixel_tablet";
field public static final String PIXEL_XL = "id:pixel_xl";
field public static final String TABLET = "spec:id=reference_tablet,shape=Normal,width=1280,height=800,unit=dp,dpi=240";
field public static final String TV_1080p = "spec:shape=Normal,width=1920,height=1080,unit=dp,dpi=420";
diff --git a/compose/ui/ui-tooling-preview/api/restricted_current.txt b/compose/ui/ui-tooling-preview/api/restricted_current.txt
index 80d0e53..4d4cc08 100644
--- a/compose/ui/ui-tooling-preview/api/restricted_current.txt
+++ b/compose/ui/ui-tooling-preview/api/restricted_current.txt
@@ -24,8 +24,18 @@
field public static final String PIXEL_3A_XL = "id:pixel_3a_xl";
field public static final String PIXEL_3_XL = "id:pixel_3_xl";
field public static final String PIXEL_4 = "id:pixel_4";
+ field public static final String PIXEL_4A = "id:pixel_4a";
field public static final String PIXEL_4_XL = "id:pixel_4_xl";
+ field public static final String PIXEL_5 = "id:pixel_5";
+ field public static final String PIXEL_6 = "id:pixel_6";
+ field public static final String PIXEL_6A = "id:pixel_6a";
+ field public static final String PIXEL_6_PRO = "id:pixel_6_pro";
+ field public static final String PIXEL_7 = "id:pixel_7";
+ field public static final String PIXEL_7A = "id:pixel_7a";
+ field public static final String PIXEL_7_PRO = "id:pixel_7_pro";
field public static final String PIXEL_C = "id:pixel_c";
+ field public static final String PIXEL_FOLD = "id:pixel_fold";
+ field public static final String PIXEL_TABLET = "id:pixel_tablet";
field public static final String PIXEL_XL = "id:pixel_xl";
field public static final String TABLET = "spec:id=reference_tablet,shape=Normal,width=1280,height=800,unit=dp,dpi=240";
field public static final String TV_1080p = "spec:shape=Normal,width=1920,height=1080,unit=dp,dpi=420";
diff --git a/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt
index 6a5088e..c140060 100644
--- a/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt
+++ b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt
@@ -43,6 +43,16 @@
const val PIXEL_3A_XL = "id:pixel_3a_xl"
const val PIXEL_4 = "id:pixel_4"
const val PIXEL_4_XL = "id:pixel_4_xl"
+ const val PIXEL_4A = "id:pixel_4a"
+ const val PIXEL_5 = "id:pixel_5"
+ const val PIXEL_6 = "id:pixel_6"
+ const val PIXEL_6_PRO = "id:pixel_6_pro"
+ const val PIXEL_6A = "id:pixel_6a"
+ const val PIXEL_7 = "id:pixel_7"
+ const val PIXEL_7_PRO = "id:pixel_7_pro"
+ const val PIXEL_7A = "id:pixel_7a"
+ const val PIXEL_FOLD = "id:pixel_fold"
+ const val PIXEL_TABLET = "id:pixel_tablet"
const val AUTOMOTIVE_1024p = "id:automotive_1024p_landscape"
@@ -101,6 +111,16 @@
Devices.PIXEL_3A_XL,
Devices.PIXEL_4,
Devices.PIXEL_4_XL,
+ Devices.PIXEL_4A,
+ Devices.PIXEL_5,
+ Devices.PIXEL_6,
+ Devices.PIXEL_6_PRO,
+ Devices.PIXEL_6A,
+ Devices.PIXEL_7,
+ Devices.PIXEL_7_PRO,
+ Devices.PIXEL_7A,
+ Devices.PIXEL_FOLD,
+ Devices.PIXEL_TABLET,
Devices.AUTOMOTIVE_1024p,
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index f6193de..8a407fd 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -72,6 +72,8 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.platform.coreshims.ContentCaptureSessionCompat
+import androidx.compose.ui.platform.coreshims.ViewStructureCompat
import androidx.compose.ui.platform.getAllUncoveredSemanticsNodesToMap
import androidx.compose.ui.platform.invertTo
import androidx.compose.ui.semantics.CustomAccessibilityAction
@@ -138,10 +140,8 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toOffset
import androidx.core.view.ViewCompat
-import androidx.core.view.ViewStructureCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
-import androidx.core.view.contentcapture.ContentCaptureSessionCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt
index 8a72128..21fab87 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/MemoryLeakTest.kt
@@ -62,6 +62,7 @@
val activityTestRule = androidx.test.rule.ActivityTestRule(ComponentActivity::class.java)
@Test
+ @SdkSuppress(minSdkVersion = 22) // b/266743031
fun disposeAndRemoveOwnerView_assertViewWasGarbageCollected() = runBlocking {
class SimpleTestCase : ComposeTestCase {
@Composable
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
index d052e2e..69554df 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
@@ -58,8 +58,10 @@
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.zIndex
@@ -1145,6 +1147,39 @@
assertEquals(
AnnotatedString("hello"), newConfig.getOrNull(SemanticsProperties.OriginalText))
}
+
+ @Test
+ fun testGetTextSizeFromTextLayoutResult() {
+ var density = Float.NaN
+ rule.setContent {
+ with(LocalDensity.current) {
+ density = 1.sp.toPx()
+ }
+ Surface {
+ Text(
+ AnnotatedString("hello"),
+ Modifier
+ .testTag(TestTag),
+ fontSize = 14.sp,
+ )
+ }
+ }
+
+ val config = rule.onNodeWithTag(TestTag, true).fetchSemanticsNode().config
+
+ val textLayoutResult: TextLayoutResult
+ val textLayoutResults = mutableListOf<TextLayoutResult>()
+ val getLayoutResult = config[SemanticsActions.GetTextLayoutResult]
+ .action?.invoke(textLayoutResults)
+
+ assertEquals(true, getLayoutResult)
+
+ textLayoutResult = textLayoutResults[0]
+ val result = textLayoutResult.layoutInput
+
+ assertEquals(density, result.density.density)
+ assertEquals(14.0f, result.style.fontSize.value)
+ }
}
private fun SemanticsNodeInteraction.assertDoesNotHaveProperty(property: SemanticsPropertyKey<*>) {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
index 18b6f6e..b5aa683 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingListParityTest.kt
@@ -29,7 +29,6 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.Text
@@ -83,7 +82,7 @@
}
@Test
- fun equalLists_withEqualFlings_shouldFinishAtTheSameItem() = runBlocking {
+ fun equalLists_withEqualFlings_shouldFinishAtTheSameItem_smallVeryFast() = runBlocking {
val state = LazyListState()
// starting with view
@@ -91,7 +90,8 @@
checkVisibility(composeView(), View.GONE)
checkVisibility(recyclerView(), View.VISIBLE)
- swipeView(R.id.view_list)
+ smallGestureVeryFast(R.id.view_list)
+ rule.waitForIdle()
recyclerView().awaitScrollIdle()
val childAtTheTopOfView = layoutManager?.findFirstVisibleItemPosition() ?: 0
@@ -117,7 +117,219 @@
rule.runOnIdle {
val currentTopInCompose = state.firstVisibleItemIndex
val diff = (currentTopInCompose - childAtTheTopOfView).absoluteValue
- assertTrue("Difference was=$diff") { diff <= 3 }
+ val message = "Compose=$currentTopInCompose View=$childAtTheTopOfView " +
+ "Difference was=$diff"
+ assertTrue(message) { diff <= ItemDifferenceThreshold }
+ }
+ }
+
+ @Test
+ fun equalLists_withEqualFlings_shouldFinishAtTheSameItem_smallFast() = runBlocking {
+ val state = LazyListState()
+
+ // starting with view
+ createActivity(state)
+ checkVisibility(composeView(), View.GONE)
+ checkVisibility(recyclerView(), View.VISIBLE)
+
+ smallGestureFast(R.id.view_list)
+ rule.waitForIdle()
+ recyclerView().awaitScrollIdle()
+
+ val childAtTheTopOfView = layoutManager?.findFirstVisibleItemPosition() ?: 0
+
+ // switch visibilities
+ rule.runOnUiThread {
+ rule.activity.findViewById<RecyclerView>(R.id.view_list).visibility = View.GONE
+ rule.activity.findViewById<ComposeView>(R.id.compose_view).visibility = View.VISIBLE
+ }
+
+ checkVisibility(composeView(), View.VISIBLE)
+ checkVisibility(recyclerView(), View.GONE)
+
+ assertTrue { isValidGesture(recyclerView().motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ rule.runOnUiThread {
+ for (event in recyclerView().motionEvents) {
+ composeView().dispatchTouchEvent(event)
+ }
+ }
+
+ rule.runOnIdle {
+ val currentTopInCompose = state.firstVisibleItemIndex
+ val diff = (currentTopInCompose - childAtTheTopOfView).absoluteValue
+ val message = "Compose=$currentTopInCompose View=$childAtTheTopOfView " +
+ "Difference was=$diff"
+ assertTrue(message) { diff <= ItemDifferenceThreshold }
+ }
+ }
+
+ @Test
+ fun equalLists_withEqualFlings_shouldFinishAtTheSameItem_smallSlow() = runBlocking {
+ val state = LazyListState()
+
+ // starting with view
+ createActivity(state)
+ checkVisibility(composeView(), View.GONE)
+ checkVisibility(recyclerView(), View.VISIBLE)
+
+ smallGestureSlow(R.id.view_list)
+ rule.waitForIdle()
+ recyclerView().awaitScrollIdle()
+
+ val childAtTheTopOfView = layoutManager?.findFirstVisibleItemPosition() ?: 0
+
+ // switch visibilities
+ rule.runOnUiThread {
+ rule.activity.findViewById<RecyclerView>(R.id.view_list).visibility = View.GONE
+ rule.activity.findViewById<ComposeView>(R.id.compose_view).visibility = View.VISIBLE
+ }
+
+ checkVisibility(composeView(), View.VISIBLE)
+ checkVisibility(recyclerView(), View.GONE)
+
+ assertTrue { isValidGesture(recyclerView().motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ rule.runOnUiThread {
+ for (event in recyclerView().motionEvents) {
+ composeView().dispatchTouchEvent(event)
+ }
+ }
+
+ rule.runOnIdle {
+ val currentTopInCompose = state.firstVisibleItemIndex
+ val diff = (currentTopInCompose - childAtTheTopOfView).absoluteValue
+ val message = "Compose=$currentTopInCompose View=$childAtTheTopOfView " +
+ "Difference was=$diff"
+ assertTrue(message) { diff <= ItemDifferenceThreshold }
+ }
+ }
+
+ @Test
+ fun equalLists_withEqualFlings_shouldFinishAtTheSameItem_largeFast() = runBlocking {
+ val state = LazyListState()
+
+ // starting with view
+ createActivity(state)
+ checkVisibility(composeView(), View.GONE)
+ checkVisibility(recyclerView(), View.VISIBLE)
+
+ largeGestureFast(R.id.view_list)
+ rule.waitForIdle()
+ recyclerView().awaitScrollIdle()
+
+ val childAtTheTopOfView = layoutManager?.findFirstVisibleItemPosition() ?: 0
+
+ // switch visibilities
+ rule.runOnUiThread {
+ rule.activity.findViewById<RecyclerView>(R.id.view_list).visibility = View.GONE
+ rule.activity.findViewById<ComposeView>(R.id.compose_view).visibility = View.VISIBLE
+ }
+
+ checkVisibility(composeView(), View.VISIBLE)
+ checkVisibility(recyclerView(), View.GONE)
+
+ assertTrue { isValidGesture(recyclerView().motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ rule.runOnUiThread {
+ for (event in recyclerView().motionEvents) {
+ composeView().dispatchTouchEvent(event)
+ }
+ }
+
+ rule.runOnIdle {
+ val currentTopInCompose = state.firstVisibleItemIndex
+ val diff = (currentTopInCompose - childAtTheTopOfView).absoluteValue
+ val message = "Compose=$currentTopInCompose View=$childAtTheTopOfView " +
+ "Difference was=$diff"
+ assertTrue(message) { diff <= ItemDifferenceThreshold }
+ }
+ }
+
+ @Test
+ fun equalLists_withEqualFlings_shouldFinishAtTheSameItem_largeVeryFast() = runBlocking {
+ val state = LazyListState()
+
+ // starting with view
+ createActivity(state)
+ checkVisibility(composeView(), View.GONE)
+ checkVisibility(recyclerView(), View.VISIBLE)
+
+ largeGestureVeryFast(R.id.view_list)
+ rule.waitForIdle()
+ recyclerView().awaitScrollIdle()
+
+ val childAtTheTopOfView = layoutManager?.findFirstVisibleItemPosition() ?: 0
+
+ // switch visibilities
+ rule.runOnUiThread {
+ rule.activity.findViewById<RecyclerView>(R.id.view_list).visibility = View.GONE
+ rule.activity.findViewById<ComposeView>(R.id.compose_view).visibility = View.VISIBLE
+ }
+
+ checkVisibility(composeView(), View.VISIBLE)
+ checkVisibility(recyclerView(), View.GONE)
+
+ assertTrue { isValidGesture(recyclerView().motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ rule.runOnUiThread {
+ for (event in recyclerView().motionEvents) {
+ composeView().dispatchTouchEvent(event)
+ }
+ }
+
+ rule.runOnIdle {
+ val currentTopInCompose = state.firstVisibleItemIndex
+ val diff = (currentTopInCompose - childAtTheTopOfView).absoluteValue
+ val message = "Compose=$currentTopInCompose View=$childAtTheTopOfView " +
+ "Difference was=$diff"
+ assertTrue(message) { diff <= ItemDifferenceThreshold }
+ }
+ }
+
+ @Test
+ fun equalLists_withEqualFlings_shouldFinishAtTheSameItem_orthogonal() = runBlocking {
+ val state = LazyListState()
+
+ // starting with view
+ createActivity(state)
+ checkVisibility(composeView(), View.GONE)
+ checkVisibility(recyclerView(), View.VISIBLE)
+
+ orthogonalGesture(R.id.view_list)
+ rule.waitForIdle()
+ recyclerView().awaitScrollIdle()
+
+ val childAtTheTopOfView = layoutManager?.findFirstVisibleItemPosition() ?: 0
+
+ // switch visibilities
+ rule.runOnUiThread {
+ rule.activity.findViewById<RecyclerView>(R.id.view_list).visibility = View.GONE
+ rule.activity.findViewById<ComposeView>(R.id.compose_view).visibility = View.VISIBLE
+ }
+
+ checkVisibility(composeView(), View.VISIBLE)
+ checkVisibility(recyclerView(), View.GONE)
+
+ assertTrue { isValidGesture(recyclerView().motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ rule.runOnUiThread {
+ for (event in recyclerView().motionEvents) {
+ composeView().dispatchTouchEvent(event)
+ }
+ }
+
+ rule.runOnIdle {
+ val currentTopInCompose = state.firstVisibleItemIndex
+ val diff = (currentTopInCompose - childAtTheTopOfView).absoluteValue
+ val message = "Compose=$currentTopInCompose View=$childAtTheTopOfView " +
+ "Difference was=$diff"
+ assertTrue(message) { diff <= ItemDifferenceThreshold }
}
}
@@ -171,22 +383,16 @@
view.visibility == visibility
}
}
-
- private fun swipeView(id: Int) {
- controlledSwipeUp(id)
- rule.waitForIdle()
- }
}
@Composable
fun TestComposeList(state: LazyListState) {
LazyColumn(Modifier.fillMaxSize(), state = state) {
- items(200) {
+ items(1000) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
- .padding(2.dp)
.background(Color.Black)
) {
Text(text = it.toString(), color = Color.White)
@@ -196,7 +402,7 @@
}
private class ListAdapter : RecyclerView.Adapter<ListViewHolder>() {
- val items = (0 until 200).map { it.toString() }
+ val items = (0 until 1000).map { it.toString() }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
return ListViewHolder(
LayoutInflater.from(parent.context)
@@ -250,3 +456,5 @@
}
}
}
+
+private const val ItemDifferenceThreshold = 3
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
index 5663ea1..124d13f 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/VelocityTrackingParityTest.kt
@@ -24,34 +24,53 @@
import androidx.activity.ComponentActivity
import androidx.annotation.LayoutRes
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.awaitTouchSlopOrCancellation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.background
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.AwaitPointerEventScope
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerId
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChangedIgnoreConsumed
import androidx.compose.ui.input.pointer.util.VelocityTrackerAddPointsFix
+import androidx.compose.ui.input.pointer.util.addPointerInputChange
import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.tests.R
import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.util.fastFirstOrNull
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso
+import androidx.test.espresso.UiController
import androidx.test.espresso.action.CoordinatesProvider
import androidx.test.espresso.action.GeneralLocation
import androidx.test.espresso.action.GeneralSwipeAction
+import androidx.test.espresso.action.MotionEvents
import androidx.test.espresso.action.Press
-import androidx.test.espresso.action.Swipe
+import androidx.test.espresso.action.Swiper
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
+import com.google.errorprone.annotations.CanIgnoreReturnValue
+import kotlin.math.absoluteValue
import kotlin.test.assertTrue
-import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -70,12 +89,12 @@
private val composeView: ComposeView
get() = rule.activity.findViewById(R.id.compose_view)
- private var latestComposeVelocity = 0f
+ private var latestComposeVelocity = Velocity.Zero
@OptIn(ExperimentalComposeUiApi::class)
@Before
fun setUp() {
- latestComposeVelocity = 0f
+ latestComposeVelocity = Velocity.Zero
VelocityTrackerAddPointsFix = true
}
@@ -84,16 +103,17 @@
}
@Test
- fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity() {
+ fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_smallVeryFast() {
// Arrange
createActivity()
checkVisibility(composeView, View.GONE)
checkVisibility(draggableView, View.VISIBLE)
// Act: Use system to send motion events and collect them.
- swipeView(R.id.draggable_view)
+ smallGestureVeryFast(R.id.draggable_view)
- val latestVelocityInView = draggableView.latestVelocity.y
+ val latestVelocityInViewX = draggableView.latestVelocity.x
+ val latestVelocityInViewY = draggableView.latestVelocity.y
// switch visibility
rule.runOnUiThread {
@@ -110,21 +130,189 @@
for (event in draggableView.motionEvents) {
composeView.dispatchTouchEvent(event)
}
- val latestVelocityInCompose = latestComposeVelocity
- val diff = kotlin.math.abs(latestVelocityInView - latestVelocityInCompose)
// assert
- assertThat(diff).isWithin(VelocityDifferenceTolerance * latestVelocityInView)
+ assertIsWithinTolerance(latestComposeVelocity.x, latestVelocityInViewX)
+ assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
}
- private fun createActivity() {
+ @Test
+ fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_smallFast() {
+ // Arrange
+ createActivity()
+ checkVisibility(composeView, View.GONE)
+ checkVisibility(draggableView, View.VISIBLE)
+
+ // Act: Use system to send motion events and collect them.
+ smallGestureFast(R.id.draggable_view)
+
+ val latestVelocityInViewX = draggableView.latestVelocity.x
+ val latestVelocityInViewY = draggableView.latestVelocity.y
+
+ // switch visibility
+ rule.runOnUiThread {
+ composeView.visibility = View.VISIBLE
+ draggableView.visibility = View.GONE
+ }
+
+ checkVisibility(composeView, View.VISIBLE)
+ checkVisibility(draggableView, View.GONE)
+
+ assertTrue { isValidGesture(draggableView.motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ for (event in draggableView.motionEvents) {
+ composeView.dispatchTouchEvent(event)
+ }
+
+ // assert
+ assertIsWithinTolerance(latestComposeVelocity.x, latestVelocityInViewX)
+ assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
+ }
+
+ @Test
+ fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_smallSlow() {
+ // Arrange
+ createActivity()
+ checkVisibility(composeView, View.GONE)
+ checkVisibility(draggableView, View.VISIBLE)
+
+ // Act: Use system to send motion events and collect them.
+ smallGestureSlow(R.id.draggable_view)
+
+ val latestVelocityInViewX = draggableView.latestVelocity.x
+ val latestVelocityInViewY = draggableView.latestVelocity.y
+
+ // switch visibility
+ rule.runOnUiThread {
+ composeView.visibility = View.VISIBLE
+ draggableView.visibility = View.GONE
+ }
+
+ checkVisibility(composeView, View.VISIBLE)
+ checkVisibility(draggableView, View.GONE)
+
+ assertTrue { isValidGesture(draggableView.motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ for (event in draggableView.motionEvents) {
+ composeView.dispatchTouchEvent(event)
+ }
+ // assert
+ assertIsWithinTolerance(latestComposeVelocity.x, latestVelocityInViewX)
+ assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
+ }
+
+ @Test
+ fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_largeFast() {
+ // Arrange
+ createActivity()
+ checkVisibility(composeView, View.GONE)
+ checkVisibility(draggableView, View.VISIBLE)
+
+ // Act: Use system to send motion events and collect them.
+ largeGestureFast(R.id.draggable_view)
+
+ val latestVelocityInViewX = draggableView.latestVelocity.x
+ val latestVelocityInViewY = draggableView.latestVelocity.y
+
+ // switch visibility
+ rule.runOnUiThread {
+ composeView.visibility = View.VISIBLE
+ draggableView.visibility = View.GONE
+ }
+
+ checkVisibility(composeView, View.VISIBLE)
+ checkVisibility(draggableView, View.GONE)
+
+ assertTrue { isValidGesture(draggableView.motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ for (event in draggableView.motionEvents) {
+ composeView.dispatchTouchEvent(event)
+ }
+
+ // assert
+ assertIsWithinTolerance(latestComposeVelocity.x, latestVelocityInViewX)
+ assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
+ }
+
+ @Test
+ fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_largeVeryFast() {
+ // Arrange
+ createActivity()
+ checkVisibility(composeView, View.GONE)
+ checkVisibility(draggableView, View.VISIBLE)
+
+ // Act: Use system to send motion events and collect them.
+ largeGestureVeryFast(R.id.draggable_view)
+
+ val latestVelocityInViewX = draggableView.latestVelocity.x
+ val latestVelocityInViewY = draggableView.latestVelocity.y
+
+ // switch visibility
+ rule.runOnUiThread {
+ composeView.visibility = View.VISIBLE
+ draggableView.visibility = View.GONE
+ }
+
+ checkVisibility(composeView, View.VISIBLE)
+ checkVisibility(draggableView, View.GONE)
+
+ assertTrue { isValidGesture(draggableView.motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ for (event in draggableView.motionEvents) {
+ composeView.dispatchTouchEvent(event)
+ }
+
+ // assert
+ assertIsWithinTolerance(latestComposeVelocity.x, latestVelocityInViewX)
+ assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
+ }
+
+ @Test
+ fun equalDraggable_withEqualSwipes_shouldProduceSimilarVelocity_orthogonal() {
+ // Arrange
+ createActivity(true)
+ checkVisibility(composeView, View.GONE)
+ checkVisibility(draggableView, View.VISIBLE)
+
+ // Act: Use system to send motion events and collect them.
+ orthogonalGesture(R.id.draggable_view)
+
+ val latestVelocityInViewX = draggableView.latestVelocity.x
+ val latestVelocityInViewY = draggableView.latestVelocity.y
+
+ // switch visibility
+ rule.runOnUiThread {
+ composeView.visibility = View.VISIBLE
+ draggableView.visibility = View.GONE
+ }
+
+ checkVisibility(composeView, View.VISIBLE)
+ checkVisibility(draggableView, View.GONE)
+
+ assertTrue { isValidGesture(draggableView.motionEvents.filterNotNull()) }
+
+ // Inject the same events in compose view
+ for (event in draggableView.motionEvents) {
+ composeView.dispatchTouchEvent(event)
+ }
+
+ // assert
+ assertIsWithinTolerance(latestComposeVelocity.x, latestVelocityInViewX)
+ assertIsWithinTolerance(latestComposeVelocity.y, latestVelocityInViewY)
+ }
+
+ private fun createActivity(twoDimensional: Boolean = false) {
rule
.activityRule
.scenario
.createActivityWithComposeContent(
R.layout.velocity_tracker_compose_vs_view
) {
- TestComposeDraggable {
+ TestComposeDraggable(twoDimensional) {
latestComposeVelocity = it
}
}
@@ -134,46 +322,154 @@
view.visibility == visibility
}
- private fun swipeView(id: Int) {
- controlledSwipeUp(id)
- rule.waitForIdle()
+ private fun assertIsWithinTolerance(composeVelocity: Float, viewVelocity: Float) {
+ if (composeVelocity.absoluteValue > 1f && viewVelocity.absoluteValue > 1f) {
+ val tolerance = VelocityDifferenceTolerance * kotlin.math.abs(viewVelocity)
+ assertThat(composeVelocity).isWithin(tolerance).of(viewVelocity)
+ } else {
+ assertThat(composeVelocity.toInt()).isEqualTo(viewVelocity.toInt())
+ }
}
}
-internal fun controlledSwipeUp(id: Int) {
+internal fun smallGestureVeryFast(id: Int) {
Espresso.onView(withId(id))
.perform(
espressoSwipe(
+ SwiperWithTime(15),
GeneralLocation.CENTER,
- GeneralLocation.TOP_CENTER
+ GeneralLocation.translate(GeneralLocation.CENTER, 0f, -50f)
+ )
+ )
+}
+
+internal fun smallGestureFast(id: Int) {
+ Espresso.onView(withId(id))
+ .perform(
+ espressoSwipe(
+ SwiperWithTime(25),
+ GeneralLocation.CENTER,
+ GeneralLocation.translate(GeneralLocation.CENTER, 0f, -50f)
+ )
+ )
+}
+
+internal fun smallGestureSlow(id: Int) {
+ Espresso.onView(withId(id))
+ .perform(
+ espressoSwipe(
+ SwiperWithTime(200),
+ GeneralLocation.CENTER,
+ GeneralLocation.translate(GeneralLocation.CENTER, 0f, -50f)
+ )
+ )
+}
+
+internal fun largeGestureFast(id: Int) {
+ Espresso.onView(withId(id))
+ .perform(
+ espressoSwipe(
+ SwiperWithTime(25),
+ GeneralLocation.CENTER,
+ GeneralLocation.translate(GeneralLocation.CENTER, 0f, -500f)
+ )
+ )
+}
+
+internal fun largeGestureVeryFast(id: Int) {
+ Espresso.onView(withId(id))
+ .perform(
+ espressoSwipe(
+ SwiperWithTime(15),
+ GeneralLocation.CENTER,
+ GeneralLocation.translate(GeneralLocation.CENTER, 0f, -500f)
+ )
+ )
+}
+
+internal fun orthogonalGesture(id: Int) {
+ Espresso.onView(withId(id))
+ .perform(
+ espressoSwipe(
+ SwiperWithTime(50),
+ GeneralLocation.CENTER,
+ GeneralLocation.translate(GeneralLocation.CENTER, -200f, -200f)
)
)
}
private fun espressoSwipe(
+ swiper: Swiper,
start: CoordinatesProvider,
end: CoordinatesProvider
): GeneralSwipeAction {
return GeneralSwipeAction(
- Swipe.FAST, start, end,
+ swiper, start, end,
Press.FINGER
)
}
@Composable
-fun TestComposeDraggable(onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit) {
- Box(
- Modifier
- .fillMaxSize()
- .background(Color.Black)
- .draggable(
- rememberDraggableState(onDelta = { }),
- onDragStopped = onDragStopped,
- orientation = Orientation.Vertical
- )
- )
+fun TestComposeDraggable(
+ twoDimensional: Boolean = false,
+ onDragStopped: (velocity: Velocity) -> Unit
+) {
+ val viewConfiguration = object : ViewConfiguration by LocalViewConfiguration.current {
+ override val maximumFlingVelocity: Int
+ get() = Int.MAX_VALUE // unlimited
+ }
+ CompositionLocalProvider(LocalViewConfiguration provides viewConfiguration) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(Color.Black)
+ .then(
+ if (twoDimensional) {
+ Modifier.draggable2D(onDragStopped)
+ } else {
+ Modifier.draggable(
+ rememberDraggableState(onDelta = { }),
+ onDragStopped = { onDragStopped.invoke(Velocity(0.0f, it)) },
+ orientation = Orientation.Vertical
+ )
+ }
+ )
+ )
+ }
}
+fun Modifier.draggable2D(onDragStopped: (Velocity) -> Unit) =
+ this.pointerInput(Unit) {
+ coroutineScope {
+ awaitEachGesture {
+ val tracker = androidx.compose.ui.input.pointer.util.VelocityTracker()
+ val initialDown =
+ awaitFirstDown(
+ requireUnconsumed = false,
+ pass = PointerEventPass.Initial
+ )
+ tracker.addPointerInputChange(initialDown)
+
+ awaitTouchSlopOrCancellation(initialDown.id) { change, _ ->
+ tracker.addPointerInputChange(change)
+ change.consume()
+ }
+
+ val lastEvent = awaitDragOrUp(initialDown.id) {
+ tracker.addPointerInputChange(it)
+ it.consume()
+ it.positionChangedIgnoreConsumed()
+ }
+ lastEvent?.let {
+ tracker.addPointerInputChange(it)
+ }
+ onDragStopped(
+ tracker.calculateVelocity()
+ )
+ }
+ }
+ }
+
private fun ActivityScenario<*>.createActivityWithComposeContent(
@LayoutRes layout: Int,
content: @Composable () -> Unit,
@@ -237,4 +533,115 @@
}
// 1% tolerance
-private const val VelocityDifferenceTolerance = 0.01f
+private const val VelocityDifferenceTolerance = 0.1f
+
+/**
+ * Copied from androidx.test.espresso.action.Swipe
+ */
+internal data class SwiperWithTime(val gestureDurationMs: Int) : Swiper {
+ override fun sendSwipe(
+ uiController: UiController,
+ startCoordinates: FloatArray,
+ endCoordinates: FloatArray,
+ precision: FloatArray
+ ): Swiper.Status {
+ return sendLinearSwipe(
+ uiController,
+ startCoordinates,
+ endCoordinates,
+ precision,
+ gestureDurationMs
+ )
+ }
+
+ private fun checkElementIndex(index: Int, size: Int): Int {
+ return checkElementIndex(index, size, "index")
+ }
+
+ @CanIgnoreReturnValue
+ private fun checkElementIndex(index: Int, size: Int, desc: String): Int {
+ // Carefully optimized for execution by hotspot (explanatory comment above)
+ if (index < 0 || index >= size) {
+ throw IndexOutOfBoundsException(badElementIndex(index, size, desc))
+ }
+ return index
+ }
+
+ private fun badElementIndex(index: Int, size: Int, desc: String): String {
+ return if (index < 0) {
+ String.format("%s (%s) must not be negative", desc, index)
+ } else if (size < 0) {
+ throw IllegalArgumentException("negative size: $size")
+ } else { // index >= size
+ String.format("%s (%s) must be less than size (%s)", desc, index, size)
+ }
+ }
+
+ private fun interpolate(start: FloatArray, end: FloatArray, steps: Int): Array<FloatArray> {
+ checkElementIndex(1, start.size)
+ checkElementIndex(1, end.size)
+ val res = Array(steps) {
+ FloatArray(
+ 2
+ )
+ }
+ for (i in 1 until steps + 1) {
+ res[i - 1][0] = start[0] + (end[0] - start[0]) * i / (steps + 2f)
+ res[i - 1][1] = start[1] + (end[1] - start[1]) * i / (steps + 2f)
+ }
+ return res
+ }
+
+ private fun sendLinearSwipe(
+ uiController: UiController,
+ startCoordinates: FloatArray,
+ endCoordinates: FloatArray,
+ precision: FloatArray,
+ duration: Int
+ ): Swiper.Status {
+ val steps = interpolate(startCoordinates, endCoordinates, 10)
+ val events: MutableList<MotionEvent> = ArrayList()
+ val downEvent = MotionEvents.obtainDownEvent(startCoordinates, precision)
+ events.add(downEvent)
+ try {
+ val intervalMS = (duration / steps.size).toLong()
+ var eventTime = downEvent.downTime
+ for (step in steps) {
+ eventTime += intervalMS
+ events.add(MotionEvents.obtainMovement(downEvent, eventTime, step))
+ }
+ eventTime += intervalMS
+ events.add(MotionEvents.obtainUpEvent(downEvent, eventTime, endCoordinates))
+ uiController.injectMotionEventSequence(events)
+ } catch (e: Exception) {
+ return Swiper.Status.FAILURE
+ } finally {
+ for (event in events) {
+ event.recycle()
+ }
+ }
+ return Swiper.Status.SUCCESS
+ }
+}
+
+private suspend inline fun AwaitPointerEventScope.awaitDragOrUp(
+ pointerId: PointerId,
+ hasDragged: (PointerInputChange) -> Boolean
+): PointerInputChange? {
+ var pointer = pointerId
+ while (true) {
+ val event = awaitPointerEvent()
+ val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null
+ if (dragEvent.changedToUpIgnoreConsumed()) {
+ val otherDown = event.changes.fastFirstOrNull { it.pressed }
+ if (otherDown == null) {
+ // This is the last "up"
+ return dragEvent
+ } else {
+ pointer = otherDown.id
+ }
+ } else if (hasDragged(dragEvent)) {
+ return dragEvent
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/res/layout/android_compose_lists_fling_item.xml b/compose/ui/ui/src/androidAndroidTest/res/layout/android_compose_lists_fling_item.xml
index bbcfe5a..50744cd 100644
--- a/compose/ui/ui/src/androidAndroidTest/res/layout/android_compose_lists_fling_item.xml
+++ b/compose/ui/ui/src/androidAndroidTest/res/layout/android_compose_lists_fling_item.xml
@@ -18,7 +18,6 @@
android:layout_width="match_parent"
android:background="@android:color/black"
android:layout_height="64dp"
- android:layout_margin="2dp"
android:orientation="vertical">
<TextView
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 0c064f9..18e0502 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -66,6 +66,9 @@
import androidx.compose.ui.platform.accessibility.hasCollectionInfo
import androidx.compose.ui.platform.accessibility.setCollectionInfo
import androidx.compose.ui.platform.accessibility.setCollectionItemInfo
+import androidx.compose.ui.platform.coreshims.ContentCaptureSessionCompat
+import androidx.compose.ui.platform.coreshims.ViewCompatShims
+import androidx.compose.ui.platform.coreshims.ViewStructureCompat
import androidx.compose.ui.semantics.AccessibilityAction
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.LiveRegionMode
@@ -97,12 +100,10 @@
import androidx.core.view.ViewCompat
import androidx.core.view.ViewCompat.ACCESSIBILITY_LIVE_REGION_ASSERTIVE
import androidx.core.view.ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE
-import androidx.core.view.ViewStructureCompat
import androidx.core.view.accessibility.AccessibilityEventCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
-import androidx.core.view.contentcapture.ContentCaptureSessionCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
@@ -1947,16 +1948,7 @@
Log.e(LogTag, "Invalid arguments for accessibility character locations")
return
}
- val textLayoutResults = mutableListOf<TextLayoutResult>()
- // Note now it only works for single Text/TextField until we fix b/157474582.
- val getLayoutResult = node.unmergedConfig[SemanticsActions.GetTextLayoutResult]
- .action?.invoke(textLayoutResults)
- val textLayoutResult: TextLayoutResult
- if (getLayoutResult == true) {
- textLayoutResult = textLayoutResults[0]
- } else {
- return
- }
+ val textLayoutResult = getTextLayoutResult(node.unmergedConfig) ?: return
val boundingRects = mutableListOf<RectF?>()
for (i in 0 until positionInfoLength) {
// This is a workaround until we fix the merging issue in b/157474582.
@@ -2774,8 +2766,22 @@
}
private fun View.getContentCaptureSessionCompat(): ContentCaptureSessionCompat? {
- ViewCompat.setImportantForContentCapture(this, ViewCompat.IMPORTANT_FOR_CONTENT_CAPTURE_YES)
- return ViewCompat.getContentCaptureSession(this)
+ ViewCompatShims.setImportantForContentCapture(
+ this,
+ ViewCompatShims.IMPORTANT_FOR_CONTENT_CAPTURE_YES
+ )
+ return ViewCompatShims.getContentCaptureSession(this)
+ }
+
+ private fun getTextLayoutResult(configuration: SemanticsConfiguration): TextLayoutResult? {
+ val textLayoutResults = mutableListOf<TextLayoutResult>()
+ val getLayoutResult = configuration.getOrNull(SemanticsActions.GetTextLayoutResult)
+ ?.action?.invoke(textLayoutResults) ?: return null
+ return if (getLayoutResult) {
+ textLayoutResults[0]
+ } else {
+ null
+ }
}
private fun SemanticsNode.toViewStructure(): ViewStructureCompat? {
@@ -2784,7 +2790,7 @@
return null
}
- val rootAutofillId = ViewCompat.getAutofillId(view) ?: return null
+ val rootAutofillId = ViewCompatShims.getAutofillId(view) ?: return null
val parentNode = parent
val parentAutofillId = if (parentNode != null) {
session.newAutofillId(parentNode.id.toLong()) ?: return null
@@ -2814,6 +2820,12 @@
structure.setClassName(it)
}
+ getTextLayoutResult(configuration)?.let {
+ val input = it.layoutInput
+ val px = input.style.fontSize.value * input.density.density * input.density.fontScale
+ structure.setTextStyle(px, 0, 0, 0)
+ }
+
with(boundsInParent) {
structure.setDimens(
left.toInt(), top.toInt(), 0, 0, width.toInt(), height.toInt()
@@ -3208,17 +3220,7 @@
if (!node.unmergedConfig.contains(SemanticsActions.GetTextLayoutResult)) {
return null
}
- // TODO(b/157474582): Note now it only works for single Text/TextField until we
- // fix the merging issue.
- val textLayoutResults = mutableListOf<TextLayoutResult>()
- val textLayoutResult: TextLayoutResult
- val getLayoutResult = node.unmergedConfig[SemanticsActions.GetTextLayoutResult]
- .action?.invoke(textLayoutResults)
- if (getLayoutResult == true) {
- textLayoutResult = textLayoutResults[0]
- } else {
- return null
- }
+ val textLayoutResult = getTextLayoutResult(node.unmergedConfig) ?: return null
if (granularity == AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_LINE) {
iterator = AccessibilityIterators.LineTextSegmentIterator.getInstance()
iterator.initialize(text, textLayoutResult)
diff --git a/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/AutofillIdCompat.java b/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/AutofillIdCompat.java
new file mode 100644
index 0000000..53360b6
--- /dev/null
+++ b/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/AutofillIdCompat.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.platform.coreshims;
+
+import android.view.autofill.AutofillId;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Helper for accessing features in {@link AutofillId}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class AutofillIdCompat {
+ // Only guaranteed to be non-null on SDK_INT >= 26.
+ private final Object mWrappedObj;
+
+ @RequiresApi(26)
+ private AutofillIdCompat(@NonNull AutofillId obj) {
+ mWrappedObj = obj;
+ }
+
+ /**
+ * Provides a backward-compatible wrapper for {@link AutofillId}.
+ * <p>
+ * This method is not supported on devices running SDK < 26 since the platform
+ * class will not be available.
+ *
+ * @param autofillId platform class to wrap
+ * @return wrapped class
+ */
+ @RequiresApi(26)
+ @NonNull
+ public static AutofillIdCompat toAutofillIdCompat(@NonNull AutofillId autofillId) {
+ return new AutofillIdCompat(autofillId);
+ }
+
+ /**
+ * Provides the {@link AutofillId} represented by this object.
+ * <p>
+ * This method is not supported on devices running SDK < 26 since the platform
+ * class will not be available.
+ *
+ * @return platform class object
+ * @see AutofillIdCompat#toAutofillIdCompat(AutofillId)
+ */
+ @RequiresApi(26)
+ @NonNull
+ public AutofillId toAutofillId() {
+ return (AutofillId) mWrappedObj;
+ }
+}
diff --git a/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/ContentCaptureSessionCompat.java b/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/ContentCaptureSessionCompat.java
new file mode 100644
index 0000000..788ae0b
--- /dev/null
+++ b/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/ContentCaptureSessionCompat.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.platform.coreshims;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewStructure;
+import android.view.autofill.AutofillId;
+import android.view.contentcapture.ContentCaptureSession;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Helper for accessing features in {@link ContentCaptureSession}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class ContentCaptureSessionCompat {
+
+ private static final String KEY_VIEW_TREE_APPEARING = "TREAT_AS_VIEW_TREE_APPEARING";
+ private static final String KEY_VIEW_TREE_APPEARED = "TREAT_AS_VIEW_TREE_APPEARED";
+ // Only guaranteed to be non-null on SDK_INT >= 29.
+ private final Object mWrappedObj;
+ private final View mView;
+
+ /**
+ * Provides a backward-compatible wrapper for {@link ContentCaptureSession}.
+ * <p>
+ * This method is not supported on devices running SDK < 29 since the platform
+ * class will not be available.
+ *
+ * @param contentCaptureSession platform class to wrap
+ * @param host view hosting the session.
+ * @return wrapped class
+ */
+ @RequiresApi(29)
+ @NonNull
+ public static ContentCaptureSessionCompat toContentCaptureSessionCompat(
+ @NonNull ContentCaptureSession contentCaptureSession, @NonNull View host) {
+ return new ContentCaptureSessionCompat(contentCaptureSession, host);
+ }
+
+ /**
+ * Provides the {@link ContentCaptureSession} represented by this object.
+ * <p>
+ * This method is not supported on devices running SDK < 29 since the platform
+ * class will not be available.
+ *
+ * @return platform class object
+ * @see ContentCaptureSessionCompat#toContentCaptureSessionCompat(ContentCaptureSession, View)
+ */
+ @RequiresApi(29)
+ @NonNull
+ public ContentCaptureSession toContentCaptureSession() {
+ return (ContentCaptureSession) mWrappedObj;
+ }
+
+ /**
+ * Creates a {@link ContentCaptureSessionCompat} instance.
+ *
+ * @param contentCaptureSession {@link ContentCaptureSession} for this host View.
+ * @param host view hosting the session.
+ */
+ @RequiresApi(29)
+ private ContentCaptureSessionCompat(@NonNull ContentCaptureSession contentCaptureSession,
+ @NonNull View host) {
+ this.mWrappedObj = contentCaptureSession;
+ this.mView = host;
+ }
+
+ /**
+ * Creates a new {@link AutofillId} for a virtual child, so it can be used to uniquely identify
+ * the children in the session.
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>SDK 29 and above, this method matches platform behavior.
+ * <li>SDK 28 and below, this method returns null.
+ * </ul>
+ *
+ * @param virtualChildId id of the virtual child, relative to the parent.
+ *
+ * @return {@link AutofillId} for the virtual child
+ */
+ @Nullable
+ public AutofillId newAutofillId(long virtualChildId) {
+ if (SDK_INT >= 29) {
+ return Api29Impl.newAutofillId(
+ (ContentCaptureSession) mWrappedObj,
+ Objects.requireNonNull(ViewCompatShims.getAutofillId(mView)).toAutofillId(),
+ virtualChildId);
+ }
+ return null;
+ }
+
+ /**
+ * Creates a {@link ViewStructure} for a "virtual" view, so it can be passed to
+ * {@link #notifyViewsAppeared} by the view managing the virtual view hierarchy.
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>SDK 29 and above, this method matches platform behavior.
+ * <li>SDK 28 and below, this method returns null.
+ * </ul>
+ *
+ * @param parentId id of the virtual view parent (it can be obtained by calling
+ * {@link ViewStructure#getAutofillId()} on the parent).
+ * @param virtualId id of the virtual child, relative to the parent.
+ *
+ * @return a new {@link ViewStructure} that can be used for Content Capture purposes.
+ */
+ @Nullable
+ public ViewStructureCompat newVirtualViewStructure(
+ @NonNull AutofillId parentId, long virtualId) {
+ if (SDK_INT >= 29) {
+ return ViewStructureCompat.toViewStructureCompat(
+ Api29Impl.newVirtualViewStructure(
+ (ContentCaptureSession) mWrappedObj, parentId, virtualId));
+ }
+ return null;
+ }
+
+ /**
+ * Notifies the Content Capture Service that a list of nodes has appeared in the view structure.
+ *
+ * <p>Typically called manually by views that handle their own virtual view hierarchy.
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>SDK 34 and above, this method matches platform behavior.
+ * <li>SDK 29 through 33, this method is a best-effort to match platform behavior, by
+ * wrapping the virtual children with a pair of special view appeared events.
+ * <li>SDK 28 and below, this method does nothing.
+ *
+ * @param appearedNodes nodes that have appeared. Each element represents a view node that has
+ * been added to the view structure. The order of the elements is important, which should be
+ * preserved as the attached order of when the node is attached to the virtual view hierarchy.
+ */
+ public void notifyViewsAppeared(@NonNull List<ViewStructure> appearedNodes) {
+ if (SDK_INT >= 34) {
+ Api34Impl.notifyViewsAppeared((ContentCaptureSession) mWrappedObj, appearedNodes);
+ } else if (SDK_INT >= 29) {
+ ViewStructure treeAppearing = Api29Impl.newViewStructure(
+ (ContentCaptureSession) mWrappedObj, mView);
+ Api23Impl.getExtras(treeAppearing).putBoolean(KEY_VIEW_TREE_APPEARING, true);
+ Api29Impl.notifyViewAppeared((ContentCaptureSession) mWrappedObj, treeAppearing);
+
+ for (int i = 0; i < appearedNodes.size(); i++) {
+ Api29Impl.notifyViewAppeared(
+ (ContentCaptureSession) mWrappedObj, appearedNodes.get(i));
+ }
+
+ ViewStructure treeAppeared = Api29Impl.newViewStructure(
+ (ContentCaptureSession) mWrappedObj, mView);
+ Api23Impl.getExtras(treeAppeared).putBoolean(KEY_VIEW_TREE_APPEARED, true);
+ Api29Impl.notifyViewAppeared((ContentCaptureSession) mWrappedObj, treeAppeared);
+ }
+ }
+
+ /**
+ * Notifies the Content Capture Service that many nodes has been removed from a virtual view
+ * structure.
+ *
+ * <p>Should only be called by views that handle their own virtual view hierarchy.
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>SDK 34 and above, this method matches platform behavior.
+ * <li>SDK 29 through 33, this method is a best-effort to match platform behavior, by
+ * wrapping the virtual children with a pair of special view appeared events.
+ * <li>SDK 28 and below, this method does nothing.
+ * </ul>
+ *
+ * @param virtualIds ids of the virtual children.
+ */
+ public void notifyViewsDisappeared(@NonNull long[] virtualIds) {
+ if (SDK_INT >= 34) {
+ Api29Impl.notifyViewsDisappeared(
+ (ContentCaptureSession) mWrappedObj,
+ Objects.requireNonNull(ViewCompatShims.getAutofillId(mView)).toAutofillId(),
+ virtualIds);
+ } else if (SDK_INT >= 29) {
+ ViewStructure treeAppearing = Api29Impl.newViewStructure(
+ (ContentCaptureSession) mWrappedObj, mView);
+ Api23Impl.getExtras(treeAppearing).putBoolean(KEY_VIEW_TREE_APPEARING, true);
+ Api29Impl.notifyViewAppeared((ContentCaptureSession) mWrappedObj, treeAppearing);
+
+ Api29Impl.notifyViewsDisappeared(
+ (ContentCaptureSession) mWrappedObj,
+ Objects.requireNonNull(ViewCompatShims.getAutofillId(mView)).toAutofillId(),
+ virtualIds);
+
+ ViewStructure treeAppeared = Api29Impl.newViewStructure(
+ (ContentCaptureSession) mWrappedObj, mView);
+ Api23Impl.getExtras(treeAppeared).putBoolean(KEY_VIEW_TREE_APPEARED, true);
+ Api29Impl.notifyViewAppeared((ContentCaptureSession) mWrappedObj, treeAppeared);
+ }
+ }
+
+ /**
+ * Notifies the Intelligence Service that the value of a text node has been changed.
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>SDK 29 and above, this method matches platform behavior.
+ * <li>SDK 28 and below, this method does nothing.
+ * </ul>
+ *
+ * @param id of the node.
+ * @param text new text.
+ */
+ public void notifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) {
+ if (SDK_INT >= 29) {
+ Api29Impl.notifyViewTextChanged((ContentCaptureSession) mWrappedObj, id, text);
+ }
+ }
+
+ @RequiresApi(34)
+ private static class Api34Impl {
+ private Api34Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static void notifyViewsAppeared(
+ ContentCaptureSession contentCaptureSession, List<ViewStructure> appearedNodes) {
+ // new API in U
+ contentCaptureSession.notifyViewsAppeared(appearedNodes);
+ }
+ }
+ @RequiresApi(29)
+ private static class Api29Impl {
+ private Api29Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static void notifyViewsDisappeared(
+ ContentCaptureSession contentCaptureSession, AutofillId hostId, long[] virtualIds) {
+ contentCaptureSession.notifyViewsDisappeared(hostId, virtualIds);
+ }
+
+ @DoNotInline
+ static void notifyViewAppeared(
+ ContentCaptureSession contentCaptureSession, ViewStructure node) {
+ contentCaptureSession.notifyViewAppeared(node);
+ }
+ @DoNotInline
+ static ViewStructure newViewStructure(
+ ContentCaptureSession contentCaptureSession, View view) {
+ return contentCaptureSession.newViewStructure(view);
+ }
+
+ @DoNotInline
+ static ViewStructure newVirtualViewStructure(ContentCaptureSession contentCaptureSession,
+ AutofillId parentId, long virtualId) {
+ return contentCaptureSession.newVirtualViewStructure(parentId, virtualId);
+ }
+
+
+ @DoNotInline
+ static AutofillId newAutofillId(ContentCaptureSession contentCaptureSession,
+ AutofillId hostId, long virtualChildId) {
+ return contentCaptureSession.newAutofillId(hostId, virtualChildId);
+ }
+
+ @DoNotInline
+ public static void notifyViewTextChanged(ContentCaptureSession contentCaptureSession,
+ AutofillId id, CharSequence charSequence) {
+ contentCaptureSession.notifyViewTextChanged(id, charSequence);
+
+ }
+ }
+ @RequiresApi(23)
+ private static class Api23Impl {
+ private Api23Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static Bundle getExtras(ViewStructure viewStructure) {
+ return viewStructure.getExtras();
+ }
+
+ }
+}
diff --git a/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/ViewCompatShims.java b/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/ViewCompatShims.java
new file mode 100644
index 0000000..f94c668
--- /dev/null
+++ b/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/ViewCompatShims.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.platform.coreshims;
+
+import android.os.Build;
+import android.view.View;
+import android.view.autofill.AutofillId;
+import android.view.contentcapture.ContentCaptureSession;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Helper for accessing features in {@link View}.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class ViewCompatShims {
+ private ViewCompatShims() {
+ // This class is not instantiable.
+ }
+
+ @IntDef({
+ IMPORTANT_FOR_CONTENT_CAPTURE_AUTO,
+ IMPORTANT_FOR_CONTENT_CAPTURE_YES,
+ IMPORTANT_FOR_CONTENT_CAPTURE_NO,
+ IMPORTANT_FOR_CONTENT_CAPTURE_YES_EXCLUDE_DESCENDANTS,
+ IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface ImportantForContentCapture {}
+
+ /**
+ * Automatically determine whether a view is important for content capture.
+ */
+ public static final int IMPORTANT_FOR_CONTENT_CAPTURE_AUTO = 0x0;
+
+ /**
+ * The view is important for content capture, and its children (if any) will be traversed.
+ */
+ public static final int IMPORTANT_FOR_CONTENT_CAPTURE_YES = 0x1;
+
+ /**
+ * The view is not important for content capture, but its children (if any) will be traversed.
+ */
+ public static final int IMPORTANT_FOR_CONTENT_CAPTURE_NO = 0x2;
+
+ /**
+ * The view is important for content capture, but its children (if any) will not be traversed.
+ */
+ public static final int IMPORTANT_FOR_CONTENT_CAPTURE_YES_EXCLUDE_DESCENDANTS = 0x4;
+
+ /**
+ * The view is not important for content capture, and its children (if any) will not be
+ * traversed.
+ */
+ public static final int IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS = 0x8;
+
+ /**
+ * Sets the mode for determining whether this view is considered important for content capture.
+ *
+ * <p>The platform determines the importance for autofill automatically but you
+ * can use this method to customize the behavior. Typically, a view that provides text should
+ * be marked as {@link #IMPORTANT_FOR_CONTENT_CAPTURE_YES}.
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>SDK 30 and above, this method matches platform behavior.
+ * <li>SDK 29 and below, this method does nothing.
+ * </ul>
+ *
+ * @param v The View against which to invoke the method.
+ * @param mode {@link #IMPORTANT_FOR_CONTENT_CAPTURE_AUTO},
+ * {@link #IMPORTANT_FOR_CONTENT_CAPTURE_YES}, {@link #IMPORTANT_FOR_CONTENT_CAPTURE_NO},
+ * {@link #IMPORTANT_FOR_CONTENT_CAPTURE_YES_EXCLUDE_DESCENDANTS},
+ * or {@link #IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS}.
+ *
+ * @attr ref android.R.styleable#View_importantForContentCapture
+ */
+ public static void setImportantForContentCapture(@NonNull View v,
+ @ImportantForContentCapture int mode) {
+ if (Build.VERSION.SDK_INT >= 30) {
+ Api30Impl.setImportantForContentCapture(v, mode);
+ }
+ }
+
+ /**
+ * Gets the session used to notify content capture events.
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>SDK 29 and above, this method matches platform behavior.
+ * <li>SDK 28 and below, this method always return null.
+ * </ul>
+ *
+ * @param v The View against which to invoke the method.
+ * @return session explicitly set by {@link #setContentCaptureSession(ContentCaptureSession)},
+ * inherited by ancestors, default session or {@code null} if content capture is disabled for
+ * this view.
+ */
+ @Nullable
+ public static ContentCaptureSessionCompat getContentCaptureSession(@NonNull View v) {
+ if (Build.VERSION.SDK_INT >= 29) {
+ ContentCaptureSession session = Api29Impl.getContentCaptureSession(v);
+ if (session == null) {
+ return null;
+ }
+ return ContentCaptureSessionCompat.toContentCaptureSessionCompat(session, v);
+ }
+ return null;
+ }
+
+ /**
+ * Gets the unique, logical identifier of this view in the activity, for autofill purposes.
+ *
+ * <p>The autofill id is created on demand, unless it is explicitly set by
+ * {@link #setAutofillId(AutofillId)}.
+ *
+ * <p>See {@link #setAutofillId(AutofillId)} for more info.
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>SDK 26 and above, this method matches platform behavior.
+ * <li>SDK 25 and below, this method always return null.
+ * </ul>
+ *
+ * @param v The View against which to invoke the method.
+ * @return The View's autofill id.
+ */
+ @Nullable
+ public static AutofillIdCompat getAutofillId(@NonNull View v) {
+ if (Build.VERSION.SDK_INT >= 26) {
+ return AutofillIdCompat.toAutofillIdCompat(Api26Impl.getAutofillId(v));
+ }
+ return null;
+ }
+
+ @RequiresApi(26)
+ static class Api26Impl {
+ private Api26Impl() {
+ // This class is not instantiable.
+ }
+ @DoNotInline
+ public static AutofillId getAutofillId(View view) {
+ return view.getAutofillId();
+ }
+ }
+
+ @RequiresApi(29)
+ private static class Api29Impl {
+ private Api29Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static ContentCaptureSession getContentCaptureSession(View view) {
+ return view.getContentCaptureSession();
+ }
+ }
+
+ @RequiresApi(30)
+ private static class Api30Impl {
+ private Api30Impl() {
+ // This class is not instantiable.
+ }
+ @DoNotInline
+ static void setImportantForContentCapture(View view, int mode) {
+ view.setImportantForContentCapture(mode);
+ }
+ }
+}
diff --git a/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/ViewStructureCompat.java b/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/ViewStructureCompat.java
new file mode 100644
index 0000000..3ffcfc2
--- /dev/null
+++ b/compose/ui/ui/src/main/java/androidx/compose/ui/platform/coreshims/ViewStructureCompat.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.platform.coreshims;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import android.view.ViewStructure;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Helper for accessing features in {@link ViewStructure}.
+ * <p>
+ * Currently this helper class only has features for content capture usage. Other features for
+ * Autofill are not available.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class ViewStructureCompat {
+
+ // Only guaranteed to be non-null on SDK_INT >= 23.
+ private final Object mWrappedObj;
+
+ /**
+ * Provides a backward-compatible wrapper for {@link ViewStructure}.
+ * <p>
+ * This method is not supported on devices running SDK < 23 since the platform
+ * class will not be available.
+ *
+ * @param contentCaptureSession platform class to wrap
+ * @return wrapped class
+ */
+ @RequiresApi(23)
+ @NonNull
+ public static ViewStructureCompat toViewStructureCompat(
+ @NonNull ViewStructure contentCaptureSession) {
+ return new ViewStructureCompat(contentCaptureSession);
+ }
+
+ /**
+ * Provides the {@link ViewStructure} represented by this object.
+ * <p>
+ * This method is not supported on devices running SDK < 23 since the platform
+ * class will not be available.
+ *
+ * @return platform class object
+ * @see ViewStructureCompat#toViewStructureCompat(ViewStructure)
+ */
+ @RequiresApi(23)
+ @NonNull
+ public ViewStructure toViewStructure() {
+ return (ViewStructure) mWrappedObj;
+ }
+
+ private ViewStructureCompat(@NonNull ViewStructure viewStructure) {
+ this.mWrappedObj = viewStructure;
+ }
+
+ /**
+ * Set the text that is associated with this view. There is no selection
+ * associated with the text. The text may have style spans to supply additional
+ * display and semantic information.
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>SDK 23 and above, this method matches platform behavior.
+ * <li>SDK 22 and below, this method does nothing.
+ * </ul>
+ */
+ public void setText(@NonNull CharSequence charSequence) {
+ if (SDK_INT >= 23) {
+ Api23Impl.setText((ViewStructure) mWrappedObj, charSequence);
+ }
+ }
+
+ /**
+ * Set the class name of the view, as per
+ * {@link android.view.View#getAccessibilityClassName View.getAccessibilityClassName()}.
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>SDK 23 and above, this method matches platform behavior.
+ * <li>SDK 22 and below, this method does nothing.
+ * </ul>
+ */
+ public void setClassName(@NonNull String string) {
+ if (SDK_INT >= 23) {
+ Api23Impl.setClassName((ViewStructure) mWrappedObj, string);
+ }
+ }
+
+ /**
+ * Explicitly set default global style information for text that was previously set with
+ * {@link #setText}.
+ *
+ * @param size The size, in pixels, of the text.
+ * @param fgColor The foreground color, packed as 0xAARRGGBB.
+ * @param bgColor The background color, packed as 0xAARRGGBB.
+ * @param style Style flags, as defined by {@link android.app.assist.AssistStructure.ViewNode}.
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>SDK 23 and above, this method matches platform behavior.
+ * <li>SDK 22 and below, this method does nothing.
+ * </ul>
+ */
+ public void setTextStyle(float size, int fgColor, int bgColor, int style) {
+ if (SDK_INT >= 23) {
+ Api23Impl.setTextStyle((ViewStructure) mWrappedObj, size, fgColor, bgColor, style);
+ }
+ }
+
+ /**
+ * Set the content description of the view, as per
+ * {@link android.view.View#getContentDescription View.getContentDescription()}.
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>SDK 23 and above, this method matches platform behavior.
+ * <li>SDK 22 and below, this method does nothing.
+ * </ul>
+ */
+ public void setContentDescription(@NonNull CharSequence charSequence) {
+ if (SDK_INT >= 23) {
+ Api23Impl.setContentDescription((ViewStructure) mWrappedObj, charSequence);
+ }
+ }
+
+ /**
+ * Set the basic dimensions of this view.
+ *
+ * @param left The view's left position, in pixels relative to its parent's left edge.
+ * @param top The view's top position, in pixels relative to its parent's top edge.
+ * @param scrollX How much the view's x coordinate space has been scrolled, in pixels.
+ * @param scrollY How much the view's y coordinate space has been scrolled, in pixels.
+ * @param width The view's visible width, in pixels. This is the width visible on screen,
+ * not the total data width of a scrollable view.
+ * @param height The view's visible height, in pixels. This is the height visible on
+ * screen, not the total data height of a scrollable view.
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>SDK 23 and above, this method matches platform behavior.
+ * <li>SDK 22 and below, this method does nothing.
+ * </ul>
+ */
+ public void setDimens(int left, int top, int scrollX, int scrollY, int width, int height) {
+ if (SDK_INT >= 23) {
+ Api23Impl.setDimens(
+ (ViewStructure) mWrappedObj, left, top, scrollX, scrollY, width, height);
+ }
+ }
+
+ @RequiresApi(23)
+ private static class Api23Impl {
+ private Api23Impl() {
+ // This class is not instantiable.
+ }
+
+ @DoNotInline
+ static void setDimens(ViewStructure viewStructure, int left, int top, int scrollX,
+ int scrollY, int width, int height) {
+ viewStructure.setDimens(left, top, scrollX, scrollY, width, height);
+ }
+
+ @DoNotInline
+ static void setText(ViewStructure viewStructure, CharSequence charSequence) {
+ viewStructure.setText(charSequence);
+ }
+
+ @DoNotInline
+ static void setClassName(ViewStructure viewStructure, String string) {
+ viewStructure.setClassName(string);
+ }
+
+ @DoNotInline
+ static void setContentDescription(ViewStructure viewStructure, CharSequence charSequence) {
+ viewStructure.setContentDescription(charSequence);
+ }
+
+ @DoNotInline
+ static void setTextStyle(
+ ViewStructure viewStructure, float size, int fgColor, int bgColor, int style) {
+ viewStructure.setTextStyle(size, fgColor, bgColor, style);
+ }
+ }
+}
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/PublicKeyCredentialControllerUtilityTest.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/PublicKeyCredentialControllerUtilityTest.kt
index dbcb914..ed80e40 100644
--- a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/PublicKeyCredentialControllerUtilityTest.kt
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/createpublickeycredential/PublicKeyCredentialControllerUtilityTest.kt
@@ -19,9 +19,11 @@
import androidx.credentials.GetPublicKeyCredentialOption
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
import com.google.android.gms.fido.fido2.api.common.ErrorCode
import com.google.common.truth.Truth.assertThat
import org.json.JSONArray
+import org.json.JSONException
import org.json.JSONObject
import org.junit.Test
import org.junit.runner.RunWith
@@ -389,4 +391,263 @@
assertThat(response.get(PublicKeyCredentialControllerUtility.JSON_KEY_TRANSPORTS))
.isEqualTo(JSONArray(transportArray))
}
+
+ @Test
+ fun convertJSON_requiredFields_success() {
+ var json =
+ JSONObject(
+ "{" +
+ "\"rp\": {" +
+ "\"id\": \"rpidvalue\"," +
+ "\"name\": \"Name of RP\"," +
+ "\"icon\": \"rpicon.png\"" +
+ "}," +
+ "\"pubKeyCredParams\": [{" +
+ "\"alg\": -7," +
+ "\"type\": \"public-key\"" +
+ "}]," +
+ "\"challenge\": \"dGVzdA==\"," +
+ "\"user\": {" +
+ "\"id\": \"idvalue\"," +
+ "\"name\": \"Name of User\"," +
+ "\"displayName\": \"Display Name of User\"," +
+ "\"icon\": \"icon.png\"" +
+ "}" +
+ "}"
+ )
+ var output = PublicKeyCredentialControllerUtility.convertJSON(json)
+
+ assertThat(output.getUser().getId()).isNotEmpty()
+ assertThat(output.getUser().getName()).isEqualTo("Name of User")
+ assertThat(output.getUser().getDisplayName()).isEqualTo("Display Name of User")
+ assertThat(output.getUser().getIcon()).isEqualTo("icon.png")
+ assertThat(output.getChallenge()).isNotEmpty()
+ assertThat(output.getRp().getId()).isNotEmpty()
+ assertThat(output.getRp().getName()).isEqualTo("Name of RP")
+ assertThat(output.getRp().getIcon()).isEqualTo("rpicon.png")
+ assertThat(output.getParameters().get(0).getAlgorithmIdAsInteger()).isEqualTo(-7)
+ assertThat(output.getParameters().get(0).getTypeAsString()).isEqualTo("public-key")
+ }
+
+ @Test
+ fun convertJSON_requiredFields_failOnMissingRpId() {
+ var json =
+ JSONObject(
+ "{" +
+ "\"rp\": {" +
+ "\"name\": \"Name of RP\"," +
+ "\"icon\": \"rpicon.png\"" +
+ "}," +
+ "\"pubKeyCredParams\": [{" +
+ "\"alg\": -7," +
+ "\"type\": \"public-key\"" +
+ "}]," +
+ "\"challenge\": \"dGVzdA==\"," +
+ "\"user\": {" +
+ "\"id\": \"idvalue\"," +
+ "\"name\": \"Name of User\"," +
+ "\"displayName\": \"Display Name of User\"," +
+ "\"icon\": \"icon.png\"" +
+ "}" +
+ "}"
+ )
+
+ assertThrows<JSONException> { PublicKeyCredentialControllerUtility.convertJSON(json) }
+ }
+
+ @Test
+ fun convertJSON_requiredFields_failOnMissingRpName() {
+ var json =
+ JSONObject(
+ "{" +
+ "\"rp\": {" +
+ "\"id\": \"rpidvalue\"," +
+ "\"icon\": \"rpicon.png\"" +
+ "}," +
+ "\"pubKeyCredParams\": [{" +
+ "\"alg\": -7," +
+ "\"type\": \"public-key\"" +
+ "}]," +
+ "\"challenge\": \"dGVzdA==\"," +
+ "\"user\": {" +
+ "\"id\": \"idvalue\"," +
+ "\"name\": \"Name of User\"," +
+ "\"displayName\": \"Display Name of User\"," +
+ "\"icon\": \"icon.png\"" +
+ "}" +
+ "}"
+ )
+
+ assertThrows<JSONException> { PublicKeyCredentialControllerUtility.convertJSON(json) }
+ }
+
+ @Test
+ fun convertJSON_requiredFields_failOnMissingRp() {
+ var json =
+ JSONObject(
+ "{" +
+ "\"pubKeyCredParams\": [{" +
+ "\"alg\": -7," +
+ "\"type\": \"public-key\"" +
+ "}]," +
+ "\"challenge\": \"dGVzdA==\"," +
+ "\"user\": {" +
+ "\"id\": \"idvalue\"," +
+ "\"name\": \"Name of User\"," +
+ "\"displayName\": \"Display Name of User\"," +
+ "\"icon\": \"icon.png\"" +
+ "}" +
+ "}"
+ )
+
+ assertThrows<JSONException> { PublicKeyCredentialControllerUtility.convertJSON(json) }
+ }
+
+ @Test
+ fun convertJSON_requiredFields_failOnMissingPubKeyCredParams() {
+ var json =
+ JSONObject(
+ "{" +
+ "\"rp\": {" +
+ "\"id\": \"rpidvalue\"," +
+ "\"name\": \"Name of RP\"," +
+ "\"icon\": \"rpicon.png\"" +
+ "}," +
+ "\"challenge\": \"dGVzdA==\"," +
+ "\"user\": {" +
+ "\"id\": \"idvalue\"," +
+ "\"name\": \"Name of User\"," +
+ "\"displayName\": \"Display Name of User\"," +
+ "\"icon\": \"icon.png\"" +
+ "}" +
+ "}"
+ )
+
+ assertThrows<JSONException> { PublicKeyCredentialControllerUtility.convertJSON(json) }
+ }
+
+ @Test
+ fun convertJSON_requiredFields_failOnMissingChallenge() {
+ var json =
+ JSONObject(
+ "{" +
+ "\"rp\": {" +
+ "\"id\": \"rpidvalue\"," +
+ "\"name\": \"Name of RP\"," +
+ "\"icon\": \"rpicon.png\"" +
+ "}," +
+ "\"pubKeyCredParams\": [{" +
+ "\"alg\": -7," +
+ "\"type\": \"public-key\"" +
+ "}]," +
+ "\"user\": {" +
+ "\"id\": \"idvalue\"," +
+ "\"name\": \"Name of User\"," +
+ "\"displayName\": \"Display Name of User\"," +
+ "\"icon\": \"icon.png\"" +
+ "}" +
+ "}"
+ )
+
+ assertThrows<JSONException> { PublicKeyCredentialControllerUtility.convertJSON(json) }
+ }
+
+ @Test
+ fun convertJSON_requiredFields_failOnMissingUser() {
+ var json =
+ JSONObject(
+ "{" +
+ "\"rp\": {" +
+ "\"id\": \"rpidvalue\"," +
+ "\"name\": \"Name of RP\"," +
+ "\"icon\": \"rpicon.png\"" +
+ "}," +
+ "\"pubKeyCredParams\": [{" +
+ "\"alg\": -7," +
+ "\"type\": \"public-key\"" +
+ "}]," +
+ "\"challenge\": \"dGVzdA==\"" +
+ "}"
+ )
+
+ assertThrows<JSONException> { PublicKeyCredentialControllerUtility.convertJSON(json) }
+ }
+
+ @Test
+ fun convertJSON_requiredFields_failOnMissingUserId() {
+ var json =
+ JSONObject(
+ "{" +
+ "\"rp\": {" +
+ "\"id\": \"rpidvalue\"," +
+ "\"name\": \"Name of RP\"," +
+ "\"icon\": \"rpicon.png\"" +
+ "}," +
+ "\"pubKeyCredParams\": [{" +
+ "\"alg\": -7," +
+ "\"type\": \"public-key\"" +
+ "}]," +
+ "\"challenge\": \"dGVzdA==\"," +
+ "\"user\": {" +
+ "\"name\": \"Name of User\"," +
+ "\"displayName\": \"Display Name of User\"," +
+ "\"icon\": \"icon.png\"" +
+ "}" +
+ "}"
+ )
+
+ assertThrows<JSONException> { PublicKeyCredentialControllerUtility.convertJSON(json) }
+ }
+
+ @Test
+ fun convertJSON_requiredFields_failOnMissingUserName() {
+ var json =
+ JSONObject(
+ "{" +
+ "\"rp\": {" +
+ "\"id\": \"rpidvalue\"," +
+ "\"name\": \"Name of RP\"," +
+ "\"icon\": \"rpicon.png\"" +
+ "}," +
+ "\"pubKeyCredParams\": [{" +
+ "\"alg\": -7," +
+ "\"type\": \"public-key\"" +
+ "}]," +
+ "\"challenge\": \"dGVzdA==\"," +
+ "\"user\": {" +
+ "\"id\": \"idvalue\"," +
+ "\"displayName\": \"Display Name of User\"," +
+ "\"icon\": \"icon.png\"" +
+ "}" +
+ "}"
+ )
+
+ assertThrows<JSONException> { PublicKeyCredentialControllerUtility.convertJSON(json) }
+ }
+
+ @Test
+ fun convertJSON_requiredFields_failOnMissingUserDisplayName() {
+ var json =
+ JSONObject(
+ "{" +
+ "\"rp\": {" +
+ "\"id\": \"rpidvalue\"," +
+ "\"name\": \"Name of RP\"," +
+ "\"icon\": \"rpicon.png\"" +
+ "}," +
+ "\"pubKeyCredParams\": [{" +
+ "\"alg\": -7," +
+ "\"type\": \"public-key\"" +
+ "}]," +
+ "\"challenge\": \"dGVzdA==\"," +
+ "\"user\": {" +
+ "\"id\": \"idvalue\"," +
+ "\"name\": \"Name of User\"," +
+ "\"icon\": \"icon.png\"" +
+ "}" +
+ "}"
+ )
+
+ assertThrows<JSONException> { PublicKeyCredentialControllerUtility.convertJSON(json) }
+ }
}
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
index a189e99..4b38772 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
@@ -118,8 +118,10 @@
*/
@JvmStatic
fun convert(request: CreatePublicKeyCredentialRequest): PublicKeyCredentialCreationOptions {
- val requestJson = request.requestJson
- val json = JSONObject(requestJson)
+ return convertJSON(JSONObject(request.requestJson))
+ }
+
+ internal fun convertJSON(json: JSONObject): PublicKeyCredentialCreationOptions {
val builder = PublicKeyCredentialCreationOptions.Builder()
parseRequiredChallengeAndUser(json, builder)
diff --git a/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/SimpleActorTest.kt b/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/SimpleActorTest.kt
index bb3b250..bd6186d 100644
--- a/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/SimpleActorTest.kt
+++ b/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/SimpleActorTest.kt
@@ -22,16 +22,19 @@
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.coroutineContext
+import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
+import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
@@ -40,7 +43,7 @@
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
-import org.junit.Ignore
+import kotlinx.coroutines.withTimeout
import org.junit.Rule
import org.junit.Test
import org.junit.rules.Timeout
@@ -73,16 +76,15 @@
assertThat(msgs).isEqualTo(listOf(1, 2, 3, 4))
}
- @Ignore("b/281516026")
@Test
fun testOnCompleteIsCalledWhenScopeIsCancelled() = runBlocking<Unit> {
val scope = CoroutineScope(Job())
- val called = AtomicBoolean(false)
+ val called = CompletableDeferred<Unit>()
val actor = SimpleActor<Int>(
scope,
onComplete = {
- assertThat(called.compareAndSet(false, true)).isTrue()
+ called.complete(Unit)
},
onUndeliveredElement = { _, _ -> }
) {
@@ -93,7 +95,13 @@
scope.coroutineContext.job.cancelAndJoin()
- assertThat(called.get()).isTrue()
+ try {
+ withTimeout(5.seconds) {
+ called.await()
+ }
+ } catch (timeout: TimeoutCancellationException) {
+ throw AssertionError("on complete has not been called")
+ }
}
@Test
@@ -244,11 +252,9 @@
sender.await()
}
- @Ignore // b/250077079
@Test
fun testAllMessagesAreRespondedTo() = runBlocking<Unit> {
- val myScope =
- CoroutineScope(Job() + Executors.newFixedThreadPool(4).asCoroutineDispatcher())
+ val myScope = CoroutineScope(Job() + Dispatchers.IO)
val actorScope = CoroutineScope(Job())
val actor = SimpleActor<CompletableDeferred<Unit?>>(
@@ -261,23 +267,20 @@
it.complete(Unit)
}
- val waiters = myScope.async {
- repeat(100_000) { _ ->
- launch {
- try {
- CompletableDeferred<Unit?>().also {
- actor.offer(it)
- }.await()
- } catch (cancelled: CancellationException) {
- // This is OK
- }
+ val waiters = (0 until 10_000).map {
+ myScope.async {
+ try {
+ CompletableDeferred<Unit?>().also {
+ actor.offer(it)
+ }.await()
+ } catch (cancelled: CancellationException) {
+ // This is OK
}
}
}
-
delay(100)
actorScope.coroutineContext.job.cancelAndJoin()
- waiters.await()
+ waiters.awaitAll()
}
class TestElement(val name: String) : AbstractCoroutineContextElement(Key) {
diff --git a/development/importMaven/importMaven.sh b/development/importMaven/importMaven.sh
index 91a8fc8..6e4b96f 100755
--- a/development/importMaven/importMaven.sh
+++ b/development/importMaven/importMaven.sh
@@ -3,7 +3,18 @@
set -e
WORKING_DIR=`pwd`
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
+ALLOW_JETBRAINS_DEV="false"
+
+for arg in "$@"
+do
+ if [ "$arg" == "--allow-jetbrains-dev" ]; then
+ ALLOW_JETBRAINS_DEV="true"
+ fi
+done
+
# build importMaven
-(cd $SCRIPT_DIR && ./gradlew installDist -q)
+(cd $SCRIPT_DIR && ./gradlew installDist -q -Pandroidx.allowJetbrainsDev="$ALLOW_JETBRAINS_DEV")
+
+
# execute the output binary
(SUPPORT_REPO=$SCRIPT_DIR/../.. $SCRIPT_DIR/build/install/importMaven/bin/importMaven $@)
\ No newline at end of file
diff --git a/development/importMaven/settings.gradle.kts b/development/importMaven/settings.gradle.kts
index 3813656..92b125d 100644
--- a/development/importMaven/settings.gradle.kts
+++ b/development/importMaven/settings.gradle.kts
@@ -19,6 +19,10 @@
mavenCentral()
gradlePluginPortal()
google()
+
+ if (settings.extra.get("androidx.allowJetbrainsDev") == "true") {
+ maven(url = "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev/")
+ }
}
}
@@ -27,6 +31,10 @@
mavenCentral()
google()
gradlePluginPortal()
+
+ if (settings.extra.get("androidx.allowJetbrainsDev") == "true") {
+ maven(url = "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev/")
+ }
}
versionCatalogs {
create("importMavenLibs") {
diff --git a/development/plot-benchmarks/src/lib/Session.svelte b/development/plot-benchmarks/src/lib/Session.svelte
index d4307ee..25576af 100644
--- a/development/plot-benchmarks/src/lib/Session.svelte
+++ b/development/plot-benchmarks/src/lib/Session.svelte
@@ -146,22 +146,24 @@
async function handleFileDragDrop(event: DragEvent) {
const items = [...event.dataTransfer.items];
- const newFiles: FileMetadata[] = [];
if (items) {
- for (let i = 0; i < items.length; i += 1) {
- if (items[i].kind === "file") {
- const file = items[i].getAsFile();
- if (file.name.endsWith(".json")) {
+ let newFiles = await Promise.all(
+ items
+ .filter(
+ (item) =>
+ item.kind === "file" && item.getAsFile().name.endsWith(".json")
+ )
+ .map(async (item) => {
+ const file = item.getAsFile();
const benchmarks = await readBenchmarks(file);
const entry: FileMetadata = {
enabled: true,
file: file,
container: benchmarks,
};
- newFiles.push(entry);
- }
- }
- }
+ return entry;
+ })
+ );
// Deep copy & notify
eventDispatcher("entries", [...fileEntries, ...newFiles]);
}
diff --git a/development/update_kotlin.sh b/development/update_kotlin.sh
index a5c3673..ec61013 100755
--- a/development/update_kotlin.sh
+++ b/development/update_kotlin.sh
@@ -1,21 +1,27 @@
#!/bin/bash
set -e
-export KOTLIN_VERSION="1.9.0-dev-6188"
-
-# Download and place konan
-export KONAN_DIR=../../prebuilts/androidx/konan/nativeCompilerPrebuilts/dev/$KOTLIN_VERSION/linux-x86_64
-mkdir -p $KONAN_DIR
-curl -o $KONAN_DIR/kotlin-native-prebuilt-linux-x86_64-$KOTLIN_VERSION.tar.gz https://download-cf.jetbrains.com/kotlin/native/builds/dev/$KOTLIN_VERSION/linux-x86_64/kotlin-native-prebuilt-linux-x86_64-$KOTLIN_VERSION.tar.gz
+KOTLIN_VERSION="$1"
# Download maven artifacts
ARTIFACTS_TO_DOWNLOAD="org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION,"
ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin:$KOTLIN_VERSION,"
ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin:$KOTLIN_VERSION,"
-ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin:kotlinx-serialization-compiler-plugin-embeddable:$KOTLIN_VERSION,"
+ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin:kotlin-serialization-compiler-plugin-embeddable:$KOTLIN_VERSION,"
ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin:kotlin-test:$KOTLIN_VERSION,"
ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin:kotlin-test-junit:$KOTLIN_VERSION,"
+ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin:kotlin-stdlib-common:$KOTLIN_VERSION,"
ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin:kotlin-stdlib-jdk8:$KOTLIN_VERSION,"
-ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:$KOTLIN_VERSION"
+ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:$KOTLIN_VERSION,"
+ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin:kotlin-compiler:$KOTLIN_VERSION,"
+ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin:kotlin-compiler-embeddable:$KOTLIN_VERSION,"
+ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin:kotlin-annotation-processing-embeddable:$KOTLIN_VERSION,"
+ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin:kotlin-parcelize-runtime:$KOTLIN_VERSION,"
+ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin:kotlin-annotation-processing-gradle:$KOTLIN_VERSION,"
+ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin:kotlin-parcelize-compiler:$KOTLIN_VERSION,"
+ARTIFACTS_TO_DOWNLOAD+="org.jetbrains.kotlin:kotlin-bom:$KOTLIN_VERSION,"
./development/importMaven/importMaven.sh --allow-jetbrains-dev "$ARTIFACTS_TO_DOWNLOAD"
+
+# Import konan binaries
+./development/importMaven/importMaven.sh import-konan-binaries --konan-compiler-version "$KOTLIN_VERSION"
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index eabfc57..d65c0f9 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -87,6 +87,7 @@
samples(project(":compose:foundation:foundation:foundation-samples"))
kmpDocs(project(":compose:material3:material3"))
kmpDocs(project(":compose:material3:material3-adaptive"))
+ samples(project(":compose:material3:material3-adaptive:material3-adaptive-samples"))
samples(project(":compose:material3:material3:material3-samples"))
kmpDocs(project(":compose:material3:material3-window-size-class"))
samples(project(":compose:material3:material3-window-size-class:material3-window-size-class-samples"))
@@ -190,7 +191,9 @@
docs(project(":fragment:fragment-testing"))
docs(project(":glance:glance"))
docs(project(":glance:glance-appwidget"))
+ docs(project(":glance:glance-appwidget-testing"))
samples(project(":glance:glance-appwidget:glance-appwidget-samples"))
+ samples(project(":glance:glance-appwidget-testing:glance-appwidget-testing-samples"))
docs(project(":glance:glance-appwidget-preview"))
docs(project(":glance:glance-preview"))
docs(project(":glance:glance-testing"))
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionAnimTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionAnimTest.kt
index 91055a8..da7111d 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionAnimTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionAnimTest.kt
@@ -320,9 +320,9 @@
.commit()
executePendingTransactions()
- assertThat(fragment2.startTransitionCountDownLatch.await(1000, TimeUnit.MILLISECONDS))
+ // We need to wait for the exit transitions to end
+ assertThat(fragment2.endTransitionCountDownLatch.await(1000, TimeUnit.MILLISECONDS))
.isTrue()
- // We need to wait for the exit animation to end
assertThat(
fragment1.endTransitionCountDownLatch.await(
1000,
@@ -340,7 +340,7 @@
dispatcher.dispatchOnBackProgressed(
BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
)
- dispatcher.onBackPressed()
+ withActivity { dispatcher.onBackPressed() }
executePendingTransactions()
fragment1.waitForTransition()
@@ -405,12 +405,7 @@
companion object {
@JvmStatic
@Parameterized.Parameters(name = "ordering={0}")
- fun data() = mutableListOf<Array<Any>>().apply {
- arrayOf(
- Ordered,
- Reordered
- )
- }
+ fun data() = arrayOf(Ordered, Reordered)
@AnimRes
private val ENTER = 1
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
index 4107c70..6240244 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
@@ -89,11 +89,10 @@
}
// Start transition special effects
- val startedTransitions = createTransitionEffect(transitions, isPop, firstOut, lastIn)
- val startedAnyTransition = startedTransitions.containsValue(true)
+ createTransitionEffect(transitions, isPop, firstOut, lastIn)
// Collect Animation and Animator Effects
- collectAnimEffects(animations, startedAnyTransition, startedTransitions)
+ collectAnimEffects(animations)
}
/**
@@ -114,12 +113,11 @@
}
@SuppressLint("NewApi", "PrereleaseSdkCoreDependency")
- private fun collectAnimEffects(
- animationInfos: List<AnimationInfo>,
- startedAnyTransition: Boolean,
- startedTransitions: Map<Operation, Boolean>
- ) {
+ private fun collectAnimEffects(animationInfos: List<AnimationInfo>) {
val animationsToRun = mutableListOf<AnimationInfo>()
+ val startedAnyTransition = animationInfos.flatMap {
+ it.operation.effects
+ }.isNotEmpty()
var startedAnyAnimator = false
// Find all Animators and add the effect to the operation
for (animatorInfo: AnimationInfo in animationInfos) {
@@ -139,7 +137,7 @@
// First make sure we haven't already started a Transition for this Operation
val fragment = operation.fragment
- val startedTransition = startedTransitions[operation] == true
+ val startedTransition = operation.effects.isNotEmpty()
if (startedTransition) {
if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
Log.v(FragmentManager.TAG,
@@ -189,10 +187,7 @@
isPop: Boolean,
firstOut: Operation?,
lastIn: Operation?
- ): Map<Operation, Boolean> {
- // Start transition special effects
- val startedTransitions = mutableMapOf<Operation, Boolean>()
-
+ ) {
// First verify that we can run all transitions together
val transitionImpl = transitionInfos.filterNot { transitionInfo ->
// If there is no change in visibility, we can skip the TransitionInfo
@@ -208,14 +203,8 @@
"type than other Fragments."
}
handlingImpl
- }
- if (transitionImpl == null) {
- // There were no transitions at all so we can just complete all of them
- for (transitionInfo: TransitionInfo in transitionInfos) {
- startedTransitions[transitionInfo.operation] = false
- }
- return startedTransitions
- }
+ } ?: // Early return if there were no transitions at all
+ return
// Now find the shared element transition if it exists
var sharedElementTransition: Any? = null
@@ -357,14 +346,12 @@
val transitionEffect = TransitionEffect(
transitionInfos, firstOut, lastIn, transitionImpl, sharedElementTransition,
sharedElementFirstOutViews, sharedElementLastInViews, sharedElementNameMapping,
- enteringNames, exitingNames, firstOutViews, lastInViews, isPop, startedTransitions
+ enteringNames, exitingNames, firstOutViews, lastInViews, isPop
)
transitionInfos.forEach { transitionInfo ->
transitionInfo.operation.addEffect(transitionEffect)
}
-
- return startedTransitions
}
/**
@@ -709,8 +696,7 @@
val exitingNames: ArrayList<String>,
val firstOutViews: ArrayMap<String, View>,
val lastInViews: ArrayMap<String, View>,
- val isPop: Boolean,
- val startedTransitions: MutableMap<Operation, Boolean>
+ val isPop: Boolean
) : Effect() {
val transitionSignal = CancellationSignal()
@@ -775,10 +761,6 @@
// runs directly after the swap
transitionImpl.scheduleRemoveTargets(sharedElementTransition, null, null,
null, null, sharedElementTransition, sharedElementLastInViews)
- // Both the firstOut and lastIn Operations are now associated
- // with a Transition
- startedTransitions[firstOut] = true
- startedTransitions[lastIn] = true
}
}
}
@@ -792,7 +774,6 @@
val operation: Operation = transitionInfo.operation
if (transitionInfo.isVisibilityUnchanged) {
// No change in visibility, so we can immediately complete the transition
- startedTransitions[transitionInfo.operation] = false
transitionInfo.operation.completeEffect(this)
continue
}
@@ -805,7 +786,6 @@
// Only complete the transition if this fragment isn't involved
// in the shared element transition (as otherwise we need to wait
// for that to finish)
- startedTransitions[operation] = false
transitionInfo.operation.completeEffect(this)
}
} else {
@@ -856,7 +836,6 @@
} else {
transitionImpl.setEpicenter(transition, firstOutEpicenterView)
}
- startedTransitions[operation] = true
// Now determine how this transition should be merged together
if (transitionInfo.isOverlapAllowed) {
// Overlap is allowed, so add them to the mergeTransition set
diff --git a/glance/glance-appwidget-testing/api/current.txt b/glance/glance-appwidget-testing/api/current.txt
new file mode 100644
index 0000000..4c77ba8
--- /dev/null
+++ b/glance/glance-appwidget-testing/api/current.txt
@@ -0,0 +1,24 @@
+// Signature format: 4.0
+package androidx.glance.appwidget.testing.unit {
+
+ public sealed interface GlanceAppWidgetUnitTest extends androidx.glance.testing.GlanceNodeAssertionsProvider<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> {
+ method public void awaitIdle();
+ method public void provideComposable(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+ method public void setAppWidgetSize(long size);
+ method public void setContext(android.content.Context context);
+ method public <T> void setState(T state);
+ }
+
+ public final class GlanceAppWidgetUnitTestDefaults {
+ method public androidx.glance.GlanceId glanceId();
+ method public int hostCategory();
+ method public long size();
+ field public static final androidx.glance.appwidget.testing.unit.GlanceAppWidgetUnitTestDefaults INSTANCE;
+ }
+
+ public final class GlanceAppWidgetUnitTestKt {
+ method public static void runGlanceAppWidgetUnitTest(optional long timeout, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.testing.unit.GlanceAppWidgetUnitTest,kotlin.Unit> block);
+ }
+
+}
+
diff --git a/glance/glance-appwidget-testing/api/res-current.txt b/glance/glance-appwidget-testing/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/glance/glance-appwidget-testing/api/res-current.txt
diff --git a/glance/glance-appwidget-testing/api/restricted_current.txt b/glance/glance-appwidget-testing/api/restricted_current.txt
new file mode 100644
index 0000000..4c77ba8
--- /dev/null
+++ b/glance/glance-appwidget-testing/api/restricted_current.txt
@@ -0,0 +1,24 @@
+// Signature format: 4.0
+package androidx.glance.appwidget.testing.unit {
+
+ public sealed interface GlanceAppWidgetUnitTest extends androidx.glance.testing.GlanceNodeAssertionsProvider<androidx.glance.testing.unit.MappedNode,androidx.glance.testing.unit.GlanceMappedNode> {
+ method public void awaitIdle();
+ method public void provideComposable(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+ method public void setAppWidgetSize(long size);
+ method public void setContext(android.content.Context context);
+ method public <T> void setState(T state);
+ }
+
+ public final class GlanceAppWidgetUnitTestDefaults {
+ method public androidx.glance.GlanceId glanceId();
+ method public int hostCategory();
+ method public long size();
+ field public static final androidx.glance.appwidget.testing.unit.GlanceAppWidgetUnitTestDefaults INSTANCE;
+ }
+
+ public final class GlanceAppWidgetUnitTestKt {
+ method public static void runGlanceAppWidgetUnitTest(optional long timeout, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.testing.unit.GlanceAppWidgetUnitTest,kotlin.Unit> block);
+ }
+
+}
+
diff --git a/glance/glance-appwidget-testing/build.gradle b/glance/glance-appwidget-testing/build.gradle
new file mode 100644
index 0000000..8d5db0b
--- /dev/null
+++ b/glance/glance-appwidget-testing/build.gradle
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ api(libs.kotlinCoroutinesTest)
+ api(project(":glance:glance-testing"))
+ api(project(":glance:glance-appwidget"))
+
+ testImplementation("androidx.core:core:1.7.0")
+ testImplementation("androidx.core:core-ktx:1.7.0")
+ testImplementation(libs.junit)
+ testImplementation(libs.kotlinCoroutinesTest)
+ testImplementation(libs.kotlinTest)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.testCore)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.truth)
+
+ samples(projectOrArtifact(":glance:glance-appwidget-testing:glance-appwidget-testing-samples"))
+}
+
+android {
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ }
+ }
+
+ defaultConfig {
+ minSdkVersion 23
+ }
+ namespace "androidx.glance.appwidget.testing"
+}
+
+androidx {
+ name = "androidx.glance:glance-appwidget-testing"
+ type = LibraryType.PUBLISHED_LIBRARY
+ targetsJavaConsumers = false
+ inceptionYear = "2023"
+ description = "This library provides APIs for developers to use for testing their appWidget specific Glance composables."
+}
diff --git a/glance/glance-appwidget-testing/samples/build.gradle b/glance/glance-appwidget-testing/samples/build.gradle
new file mode 100644
index 0000000..a5da7fa1
--- /dev/null
+++ b/glance/glance-appwidget-testing/samples/build.gradle
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ implementation(libs.kotlinStdlib)
+ compileOnly(project(":annotation:annotation-sampled"))
+
+ implementation(project(":glance:glance"))
+ implementation(project(":glance:glance-testing"))
+ implementation(project(":glance:glance-appwidget-testing"))
+
+ implementation(libs.junit)
+ implementation(libs.testCore)
+ implementation("androidx.core:core:1.7.0")
+ implementation("androidx.core:core-ktx:1.7.0")
+}
+
+androidx {
+ name = "Glance AppWidget Testing Samples"
+ type = LibraryType.SAMPLES
+ targetsJavaConsumers = false
+ inceptionYear = "2023"
+ description = "Contains the sample code for testing the Glance AppWidget Composables"
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 23
+ }
+ namespace "androidx.glance.appwidget.testing.samples"
+}
diff --git a/glance/glance-appwidget-testing/samples/src/main/java/androidx/glance/appwidget/testing/samples/IsolatedGlanceComposableTestSamples.kt b/glance/glance-appwidget-testing/samples/src/main/java/androidx/glance/appwidget/testing/samples/IsolatedGlanceComposableTestSamples.kt
new file mode 100644
index 0000000..28a29c2
--- /dev/null
+++ b/glance/glance-appwidget-testing/samples/src/main/java/androidx/glance/appwidget/testing/samples/IsolatedGlanceComposableTestSamples.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget.testing.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.glance.GlanceModifier
+import androidx.glance.LocalSize
+import androidx.glance.appwidget.testing.unit.runGlanceAppWidgetUnitTest
+import androidx.glance.layout.Column
+import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.width
+import androidx.glance.semantics.semantics
+import androidx.glance.semantics.testTag
+import androidx.glance.testing.unit.hasTestTag
+import androidx.glance.testing.unit.hasText
+import androidx.glance.text.Text
+import org.junit.Test
+
+@Sampled
+@Suppress("unused")
+fun isolatedGlanceComposableTestSamples() {
+ class TestSample {
+ @Test
+ fun statusContent_statusFalse_outputsPending() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ StatusRow(
+ status = false
+ )
+ }
+
+ onNode(hasTestTag("status-text"))
+ .assert(hasText("Pending"))
+ }
+
+ @Test
+ fun statusContent_statusTrue_outputsFinished() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ StatusRow(
+ status = true
+ )
+ }
+
+ onNode(hasTestTag("status-text"))
+ .assert(hasText("Finished"))
+ }
+
+ @Test
+ fun header_smallSize_showsShortHeaderText() = runGlanceAppWidgetUnitTest {
+ setAppWidgetSize(DpSize(width = 50.dp, height = 100.dp))
+
+ provideComposable {
+ StatusRow(
+ status = false
+ )
+ }
+
+ onNode(hasTestTag("header-text"))
+ .assert(hasText("MyApp"))
+ }
+
+ @Test
+ fun header_largeSize_showsLongHeaderText() = runGlanceAppWidgetUnitTest {
+ setAppWidgetSize(DpSize(width = 150.dp, height = 100.dp))
+
+ provideComposable {
+ StatusRow(
+ status = false
+ )
+ }
+
+ onNode(hasTestTag("header-text"))
+ .assert(hasText("MyApp (Last order)"))
+ }
+
+ @Composable
+ fun WidgetContent(status: Boolean) {
+ Column {
+ Header()
+ Spacer()
+ StatusRow(status)
+ }
+ }
+
+ @Composable
+ fun Header() {
+ val width = LocalSize.current.width
+ Row(modifier = GlanceModifier.fillMaxSize()) {
+ Text(
+ text = if (width > 50.dp) {
+ "MyApp (Last order)"
+ } else {
+ "MyApp"
+ },
+ modifier = GlanceModifier.semantics { testTag = "header-text" }
+ )
+ }
+ }
+
+ @Composable
+ fun StatusRow(status: Boolean) {
+ Row(modifier = GlanceModifier.fillMaxSize()) {
+ Text(
+ text = "Status",
+ )
+ Spacer(modifier = GlanceModifier.width(10.dp))
+ Text(
+ text = if (status) {
+ "Pending"
+ } else {
+ "Finished"
+ },
+ modifier = GlanceModifier.semantics { testTag = "status-text" }
+ )
+ }
+ }
+ }
+}
diff --git a/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTest.kt b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTest.kt
new file mode 100644
index 0000000..c6282eb
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTest.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget.testing.unit
+
+import android.appwidget.AppWidgetProviderInfo
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.glance.GlanceId
+import androidx.glance.appwidget.AppWidgetId
+import androidx.glance.state.GlanceStateDefinition
+import androidx.glance.testing.GlanceNodeAssertionsProvider
+import androidx.glance.testing.unit.GlanceMappedNode
+import androidx.glance.testing.unit.MappedNode
+import kotlin.time.Duration
+
+/**
+ * Sets up the test environment and runs the given unit [test block][block]. Use the methods on
+ * [GlanceAppWidgetUnitTest] in the test to provide Glance composable content, find Glance elements
+ * and make assertions on them.
+ *
+ * Test your individual Glance composable functions in isolation to verify that your logic outputs
+ * right elements. For example: if input data is 'x', an image 'y' was
+ * outputted. In sample below, the test class has a separate test for the header and the status
+ * row.
+ *
+ * Tests can be run on JVM as these don't involve rendering the UI. If your logic depends on
+ * [Context] or other android APIs, tests can be run on Android unit testing frameworks such as
+ * [Robolectric](https://github.com/robolectric/robolectric).
+ *
+ * Note: Keeping a reference to the [GlanceAppWidgetUnitTest] outside of this function is an error.
+ *
+ * @sample androidx.glance.appwidget.testing.samples.isolatedGlanceComposableTestSamples
+ *
+ * @param timeout test time out; defaults to 10s
+ * @param block The test block that involves calling methods in [GlanceAppWidgetUnitTest]
+ */
+// This and backing environment is based on pattern followed by
+// "androidx.compose.ui.test.runComposeUiTest". Alternative of exposing testRule was explored, but
+// it wasn't necessary for this case. If developers wish, they may use this function to create their
+// own test rule.
+fun runGlanceAppWidgetUnitTest(
+ timeout: Duration = DEFAULT_TIMEOUT,
+ block: GlanceAppWidgetUnitTest.() -> Unit
+) = GlanceAppWidgetUnitTestEnvironment(timeout).runTest(block)
+
+/**
+ * Provides methods to enable you to test your logic of building Glance composable content in the
+ * [runGlanceAppWidgetUnitTest] scope.
+ *
+ * @see [runGlanceAppWidgetUnitTest]
+ */
+sealed interface GlanceAppWidgetUnitTest :
+ GlanceNodeAssertionsProvider<MappedNode, GlanceMappedNode> {
+ /**
+ * Sets the size of the appWidget to be assumed for the test. This corresponds to the
+ * `LocalSize.current` composition local. If you are accessing the local size, you must
+ * call this method to set the intended size for the test.
+ *
+ * Note: This should be called before calling [provideComposable].
+ * Default is `349.dp, 455.dp` that of a 5x4 widget in Pixel 4 portrait mode. See
+ * [GlanceAppWidgetUnitTestDefaults.size]
+ *
+ * 1. If your appWidget uses `sizeMode == Single`, you can set this to the `minWidth` and
+ * `minHeight` set in your appwidget info xml.
+ * 2. If your appWidget uses `sizeMode == Exact`, you can identify the sizes to test looking
+ * at the documentation on
+ * [Determine a size for your widget](https://developer.android.com/develop/ui/views/appwidgets/layouts#anatomy_determining_size).
+ * and identifying landscape and portrait sizes that your widget may appear on.
+ * 3. If your appWidget uses `sizeMode == Responsive`, you can set this to one of the sizes from
+ * the list that you provide when specifying the sizeMode.
+ */
+ fun setAppWidgetSize(size: DpSize)
+
+ /**
+ * Sets the state to be used for the test if your composable under test accesses it via
+ * `currentState<*>()` or `LocalState.current`.
+ *
+ * Default state is `null`. Note: This should be called before calling [provideComposable],
+ * updates to the state after providing content has no effect. This matches the appWidget
+ * behavior where you need to call `update` on the widget for state changes to take effect.
+ *
+ * @param state the state to be used for testing the composable.
+ * @param T type of state used in your [GlanceStateDefinition] e.g. `Preferences` if your state
+ * definition is `GlanceStateDefinition<Preferences>`
+ */
+ fun <T> setState(state: T)
+
+ /**
+ * Sets the context to be used for the test.
+ *
+ * It is optional to call this method. However, you must set this if your composable needs
+ * access to `LocalContext`. You may need to use a Android unit test framework such as
+ * [Robolectric](https://github.com/robolectric/robolectric) to get the context.
+ *
+ * Note: This should be called before calling [provideComposable], updates to the state after
+ * providing content has no effect
+ */
+ fun setContext(context: Context)
+
+ /**
+ * Sets the Glance composable function to be tested. Each unit test should test a composable in
+ * isolation and assume specific state as input. Prefer keeping composables side-effects free.
+ * Perform any state changes needed for the test before calling [provideComposable] or
+ * [runGlanceAppWidgetUnitTest].
+ *
+ * @param composable the composable function under test
+ */
+ fun provideComposable(composable: @Composable () -> Unit)
+
+ /**
+ * Wait until all recompositions are calculated. For example if you have `LaunchedEffect` with
+ * delays in your composable.
+ */
+ fun awaitIdle()
+}
+
+/**
+ * Provides default values for various properties used in the Glance appWidget unit tests.
+ */
+object GlanceAppWidgetUnitTestDefaults {
+ /**
+ * [GlanceId] that can be assumed for state updates testing a Glance composable in isolation.
+ */
+ fun glanceId(): GlanceId = AppWidgetId(1)
+
+ /**
+ * Default size of the appWidget assumed in the unit tests. To override the size, use the
+ * [GlanceAppWidgetUnitTest.setAppWidgetSize] function.
+ *
+ * The default `349.dp, 455.dp` is that of a 5x4 widget in Pixel 4 portrait mode.
+ */
+ fun size(): DpSize = DpSize(height = 349.dp, width = 455.dp)
+
+ /**
+ * Default category of the appWidget assumed in the unit tests.
+ *
+ * The default is `WIDGET_CATEGORY_HOME_SCREEN`
+ */
+ fun hostCategory(): Int = AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
+}
diff --git a/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironment.kt b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironment.kt
new file mode 100644
index 0000000..674c8c5
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/main/java/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironment.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget.testing.unit
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.os.Bundle
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composition
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.MonotonicFrameClock
+import androidx.compose.runtime.Recomposer
+import androidx.compose.ui.unit.DpSize
+import androidx.glance.Applier
+import androidx.glance.LocalContext
+import androidx.glance.LocalGlanceId
+import androidx.glance.LocalSize
+import androidx.glance.LocalState
+import androidx.glance.appwidget.LocalAppWidgetOptions
+import androidx.glance.appwidget.RemoteViewsRoot
+import androidx.glance.session.globalSnapshotMonitor
+import androidx.glance.testing.GlanceNodeAssertion
+import androidx.glance.testing.GlanceNodeMatcher
+import androidx.glance.testing.TestContext
+import androidx.glance.testing.unit.GlanceMappedNode
+import androidx.glance.testing.unit.MappedNode
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+
+internal val DEFAULT_TIMEOUT = 10.seconds
+
+/**
+ * An implementation of [GlanceAppWidgetUnitTest] that provides APIs to run composition for
+ * appwidget-specific Glance composable content.
+ */
+internal class GlanceAppWidgetUnitTestEnvironment(
+ private val timeout: Duration
+) : GlanceAppWidgetUnitTest {
+ private var testContext = TestContext<MappedNode, GlanceMappedNode>()
+ private var testScope = TestScope()
+
+ // Data for composition locals
+ private var context: Context? = null
+ private val fakeGlanceID = GlanceAppWidgetUnitTestDefaults.glanceId()
+ private var size: DpSize = GlanceAppWidgetUnitTestDefaults.size()
+ private var state: Any? = null
+
+ private val root = RemoteViewsRoot(10)
+
+ private lateinit var recomposer: Recomposer
+ private lateinit var composition: Composition
+
+ fun runTest(block: GlanceAppWidgetUnitTest.() -> Unit) = testScope.runTest(timeout) {
+ var snapshotMonitor: Job? = null
+ try {
+ // GlobalSnapshotManager.ensureStarted() uses Dispatcher.Default, so using
+ // globalSnapshotMonitor instead to be able to use test dispatcher instead.
+ snapshotMonitor = launch { globalSnapshotMonitor() }
+ val applier = Applier(root)
+ recomposer = Recomposer(testScope.coroutineContext)
+ composition = Composition(applier, recomposer)
+ block()
+ } finally {
+ composition.dispose()
+ snapshotMonitor?.cancel()
+ recomposer.cancel()
+ recomposer.join()
+ }
+ }
+
+ // Among the appWidgetOptions available, size related options shouldn't generally be necessary
+ // for developers to look up - the LocalSize composition local should suffice. So, currently, we
+ // only initialize host category.
+ private val appWidgetOptions = Bundle().apply {
+ putInt(
+ AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY,
+ GlanceAppWidgetUnitTestDefaults.hostCategory()
+ )
+ }
+
+ override fun provideComposable(composable: @Composable () -> Unit) {
+ check(testContext.rootGlanceNode == null) {
+ "provideComposable can only be called once"
+ }
+
+ testScope.launch {
+ var compositionLocals = arrayOf(
+ LocalGlanceId provides fakeGlanceID,
+ LocalState provides state,
+ LocalAppWidgetOptions provides appWidgetOptions,
+ LocalSize provides size
+ )
+ context?.let {
+ compositionLocals = compositionLocals.plus(LocalContext provides it)
+ }
+
+ composition.setContent {
+ CompositionLocalProvider(
+ values = compositionLocals,
+ content = composable,
+ )
+ }
+
+ launch(currentCoroutineContext() + TestFrameClock()) {
+ recomposer.runRecomposeAndApplyChanges()
+ }
+
+ launch {
+ recomposer.currentState.collect { curState ->
+ when (curState) {
+ Recomposer.State.Idle -> {
+ testContext.rootGlanceNode = GlanceMappedNode(
+ emittable = root.copy()
+ )
+ }
+
+ Recomposer.State.ShutDown -> {
+ cancel()
+ }
+
+ else -> {}
+ }
+ }
+ }
+ }
+ }
+
+ override fun awaitIdle() {
+ testScope.testScheduler.advanceUntilIdle()
+ }
+
+ override fun onNode(
+ matcher: GlanceNodeMatcher<MappedNode>
+ ): GlanceNodeAssertion<MappedNode, GlanceMappedNode> {
+ // Always let all the enqueued tasks finish before inspecting the tree.
+ testScope.testScheduler.runCurrent()
+ // Calling onNode resets the previously matched nodes and starts a new matching chain.
+ testContext.reset()
+ // Delegates matching to the next assertion.
+ return GlanceNodeAssertion(matcher, testContext)
+ }
+
+ override fun setAppWidgetSize(size: DpSize) {
+ check(testContext.rootGlanceNode == null) {
+ "setApWidgetSize should be called before calling provideComposable"
+ }
+ this.size = size
+ }
+
+ override fun <T> setState(state: T) {
+ check(testContext.rootGlanceNode == null) {
+ "setState should be called before calling provideComposable"
+ }
+ this.state = state
+ }
+
+ override fun setContext(context: Context) {
+ check(testContext.rootGlanceNode == null) {
+ "setContext should be called before calling provideComposable"
+ }
+ this.context = context
+ }
+
+ /**
+ * Test clock that sends all frames immediately.
+ */
+ // Same as TestUtils.TestFrameClock used in Glance unit tests.
+ private class TestFrameClock : MonotonicFrameClock {
+ override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R) =
+ onFrame(System.currentTimeMillis())
+ }
+}
diff --git a/glance/glance-appwidget-testing/src/test/AndroidManifest.xml b/glance/glance-appwidget-testing/src/test/AndroidManifest.xml
new file mode 100644
index 0000000..f125c7b
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest>
+ <application/>
+</manifest>
\ No newline at end of file
diff --git a/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentRobolectricTest.kt b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentRobolectricTest.kt
new file mode 100644
index 0000000..514826d
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentRobolectricTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget.testing.unit
+
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.glance.GlanceModifier
+import androidx.glance.LocalContext
+import androidx.glance.appwidget.testing.test.R
+import androidx.glance.layout.Column
+import androidx.glance.semantics.semantics
+import androidx.glance.semantics.testTag
+import androidx.glance.testing.unit.hasTestTag
+import androidx.glance.testing.unit.hasText
+import androidx.glance.text.Text
+import androidx.test.core.app.ApplicationProvider
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [33])
+/**
+ * Holds tests that use Robolectric for providing application resources and context.
+ */
+class GlanceAppWidgetUnitTestEnvironmentRobolectricTest {
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ context = ApplicationProvider.getApplicationContext()
+ }
+
+ @Test
+ fun runTest_localContextRead() = runGlanceAppWidgetUnitTest {
+ setContext(context)
+
+ provideComposable {
+ ComposableReadingLocalContext()
+ }
+
+ onNode(hasTestTag("test-tag"))
+ .assert(hasText("Test string: MyTest"))
+ }
+
+ @Composable
+ fun ComposableReadingLocalContext() {
+ val context = LocalContext.current
+
+ Column {
+ Text(
+ text = "Test string: ${context.getString(R.string.glance_test_string)}",
+ modifier = GlanceModifier.semantics { testTag = "test-tag" }
+ )
+ }
+ }
+}
diff --git a/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt
new file mode 100644
index 0000000..a1449d8
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/kotlin/androidx/glance/appwidget/testing/unit/GlanceAppWidgetUnitTestEnvironmentTest.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget.testing.unit
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.preferencesOf
+import androidx.glance.GlanceModifier
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.LocalSize
+import androidx.glance.appwidget.ImageProvider
+import androidx.glance.appwidget.testing.test.R
+import androidx.glance.currentState
+import androidx.glance.layout.Column
+import androidx.glance.layout.Spacer
+import androidx.glance.semantics.semantics
+import androidx.glance.semantics.testTag
+import androidx.glance.testing.unit.hasTestTag
+import androidx.glance.testing.unit.hasText
+import androidx.glance.text.Text
+import kotlinx.coroutines.delay
+import org.junit.Test
+
+// In this test we aren't specifically testing anything bound to SDK, so we can run it without
+// android unit test runners such as Robolectric.
+class GlanceAppWidgetUnitTestEnvironmentTest {
+ @Test
+ fun runTest_localSizeRead() = runGlanceAppWidgetUnitTest {
+ setAppWidgetSize(DpSize(width = 120.dp, height = 200.dp))
+
+ provideComposable {
+ ComposableReadingLocalSize()
+ }
+
+ onNode(hasText("120.0 dp x 200.0 dp")).assertExists()
+ }
+
+ @Composable
+ fun ComposableReadingLocalSize() {
+ val size = LocalSize.current
+ Column {
+ Text(text = "${size.width.value} dp x ${size.height.value} dp")
+ Spacer()
+ Image(
+ provider = ImageProvider(R.drawable.glance_test_android),
+ contentDescription = "test-image",
+ )
+ }
+ }
+
+ @Test
+ fun runTest_currentStateRead() = runGlanceAppWidgetUnitTest {
+ setState(preferencesOf(toggleKey to true))
+
+ provideComposable {
+ ComposableReadingState()
+ }
+
+ onNode(hasText("isToggled")).assertExists()
+ }
+
+ @Composable
+ fun ComposableReadingState() {
+ Column {
+ Text(text = "A text")
+ Spacer()
+ Text(text = getTitle(currentState<Preferences>()[toggleKey] == true))
+ Spacer()
+ Image(
+ provider = ImageProvider(R.drawable.glance_test_android),
+ contentDescription = "test-image",
+ modifier = GlanceModifier.semantics { testTag = "img" }
+ )
+ }
+ }
+
+ @Test
+ fun runTest_onNodeCalledMultipleTimes() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ Text(text = "abc")
+ Spacer()
+ Text(text = "xyz")
+ }
+
+ onNode(hasText("abc")).assertExists()
+ // test context reset and new filter matched onNode
+ onNode(hasText("xyz")).assertExists()
+ onNode(hasText("def")).assertDoesNotExist()
+ }
+
+ @Test
+ fun runTest_effect() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ var text by remember { mutableStateOf("initial") }
+
+ Text(text = text, modifier = GlanceModifier.semantics { testTag = "mutable-test" })
+ Spacer()
+ Text(text = "xyz")
+
+ LaunchedEffect(Unit) {
+ text = "changed"
+ }
+ }
+
+ onNode(hasTestTag("mutable-test")).assert(hasText("changed"))
+ }
+
+ @Test
+ fun runTest_effectWithDelay() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ var text by remember { mutableStateOf("initial") }
+
+ Text(text = text, modifier = GlanceModifier.semantics { testTag = "mutable-test" })
+ Spacer()
+ Text(text = "xyz")
+
+ LaunchedEffect(Unit) {
+ delay(100L)
+ text = "changed"
+ }
+ }
+
+ awaitIdle() // Since the launched effect has a delay.
+ onNode(hasTestTag("mutable-test")).assert(hasText("changed"))
+ }
+
+ @Test
+ fun runTest_effectWithDelayWithoutAdvancing() = runGlanceAppWidgetUnitTest {
+ provideComposable {
+ var text by remember { mutableStateOf("initial") }
+
+ Text(text = text, modifier = GlanceModifier.semantics { testTag = "mutable-test" })
+ Spacer()
+ Text(text = "xyz")
+
+ LaunchedEffect(Unit) {
+ delay(100L)
+ text = "changed"
+ }
+ }
+
+ onNode(hasTestTag("mutable-test")).assert(hasText("initial"))
+ }
+}
+
+private val toggleKey = booleanPreferencesKey("title_toggled_key")
+private fun getTitle(toggled: Boolean) = if (toggled) "isToggled" else "notToggled"
diff --git a/glance/glance-appwidget-testing/src/test/res/drawable/glance_test_android.xml b/glance/glance-appwidget-testing/src/test/res/drawable/glance_test_android.xml
new file mode 100644
index 0000000..49a3142
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/res/drawable/glance_test_android.xml
@@ -0,0 +1,21 @@
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector android:alpha="0.9" android:height="24dp"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M17.6,9.48l1.84,-3.18c0.16,-0.31 0.04,-0.69 -0.26,-0.85c-0.29,-0.15 -0.65,-0.06 -0.83,0.22l-1.88,3.24c-2.86,-1.21 -6.08,-1.21 -8.94,0L5.65,5.67c-0.19,-0.29 -0.58,-0.38 -0.87,-0.2C4.5,5.65 4.41,6.01 4.56,6.3L6.4,9.48C3.3,11.25 1.28,14.44 1,18h22C22.72,14.44 20.7,11.25 17.6,9.48zM7,15.25c-0.69,0 -1.25,-0.56 -1.25,-1.25c0,-0.69 0.56,-1.25 1.25,-1.25S8.25,13.31 8.25,14C8.25,14.69 7.69,15.25 7,15.25zM17,15.25c-0.69,0 -1.25,-0.56 -1.25,-1.25c0,-0.69 0.56,-1.25 1.25,-1.25s1.25,0.56 1.25,1.25C18.25,14.69 17.69,15.25 17,15.25z"/>
+</vector>
diff --git a/glance/glance-appwidget-testing/src/test/res/values/strings.xml b/glance/glance-appwidget-testing/src/test/res/values/strings.xml
new file mode 100644
index 0000000..88a0850
--- /dev/null
+++ b/glance/glance-appwidget-testing/src/test/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <string name="glance_test_string">MyTest</string>
+</resources>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
index f667413..a1d03ba 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -23,6 +23,8 @@
import android.util.Log
import android.widget.RemoteViews
import androidx.annotation.LayoutRes
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
import androidx.compose.runtime.Composable
import androidx.glance.GlanceComposable
import androidx.glance.GlanceId
@@ -194,7 +196,8 @@
}
}
-internal data class AppWidgetId(val appWidgetId: Int) : GlanceId
+@RestrictTo(Scope.LIBRARY_GROUP)
+data class AppWidgetId(val appWidgetId: Int) : GlanceId
/** Update all App Widgets managed by the [GlanceAppWidget] class. */
suspend fun GlanceAppWidget.updateAll(@Suppress("ContextFirst") context: Context) {
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt
index 1428527..df0e178 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/RemoteViewsRoot.kt
@@ -16,6 +16,8 @@
package androidx.glance.appwidget
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
import androidx.glance.Emittable
import androidx.glance.EmittableWithChildren
import androidx.glance.GlanceModifier
@@ -24,7 +26,8 @@
* Root view, with a maximum depth. No default value is specified, as the exact value depends on
* specific circumstances.
*/
-internal class RemoteViewsRoot(private val maxDepth: Int) : EmittableWithChildren(maxDepth) {
+@RestrictTo(Scope.LIBRARY_GROUP)
+ class RemoteViewsRoot(private val maxDepth: Int) : EmittableWithChildren(maxDepth) {
override var modifier: GlanceModifier = GlanceModifier
override fun copy(): Emittable = RemoteViewsRoot(maxDepth).also {
it.modifier = modifier
diff --git a/glance/glance-testing/api/current.txt b/glance/glance-testing/api/current.txt
index 51dd245..e46a742 100644
--- a/glance/glance-testing/api/current.txt
+++ b/glance/glance-testing/api/current.txt
@@ -14,6 +14,10 @@
method public androidx.glance.testing.GlanceNodeAssertion<R,T> assertExists();
}
+ public interface GlanceNodeAssertionsProvider<R, T extends androidx.glance.testing.GlanceNode<R>> {
+ method public androidx.glance.testing.GlanceNodeAssertion<R,T> onNode(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
+ }
+
public final class GlanceNodeMatcher<R> {
ctor public GlanceNodeMatcher(String description, kotlin.jvm.functions.Function1<? super androidx.glance.testing.GlanceNode<R>,java.lang.Boolean> matcher);
method public boolean matches(androidx.glance.testing.GlanceNode<R> node);
diff --git a/glance/glance-testing/api/restricted_current.txt b/glance/glance-testing/api/restricted_current.txt
index 51dd245..e46a742 100644
--- a/glance/glance-testing/api/restricted_current.txt
+++ b/glance/glance-testing/api/restricted_current.txt
@@ -14,6 +14,10 @@
method public androidx.glance.testing.GlanceNodeAssertion<R,T> assertExists();
}
+ public interface GlanceNodeAssertionsProvider<R, T extends androidx.glance.testing.GlanceNode<R>> {
+ method public androidx.glance.testing.GlanceNodeAssertion<R,T> onNode(androidx.glance.testing.GlanceNodeMatcher<R> matcher);
+ }
+
public final class GlanceNodeMatcher<R> {
ctor public GlanceNodeMatcher(String description, kotlin.jvm.functions.Function1<? super androidx.glance.testing.GlanceNode<R>,java.lang.Boolean> matcher);
method public boolean matches(androidx.glance.testing.GlanceNode<R> node);
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionsProvider.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionsProvider.kt
new file mode 100644
index 0000000..624b529
--- /dev/null
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/GlanceNodeAssertionsProvider.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.testing
+
+/**
+ * Provides an entry point into testing exposing methods to find glance nodes
+ */
+// Equivalent to "androidx.compose.ui.test.SemanticsNodeInteractionsProvider" from compose.
+interface GlanceNodeAssertionsProvider<R, T : GlanceNode<R>> {
+ /**
+ * Finds a Glance node that matches the given condition.
+ *
+ * Any subsequent operation on its result will expect exactly one element found and will throw
+ * [AssertionError] if none or more than one element is found.
+ *
+ * @param matcher Matcher used for filtering
+ */
+ fun onNode(matcher: GlanceNodeMatcher<R>): GlanceNodeAssertion<R, T>
+}
diff --git a/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt b/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt
index 1399349..1ab76a3 100644
--- a/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt
+++ b/glance/glance-testing/src/main/java/androidx/glance/testing/TestContext.kt
@@ -23,6 +23,13 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class TestContext<R, T : GlanceNode<R>> {
+ /**
+ * To be called on every onNode to restart matching and clear cache.
+ */
+ fun reset() {
+ cachedMatchedNodes = emptyList()
+ }
+
var rootGlanceNode: T? = null
var cachedMatchedNodes: List<GlanceNode<R>> = emptyList()
}
diff --git a/glance/glance/src/main/java/androidx/glance/session/GlobalSnapshotManager.kt b/glance/glance/src/main/java/androidx/glance/session/GlobalSnapshotManager.kt
index cebf899..d5f6d8d 100644
--- a/glance/glance/src/main/java/androidx/glance/session/GlobalSnapshotManager.kt
+++ b/glance/glance/src/main/java/androidx/glance/session/GlobalSnapshotManager.kt
@@ -17,6 +17,7 @@
package androidx.glance.session
import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
import androidx.compose.runtime.snapshots.Snapshot
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineScope
@@ -33,7 +34,7 @@
* state changes). These will be sent on Dispatchers.Default.
* This is based on [androidx.compose.ui.platform.GlobalSnapshotManager].
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY_GROUP)
object GlobalSnapshotManager {
private val started = AtomicBoolean(false)
private val sent = AtomicBoolean(false)
@@ -59,7 +60,8 @@
/**
* Monitors global snapshot state writes and sends apply notifications.
*/
-internal suspend fun globalSnapshotMonitor() {
+@RestrictTo(Scope.LIBRARY_GROUP)
+suspend fun globalSnapshotMonitor() {
val channel = Channel<Unit>(1)
val sent = AtomicBoolean(false)
val observerHandle = Snapshot.registerGlobalWriteObserver {
diff --git a/gradle.properties b/gradle.properties
index 17b7b6e..bec21dd 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -18,8 +18,8 @@
# .gradle/.android/analytics.settings -> b/278767328
# fullsdk-linux/**/package.xml -> b/291331139
# androidx/compose/lint/common/build/libs/common.jar -> b/295395616
-# .konan/kotlin-native-prebuilt-linux-x86_64-1.9.0 -> https://youtrack.jetbrains.com/issue/KT-61154/
-org.gradle.configuration-cache.inputs.unsafe.ignore.file-system-checks=**/.gradle/.android/analytics.settings;**/prebuilts/fullsdk-linux;**/prebuilts/fullsdk-linux/platforms/android-*/package.xml;**/androidx/compose/lint/common/build/libs/common.jar;**/.konan/kotlin-native-prebuilt-linux-x86_64-1.9.0/klib/common/stdlib;**/.konan/kotlin-native-prebuilt-linux-x86_64-1.9.0/konan/lib/*
+# .konan/kotlin-native-prebuilt-linux-x86_64-1.9.10 -> https://youtrack.jetbrains.com/issue/KT-61154/
+org.gradle.configuration-cache.inputs.unsafe.ignore.file-system-checks=**/.gradle/.android/analytics.settings;**/prebuilts/fullsdk-linux;**/prebuilts/fullsdk-linux/platforms/android-*/package.xml;**/androidx/compose/lint/common/build/libs/common.jar;**/.konan/kotlin-native-prebuilt-linux-x86_64-1.9.10/klib/common/stdlib;**/.konan/kotlin-native-prebuilt-linux-x86_64-1.9.10/konan/lib/*
android.lint.baselineOmitLineNumbers=true
android.lint.printStackTrace=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4718b38..608df2d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -38,10 +38,10 @@
jcodec = "0.2.5"
kotlin17 = "1.7.10"
kotlin18 = "1.8.22"
-kotlin19 = "1.9.0"
-kotlin = "1.9.0"
+kotlin19 = "1.9.10"
+kotlin = "1.9.10"
kotlinBenchmark = "0.4.8"
-kotlinNative = "1.9.0"
+kotlinNative = "1.9.10"
kotlinCompileTesting = "1.4.9"
kotlinCoroutines = "1.7.1"
kotlinSerialization = "1.3.3"
@@ -59,7 +59,6 @@
skiko = "0.7.7"
spdxGradlePlugin = "0.1.0"
sqldelight = "1.3.0"
-stately = "2.0.0-rc3"
retrofit = "2.7.2"
wire = "4.7.0"
@@ -265,8 +264,6 @@
sqldelightAndroid = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelightCoroutinesExt = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
sqliteJdbc = { module = "org.xerial:sqlite-jdbc", version = "3.41.2.2" }
-statelyConcurrency = { module = "co.touchlab:stately-concurrency", version.ref = "stately" }
-statelyConcurrentCollections = { module = "co.touchlab:stately-concurrent-collections", version.ref = "stately" }
testCore = { module = "androidx.test:core", version.ref = "androidxTestCore" }
testCoreKtx = { module = "androidx.test:core-ktx", version.ref = "androidxTestCore" }
testExtJunit = { module = "androidx.test.ext:junit", version.ref = "androidxTestExtJunit" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 2c9bedec..774ff87 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -463,19 +463,19 @@
</trusted-keys>
</configuration>
<components>
- <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.9.0">
- <artifact name="kotlin-native-prebuilt-linux-x86_64-1.9.0.tar.gz">
- <sha256 value="31737a9739fc37208e1f532b7472c3fbbf0d753f3621c9dfc1d72a69d5bc35c0" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.9.0.tar.gz" reason="Artifact is not signed"/>
+ <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.9.10">
+ <artifact name="kotlin-native-prebuilt-linux-x86_64-1.9.10.tar.gz">
+ <sha256 value="0e10e98c9310cf458dd58f3cce01bd6287b7101a14f57ffa233afbae6282e165" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.9.0.tar.gz" reason="Artifact is not signed"/>
</artifact>
</component>
- <component group="" name="kotlin-native-prebuilt-macos-aarch64" version="1.9.0">
- <artifact name="kotlin-native-prebuilt-macos-aarch64-1.9.0.tar.gz">
- <sha256 value="cbb700baef01980b9b9a6d499da7adff5c611dc61ed247efdf649a073c4dbb3c" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-aarch64-1.9.0.tar.gz"/>
+ <component group="" name="kotlin-native-prebuilt-macos-aarch64" version="1.9.10">
+ <artifact name="kotlin-native-prebuilt-macos-aarch64-1.9.10.tar.gz">
+ <sha256 value="5edce17e755f49915e82e9702c030f77b568e742511bcff5aada1de8b9f01335" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-aarch64-1.9.0.tar.gz"/>
</artifact>
</component>
- <component group="" name="kotlin-native-prebuilt-macos-x86_64" version="1.9.0">
- <artifact name="kotlin-native-prebuilt-macos-x86_64-1.9.0.tar.gz">
- <sha256 value="ab02e67bc82d986875941036e147179e2812502bd2d6a8d8b3c511a93a8dbd1d" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-x86_64-1.9.0.tar.gz"/>
+ <component group="" name="kotlin-native-prebuilt-macos-x86_64" version="1.9.10">
+ <artifact name="kotlin-native-prebuilt-macos-x86_64-1.9.10.tar.gz">
+ <sha256 value="f19eeb858a76631d6b6691354d6975afe6b50f381fa527051f73e1d42daa0aaa" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-x86_64-1.9.0.tar.gz"/>
</artifact>
</component>
<component group="aopalliance" name="aopalliance" version="1.0">
diff --git a/graphics/graphics-shapes/api/current.txt b/graphics/graphics-shapes/api/current.txt
index 55829f0..b2e61e0 100644
--- a/graphics/graphics-shapes/api/current.txt
+++ b/graphics/graphics-shapes/api/current.txt
@@ -14,31 +14,26 @@
public static final class CornerRounding.Companion {
}
- public final class Cubic {
- ctor public Cubic(androidx.graphics.shapes.Cubic cubic);
+ public class Cubic {
ctor public Cubic(float anchor0X, float anchor0Y, float control0X, float control0Y, float control1X, float control1Y, float anchor1X, float anchor1Y);
- method public static androidx.graphics.shapes.Cubic circularArc(float centerX, float centerY, float x0, float y0, float x1, float y1);
- method public operator androidx.graphics.shapes.Cubic div(float x);
- method public operator androidx.graphics.shapes.Cubic div(int x);
- method public float getAnchor0X();
- method public float getAnchor0Y();
- method public float getAnchor1X();
- method public float getAnchor1Y();
- method public float getControl0X();
- method public float getControl0Y();
- method public float getControl1X();
- method public float getControl1Y();
- method public static androidx.graphics.shapes.Cubic interpolate(androidx.graphics.shapes.Cubic start, androidx.graphics.shapes.Cubic end, float t);
- method public operator androidx.graphics.shapes.Cubic plus(androidx.graphics.shapes.Cubic o);
- method public android.graphics.PointF pointOnCurve(float t);
- method public android.graphics.PointF pointOnCurve(float t, optional android.graphics.PointF result);
- method public androidx.graphics.shapes.Cubic reverse();
- method public kotlin.Pair<androidx.graphics.shapes.Cubic,androidx.graphics.shapes.Cubic> split(float t);
- method public static androidx.graphics.shapes.Cubic straightLine(float x0, float y0, float x1, float y1);
- method public operator androidx.graphics.shapes.Cubic times(float x);
- method public operator androidx.graphics.shapes.Cubic times(int x);
- method public void transform(android.graphics.Matrix matrix);
- method public void transform(android.graphics.Matrix matrix, optional float[] points);
+ method public static final androidx.graphics.shapes.Cubic circularArc(float centerX, float centerY, float x0, float y0, float x1, float y1);
+ method public final operator androidx.graphics.shapes.Cubic div(float x);
+ method public final operator androidx.graphics.shapes.Cubic div(int x);
+ method public final float getAnchor0X();
+ method public final float getAnchor0Y();
+ method public final float getAnchor1X();
+ method public final float getAnchor1Y();
+ method public final float getControl0X();
+ method public final float getControl0Y();
+ method public final float getControl1X();
+ method public final float getControl1Y();
+ method public final operator androidx.graphics.shapes.Cubic plus(androidx.graphics.shapes.Cubic o);
+ method public final androidx.graphics.shapes.Cubic reverse();
+ method public final kotlin.Pair<androidx.graphics.shapes.Cubic,androidx.graphics.shapes.Cubic> split(float t);
+ method public static final androidx.graphics.shapes.Cubic straightLine(float x0, float y0, float x1, float y1);
+ method public final operator androidx.graphics.shapes.Cubic times(float x);
+ method public final operator androidx.graphics.shapes.Cubic times(int x);
+ method public final androidx.graphics.shapes.Cubic transformed(androidx.graphics.shapes.PointTransformer f);
property public final float anchor0X;
property public final float anchor0Y;
property public final float anchor1X;
@@ -52,54 +47,43 @@
public static final class Cubic.Companion {
method public androidx.graphics.shapes.Cubic circularArc(float centerX, float centerY, float x0, float y0, float x1, float y1);
- method public androidx.graphics.shapes.Cubic interpolate(androidx.graphics.shapes.Cubic start, androidx.graphics.shapes.Cubic end, float t);
method public androidx.graphics.shapes.Cubic straightLine(float x0, float y0, float x1, float y1);
}
- public final class CubicShape {
- ctor public CubicShape(androidx.graphics.shapes.CubicShape sourceShape);
- ctor public CubicShape(java.util.List<androidx.graphics.shapes.Cubic> cubics);
- method public android.graphics.RectF getBounds();
- method public java.util.List<androidx.graphics.shapes.Cubic> getCubics();
- method public android.graphics.Path toPath();
- method public void transform(android.graphics.Matrix matrix);
- method public void transform(android.graphics.Matrix matrix, optional float[] points);
- property public final android.graphics.RectF bounds;
- property public final java.util.List<androidx.graphics.shapes.Cubic> cubics;
- }
-
- public final class CubicShapeKt {
- method public static void drawCubicShape(android.graphics.Canvas, androidx.graphics.shapes.CubicShape shape, android.graphics.Paint paint);
- }
-
public final class Morph {
ctor public Morph(androidx.graphics.shapes.RoundedPolygon start, androidx.graphics.shapes.RoundedPolygon end);
method public java.util.List<androidx.graphics.shapes.Cubic> asCubics(float progress);
- method public android.graphics.Path asPath(float progress);
- method public android.graphics.Path asPath(float progress, optional android.graphics.Path path);
- method public android.graphics.RectF getBounds();
- method public void transform(android.graphics.Matrix matrix);
- property public final android.graphics.RectF bounds;
+ method public kotlin.sequences.Sequence<androidx.graphics.shapes.MutableCubic> asMutableCubics(float progress);
+ method public kotlin.sequences.Sequence<androidx.graphics.shapes.MutableCubic> asMutableCubics(float progress, optional androidx.graphics.shapes.MutableCubic mutableCubic);
}
- public final class MorphKt {
- method public static void drawMorph(android.graphics.Canvas, androidx.graphics.shapes.Morph morph, android.graphics.Paint paint, optional float progress);
+ public final class MutableCubic extends androidx.graphics.shapes.Cubic {
+ method public void transform(androidx.graphics.shapes.PointTransformer f);
+ }
+
+ public interface MutablePoint {
+ method public float getX();
+ method public float getY();
+ method public void setX(float);
+ method public void setY(float);
+ property public abstract float x;
+ property public abstract float y;
+ }
+
+ public fun interface PointTransformer {
+ method public void transform(androidx.graphics.shapes.MutablePoint);
}
public final class RoundedPolygon {
- ctor public RoundedPolygon(androidx.graphics.shapes.RoundedPolygon source);
- ctor public RoundedPolygon(float[] vertices, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding, optional float centerX, optional float centerY);
- ctor public RoundedPolygon(@IntRange(from=3L) int numVertices, optional float radius, optional float centerX, optional float centerY, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding);
- method public android.graphics.RectF getBounds();
+ method public float[] calculateBounds(optional float[] bounds);
method public float getCenterX();
method public float getCenterY();
- method public void setBounds(android.graphics.RectF);
- method public androidx.graphics.shapes.CubicShape toCubicShape();
- method public android.graphics.Path toPath();
- method public void transform(android.graphics.Matrix matrix);
- property public final android.graphics.RectF bounds;
+ method public java.util.List<androidx.graphics.shapes.Cubic> getCubics();
+ method public androidx.graphics.shapes.RoundedPolygon normalized();
+ method public androidx.graphics.shapes.RoundedPolygon transformed(androidx.graphics.shapes.PointTransformer f);
property public final float centerX;
property public final float centerY;
+ property public final java.util.List<androidx.graphics.shapes.Cubic> cubics;
field public static final androidx.graphics.shapes.RoundedPolygon.Companion Companion;
}
@@ -107,7 +91,18 @@
}
public final class RoundedPolygonKt {
- method public static void drawPolygon(android.graphics.Canvas, androidx.graphics.shapes.RoundedPolygon polygon, android.graphics.Paint paint);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(androidx.graphics.shapes.RoundedPolygon source);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(float[] vertices);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(float[] vertices, optional androidx.graphics.shapes.CornerRounding rounding);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(float[] vertices, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(float[] vertices, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding, optional float centerX);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(float[] vertices, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding, optional float centerX, optional float centerY);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(@IntRange(from=3L) int numVertices);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(@IntRange(from=3L) int numVertices, optional float radius);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(@IntRange(from=3L) int numVertices, optional float radius, optional float centerX);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(@IntRange(from=3L) int numVertices, optional float radius, optional float centerX, optional float centerY);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(@IntRange(from=3L) int numVertices, optional float radius, optional float centerX, optional float centerY, optional androidx.graphics.shapes.CornerRounding rounding);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(@IntRange(from=3L) int numVertices, optional float radius, optional float centerX, optional float centerY, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding);
}
public final class ShapesKt {
diff --git a/graphics/graphics-shapes/api/restricted_current.txt b/graphics/graphics-shapes/api/restricted_current.txt
index 55829f0..b2e61e0 100644
--- a/graphics/graphics-shapes/api/restricted_current.txt
+++ b/graphics/graphics-shapes/api/restricted_current.txt
@@ -14,31 +14,26 @@
public static final class CornerRounding.Companion {
}
- public final class Cubic {
- ctor public Cubic(androidx.graphics.shapes.Cubic cubic);
+ public class Cubic {
ctor public Cubic(float anchor0X, float anchor0Y, float control0X, float control0Y, float control1X, float control1Y, float anchor1X, float anchor1Y);
- method public static androidx.graphics.shapes.Cubic circularArc(float centerX, float centerY, float x0, float y0, float x1, float y1);
- method public operator androidx.graphics.shapes.Cubic div(float x);
- method public operator androidx.graphics.shapes.Cubic div(int x);
- method public float getAnchor0X();
- method public float getAnchor0Y();
- method public float getAnchor1X();
- method public float getAnchor1Y();
- method public float getControl0X();
- method public float getControl0Y();
- method public float getControl1X();
- method public float getControl1Y();
- method public static androidx.graphics.shapes.Cubic interpolate(androidx.graphics.shapes.Cubic start, androidx.graphics.shapes.Cubic end, float t);
- method public operator androidx.graphics.shapes.Cubic plus(androidx.graphics.shapes.Cubic o);
- method public android.graphics.PointF pointOnCurve(float t);
- method public android.graphics.PointF pointOnCurve(float t, optional android.graphics.PointF result);
- method public androidx.graphics.shapes.Cubic reverse();
- method public kotlin.Pair<androidx.graphics.shapes.Cubic,androidx.graphics.shapes.Cubic> split(float t);
- method public static androidx.graphics.shapes.Cubic straightLine(float x0, float y0, float x1, float y1);
- method public operator androidx.graphics.shapes.Cubic times(float x);
- method public operator androidx.graphics.shapes.Cubic times(int x);
- method public void transform(android.graphics.Matrix matrix);
- method public void transform(android.graphics.Matrix matrix, optional float[] points);
+ method public static final androidx.graphics.shapes.Cubic circularArc(float centerX, float centerY, float x0, float y0, float x1, float y1);
+ method public final operator androidx.graphics.shapes.Cubic div(float x);
+ method public final operator androidx.graphics.shapes.Cubic div(int x);
+ method public final float getAnchor0X();
+ method public final float getAnchor0Y();
+ method public final float getAnchor1X();
+ method public final float getAnchor1Y();
+ method public final float getControl0X();
+ method public final float getControl0Y();
+ method public final float getControl1X();
+ method public final float getControl1Y();
+ method public final operator androidx.graphics.shapes.Cubic plus(androidx.graphics.shapes.Cubic o);
+ method public final androidx.graphics.shapes.Cubic reverse();
+ method public final kotlin.Pair<androidx.graphics.shapes.Cubic,androidx.graphics.shapes.Cubic> split(float t);
+ method public static final androidx.graphics.shapes.Cubic straightLine(float x0, float y0, float x1, float y1);
+ method public final operator androidx.graphics.shapes.Cubic times(float x);
+ method public final operator androidx.graphics.shapes.Cubic times(int x);
+ method public final androidx.graphics.shapes.Cubic transformed(androidx.graphics.shapes.PointTransformer f);
property public final float anchor0X;
property public final float anchor0Y;
property public final float anchor1X;
@@ -52,54 +47,43 @@
public static final class Cubic.Companion {
method public androidx.graphics.shapes.Cubic circularArc(float centerX, float centerY, float x0, float y0, float x1, float y1);
- method public androidx.graphics.shapes.Cubic interpolate(androidx.graphics.shapes.Cubic start, androidx.graphics.shapes.Cubic end, float t);
method public androidx.graphics.shapes.Cubic straightLine(float x0, float y0, float x1, float y1);
}
- public final class CubicShape {
- ctor public CubicShape(androidx.graphics.shapes.CubicShape sourceShape);
- ctor public CubicShape(java.util.List<androidx.graphics.shapes.Cubic> cubics);
- method public android.graphics.RectF getBounds();
- method public java.util.List<androidx.graphics.shapes.Cubic> getCubics();
- method public android.graphics.Path toPath();
- method public void transform(android.graphics.Matrix matrix);
- method public void transform(android.graphics.Matrix matrix, optional float[] points);
- property public final android.graphics.RectF bounds;
- property public final java.util.List<androidx.graphics.shapes.Cubic> cubics;
- }
-
- public final class CubicShapeKt {
- method public static void drawCubicShape(android.graphics.Canvas, androidx.graphics.shapes.CubicShape shape, android.graphics.Paint paint);
- }
-
public final class Morph {
ctor public Morph(androidx.graphics.shapes.RoundedPolygon start, androidx.graphics.shapes.RoundedPolygon end);
method public java.util.List<androidx.graphics.shapes.Cubic> asCubics(float progress);
- method public android.graphics.Path asPath(float progress);
- method public android.graphics.Path asPath(float progress, optional android.graphics.Path path);
- method public android.graphics.RectF getBounds();
- method public void transform(android.graphics.Matrix matrix);
- property public final android.graphics.RectF bounds;
+ method public kotlin.sequences.Sequence<androidx.graphics.shapes.MutableCubic> asMutableCubics(float progress);
+ method public kotlin.sequences.Sequence<androidx.graphics.shapes.MutableCubic> asMutableCubics(float progress, optional androidx.graphics.shapes.MutableCubic mutableCubic);
}
- public final class MorphKt {
- method public static void drawMorph(android.graphics.Canvas, androidx.graphics.shapes.Morph morph, android.graphics.Paint paint, optional float progress);
+ public final class MutableCubic extends androidx.graphics.shapes.Cubic {
+ method public void transform(androidx.graphics.shapes.PointTransformer f);
+ }
+
+ public interface MutablePoint {
+ method public float getX();
+ method public float getY();
+ method public void setX(float);
+ method public void setY(float);
+ property public abstract float x;
+ property public abstract float y;
+ }
+
+ public fun interface PointTransformer {
+ method public void transform(androidx.graphics.shapes.MutablePoint);
}
public final class RoundedPolygon {
- ctor public RoundedPolygon(androidx.graphics.shapes.RoundedPolygon source);
- ctor public RoundedPolygon(float[] vertices, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding, optional float centerX, optional float centerY);
- ctor public RoundedPolygon(@IntRange(from=3L) int numVertices, optional float radius, optional float centerX, optional float centerY, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding);
- method public android.graphics.RectF getBounds();
+ method public float[] calculateBounds(optional float[] bounds);
method public float getCenterX();
method public float getCenterY();
- method public void setBounds(android.graphics.RectF);
- method public androidx.graphics.shapes.CubicShape toCubicShape();
- method public android.graphics.Path toPath();
- method public void transform(android.graphics.Matrix matrix);
- property public final android.graphics.RectF bounds;
+ method public java.util.List<androidx.graphics.shapes.Cubic> getCubics();
+ method public androidx.graphics.shapes.RoundedPolygon normalized();
+ method public androidx.graphics.shapes.RoundedPolygon transformed(androidx.graphics.shapes.PointTransformer f);
property public final float centerX;
property public final float centerY;
+ property public final java.util.List<androidx.graphics.shapes.Cubic> cubics;
field public static final androidx.graphics.shapes.RoundedPolygon.Companion Companion;
}
@@ -107,7 +91,18 @@
}
public final class RoundedPolygonKt {
- method public static void drawPolygon(android.graphics.Canvas, androidx.graphics.shapes.RoundedPolygon polygon, android.graphics.Paint paint);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(androidx.graphics.shapes.RoundedPolygon source);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(float[] vertices);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(float[] vertices, optional androidx.graphics.shapes.CornerRounding rounding);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(float[] vertices, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(float[] vertices, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding, optional float centerX);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(float[] vertices, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding, optional float centerX, optional float centerY);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(@IntRange(from=3L) int numVertices);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(@IntRange(from=3L) int numVertices, optional float radius);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(@IntRange(from=3L) int numVertices, optional float radius, optional float centerX);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(@IntRange(from=3L) int numVertices, optional float radius, optional float centerX, optional float centerY);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(@IntRange(from=3L) int numVertices, optional float radius, optional float centerX, optional float centerY, optional androidx.graphics.shapes.CornerRounding rounding);
+ method public static androidx.graphics.shapes.RoundedPolygon RoundedPolygon(@IntRange(from=3L) int numVertices, optional float radius, optional float centerX, optional float centerY, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding);
}
public final class ShapesKt {
diff --git a/graphics/graphics-shapes/build.gradle b/graphics/graphics-shapes/build.gradle
index 5e5dfc1..5434a7d 100644
--- a/graphics/graphics-shapes/build.gradle
+++ b/graphics/graphics-shapes/build.gradle
@@ -15,21 +15,54 @@
*/
import androidx.build.LibraryType
+import androidx.build.PlatformIdentifier
plugins {
id("AndroidXPlugin")
id("com.android.library")
- id("org.jetbrains.kotlin.android")
}
-dependencies {
- api(libs.kotlinStdlib)
- implementation("androidx.annotation:annotation:1.4.0")
- implementation("androidx.core:core-ktx:1.10.0-rc01")
- androidTestImplementation(libs.testExtJunit)
- androidTestImplementation(libs.testCore)
- androidTestImplementation(libs.testRunner)
- androidTestImplementation(libs.testRules)
+androidXMultiplatform {
+ android()
+
+ defaultPlatform(PlatformIdentifier.ANDROID)
+
+ sourceSets {
+ all {
+ languageSettings.optIn("kotlin.RequiresOptIn")
+ languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
+ }
+
+ commonMain {
+ dependencies {
+ api(libs.kotlinStdlib)
+ implementation 'androidx.annotation:annotation:1.7.0-alpha02'
+ }
+ }
+
+ commonTest {
+ dependencies {
+ }
+ }
+
+ androidMain {
+ dependsOn(commonMain)
+ dependencies {
+ implementation("androidx.core:core-ktx:1.10.0-rc01")
+ implementation("androidx.core:core-ktx:1.8.0")
+ }
+ }
+
+ androidAndroidTest {
+ dependsOn(commonTest)
+ dependencies {
+ implementation(libs.testExtJunit)
+ implementation(libs.testCore)
+ implementation(libs.testRunner)
+ implementation(libs.testRules)
+ }
+ }
+ }
}
android {
diff --git a/graphics/graphics-shapes/src/androidTest/AndroidManifest.xml b/graphics/graphics-shapes/src/androidAndroidTest/AndroidManifest.xml
similarity index 100%
rename from graphics/graphics-shapes/src/androidTest/AndroidManifest.xml
rename to graphics/graphics-shapes/src/androidAndroidTest/AndroidManifest.xml
diff --git a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/CornerRoundingTest.kt b/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/CornerRoundingTest.kt
similarity index 100%
rename from graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/CornerRoundingTest.kt
rename to graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/CornerRoundingTest.kt
diff --git a/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/CubicTest.kt b/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/CubicTest.kt
new file mode 100644
index 0000000..b422df5
--- /dev/null
+++ b/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/CubicTest.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2022 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.graphics.shapes
+
+import androidx.test.filters.SmallTest
+import kotlin.math.max
+import kotlin.math.min
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@SmallTest
+class CubicTest {
+
+ // These points create a roughly circular arc in the upper-right quadrant around (0,0)
+ private val zero = Point(0f, 0f)
+ private val p0 = Point(1f, 0f)
+ private val p1 = Point(1f, .5f)
+ private val p2 = Point(.5f, 1f)
+ private val p3 = Point(0f, 1f)
+ val cubic = Cubic(p0, p1, p2, p3)
+
+ @Test
+ fun constructionTest() {
+ assertEquals(p0, Point(cubic.anchor0X, cubic.anchor0Y))
+ assertEquals(p1, Point(cubic.control0X, cubic.control0Y))
+ assertEquals(p2, Point(cubic.control1X, cubic.control1Y))
+ assertEquals(p3, Point(cubic.anchor1X, cubic.anchor1Y))
+ }
+
+ @Test
+ fun circularArcTest() {
+ val arcCubic = Cubic.circularArc(zero.x, zero.y, p0.x, p0.y, p3.x, p3.y)
+ assertEquals(p0, Point(arcCubic.anchor0X, arcCubic.anchor0Y))
+ assertEquals(p3, Point(arcCubic.anchor1X, arcCubic.anchor1Y))
+ }
+
+ @Test
+ fun divTest() {
+ var divCubic = cubic / 1f
+ assertCubicsEqualish(cubic, divCubic)
+ divCubic = cubic / 1
+ assertCubicsEqualish(cubic, divCubic)
+ divCubic = cubic / 2f
+ assertPointsEqualish(p0 / 2f, Point(divCubic.anchor0X, divCubic.anchor0Y))
+ assertPointsEqualish(p1 / 2f, Point(divCubic.control0X, divCubic.control0Y))
+ assertPointsEqualish(p2 / 2f, Point(divCubic.control1X, divCubic.control1Y))
+ assertPointsEqualish(p3 / 2f, Point(divCubic.anchor1X, divCubic.anchor1Y))
+ divCubic = cubic / 2
+ assertPointsEqualish(p0 / 2f, Point(divCubic.anchor0X, divCubic.anchor0Y))
+ assertPointsEqualish(p1 / 2f, Point(divCubic.control0X, divCubic.control0Y))
+ assertPointsEqualish(p2 / 2f, Point(divCubic.control1X, divCubic.control1Y))
+ assertPointsEqualish(p3 / 2f, Point(divCubic.anchor1X, divCubic.anchor1Y))
+ }
+
+ @Test
+ fun timesTest() {
+ var timesCubic = cubic * 1f
+ assertEquals(p0, Point(timesCubic.anchor0X, timesCubic.anchor0Y))
+ assertEquals(p1, Point(timesCubic.control0X, timesCubic.control0Y))
+ assertEquals(p2, Point(timesCubic.control1X, timesCubic.control1Y))
+ assertEquals(p3, Point(timesCubic.anchor1X, timesCubic.anchor1Y))
+ timesCubic = cubic * 1
+ assertEquals(p0, Point(timesCubic.anchor0X, timesCubic.anchor0Y))
+ assertEquals(p1, Point(timesCubic.control0X, timesCubic.control0Y))
+ assertEquals(p2, Point(timesCubic.control1X, timesCubic.control1Y))
+ assertEquals(p3, Point(timesCubic.anchor1X, timesCubic.anchor1Y))
+ timesCubic = cubic * 2f
+ assertPointsEqualish(p0 * 2f, Point(timesCubic.anchor0X, timesCubic.anchor0Y))
+ assertPointsEqualish(p1 * 2f, Point(timesCubic.control0X, timesCubic.control0Y))
+ assertPointsEqualish(p2 * 2f, Point(timesCubic.control1X, timesCubic.control1Y))
+ assertPointsEqualish(p3 * 2f, Point(timesCubic.anchor1X, timesCubic.anchor1Y))
+ timesCubic = cubic * 2
+ assertPointsEqualish(p0 * 2f, Point(timesCubic.anchor0X, timesCubic.anchor0Y))
+ assertPointsEqualish(p1 * 2f, Point(timesCubic.control0X, timesCubic.control0Y))
+ assertPointsEqualish(p2 * 2f, Point(timesCubic.control1X, timesCubic.control1Y))
+ assertPointsEqualish(p3 * 2f, Point(timesCubic.anchor1X, timesCubic.anchor1Y))
+ }
+
+ @Test
+ fun plusTest() {
+ val offsetCubic = cubic * 2f
+ var plusCubic = cubic + offsetCubic
+ assertPointsEqualish(p0 + Point(offsetCubic.anchor0X, offsetCubic.anchor0Y),
+ Point(plusCubic.anchor0X, plusCubic.anchor0Y))
+ assertPointsEqualish(p1 + Point(offsetCubic.control0X, offsetCubic.control0Y),
+ Point(plusCubic.control0X, plusCubic.control0Y))
+ assertPointsEqualish(p2 + Point(offsetCubic.control1X, offsetCubic.control1Y),
+ Point(plusCubic.control1X, plusCubic.control1Y))
+ assertPointsEqualish(p3 + Point(offsetCubic.anchor1X, offsetCubic.anchor1Y),
+ Point(plusCubic.anchor1X, plusCubic.anchor1Y))
+ }
+
+ @Test
+ fun reverseTest() {
+ val reverseCubic = cubic.reverse()
+ assertEquals(p3, Point(reverseCubic.anchor0X, reverseCubic.anchor0Y))
+ assertEquals(p2, Point(reverseCubic.control0X, reverseCubic.control0Y))
+ assertEquals(p1, Point(reverseCubic.control1X, reverseCubic.control1Y))
+ assertEquals(p0, Point(reverseCubic.anchor1X, reverseCubic.anchor1Y))
+ }
+
+ private fun assertBetween(end0: Point, end1: Point, actual: Point) {
+ val minX = min(end0.x, end1.x)
+ val minY = min(end0.y, end1.y)
+ val maxX = max(end0.x, end1.x)
+ val maxY = max(end0.y, end1.y)
+ assertTrue(minX <= actual.x)
+ assertTrue(minY <= actual.y)
+ assertTrue(maxX >= actual.x)
+ assertTrue(maxY >= actual.y)
+ }
+
+ @Test
+ fun straightLineTest() {
+ val lineCubic = Cubic.straightLine(p0.x, p0.y, p3.x, p3.y)
+ assertEquals(p0, Point(lineCubic.anchor0X, lineCubic.anchor0Y))
+ assertEquals(p3, Point(lineCubic.anchor1X, lineCubic.anchor1Y))
+ assertBetween(p0, p3, Point(lineCubic.control0X, lineCubic.control0Y))
+ assertBetween(p0, p3, Point(lineCubic.control1X, lineCubic.control1Y))
+ }
+
+ @Test
+ fun splitTest() {
+ val (split0, split1) = cubic.split(.5f)
+ assertEquals(Point(cubic.anchor0X, cubic.anchor0Y),
+ Point(split0.anchor0X, split0.anchor0Y))
+ assertEquals(Point(cubic.anchor1X, cubic.anchor1Y),
+ Point(split1.anchor1X, split1.anchor1Y))
+ assertBetween(Point(cubic.anchor0X, cubic.anchor0Y),
+ Point(cubic.anchor1X, cubic.anchor1Y),
+ Point(split0.anchor1X, split0.anchor1Y))
+ assertBetween(Point(cubic.anchor0X, cubic.anchor0Y),
+ Point(cubic.anchor1X, cubic.anchor1Y),
+ Point(split1.anchor0X, split1.anchor0Y))
+ }
+
+ @Test
+ fun pointOnCurveTest() {
+ var halfway = cubic.pointOnCurve(.5f)
+ assertBetween(Point(cubic.anchor0X, cubic.anchor0Y),
+ Point(cubic.anchor1X, cubic.anchor1Y), halfway)
+ val straightLineCubic = Cubic.straightLine(p0.x, p0.y, p3.x, p3.y)
+ halfway = straightLineCubic.pointOnCurve(.5f)
+ val computedHalfway = Point(p0.x + .5f * (p3.x - p0.x), p0.y + .5f * (p3.y - p0.y))
+ assertPointsEqualish(computedHalfway, halfway)
+ }
+
+ @Test
+ fun transformTest() {
+ var transform = identityTransform()
+ var transformedCubic = cubic.transformed(transform)
+ assertCubicsEqualish(cubic, transformedCubic)
+
+ transform = scaleTransform(3f, 3f)
+ transformedCubic = cubic.transformed(transform)
+ assertCubicsEqualish(cubic * 3f, transformedCubic)
+
+ val tx = 200f
+ val ty = 300f
+ val translationVector = Point(tx, ty)
+ transform = translateTransform(tx, ty)
+ transformedCubic = cubic.transformed(transform)
+ assertPointsEqualish(Point(cubic.anchor0X, cubic.anchor0Y) + translationVector,
+ Point(transformedCubic.anchor0X, transformedCubic.anchor0Y))
+ assertPointsEqualish(Point(cubic.control0X, cubic.control0Y) + translationVector,
+ Point(transformedCubic.control0X, transformedCubic.control0Y))
+ assertPointsEqualish(Point(cubic.control1X, cubic.control1Y) + translationVector,
+ Point(transformedCubic.control1X, transformedCubic.control1Y))
+ assertPointsEqualish(Point(cubic.anchor1X, cubic.anchor1Y) + translationVector,
+ Point(transformedCubic.anchor1X, transformedCubic.anchor1Y))
+ }
+}
diff --git a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/FloatMappingTest.kt b/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/FloatMappingTest.kt
similarity index 100%
rename from graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/FloatMappingTest.kt
rename to graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/FloatMappingTest.kt
diff --git a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonMeasureTest.kt b/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/PolygonMeasureTest.kt
similarity index 100%
rename from graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonMeasureTest.kt
rename to graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/PolygonMeasureTest.kt
diff --git a/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/PolygonTest.kt b/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/PolygonTest.kt
new file mode 100644
index 0000000..ec0b2d0f
--- /dev/null
+++ b/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/PolygonTest.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2022 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.graphics.shapes
+
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@SmallTest
+class PolygonTest {
+ val square = RoundedPolygon(4)
+
+ @Test
+ fun constructionTest() {
+ // We can't be too specific on how exactly the square is constructed, but
+ // we can at least test whether all points are within the unit square
+ var min = Point(-1f, -1f)
+ var max = Point(1f, 1f)
+ assertInBounds(square.cubics, min, max)
+
+ val doubleSquare = RoundedPolygon(4, 2f)
+ min = min * 2f
+ max = max * 2f
+ assertInBounds(doubleSquare.cubics, min, max)
+
+ val offsetSquare = RoundedPolygon(4, centerX = 1f, centerY = 2f)
+ min = Point(0f, 1f)
+ max = Point(2f, 3f)
+ assertInBounds(offsetSquare.cubics, min, max)
+
+ val squareCopy = RoundedPolygon(square)
+ min = Point(-1f, -1f)
+ max = Point(1f, 1f)
+ assertInBounds(squareCopy.cubics, min, max)
+
+ val p0 = Point(1f, 0f)
+ val p1 = Point(0f, 1f)
+ val p2 = Point(-1f, 0f)
+ val p3 = Point(0f, -1f)
+ val manualSquare = RoundedPolygon(floatArrayOf(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y,
+ p3.x, p3.y))
+ min = Point(-1f, -1f)
+ max = Point(1f, 1f)
+ assertInBounds(manualSquare.cubics, min, max)
+
+ val offset = Point(1f, 2f)
+ val p0Offset = p0 + offset
+ val p1Offset = p1 + offset
+ val p2Offset = p2 + offset
+ val p3Offset = p3 + offset
+ val manualSquareOffset = RoundedPolygon(
+ vertices = floatArrayOf(p0Offset.x, p0Offset.y, p1Offset.x, p1Offset.y,
+ p2Offset.x, p2Offset.y, p3Offset.x, p3Offset.y),
+ centerX = offset.x, centerY = offset.y)
+ min = Point(0f, 1f)
+ max = Point(2f, 3f)
+ assertInBounds(manualSquareOffset.cubics, min, max)
+ }
+
+ @Test
+ fun boundsTest() {
+ val bounds = square.calculateBounds()
+ assertEqualish(-1f, bounds[0]) // Left
+ assertEqualish(-1f, bounds[1]) // Top
+ assertEqualish(1f, bounds[2]) // Right
+ assertEqualish(1f, bounds[3]) // Bottom
+ }
+
+ @Test
+ fun centerTest() {
+ assertPointsEqualish(Point(0f, 0f), Point(square.centerX, square.centerY))
+ }
+
+ @Test
+ fun transformTest() {
+ // First, make sure the shape doesn't change when transformed by the identity
+ val squareCopy = square.transformed(identityTransform())
+ val n = square.cubics.size
+
+ assertEquals(n, squareCopy.cubics.size)
+ for (i in 0 until n) {
+ assertCubicsEqualish(square.cubics[i], squareCopy.cubics[i])
+ }
+
+ // Now create a function which translates points by (1, 2) and make sure
+ // the shape is translated similarly by it
+ val offset = Point(1f, 2f)
+ val squareCubics = square.cubics
+ val translator = translateTransform(offset.x, offset.y)
+ val translatedSquareCubics = square.transformed(translator).cubics
+
+ for (i in squareCubics.indices) {
+ assertPointsEqualish(Point(squareCubics[i].anchor0X,
+ squareCubics[i].anchor0Y) + offset,
+ Point(translatedSquareCubics[i].anchor0X, translatedSquareCubics[i].anchor0Y))
+ assertPointsEqualish(Point(squareCubics[i].control0X,
+ squareCubics[i].control0Y) + offset,
+ Point(translatedSquareCubics[i].control0X, translatedSquareCubics[i].control0Y))
+ assertPointsEqualish(Point(squareCubics[i].control1X,
+ squareCubics[i].control1Y) + offset,
+ Point(translatedSquareCubics[i].control1X, translatedSquareCubics[i].control1Y))
+ assertPointsEqualish(Point(squareCubics[i].anchor1X,
+ squareCubics[i].anchor1Y) + offset,
+ Point(translatedSquareCubics[i].anchor1X, translatedSquareCubics[i].anchor1Y))
+ }
+ }
+
+ @Test
+ fun featuresTest() {
+ val squareFeatures = square.features
+
+ // Verify that cubics of polygon == cubics of features of that polygon
+ assertTrue(square.cubics == squareFeatures.flatMap { it.cubics })
+
+ // Same as above but with rounded corners
+ val roundedSquare = RoundedPolygon(4, rounding = CornerRounding(.1f))
+ val roundedFeatures = roundedSquare.features
+ assertTrue(roundedSquare.cubics == roundedFeatures.flatMap { it.cubics })
+
+ // Same as the first polygon test, but with a copy of that polygon
+ val squareCopy = RoundedPolygon(square)
+ val squareCopyFeatures = squareCopy.features
+ assertTrue(squareCopy.cubics == squareCopyFeatures.flatMap { it.cubics })
+
+ // Test other elements of Features
+ val translator = translateTransform(1f, 2f)
+ val features = square.features
+ val preTransformVertices = mutableListOf<Point>()
+ val preTransformCenters = mutableListOf<Point>()
+ for (feature in features) {
+ if (feature is Feature.Corner) {
+ // Copy into new Point objects since the ones in the feature should transform
+ preTransformVertices.add(Point(feature.vertex.x, feature.vertex.y))
+ preTransformCenters.add(Point(feature.roundedCenter.x, feature.roundedCenter.y))
+ }
+ }
+ val transformedFeatures = square.transformed(translator).features
+ val postTransformVertices = mutableListOf<Point>()
+ val postTransformCenters = mutableListOf<Point>()
+ for (feature in transformedFeatures) {
+ if (feature is Feature.Corner) {
+ postTransformVertices.add(feature.vertex)
+ postTransformCenters.add(feature.roundedCenter)
+ }
+ }
+ assertNotEquals(preTransformVertices, postTransformVertices)
+ assertNotEquals(preTransformCenters, postTransformCenters)
+ }
+}
diff --git a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/RoundedPolygonTest.kt b/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/RoundedPolygonTest.kt
similarity index 81%
rename from graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/RoundedPolygonTest.kt
rename to graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/RoundedPolygonTest.kt
index 0df251f..2ac9a18 100644
--- a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/RoundedPolygonTest.kt
+++ b/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/RoundedPolygonTest.kt
@@ -16,8 +16,6 @@
package androidx.graphics.shapes
-import android.graphics.PointF
-import androidx.core.graphics.times
import androidx.test.filters.SmallTest
import org.junit.Assert
import org.junit.Assert.assertEquals
@@ -36,32 +34,32 @@
}
val square = RoundedPolygon(4)
- var min = PointF(-1f, -1f)
- var max = PointF(1f, 1f)
- assertInBounds(square.toCubicShape(), min, max)
+ var min = Point(-1f, -1f)
+ var max = Point(1f, 1f)
+ assertInBounds(square.cubics, min, max)
val doubleSquare = RoundedPolygon(4, 2f)
min *= 2f
max *= 2f
- assertInBounds(doubleSquare.toCubicShape(), min, max)
+ assertInBounds(doubleSquare.cubics, min, max)
val squareRounded = RoundedPolygon(4, rounding = rounding)
- min = PointF(-1f, -1f)
- max = PointF(1f, 1f)
- assertInBounds(squareRounded.toCubicShape(), min, max)
+ min = Point(-1f, -1f)
+ max = Point(1f, 1f)
+ assertInBounds(squareRounded.cubics, min, max)
val squarePVRounded = RoundedPolygon(4, perVertexRounding = perVtxRounded)
- min = PointF(-1f, -1f)
- max = PointF(1f, 1f)
- assertInBounds(squarePVRounded.toCubicShape(), min, max)
+ min = Point(-1f, -1f)
+ max = Point(1f, 1f)
+ assertInBounds(squarePVRounded.cubics, min, max)
}
@Test
fun verticesConstructorTest() {
- val p0 = PointF(1f, 0f)
- val p1 = PointF(0f, 1f)
- val p2 = PointF(-1f, 0f)
- val p3 = PointF(0f, -1f)
+ val p0 = Point(1f, 0f)
+ val p1 = Point(0f, 1f)
+ val p2 = Point(-1f, 0f)
+ val p3 = Point(0f, -1f)
val verts = floatArrayOf(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)
Assert.assertThrows(IllegalArgumentException::class.java) {
@@ -69,32 +67,32 @@
}
val manualSquare = RoundedPolygon(verts)
- var min = PointF(-1f, -1f)
- var max = PointF(1f, 1f)
- assertInBounds(manualSquare.toCubicShape(), min, max)
+ var min = Point(-1f, -1f)
+ var max = Point(1f, 1f)
+ assertInBounds(manualSquare.cubics, min, max)
- val offset = PointF(1f, 2f)
+ val offset = Point(1f, 2f)
val offsetVerts = floatArrayOf(p0.x + offset.x, p0.y + offset.y,
p1.x + offset.x, p1.y + offset.y, p2.x + offset.x, p2.y + offset.y,
p3.x + offset.x, p3.y + offset.y)
val manualSquareOffset = RoundedPolygon(offsetVerts, centerX = offset.x, centerY = offset.y)
- min = PointF(0f, 1f)
- max = PointF(2f, 3f)
- assertInBounds(manualSquareOffset.toCubicShape(), min, max)
+ min = Point(0f, 1f)
+ max = Point(2f, 3f)
+ assertInBounds(manualSquareOffset.cubics, min, max)
val manualSquareRounded = RoundedPolygon(verts, rounding = rounding)
- min = PointF(-1f, -1f)
- max = PointF(1f, 1f)
- assertInBounds(manualSquareRounded.toCubicShape(), min, max)
+ min = Point(-1f, -1f)
+ max = Point(1f, 1f)
+ assertInBounds(manualSquareRounded.cubics, min, max)
val manualSquarePVRounded = RoundedPolygon(verts,
perVertexRounding = perVtxRounded)
- min = PointF(-1f, -1f)
- max = PointF(1f, 1f)
- assertInBounds(manualSquarePVRounded.toCubicShape(), min, max)
+ min = Point(-1f, -1f)
+ max = Point(1f, 1f)
+ assertInBounds(manualSquarePVRounded.cubics, min, max)
}
- fun pointsToFloats(points: List<PointF>): FloatArray {
+ private fun pointsToFloats(points: List<Point>): FloatArray {
val result = FloatArray(points.size * 2)
var index = 0
for (point in points) {
@@ -106,9 +104,9 @@
@Test
fun roundingSpaceUsageTest() {
- val p0 = PointF(0f, 0f)
- val p1 = PointF(1f, 0f)
- val p2 = PointF(0.5f, 1f)
+ val p0 = Point(0f, 0f)
+ val p1 = Point(1f, 0f)
+ val p2 = Point(0.5f, 1f)
val pvRounding = listOf(
CornerRounding(1f, 0f),
CornerRounding(1f, 1f),
@@ -121,8 +119,8 @@
// Since there is not enough room in the p0 -> p1 side even for the roundings, we shouldn't
// take smoothing into account, so the corners should end in the middle point.
- val lowerEdgeFeature = polygon.features.first { it is RoundedPolygon.Edge }
- as RoundedPolygon.Edge
+ val lowerEdgeFeature = polygon.features.first { it is Feature.Edge }
+ as Feature.Edge
assertEquals(1, lowerEdgeFeature.cubics.size)
val lowerEdge = lowerEdgeFeature.cubics.first()
@@ -211,10 +209,10 @@
// Corner rounding parameter for vertex 3 (bottom left)
rounding3: CornerRounding = CornerRounding(0.5f)
) {
- val p0 = PointF(0f, 0f)
- val p1 = PointF(5f, 0f)
- val p2 = PointF(5f, 1f)
- val p3 = PointF(0f, 1f)
+ val p0 = Point(0f, 0f)
+ val p1 = Point(5f, 0f)
+ val p2 = Point(5f, 1f)
+ val p3 = Point(0f, 1f)
val pvRounding = listOf(
rounding0,
@@ -226,7 +224,7 @@
vertices = pointsToFloats(listOf(p0, p1, p2, p3)),
perVertexRounding = pvRounding
)
- val (e01, _, _, e30) = polygon.features.filterIsInstance<RoundedPolygon.Edge>()
+ val (e01, _, _, e30) = polygon.features.filterIsInstance<Feature.Edge>()
val msg = "r0 = ${show(rounding0)}, r3 = ${show(rounding3)}"
assertEqualish(expectedV0SX, e01.cubics.first().anchor0X, msg)
assertEqualish(expectedV0SY, e30.cubics.first().anchor1Y, msg)
diff --git a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/ShapesTest.kt b/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/ShapesTest.kt
similarity index 74%
rename from graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/ShapesTest.kt
rename to graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/ShapesTest.kt
index 34abff59..702f971 100644
--- a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/ShapesTest.kt
+++ b/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/ShapesTest.kt
@@ -16,8 +16,6 @@
package androidx.graphics.shapes
-import android.graphics.PointF
-import androidx.core.graphics.minus
import androidx.test.filters.SmallTest
import kotlin.AssertionError
import kotlin.math.sqrt
@@ -32,10 +30,10 @@
@SmallTest
class ShapesTest {
- val Zero = PointF(0f, 0f)
+ private val Zero = Point(0f, 0f)
val Epsilon = .01f
- fun distance(start: PointF, end: PointF): Float {
+ private fun distance(start: Point, end: Point): Float {
val vector = end - start
return sqrt(vector.x * vector.x + vector.y * vector.y)
}
@@ -44,11 +42,11 @@
* Test that the given point is radius distance away from [center]. If two radii are provided
* it is sufficient to lie on either one (used for testing points on stars).
*/
- fun assertPointOnRadii(
- point: PointF,
+ private fun assertPointOnRadii(
+ point: Point,
radius1: Float,
radius2: Float = radius1,
- center: PointF = Zero
+ center: Point = Zero
) {
val dist = distance(center, point)
try {
@@ -58,14 +56,14 @@
}
}
- fun assertCubicOnRadii(
+ private fun assertCubicOnRadii(
cubic: Cubic,
radius1: Float,
radius2: Float = radius1,
- center: PointF = Zero
+ center: Point = Zero
) {
- assertPointOnRadii(PointF(cubic.anchor0X, cubic.anchor0Y), radius1, radius2, center)
- assertPointOnRadii(PointF(cubic.anchor1X, cubic.anchor1Y), radius1, radius2, center)
+ assertPointOnRadii(Point(cubic.anchor0X, cubic.anchor0Y), radius1, radius2, center)
+ assertPointOnRadii(Point(cubic.anchor1X, cubic.anchor1Y), radius1, radius2, center)
}
/**
@@ -73,7 +71,7 @@
* center, compared to the requested radius. The test is very lenient since the Circle shape is
* only a 4x cubic approximation of the circle and varies from the true circle.
*/
- fun assertCircularCubic(cubic: Cubic, radius: Float, center: PointF) {
+ private fun assertCircularCubic(cubic: Cubic, radius: Float, center: Point) {
var t = 0f
while (t <= 1f) {
val pointOnCurve = cubic.pointOnCurve(t)
@@ -83,8 +81,8 @@
}
}
- fun assertCircleShape(shape: CubicShape, radius: Float = 1f, center: PointF = Zero) {
- for (cubic in shape.cubics) {
+ private fun assertCircleShape(shape: List<Cubic>, radius: Float = 1f, center: Point = Zero) {
+ for (cubic in shape) {
assertCircularCubic(cubic, radius, center)
}
}
@@ -96,20 +94,20 @@
}
val circle = RoundedPolygon.circle()
- assertCircleShape(circle.toCubicShape())
+ assertCircleShape(circle.cubics)
val simpleCircle = RoundedPolygon.circle(3)
- assertCircleShape(simpleCircle.toCubicShape())
+ assertCircleShape(simpleCircle.cubics)
val complexCircle = RoundedPolygon.circle(20)
- assertCircleShape(complexCircle.toCubicShape())
+ assertCircleShape(complexCircle.cubics)
val bigCircle = RoundedPolygon.circle(radius = 3f)
- assertCircleShape(bigCircle.toCubicShape(), radius = 3f)
+ assertCircleShape(bigCircle.cubics, radius = 3f)
- val center = PointF(1f, 2f)
+ val center = Point(1f, 2f)
val offsetCircle = RoundedPolygon.circle(centerX = center.x, centerY = center.y)
- assertCircleShape(offsetCircle.toCubicShape(), center = center)
+ assertCircleShape(offsetCircle.cubics, center = center)
}
/**
@@ -120,26 +118,26 @@
@Test
fun starTest() {
var star = RoundedPolygon.star(4, innerRadius = .5f)
- var shape = star.toCubicShape()
+ var shape = star.cubics
var radius = 1f
var innerRadius = .5f
- for (cubic in shape.cubics) {
+ for (cubic in shape) {
assertCubicOnRadii(cubic, radius, innerRadius)
}
- val center = PointF(1f, 2f)
+ val center = Point(1f, 2f)
star = RoundedPolygon.star(4, innerRadius = innerRadius,
centerX = center.x, centerY = center.y)
- shape = star.toCubicShape()
- for (cubic in shape.cubics) {
+ shape = star.cubics
+ for (cubic in shape) {
assertCubicOnRadii(cubic, radius, innerRadius, center)
}
radius = 4f
innerRadius = 2f
star = RoundedPolygon.star(4, radius, innerRadius)
- shape = star.toCubicShape()
- for (cubic in shape.cubics) {
+ shape = star.cubics
+ for (cubic in shape) {
assertCubicOnRadii(cubic, radius, innerRadius)
}
}
@@ -152,21 +150,21 @@
rounding, innerRounding, rounding, innerRounding)
var star = RoundedPolygon.star(4, innerRadius = .5f, rounding = rounding)
- val min = PointF(-1f, -1f)
- val max = PointF(1f, 1f)
- assertInBounds(star.toCubicShape(), min, max)
+ val min = Point(-1f, -1f)
+ val max = Point(1f, 1f)
+ assertInBounds(star.cubics, min, max)
star = RoundedPolygon.star(4, innerRadius = .5f, innerRounding = innerRounding)
- assertInBounds(star.toCubicShape(), min, max)
+ assertInBounds(star.cubics, min, max)
star = RoundedPolygon.star(
4, innerRadius = .5f, rounding = rounding,
innerRounding = innerRounding
)
- assertInBounds(star.toCubicShape(), min, max)
+ assertInBounds(star.cubics, min, max)
star = RoundedPolygon.star(4, innerRadius = .5f, perVertexRounding = perVtxRounded)
- assertInBounds(star.toCubicShape(), min, max)
+ assertInBounds(star.cubics, min, max)
assertThrows(IllegalArgumentException::class.java) {
star = RoundedPolygon.star(
diff --git a/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/TestUtils.kt b/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/TestUtils.kt
new file mode 100644
index 0000000..64a4684
--- /dev/null
+++ b/graphics/graphics-shapes/src/androidAndroidTest/kotlin/androidx/graphics/shapes/TestUtils.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2022 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.graphics.shapes
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+
+private val Epsilon = 1e-4f
+
+// Test equality within Epsilon
+internal fun assertPointsEqualish(expected: Point, actual: Point) {
+ val msg = "$expected vs. $actual"
+ assertEquals(msg, expected.x, actual.x, Epsilon)
+ assertEquals(msg, expected.y, actual.y, Epsilon)
+}
+
+internal fun assertCubicsEqualish(expected: Cubic, actual: Cubic) {
+ assertPointsEqualish(Point(expected.anchor0X, expected.anchor0Y),
+ Point(actual.anchor0X, actual.anchor0Y))
+ assertPointsEqualish(Point(expected.control0X, expected.control0Y),
+ Point(actual.control0X, actual.control0Y))
+ assertPointsEqualish(Point(expected.control1X, expected.control1Y),
+ Point(actual.control1X, actual.control1Y))
+ assertPointsEqualish(Point(expected.anchor1X, expected.anchor1Y),
+ Point(actual.anchor1X, actual.anchor1Y))
+}
+
+internal fun assertPointGreaterish(expected: Point, actual: Point) {
+ assertTrue(actual.x >= expected.x - Epsilon)
+ assertTrue(actual.y >= expected.y - Epsilon)
+}
+
+internal fun assertPointLessish(expected: Point, actual: Point) {
+ assertTrue(actual.x <= expected.x + Epsilon)
+ assertTrue(actual.y <= expected.y + Epsilon)
+}
+
+internal fun assertEqualish(expected: Float, actual: Float, message: String? = null) {
+ assertEquals(message ?: "", expected, actual, Epsilon)
+}
+
+internal fun assertInBounds(shape: List<Cubic>, minPoint: Point, maxPoint: Point) {
+ for (cubic in shape) {
+ assertPointGreaterish(minPoint, Point(cubic.anchor0X, cubic.anchor0Y))
+ assertPointLessish(maxPoint, Point(cubic.anchor0X, cubic.anchor0Y))
+ assertPointGreaterish(minPoint, Point(cubic.control0X, cubic.control0Y))
+ assertPointLessish(maxPoint, Point(cubic.control0X, cubic.control0Y))
+ assertPointGreaterish(minPoint, Point(cubic.control1X, cubic.control1Y))
+ assertPointLessish(maxPoint, Point(cubic.control1X, cubic.control1Y))
+ assertPointGreaterish(minPoint, Point(cubic.anchor1X, cubic.anchor1Y))
+ assertPointLessish(maxPoint, Point(cubic.anchor1X, cubic.anchor1Y))
+ }
+}
+
+internal fun identityTransform() = PointTransformer { }
+
+internal fun scaleTransform(sx: Float, sy: Float) = PointTransformer {
+ x *= sx
+ y *= sy
+}
+
+internal fun translateTransform(dx: Float, dy: Float) = PointTransformer {
+ x += dx
+ y += dy
+}
diff --git a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/CubicShapeTest.kt b/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/CubicShapeTest.kt
deleted file mode 100644
index 422b636..0000000
--- a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/CubicShapeTest.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright 2022 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.graphics.shapes
-
-import android.graphics.Matrix
-import android.graphics.PointF
-import androidx.test.filters.SmallTest
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertNotNull
-import org.junit.Test
-
-@SmallTest
-class CubicShapeTest {
-
- // ~circular arc from (1, 0) to (0, 1)
- val point0 = PointF(1f, 0f)
- val point1 = PointF(1f, .5f)
- val point2 = PointF(.5f, 1f)
- val point3 = PointF(0f, 1f)
-
- // ~circular arc from (0, 1) to (-1, 0)
- val point4 = PointF(0f, 1f)
- val point5 = PointF(-.5f, 1f)
- val point6 = PointF(-.5f, .5f)
- val point7 = PointF(-1f, 0f)
-
- val cubic0 = Cubic(point0, point1, point2, point3)
- val cubic1 = Cubic(point4, point5, point6, point7)
-
- fun getClosingCubic(first: Cubic, last: Cubic): Cubic {
- return Cubic(last.anchor1X, last.anchor1Y, last.anchor1X, last.anchor1Y,
- first.anchor0X, first.anchor0Y, first.anchor0X, first.anchor0Y)
- }
-
- @Test
- fun constructionTest() {
- var shape = CubicShape(listOf(cubic0, getClosingCubic(cubic0, cubic0)))
- assertNotNull(shape)
-
- shape = CubicShape(listOf(cubic0, cubic1, getClosingCubic(cubic0, cubic1)))
- assertNotNull(shape)
- val shape1 = CubicShape(shape)
- assertEquals(shape, shape1)
- }
-
- @Test
- fun pathTest() {
- val shape = CubicShape(listOf(cubic0, cubic1, getClosingCubic(cubic0, cubic1)))
- val path = shape.toPath()
- assertFalse(path.isEmpty)
- }
-
- @Test
- fun boundsTest() {
- val shape = CubicShape(listOf(cubic0, cubic1, getClosingCubic(cubic0, cubic1)))
- val bounds = shape.bounds
- assertPointsEqualish(PointF(-1f, 0f), PointF(bounds.left, bounds.top))
- assertPointsEqualish(PointF(1f, 1f), PointF(bounds.right, bounds.bottom))
- }
-
- @Test
- fun cubicsTest() {
- val shape = CubicShape(listOf(cubic0, cubic1, getClosingCubic(cubic0, cubic1)))
- val cubics = shape.cubics
- assertCubicsEqua1ish(cubic0, cubics[0])
- assertCubicsEqua1ish(cubic1, cubics[1])
- }
-
- @Test
- fun transformTest() {
- val shape = CubicShape(listOf(cubic0, getClosingCubic(cubic0, cubic0)))
-
- // First, make sure the shape doesn't change when transformed by the identity
- val identity = Matrix()
- shape.transform(identity)
- val cubics = shape.cubics
- assertCubicsEqua1ish(cubic0, cubics[0])
-
- // Now create a matrix which translates points by (1, 2) and make sure
- // the shape is translated similarly by it
- val translator = Matrix()
- translator.setTranslate(1f, 2f)
- val translatedPoints = floatArrayOf(point0.x, point0.y, point1.x, point1.y,
- point2.x, point2.y, point3.x, point3.y)
- translator.mapPoints(translatedPoints)
- shape.transform(translator)
- val cubic = shape.cubics[0]
- assertPointsEqualish(PointF(translatedPoints[0], translatedPoints[1]),
- PointF(cubic.anchor0X, cubic.anchor0Y))
- assertPointsEqualish(PointF(translatedPoints[2], translatedPoints[3]),
- PointF(cubic.control0X, cubic.control0Y))
- assertPointsEqualish(PointF(translatedPoints[4], translatedPoints[5]),
- PointF(cubic.control1X, cubic.control1Y))
- assertPointsEqualish(PointF(translatedPoints[6], translatedPoints[7]),
- PointF(cubic.anchor1X, cubic.anchor1Y))
- }
-}
diff --git a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/CubicTest.kt b/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/CubicTest.kt
deleted file mode 100644
index 78b58c2..0000000
--- a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/CubicTest.kt
+++ /dev/null
@@ -1,220 +0,0 @@
-/*
- * Copyright 2022 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.graphics.shapes
-
-import android.graphics.Matrix
-import android.graphics.PointF
-import androidx.core.graphics.div
-import androidx.core.graphics.plus
-import androidx.core.graphics.times
-import androidx.test.filters.SmallTest
-import kotlin.math.max
-import kotlin.math.min
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
-
-@SmallTest
-class CubicTest {
-
- // These points create a roughly circular arc in the upper-right quadrant around (0,0)
- val zero = PointF(0f, 0f)
- val p0 = PointF(1f, 0f)
- val p1 = PointF(1f, .5f)
- val p2 = PointF(.5f, 1f)
- val p3 = PointF(0f, 1f)
- val cubic = Cubic(p0, p1, p2, p3)
-
- @Test
- fun constructionTest() {
- assertEquals(p0, PointF(cubic.anchor0X, cubic.anchor0Y))
- assertEquals(p1, PointF(cubic.control0X, cubic.control0Y))
- assertEquals(p2, PointF(cubic.control1X, cubic.control1Y))
- assertEquals(p3, PointF(cubic.anchor1X, cubic.anchor1Y))
- }
-
- @Test
- fun copyTest() {
- val copy = Cubic(cubic)
- assertEquals(p0, PointF(copy.anchor0X, copy.anchor0Y))
- assertEquals(p1, PointF(copy.control0X, copy.control0Y))
- assertEquals(p2, PointF(copy.control1X, copy.control1Y))
- assertEquals(p3, PointF(copy.anchor1X, copy.anchor1Y))
- assertEquals(PointF(cubic.anchor0X, cubic.anchor0Y),
- PointF(copy.anchor0X, copy.anchor0Y))
- assertEquals(PointF(cubic.control0X, cubic.control0Y),
- PointF(copy.control0X, copy.control0Y))
- assertEquals(PointF(cubic.control1X, cubic.control1Y),
- PointF(copy.control1X, copy.control1Y))
- assertEquals(PointF(cubic.anchor1X, cubic.anchor1Y),
- PointF(copy.anchor1X, copy.anchor1Y))
- }
-
- @Test
- fun circularArcTest() {
- val arcCubic = Cubic.circularArc(zero.x, zero.y, p0.x, p0.y, p3.x, p3.y)
- assertEquals(p0, PointF(arcCubic.anchor0X, arcCubic.anchor0Y))
- assertEquals(p3, PointF(arcCubic.anchor1X, arcCubic.anchor1Y))
- }
-
- @Test
- fun divTest() {
- var divCubic = cubic / 1f
- assertCubicsEqua1ish(cubic, divCubic)
- divCubic = cubic / 1
- assertCubicsEqua1ish(cubic, divCubic)
- divCubic = cubic / 2f
- assertPointsEqualish(p0 / 2f, PointF(divCubic.anchor0X, divCubic.anchor0Y))
- assertPointsEqualish(p1 / 2f, PointF(divCubic.control0X, divCubic.control0Y))
- assertPointsEqualish(p2 / 2f, PointF(divCubic.control1X, divCubic.control1Y))
- assertPointsEqualish(p3 / 2f, PointF(divCubic.anchor1X, divCubic.anchor1Y))
- divCubic = cubic / 2
- assertPointsEqualish(p0 / 2f, PointF(divCubic.anchor0X, divCubic.anchor0Y))
- assertPointsEqualish(p1 / 2f, PointF(divCubic.control0X, divCubic.control0Y))
- assertPointsEqualish(p2 / 2f, PointF(divCubic.control1X, divCubic.control1Y))
- assertPointsEqualish(p3 / 2f, PointF(divCubic.anchor1X, divCubic.anchor1Y))
- }
-
- @Test
- fun timesTest() {
- var timesCubic = cubic * 1f
- assertEquals(p0, PointF(timesCubic.anchor0X, timesCubic.anchor0Y))
- assertEquals(p1, PointF(timesCubic.control0X, timesCubic.control0Y))
- assertEquals(p2, PointF(timesCubic.control1X, timesCubic.control1Y))
- assertEquals(p3, PointF(timesCubic.anchor1X, timesCubic.anchor1Y))
- timesCubic = cubic * 1
- assertEquals(p0, PointF(timesCubic.anchor0X, timesCubic.anchor0Y))
- assertEquals(p1, PointF(timesCubic.control0X, timesCubic.control0Y))
- assertEquals(p2, PointF(timesCubic.control1X, timesCubic.control1Y))
- assertEquals(p3, PointF(timesCubic.anchor1X, timesCubic.anchor1Y))
- timesCubic = cubic * 2f
- assertPointsEqualish(p0 * 2f, PointF(timesCubic.anchor0X, timesCubic.anchor0Y))
- assertPointsEqualish(p1 * 2f, PointF(timesCubic.control0X, timesCubic.control0Y))
- assertPointsEqualish(p2 * 2f, PointF(timesCubic.control1X, timesCubic.control1Y))
- assertPointsEqualish(p3 * 2f, PointF(timesCubic.anchor1X, timesCubic.anchor1Y))
- timesCubic = cubic * 2
- assertPointsEqualish(p0 * 2f, PointF(timesCubic.anchor0X, timesCubic.anchor0Y))
- assertPointsEqualish(p1 * 2f, PointF(timesCubic.control0X, timesCubic.control0Y))
- assertPointsEqualish(p2 * 2f, PointF(timesCubic.control1X, timesCubic.control1Y))
- assertPointsEqualish(p3 * 2f, PointF(timesCubic.anchor1X, timesCubic.anchor1Y))
- }
-
- @Test
- fun plusTest() {
- val offsetCubic = cubic * 2f
- var plusCubic = cubic + offsetCubic
- assertPointsEqualish(p0 + PointF(offsetCubic.anchor0X, offsetCubic.anchor0Y),
- PointF(plusCubic.anchor0X, plusCubic.anchor0Y))
- assertPointsEqualish(p1 + PointF(offsetCubic.control0X, offsetCubic.control0Y),
- PointF(plusCubic.control0X, plusCubic.control0Y))
- assertPointsEqualish(p2 + PointF(offsetCubic.control1X, offsetCubic.control1Y),
- PointF(plusCubic.control1X, plusCubic.control1Y))
- assertPointsEqualish(p3 + PointF(offsetCubic.anchor1X, offsetCubic.anchor1Y),
- PointF(plusCubic.anchor1X, plusCubic.anchor1Y))
- }
-
- @Test
- fun reverseTest() {
- val reverseCubic = cubic.reverse()
- assertEquals(p3, PointF(reverseCubic.anchor0X, reverseCubic.anchor0Y))
- assertEquals(p2, PointF(reverseCubic.control0X, reverseCubic.control0Y))
- assertEquals(p1, PointF(reverseCubic.control1X, reverseCubic.control1Y))
- assertEquals(p0, PointF(reverseCubic.anchor1X, reverseCubic.anchor1Y))
- }
-
- fun assertBetween(end0: PointF, end1: PointF, actual: PointF) {
- val minX = min(end0.x, end1.x)
- val minY = min(end0.y, end1.y)
- val maxX = max(end0.x, end1.x)
- val maxY = max(end0.y, end1.y)
- assertTrue(minX <= actual.x)
- assertTrue(minY <= actual.y)
- assertTrue(maxX >= actual.x)
- assertTrue(maxY >= actual.y)
- }
-
- @Test
- fun straightLineTest() {
- val lineCubic = Cubic.straightLine(p0.x, p0.y, p3.x, p3.y)
- assertEquals(p0, PointF(lineCubic.anchor0X, lineCubic.anchor0Y))
- assertEquals(p3, PointF(lineCubic.anchor1X, lineCubic.anchor1Y))
- assertBetween(p0, p3, PointF(lineCubic.control0X, lineCubic.control0Y))
- assertBetween(p0, p3, PointF(lineCubic.control1X, lineCubic.control1Y))
- }
-
- @Test
- fun interpolateTest() {
- val twiceCubic = cubic + cubic * 2f
- val quadCubic = cubic + cubic * 4f
- val halfway = Cubic.interpolate(cubic, quadCubic, .5f)
- assertCubicsEqua1ish(twiceCubic, halfway)
- }
-
- @Test
- fun splitTest() {
- val (split0, split1) = cubic.split(.5f)
- assertEquals(PointF(cubic.anchor0X, cubic.anchor0Y),
- PointF(split0.anchor0X, split0.anchor0Y))
- assertEquals(PointF(cubic.anchor1X, cubic.anchor1Y),
- PointF(split1.anchor1X, split1.anchor1Y))
- assertBetween(PointF(cubic.anchor0X, cubic.anchor0Y),
- PointF(cubic.anchor1X, cubic.anchor1Y),
- PointF(split0.anchor1X, split0.anchor1Y))
- assertBetween(PointF(cubic.anchor0X, cubic.anchor0Y),
- PointF(cubic.anchor1X, cubic.anchor1Y),
- PointF(split1.anchor0X, split1.anchor0Y))
- }
-
- @Test
- fun pointOnCurveTest() {
- var halfway = cubic.pointOnCurve(.5f)
- assertBetween(PointF(cubic.anchor0X, cubic.anchor0Y),
- PointF(cubic.anchor1X, cubic.anchor1Y), halfway)
- val straightLineCubic = Cubic.straightLine(p0.x, p0.y, p3.x, p3.y)
- halfway = straightLineCubic.pointOnCurve(.5f)
- val computedHalfway = PointF(p0.x + .5f * (p3.x - p0.x), p0.y + .5f * (p3.y - p0.y))
- assertPointsEqualish(computedHalfway, halfway)
- }
-
- @Test
- fun transformTest() {
- val matrix = Matrix()
- var transformedCubic = Cubic(cubic)
- transformedCubic.transform(matrix)
- assertCubicsEqua1ish(cubic, transformedCubic)
-
- transformedCubic = Cubic(cubic)
- matrix.setScale(3f, 3f)
- transformedCubic.transform(matrix)
- assertCubicsEqua1ish(cubic * 3f, transformedCubic)
-
- val tx = 200f
- val ty = 300f
- val translationVector = PointF(tx, ty)
- transformedCubic = Cubic(cubic)
- matrix.setTranslate(tx, ty)
- transformedCubic.transform(matrix)
- assertPointsEqualish(PointF(cubic.anchor0X, cubic.anchor0Y) + translationVector,
- PointF(transformedCubic.anchor0X, transformedCubic.anchor0Y))
- assertPointsEqualish(PointF(cubic.control0X, cubic.control0Y) + translationVector,
- PointF(transformedCubic.control0X, transformedCubic.control0Y))
- assertPointsEqualish(PointF(cubic.control1X, cubic.control1Y) + translationVector,
- PointF(transformedCubic.control1X, transformedCubic.control1Y))
- assertPointsEqualish(PointF(cubic.anchor1X, cubic.anchor1Y) + translationVector,
- PointF(transformedCubic.anchor1X, transformedCubic.anchor1Y))
- }
-}
diff --git a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonTest.kt b/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonTest.kt
deleted file mode 100644
index cb3780e..0000000
--- a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonTest.kt
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * Copyright 2022 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.graphics.shapes
-
-import android.graphics.Matrix
-import android.graphics.PointF
-import androidx.core.graphics.plus
-import androidx.core.graphics.times
-import androidx.test.filters.SmallTest
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertNotEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
-
-@SmallTest
-class PolygonTest {
-
- val square = RoundedPolygon(4)
-
- @Test
- fun constructionTest() {
- // We can't be too specific on how exactly the square is constructed, but
- // we can at least test whether all points are within the unit square
- var min = PointF(-1f, -1f)
- var max = PointF(1f, 1f)
- assertInBounds(square.toCubicShape(), min, max)
-
- val doubleSquare = RoundedPolygon(4, 2f)
- min = min * 2f
- max = max * 2f
- assertInBounds(doubleSquare.toCubicShape(), min, max)
-
- val offsetSquare = RoundedPolygon(4, centerX = 1f, centerY = 2f)
- min = PointF(0f, 1f)
- max = PointF(2f, 3f)
- assertInBounds(offsetSquare.toCubicShape(), min, max)
-
- val squareCopy = RoundedPolygon(square)
- min = PointF(-1f, -1f)
- max = PointF(1f, 1f)
- assertInBounds(squareCopy.toCubicShape(), min, max)
-
- val p0 = PointF(1f, 0f)
- val p1 = PointF(0f, 1f)
- val p2 = PointF(-1f, 0f)
- val p3 = PointF(0f, -1f)
- val manualSquare = RoundedPolygon(floatArrayOf(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y,
- p3.x, p3.y))
- min = PointF(-1f, -1f)
- max = PointF(1f, 1f)
- assertInBounds(manualSquare.toCubicShape(), min, max)
-
- val offset = PointF(1f, 2f)
- val p0Offset = p0 + offset
- val p1Offset = p1 + offset
- val p2Offset = p2 + offset
- val p3Offset = p3 + offset
- val manualSquareOffset = RoundedPolygon(
- vertices = floatArrayOf(p0Offset.x, p0Offset.y, p1Offset.x, p1Offset.y,
- p2Offset.x, p2Offset.y, p3Offset.x, p3Offset.y),
- centerX = offset.x, centerY = offset.y)
- min = PointF(0f, 1f)
- max = PointF(2f, 3f)
- assertInBounds(manualSquareOffset.toCubicShape(), min, max)
- }
-
- @Test
- fun pathTest() {
- val shape = square.toCubicShape()
- val path = shape.toPath()
- assertFalse(path.isEmpty)
- }
-
- @Test
- fun boundsTest() {
- val shape = square.toCubicShape()
- val bounds = shape.bounds
- assertPointsEqualish(PointF(-1f, 1f), PointF(bounds.left, bounds.bottom))
- assertPointsEqualish(PointF(1f, -1f), PointF(bounds.right, bounds.top))
- }
-
- @Test
- fun centerTest() {
- assertPointsEqualish(PointF(0f, 0f), PointF(square.centerX, square.centerY))
- }
-
- @Test
- fun transformTest() {
- // First, make sure the shape doesn't change when transformed by the identity
- val squareCopy = RoundedPolygon(square)
- val identity = Matrix()
- square.transform(identity)
- assertEquals(square, squareCopy)
-
- // Now create a matrix which translates points by (1, 2) and make sure
- // the shape is translated similarly by it
- val translator = Matrix()
- val offset = PointF(1f, 2f)
- translator.setTranslate(offset.x, offset.y)
- square.transform(translator)
- val squareCubics = square.toCubicShape().cubics
- val squareCopyCubics = squareCopy.toCubicShape().cubics
- for (i in 0 until squareCubics.size) {
- assertPointsEqualish(PointF(squareCopyCubics[i].anchor0X,
- squareCopyCubics[i].anchor0Y) + offset,
- PointF(squareCubics[i].anchor0X, squareCubics[i].anchor0Y))
- assertPointsEqualish(PointF(squareCopyCubics[i].control0X,
- squareCopyCubics[i].control0Y) + offset,
- PointF(squareCubics[i].control0X, squareCubics[i].control0Y))
- assertPointsEqualish(PointF(squareCopyCubics[i].control1X,
- squareCopyCubics[i].control1Y) + offset,
- PointF(squareCubics[i].control1X, squareCubics[i].control1Y))
- assertPointsEqualish(PointF(squareCopyCubics[i].anchor1X,
- squareCopyCubics[i].anchor1Y) + offset,
- PointF(squareCubics[i].anchor1X, squareCubics[i].anchor1Y))
- }
- }
-
- @Test
- fun featuresTest() {
- val squareFeatures = square.features
-
- // Verify that cubics of polygon == cubics of features of that polygon
- assertTrue(square.toCubicShape().cubics == squareFeatures.flatMap { it.cubics })
-
- // Same as above but with rounded corners
- val roundedSquare = RoundedPolygon(4, rounding = CornerRounding(.1f))
- val roundedFeatures = roundedSquare.features
- assertTrue(roundedSquare.toCubicShape().cubics == roundedFeatures.flatMap { it.cubics })
-
- // Same as the first polygon test, but with a copy of that polygon
- val squareCopy = RoundedPolygon(square)
- val squareCopyFeatures = squareCopy.features
- assertTrue(squareCopy.toCubicShape().cubics == squareCopyFeatures.flatMap { it.cubics })
-
- // Test other elements of Features
- val copy = RoundedPolygon(square)
- val matrix = Matrix()
- matrix.setTranslate(1f, 2f)
- val features = copy.features
- val preTransformVertices = mutableListOf<PointF>()
- val preTransformCenters = mutableListOf<PointF>()
- for (feature in features) {
- if (feature is RoundedPolygon.Corner) {
- // Copy into new Point objects since the ones in the feature should transform
- preTransformVertices.add(PointF(feature.vertex.x, feature.vertex.y))
- preTransformCenters.add(PointF(feature.roundedCenter.x, feature.roundedCenter.y))
- }
- }
- copy.transform(matrix)
- val postTransformVertices = mutableListOf<PointF>()
- val postTransformCenters = mutableListOf<PointF>()
- for (feature in features) {
- if (feature is RoundedPolygon.Corner) {
- postTransformVertices.add(feature.vertex)
- postTransformCenters.add(feature.roundedCenter)
- }
- }
- assertNotEquals(preTransformVertices, postTransformVertices)
- assertNotEquals(preTransformCenters, postTransformCenters)
- }
-}
diff --git a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/TestUtils.kt b/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/TestUtils.kt
deleted file mode 100644
index 1cce6ad6..0000000
--- a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/TestUtils.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2022 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.graphics.shapes
-
-import android.graphics.PointF
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-
-private val Epsilon = 1e-4f
-
-// Test equality within Epsilon
-fun assertPointsEqualish(expected: PointF, actual: PointF) {
- assertEquals(expected.x, actual.x, Epsilon)
- assertEquals(expected.y, actual.y, Epsilon)
-}
-
-fun assertCubicsEqua1ish(expected: Cubic, actual: Cubic) {
- assertPointsEqualish(PointF(expected.anchor0X, expected.anchor0Y),
- PointF(actual.anchor0X, actual.anchor0Y))
- assertPointsEqualish(PointF(expected.control0X, expected.control0Y),
- PointF(actual.control0X, actual.control0Y))
- assertPointsEqualish(PointF(expected.control1X, expected.control1Y),
- PointF(actual.control1X, actual.control1Y))
- assertPointsEqualish(PointF(expected.anchor1X, expected.anchor1Y),
- PointF(actual.anchor1X, actual.anchor1Y))
-}
-
-fun assertPointGreaterish(expected: PointF, actual: PointF) {
- assertTrue(actual.x >= expected.x - Epsilon)
- assertTrue(actual.y >= expected.y - Epsilon)
-}
-
-fun assertPointLessish(expected: PointF, actual: PointF) {
- assertTrue(actual.x <= expected.x + Epsilon)
- assertTrue(actual.y <= expected.y + Epsilon)
-}
-
-fun assertEqualish(expected: Float, actual: Float, message: String? = null) {
- assertEquals(message ?: "", expected, actual, Epsilon)
-}
-
-fun assertInBounds(shape: CubicShape, minPoint: PointF, maxPoint: PointF) {
- val cubics = shape.cubics
- for (cubic in cubics) {
- assertPointGreaterish(minPoint, PointF(cubic.anchor0X, cubic.anchor0Y))
- assertPointLessish(maxPoint, PointF(cubic.anchor0X, cubic.anchor0Y))
- assertPointGreaterish(minPoint, PointF(cubic.control0X, cubic.control0Y))
- assertPointLessish(maxPoint, PointF(cubic.control0X, cubic.control0Y))
- assertPointGreaterish(minPoint, PointF(cubic.control1X, cubic.control1Y))
- assertPointLessish(maxPoint, PointF(cubic.control1X, cubic.control1Y))
- assertPointGreaterish(minPoint, PointF(cubic.anchor1X, cubic.anchor1Y))
- assertPointLessish(maxPoint, PointF(cubic.anchor1X, cubic.anchor1Y))
- }
-}
diff --git a/graphics/graphics-shapes/src/main/androidx/graphics/shapes/androidx-graphics-graphics-shapes-documentation.md b/graphics/graphics-shapes/src/commonMain/androidx/graphics/shapes/androidx-graphics-graphics-shapes-documentation.md
similarity index 100%
rename from graphics/graphics-shapes/src/main/androidx/graphics/shapes/androidx-graphics-graphics-shapes-documentation.md
rename to graphics/graphics-shapes/src/commonMain/androidx/graphics/shapes/androidx-graphics-graphics-shapes-documentation.md
diff --git a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/CornerRounding.kt b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/CornerRounding.kt
similarity index 100%
rename from graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/CornerRounding.kt
rename to graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/CornerRounding.kt
diff --git a/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Cubic.kt b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Cubic.kt
new file mode 100644
index 0000000..4ab4abd
--- /dev/null
+++ b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Cubic.kt
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2022 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.graphics.shapes
+
+import kotlin.math.sqrt
+
+/**
+ * This class holds the anchor and control point data for a single cubic Bézier curve,
+ * with anchor points ([anchor0X], [anchor0Y]) and ([anchor1X], [anchor1Y]) at either end
+ * and control points ([control0X], [control0Y]) and ([control1X], [control1Y]) determining
+ * the slope of the curve between the anchor points.
+ */
+open class Cubic internal constructor(internal val points: FloatArray = FloatArray(8)) {
+ init { require(points.size == 8) }
+
+ /**
+ * The first anchor point x coordinate
+ */
+ val anchor0X get() = points[0]
+
+ /**
+ * The first anchor point y coordinate
+ */
+ val anchor0Y get() = points[1]
+
+ /**
+ * The first control point x coordinate
+ */
+ val control0X get() = points[2]
+
+ /**
+ * The first control point y coordinate
+ */
+ val control0Y get() = points[3]
+
+ /**
+ * The second control point x coordinate
+ */
+ val control1X get() = points[4]
+
+ /**
+ * The second control point y coordinate
+ */
+ val control1Y get() = points[5]
+
+ /**
+ * The second anchor point x coordinate
+ */
+ val anchor1X get() = points[6]
+
+ /**
+ * The second anchor point y coordinate
+ */
+ val anchor1Y get() = points[7]
+
+ /**
+ * This class holds the anchor and control point data for a single cubic Bézier curve,
+ * with anchor points ([anchor0X], [anchor0Y]) and ([anchor1X], [anchor1Y]) at either end
+ * and control points ([control0X], [control0Y]) and ([control1X], [control1Y]) determining
+ * the slope of the curve between the anchor points.
+ *
+ * This object is immutable.
+ *
+ * @param anchor0X the first anchor point x coordinate
+ * @param anchor0Y the first anchor point y coordinate
+ * @param control0X the first control point x coordinate
+ * @param control0Y the first control point y coordinate
+ * @param control1X the second control point x coordinate
+ * @param control1Y the second control point y coordinate
+ * @param anchor1X the second anchor point x coordinate
+ * @param anchor1Y the second anchor point y coordinate
+ */
+ constructor(
+ anchor0X: Float,
+ anchor0Y: Float,
+ control0X: Float,
+ control0Y: Float,
+ control1X: Float,
+ control1Y: Float,
+ anchor1X: Float,
+ anchor1Y: Float
+ ) : this(floatArrayOf(anchor0X, anchor0Y, control0X, control0Y,
+ control1X, control1Y, anchor1X, anchor1Y))
+
+ internal constructor(anchor0: Point, control0: Point, control1: Point, anchor1: Point) :
+ this(anchor0.x, anchor0.y, control0.x, control0.y,
+ control1.x, control1.y, anchor1.x, anchor1.y)
+
+ /**
+ * Returns a point on the curve for parameter t, representing the proportional distance
+ * along the curve between its starting point at anchor0 and ending point at anchor1.
+ *
+ * @param t The distance along the curve between the anchor points, where 0 is at anchor0 and
+ * 1 is at anchor1
+ */
+ internal fun pointOnCurve(t: Float): Point {
+ val u = 1 - t
+ return Point(anchor0X * (u * u * u) + control0X * (3 * t * u * u) +
+ control1X * (3 * t * t * u) + anchor1X * (t * t * t),
+ anchor0Y * (u * u * u) + control0Y * (3 * t * u * u) +
+ control1Y * (3 * t * t * u) + anchor1Y * (t * t * t)
+ )
+ }
+
+ /**
+ * Returns two Cubics, created by splitting this curve at the given
+ * distance of [t] between the original starting and ending anchor points.
+ */
+ // TODO: cartesian optimization?
+ fun split(t: Float): Pair<Cubic, Cubic> {
+ val u = 1 - t
+ val pointOnCurve = pointOnCurve(t)
+ return Cubic(
+ anchor0X, anchor0Y,
+ anchor0X * u + control0X * t, anchor0Y * u + control0Y * t,
+ anchor0X * (u * u) + control0X * (2 * u * t) + control1X * (t * t),
+ anchor0Y * (u * u) + control0Y * (2 * u * t) + control1Y * (t * t),
+ pointOnCurve.x, pointOnCurve.y
+ ) to Cubic(
+ // TODO: should calculate once and share the result
+ pointOnCurve.x, pointOnCurve.y,
+ control0X * (u * u) + control1X * (2 * u * t) + anchor1X * (t * t),
+ control0Y * (u * u) + control1Y * (2 * u * t) + anchor1Y * (t * t),
+ control1X * u + anchor1X * t, control1Y * u + anchor1Y * t,
+ anchor1X, anchor1Y
+ )
+ }
+
+ /**
+ * Utility function to reverse the control/anchor points for this curve.
+ */
+ fun reverse() = Cubic(anchor1X, anchor1Y, control1X, control1Y, control0X, control0Y,
+ anchor0X, anchor0Y)
+
+ /**
+ * Operator overload to enable adding Cubic objects together, like "c0 + c1"
+ */
+ operator fun plus(o: Cubic) = Cubic(FloatArray(8) { points[it] + o.points[it] })
+
+ /**
+ * Operator overload to enable multiplying Cubics by a scalar value x, like "c0 * x"
+ */
+ operator fun times(x: Float) = Cubic(FloatArray(8) { points[it] * x })
+
+ /**
+ * Operator overload to enable multiplying Cubics by an Int scalar value x, like "c0 * x"
+ */
+ operator fun times(x: Int) = times(x.toFloat())
+
+ /**
+ * Operator overload to enable dividing Cubics by a scalar value x, like "c0 / x"
+ */
+ operator fun div(x: Float) = times(1f / x)
+
+ /**
+ * Operator overload to enable dividing Cubics by a scalar value x, like "c0 / x"
+ */
+ operator fun div(x: Int) = div(x.toFloat())
+
+ override fun toString(): String {
+ return "anchor0: ($anchor0X, $anchor0Y) control0: ($control0X, $control0Y), " +
+ "control1: ($control1X, $control1Y), anchor1: ($anchor1X, $anchor1Y)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Cubic
+
+ return points.contentEquals(other.points)
+ }
+
+ /**
+ * Transforms the points in this [Cubic] with the given [PointTransformer] and returns a new
+ * [Cubic]
+ *
+ * @param f The [PointTransformer] used to transform this [Cubic]
+ */
+ fun transformed(f: PointTransformer): Cubic {
+ val newCubic = MutableCubic()
+ points.copyInto(newCubic.points)
+ newCubic.transform(f)
+ return newCubic
+ }
+
+ override fun hashCode() = points.contentHashCode()
+
+ companion object {
+ /**
+ * Generates a bezier curve that is a straight line between the given anchor points.
+ * The control points lie 1/3 of the distance from their respective anchor points.
+ */
+ @JvmStatic
+ fun straightLine(x0: Float, y0: Float, x1: Float, y1: Float): Cubic {
+ return Cubic(
+ x0, y0,
+ interpolate(x0, x1, 1f / 3f),
+ interpolate(y0, y1, 1f / 3f),
+ interpolate(x0, x1, 2f / 3f),
+ interpolate(y0, y1, 2f / 3f),
+ x1, y1
+ )
+ }
+
+ // TODO: consider a more general function (maybe in addition to this) that allows
+ // caller to get a list of curves surpassing 180 degrees
+ /**
+ * Generates a bezier curve that approximates a circular arc, with p0 and p1 as
+ * the starting and ending anchor points. The curve generated is the smallest of
+ * the two possible arcs around the entire 360-degree circle. Arcs of greater than 180
+ * degrees should use more than one arc together. Note that p0 and p1 should be
+ * equidistant from the center.
+ */
+ @JvmStatic
+ fun circularArc(
+ centerX: Float,
+ centerY: Float,
+ x0: Float,
+ y0: Float,
+ x1: Float,
+ y1: Float
+ ): Cubic {
+ val p0d = directionVector(x0 - centerX, y0 - centerY)
+ val p1d = directionVector(x1 - centerX, y1 - centerY)
+ val rotatedP0 = p0d.rotate90()
+ val rotatedP1 = p1d.rotate90()
+ val clockwise = rotatedP0.dotProduct(x1 - centerX, y1 - centerY) >= 0
+ val cosa = p0d.dotProduct(p1d)
+ if (cosa > 0.999f) /* p0 ~= p1 */ return straightLine(x0, y0, x1, y1)
+ val k = distance(x0 - centerX, y0 - centerY) * 4f / 3f *
+ (sqrt(2 * (1 - cosa)) - sqrt(1 - cosa * cosa)) / (1 - cosa) *
+ if (clockwise) 1f else -1f
+ return Cubic(
+ x0, y0, x0 + rotatedP0.x * k, y0 + rotatedP0.y * k,
+ x1 - rotatedP1.x * k, y1 - rotatedP1.y * k, x1, y1
+ )
+ }
+ }
+}
+
+/**
+ * This interface is used refer to Points that can be modified, as a scope to
+ * [PointTransformer]
+ */
+interface MutablePoint {
+ /**
+ * The x coordinate of the Point
+ */
+ var x: Float
+
+ /**
+ * The y coordinate of the Point
+ */
+ var y: Float
+}
+
+/**
+ * Interface for a function that can transform (rotate/scale/translate/etc.) points
+ */
+fun interface PointTransformer {
+ /**
+ * Transform the given [MutablePoint] in place.
+ */
+ fun MutablePoint.transform()
+}
+
+/**
+
+ * This is a Mutable version of [Cubic], used mostly for performance critical paths so we can
+ * avoid creating new [Cubic]s
+ *
+ * This is used in Morph.asMutableCubics, reusing a [MutableCubic] instance to avoid creating
+ * new [Cubic]s.
+ */
+class MutableCubic internal constructor() : Cubic() {
+ internal val anchor0 = ArrayMutablePoint(points, 0)
+ internal val control0 = ArrayMutablePoint(points, 2)
+ internal val control1 = ArrayMutablePoint(points, 4)
+ internal val anchor1 = ArrayMutablePoint(points, 6)
+
+ fun transform(f: PointTransformer) {
+ with(f) {
+ anchor0.transform()
+ control0.transform()
+ control1.transform()
+ anchor1.transform()
+ }
+ }
+}
+
+/**
+ * Implementation of [MutablePoint] backed by a [FloatArray], at a given position.
+ * Note that the same [FloatArray] can be used to back many [ArrayMutablePoint],
+ * see [MutableCubic]
+ */
+internal class ArrayMutablePoint(internal val arr: FloatArray, internal val ix: Int) :
+ MutablePoint {
+ init { require(arr.size >= ix + 2) }
+
+ override var x: Float
+ get() = arr[ix]
+ set(v) {
+ arr[ix] = v
+ }
+ override var y: Float
+ get() = arr[ix + 1]
+ set(v) {
+ arr[ix + 1] = v
+ }
+}
diff --git a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/FeatureMapping.kt b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/FeatureMapping.kt
similarity index 84%
rename from graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/FeatureMapping.kt
rename to graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/FeatureMapping.kt
index a761ae3..c13017d 100644
--- a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/FeatureMapping.kt
+++ b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/FeatureMapping.kt
@@ -20,22 +20,23 @@
* MeasuredFeatures contains a list of all features in a polygon along with the [0..1] progress
* at that feature
*/
-internal typealias MeasuredFeatures = List<Pair<Float, RoundedPolygon.Feature>>
+internal typealias MeasuredFeatures = List<ProgressableFeature>
+internal data class ProgressableFeature(val progress: Float, val feature: Feature)
/**
* featureMapper creates a mapping between the "features" (rounded corners) of two shapes
*/
internal fun featureMapper(features1: MeasuredFeatures, features2: MeasuredFeatures): DoubleMapper {
// We only use corners for this mapping.
- val filteredFeatures1 = features1.filter { it.second is RoundedPolygon.Corner }
- val filteredFeatures2 = features2.filter { it.second is RoundedPolygon.Corner }
+ val filteredFeatures1 = features1.filter { it.feature is Feature.Corner }
+ val filteredFeatures2 = features2.filter { it.feature is Feature.Corner }
val (m1, m2) = if (filteredFeatures1.size > filteredFeatures2.size) {
doMapping(filteredFeatures2, filteredFeatures1) to filteredFeatures2
} else {
filteredFeatures1 to doMapping(filteredFeatures1, filteredFeatures2)
}
- val mm = m1.zip(m2).map { (f1, f2) -> f1.first to f2.first }
+ val mm = m1.zip(m2).map { (f1, f2) -> f1.progress to f2.progress }
debugLog(LOG_TAG) { mm.joinToString { "${it.first} -> ${it.second}" } }
return DoubleMapper(*mm.toTypedArray()).also { dm ->
@@ -54,10 +55,10 @@
* This information is used to determine how to map features (and the curves that make up
* those features).
*/
-internal fun featureDistSquared(f1: RoundedPolygon.Feature, f2: RoundedPolygon.Feature): Float {
+internal fun featureDistSquared(f1: Feature, f2: Feature): Float {
// TODO: We might want to enable concave-convex matching in some situations. If so, the
// approach below will not work
- if (f1 is RoundedPolygon.Corner && f2 is RoundedPolygon.Corner && f1.convex != f2.convex) {
+ if (f1 is Feature.Corner && f2 is Feature.Corner && f1.convex != f2.convex) {
// Simple hack to force all features to map only to features of the same concavity, by
// returning an infinitely large distance in that case
debugLog(LOG_TAG) { "*** Feature distance ∞ for convex-vs-concave corners" }
@@ -82,7 +83,7 @@
*/
internal fun doMapping(f1: MeasuredFeatures, f2: MeasuredFeatures): MeasuredFeatures {
// Pick the first mapping in a greedy way.
- val ix = f2.indices.minBy { featureDistSquared(f1[0].second, f2[it].second) }
+ val ix = f2.indices.minBy { featureDistSquared(f1[0].feature, f2[it].feature) }
val m = f1.size
val n = f2.size
@@ -94,7 +95,7 @@
// Leave enough items in f2 to pick matches for the items left in f1.
val last = (ix - (m - i)).let { if (it > lastPicked) it else it + n }
val best = (lastPicked + 1..last).minBy {
- featureDistSquared(f1[i].second, f2[it % n].second)
+ featureDistSquared(f1[i].feature, f2[it % n].feature)
}
ret.add(f2[best % n])
lastPicked = best
diff --git a/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Features.kt b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Features.kt
new file mode 100644
index 0000000..8d2774d
--- /dev/null
+++ b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Features.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.graphics.shapes
+
+/**
+ * This class holds information about a corner (rounded or not) or an edge of a given
+ * polygon. The features of a Polygon can be used to manipulate the shape with more context
+ * of what the shape actually is, rather than simply manipulating the raw curves and lines
+ * which describe it.
+ */
+internal abstract class Feature(val cubics: List<Cubic>) {
+ internal abstract fun transformed(f: PointTransformer): Feature
+
+ /**
+ * Edges have only a list of the cubic curves which make up the edge. Edges lie between
+ * corners and have no vertex or concavity; the curves are simply straight lines (represented
+ * by Cubic curves).
+ */
+ internal class Edge(cubics: List<Cubic>) : Feature(cubics) {
+ override fun transformed(f: PointTransformer) =
+ Edge(cubics.map { it.transformed(f) })
+
+ override fun toString(): String = "Edge"
+ }
+
+ /**
+ * Corners contain the list of cubic curves which describe how the corner is rounded (or
+ * not), plus the vertex at the corner (which the cubics may or may not pass through, depending
+ * on whether the corner is rounded) and a flag indicating whether the corner is convex.
+ * A regular polygon has all convex corners, while a star polygon generally (but not
+ * necessarily) has both convex (outer) and concave (inner) corners.
+ */
+ internal class Corner(
+ cubics: List<Cubic>,
+ val vertex: Point,
+ val roundedCenter: Point,
+ val convex: Boolean = true
+ ) : Feature(cubics) {
+ override fun transformed(f: PointTransformer): Feature {
+ return Corner(
+ cubics.map { it.transformed(f = f) },
+ vertex.transformed(f),
+ roundedCenter.transformed(f),
+ convex
+ )
+ }
+
+ override fun toString(): String {
+ return "Corner: vertex=$vertex, center=$roundedCenter, convex=$convex"
+ }
+ }
+}
diff --git a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/FloatMapping.kt b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/FloatMapping.kt
similarity index 100%
rename from graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/FloatMapping.kt
rename to graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/FloatMapping.kt
diff --git a/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Morph.kt b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Morph.kt
new file mode 100644
index 0000000..2280462
--- /dev/null
+++ b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Morph.kt
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2022 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.graphics.shapes
+
+import kotlin.math.min
+
+/**
+ * This class is used to animate between start and end polygons objects.
+ *
+ * Morphing between arbitrary objects can be problematic because it can be difficult to
+ * determine how the points of a given shape map to the points of some other shape.
+ * [Morph] simplifies the problem by only operating on [RoundedPolygon] objects, which
+ * are known to have similar, contiguous structures. For one thing, the shape of a polygon
+ * is contiguous from start to end (compared to an arbitrary Path object, which could have
+ * one or more `moveTo` operations in the shape). Also, all edges of a polygon shape are
+ * represented by [Cubic] objects, thus the start and end shapes use similar operations. Two
+ * Polygon shapes then only differ in the quantity and placement of their curves.
+ * The morph works by determining how to map the curves of the two shapes together (based on
+ * proximity and other information, such as distance to polygon vertices and concavity),
+ * and splitting curves when the shapes do not have the same number of curves or when the
+ * curve placement within the shapes is very different.
+ */
+class Morph(
+ start: RoundedPolygon,
+ end: RoundedPolygon
+) {
+ // morphMatch is the structure which holds the actual shape being morphed. It contains
+ // all cubics necessary to represent the start and end shapes (the original cubics in the
+ // shapes may be cut to align the start/end shapes)
+ private var morphMatch = match(start, end)
+
+ /**
+ * Returns a representation of the morph object at a given [progress] value as a list of Cubics.
+ * Note that this function causes a new list to be created and populated, so there is some
+ * overhead.
+ *
+ * @param progress a value from 0 to 1 that determines the morph's current
+ * shape, between the start and end shapes provided at construction time. A value of 0 results
+ * in the start shape, a value of 1 results in the end shape, and any value in between
+ * results in a shape which is a linear interpolation between those two shapes.
+ * The range is generally [0..1] and values outside could result in undefined shapes, but
+ * values close to (but outside) the range can be used to get an exaggerated effect
+ * (e.g., for a bounce or overshoot animation).
+ */
+ fun asCubics(progress: Float) = morphMatch.map { match ->
+ Cubic(FloatArray(8) {
+ interpolate(
+ match.first.points[it],
+ match.second.points[it],
+ progress
+ )
+ })
+ }
+
+ /**
+ * Returns a representation of the morph object at a given [progress] value as an Iterator of
+ * [MutableCubic]. This function is faster than [asCubics], since it doesn't allocate new
+ * [Cubic] instances, but to do this it reuses the same [MutableCubic] instance during
+ * iteration.
+ *
+ * @param progress a value from 0 to 1 that determines the morph's current
+ * shape, between the start and end shapes provided at construction time. A value of 0 results
+ * in the start shape, a value of 1 results in the end shape, and any value in between
+ * results in a shape which is a linear interpolation between those two shapes.
+ * The range is generally [0..1] and values outside could result in undefined shapes, but
+ * values close to (but outside) the range can be used to get an exaggerated effect
+ * (e.g., for a bounce or overshoot animation).
+ * @param mutableCubic An instance of [MutableCubic] that will be used to set each cubic in
+ * time.
+ */
+ @JvmOverloads
+ fun asMutableCubics(progress: Float, mutableCubic: MutableCubic = MutableCubic()):
+ Sequence<MutableCubic> = morphMatch.asSequence().map { match ->
+ repeat(8) {
+ mutableCubic.points[it] = interpolate(
+ match.first.points[it],
+ match.second.points[it],
+ progress
+ )
+ }
+ mutableCubic
+ }
+
+ internal companion object {
+ /**
+ * [match], called at Morph construction time, creates the structure used to animate between
+ * the start and end shapes. The technique is to match geometry (curves) between the shapes
+ * when and where possible, and to create new/placeholder curves when necessary (when
+ * one of the shapes has more curves than the other). The result is a list of pairs of
+ * Cubic curves. Those curves are the matched pairs: the first of each pair holds the
+ * geometry of the start shape, the second holds the geometry for the end shape.
+ * Changing the progress of a Morph object simply interpolates between all pairs of
+ * curves for the morph shape.
+ *
+ * Curves on both shapes are matched by running the [Measurer] to determine where
+ * the points are in each shape (proportionally, along the outline), and then running
+ * [featureMapper] which decides how to map (match) all of the curves with each other.
+ */
+ @JvmStatic
+ internal fun match(
+ p1: RoundedPolygon,
+ p2: RoundedPolygon
+ ): List<Pair<Cubic, Cubic>> {
+ if (DEBUG) {
+ repeat(2) { polyIndex ->
+ debugLog(LOG_TAG) {
+ listOf("Initial start:\n", "Initial end:\n")[polyIndex] +
+ listOf(p1, p2)[polyIndex].features.joinToString("\n") { feature ->
+ "${feature.javaClass.name.split("$").last()} - " +
+ ((feature as? Feature.Corner)?.convex?.let {
+ if (it) "Convex - " else "Concave - " } ?: "") +
+ feature.cubics.joinToString("|")
+ }
+ }
+ }
+ }
+
+ // Measure polygons, returns lists of measured cubics for each polygon, which
+ // we then use to match start/end curves
+ val measuredPolygon1 = MeasuredPolygon.measurePolygon(
+ AngleMeasurer(p1.centerX, p1.centerY), p1)
+ val measuredPolygon2 = MeasuredPolygon.measurePolygon(
+ AngleMeasurer(p2.centerX, p2.centerY), p2)
+
+ // features1 and 2 will contain the list of corners (just the inner circular curve)
+ // along with the progress at the middle of those corners. These measurement values
+ // are then used to compare and match between the two polygons
+ val features1 = measuredPolygon1.features
+ val features2 = measuredPolygon2.features
+
+ // Map features: doubleMapper is the result of mapping the features in each shape to the
+ // closest feature in the other shape.
+ // Given a progress in one of the shapes it can be used to find the corresponding
+ // progress in the other shape (in both directions)
+ val doubleMapper = featureMapper(features1, features2)
+
+ // cut point on poly2 is the mapping of the 0 point on poly1
+ val polygon2CutPoint = doubleMapper.map(0f)
+ debugLog(LOG_TAG) { "polygon2CutPoint = $polygon2CutPoint" }
+
+ // Cut and rotate.
+ // Polygons start at progress 0, and the featureMapper has decided that we want to match
+ // progress 0 in the first polygon to `polygon2CutPoint` on the second polygon.
+ // So we need to cut the second polygon there and "rotate it", so as we walk through
+ // both polygons we can find the matching.
+ // The resulting bs1/2 are MeasuredPolygons, whose MeasuredCubics start from
+ // outlineProgress=0 and increasing until outlineProgress=1
+ val bs1 = measuredPolygon1
+ val bs2 = measuredPolygon2.cutAndShift(polygon2CutPoint)
+
+ if (DEBUG) {
+ (0 until bs1.size).forEach { index ->
+ debugLog(LOG_TAG) { "start $index: ${bs1.getOrNull(index)}" }
+ }
+ (0 until bs2.size).forEach { index ->
+ debugLog(LOG_TAG) { "End $index: ${bs2.getOrNull(index)}" }
+ }
+ }
+
+ // Match
+ // Now we can compare the two lists of measured cubics and create a list of pairs
+ // of cubics [ret], which are the start/end curves that represent the Morph object
+ // and the start and end shapes, and which can be interpolated to animate the
+ // between those shapes.
+ val ret = mutableListOf<Pair<Cubic, Cubic>>()
+ // i1/i2 are the indices of the current cubic on the start (1) and end (2) shapes
+ var i1 = 0
+ var i2 = 0
+ // b1, b2 are the current measured cubic for each polygon
+ var b1 = bs1.getOrNull(i1++)
+ var b2 = bs2.getOrNull(i2++)
+ // Iterate until all curves are accounted for and matched
+ while (b1 != null && b2 != null) {
+ // Progresses are in shape1's perspective
+ // b1a, b2a are ending progress values of current measured cubics in [0,1] range
+ val b1a = if (i1 == bs1.size) 1f else b1.endOutlineProgress
+ val b2a = if (i2 == bs2.size) 1f else doubleMapper.mapBack(
+ positiveModulo(b2.endOutlineProgress + polygon2CutPoint, 1f)
+ )
+ val minb = min(b1a, b2a)
+ debugLog(LOG_TAG) { "$b1a $b2a | $minb" }
+ // minb is the progress at which the curve that ends first ends.
+ // If both curves ends roughly there, no cutting is needed, we have a match.
+ // If one curve extends beyond, we need to cut it.
+ val (seg1, newb1) = if (b1a > minb + AngleEpsilon) {
+ debugLog(LOG_TAG) { "Cut 1" }
+ b1.cutAtProgress(minb)
+ } else {
+ b1 to bs1.getOrNull(i1++)
+ }
+ val (seg2, newb2) = if (b2a > minb + AngleEpsilon) {
+ debugLog(LOG_TAG) { "Cut 2" }
+ b2.cutAtProgress(positiveModulo(doubleMapper.map(minb) - polygon2CutPoint, 1f))
+ } else {
+ b2 to bs2.getOrNull(i2++)
+ }
+ debugLog(LOG_TAG) { "Match: $seg1 -> $seg2" }
+ ret.add(seg1.cubic to seg2.cubic)
+ b1 = newb1
+ b2 = newb2
+ }
+ require(b1 == null && b2 == null)
+
+ if (DEBUG) {
+ // Export as SVG path.
+ val showPoint: (Point) -> String = {
+ "%.3f %.3f".format(it.x * 100, it.y * 100)
+ }
+ repeat(2) { listIx ->
+ val points = ret.map { if (listIx == 0) it.first else it.second }
+ debugLog(LOG_TAG) {
+ "M " + showPoint(Point(points.first().anchor0X,
+ points.first().anchor0Y)) + " " +
+ points.joinToString(" ") {
+ "C " + showPoint(Point(it.control0X, it.control0Y)) + ", " +
+ showPoint(Point(it.control1X, it.control1Y)) + ", " +
+ showPoint(Point(it.anchor1X, it.anchor1Y))
+ } + " Z"
+ }
+ }
+ }
+ return ret
+ }
+ }
+}
+
+private val LOG_TAG = "Morph"
diff --git a/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Point.kt b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Point.kt
new file mode 100644
index 0000000..d371fa1
--- /dev/null
+++ b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Point.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.graphics.shapes;
+
+import kotlin.math.sqrt
+
+/**
+ * Constructs a Point from the given relative x and y coordinates
+ */
+internal fun Point(x: Float = 0f, y: Float = 0f) = Point(packFloats(x, y))
+
+/**
+ * An immutable 2D floating-point Point.
+ *
+ * This can be used to represent either points on a 2D plane, or also distances.
+ *
+ * Creates a Point. The first argument sets [x], the horizontal component,
+ * and the second sets [y], the vertical component.
+ */
[email protected]
+internal value class Point internal constructor(internal val packedValue: Long) {
+ val x: Float
+ get() = unpackFloat1(packedValue)
+
+ val y: Float
+ get() = unpackFloat2(packedValue)
+
+ operator fun component1(): Float = x
+
+ operator fun component2(): Float = y
+
+ /**
+ * Returns a copy of this Point instance optionally overriding the
+ * x or y parameter
+ */
+ fun copy(x: Float = this.x, y: Float = this.y) = Point(x, y)
+
+ companion object { }
+
+ /**
+ * The magnitude of the Point, which is the distance of this point from (0, 0).
+ *
+ * If you need this value to compare it to another [Point]'s distance,
+ * consider using [getDistanceSquared] instead, since it is cheaper to compute.
+ */
+ fun getDistance() = sqrt(x * x + y * y)
+
+ /**
+ * The square of the magnitude (which is the distance of this point from (0, 0)) of the Point.
+ *
+ * This is cheaper than computing the [getDistance] itself.
+ */
+ fun getDistanceSquared() = x * x + y * y
+
+ fun dotProduct(other: Point) = x * other.x + y * other.y
+
+ fun dotProduct(otherX: Float, otherY: Float) = x * otherX + y * otherY
+
+ /**
+ * Compute the Z coordinate of the cross product of two vectors, to check if the second vector is
+ * going clockwise ( > 0 ) or counterclockwise (< 0) compared with the first one.
+ * It could also be 0, if the vectors are co-linear.
+ */
+ fun clockwise(other: Point) = x * other.y - y * other.x > 0
+
+ /**
+ * Returns unit vector representing the direction to this point from (0, 0)
+ */
+ fun getDirection() = run {
+ val d = this.getDistance()
+ require(d > 0f)
+ this / d
+ }
+
+ /**
+ * Unary negation operator.
+ *
+ * Returns a Point with the coordinates negated.
+ *
+ * If the [Point] represents an arrow on a plane, this operator returns the
+ * same arrow but pointing in the reverse direction.
+ */
+ operator fun unaryMinus(): Point = Point(-x, -y)
+
+ /**
+ * Binary subtraction operator.
+ *
+ * Returns a Point whose [x] value is the left-hand-side operand's [x]
+ * minus the right-hand-side operand's [x] and whose [y] value is the
+ * left-hand-side operand's [y] minus the right-hand-side operand's [y].
+ */
+ operator fun minus(other: Point): Point = Point(x - other.x, y - other.y)
+
+ /**
+ * Binary addition operator.
+ *
+ * Returns a Point whose [x] value is the sum of the [x] values of the
+ * two operands, and whose [y] value is the sum of the [y] values of the
+ * two operands.
+ */
+ operator fun plus(other: Point): Point = Point(x + other.x, y + other.y)
+
+ /**
+ * Multiplication operator.
+ *
+ * Returns a Point whose coordinates are the coordinates of the
+ * left-hand-side operand (a Point) multiplied by the scalar
+ * right-hand-side operand (a Float).
+ */
+ operator fun times(operand: Float): Point = Point(x * operand, y * operand)
+
+ /**
+ * Division operator.
+ *
+ * Returns a Point whose coordinates are the coordinates of the
+ * left-hand-side operand (a Point) divided by the scalar right-hand-side
+ * operand (a Float).
+ */
+ operator fun div(operand: Float): Point = Point(x / operand, y / operand)
+
+ /**
+ * Modulo (remainder) operator.
+ *
+ * Returns a Point whose coordinates are the remainder of dividing the
+ * coordinates of the left-hand-side operand (a Point) by the scalar
+ * right-hand-side operand (a Float).
+ */
+ operator fun rem(operand: Float) = Point(x % operand, y % operand)
+
+ override fun toString() = "Offset(%.1f, %.1f)".format(x, y)
+}
+
+/**
+ * Linearly interpolate between two Points.
+ *
+ * The [fraction] argument represents position on the timeline, with 0.0 meaning
+ * that the interpolation has not started, returning [start] (or something
+ * equivalent to [start]), 1.0 meaning that the interpolation has finished,
+ * returning [stop] (or something equivalent to [stop]), and values in between
+ * meaning that the interpolation is at the relevant point on the timeline
+ * between [start] and [stop]. The interpolation can be extrapolated beyond 0.0 and
+ * 1.0, so negative values and values greater than 1.0 are valid (and can
+ * easily be generated by curves).
+ *
+ * Values for [fraction] are usually obtained from an [Animation<Float>], such as
+ * an `AnimationController`.
+ */
+internal fun interpolate(start: Point, stop: Point, fraction: Float): Point {
+ return Point(
+ interpolate(start.x, stop.x, fraction),
+ interpolate(start.y, stop.y, fraction)
+ )
+}
+
+/**
+ * Packs two Float values into one Long value for use in inline classes.
+ */
+internal inline fun packFloats(val1: Float, val2: Float): Long {
+ val v1 = val1.toBits().toLong()
+ val v2 = val2.toBits().toLong()
+ return v1.shl(32) or (v2 and 0xFFFFFFFF)
+}
+
+/**
+ * Unpacks the first Float value in [packFloats] from its returned Long.
+ */
+internal inline fun unpackFloat1(value: Long): Float {
+ return Float.fromBits(value.shr(32).toInt())
+}
+
+/**
+ * Unpacks the second Float value in [packFloats] from its returned Long.
+ */
+internal inline fun unpackFloat2(value: Long): Float {
+ return Float.fromBits(value.and(0xFFFFFFFF).toInt())
+}
+
+internal class MutablePointImpl(override var x: Float, override var y: Float) : MutablePoint
+
+internal fun Point.transformed(f: PointTransformer): Point {
+ val m = MutablePointImpl(x, y)
+ with(f) { m.transform() }
+ return Point(m.x, m.y)
+}
diff --git a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/PolygonMeasure.kt b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/PolygonMeasure.kt
similarity index 94%
rename from graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/PolygonMeasure.kt
rename to graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/PolygonMeasure.kt
index b060afb..6c1c032 100644
--- a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/PolygonMeasure.kt
+++ b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/PolygonMeasure.kt
@@ -16,18 +16,17 @@
package androidx.graphics.shapes
-import android.graphics.PointF
import androidx.annotation.FloatRange
import kotlin.math.abs
internal class MeasuredPolygon : AbstractList<MeasuredPolygon.MeasuredCubic> {
private val measurer: Measurer
private val cubics: List<MeasuredCubic>
- val features: List<Pair<Float, RoundedPolygon.Feature>>
+ val features: List<ProgressableFeature>
private constructor(
measurer: Measurer,
- features: List<Pair<Float, RoundedPolygon.Feature>>,
+ features: List<ProgressableFeature>,
cubics: List<Cubic>,
outlineProgress: List<Float>
) {
@@ -217,7 +216,7 @@
// Shift the feature's outline progress too.
val newFeatures = features.map { (outlineProgress, feature) ->
- positiveModulo(outlineProgress - cuttingPoint, 1f) to feature
+ ProgressableFeature(positiveModulo(outlineProgress - cuttingPoint, 1f), feature)
}
// Filter out all empty cubics (i.e. start and end anchor are (almost) the same point.)
@@ -233,13 +232,13 @@
companion object {
internal fun measurePolygon(measurer: Measurer, polygon: RoundedPolygon): MeasuredPolygon {
val cubics = mutableListOf<Cubic>()
- val featureToCubic = mutableListOf<Pair<RoundedPolygon.Feature, Int>>()
+ val featureToCubic = mutableListOf<Pair<Feature, Int>>()
// Get the cubics from the polygon, at the same time, extract the features and keep a
// reference to the representative cubic we will use.
polygon.features.forEach { feature ->
feature.cubics.forEachIndexed { index, cubic ->
- if (feature is RoundedPolygon.Corner &&
+ if (feature is Feature.Corner &&
index == feature.cubics.size / 2) {
featureToCubic.add(feature to cubics.size)
}
@@ -256,8 +255,8 @@
val features = featureToCubic.map { featureAndIndex ->
val ix = featureAndIndex.second
- (outlineProgress[ix] + outlineProgress[ix + 1]) / 2 to
- featureAndIndex.first
+ ProgressableFeature((outlineProgress[ix] + outlineProgress[ix + 1]) / 2,
+ featureAndIndex.first)
}
return MeasuredPolygon(measurer, features, cubics, outlineProgress)
@@ -298,9 +297,6 @@
*/
internal class AngleMeasurer(val centerX: Float, val centerY: Float) : Measurer {
- // Holds temporary pointOnCurve result, avoids re-allocations
- private val tempPoint = PointF()
-
/**
* The measurement for a given cubic is the difference in angles between the start
* and end points (first and last anchors) of the cubic.
@@ -319,7 +315,7 @@
val angle0 = angle(c.anchor0X - centerX, c.anchor0Y - centerY)
// TODO: use binary search.
return findMinimum(0f, 1f, tolerance = 1e-5f) { t ->
- val curvePoint = c.pointOnCurve(t, tempPoint)
+ val curvePoint = c.pointOnCurve(t)
val angle = angle(curvePoint.x - centerX, curvePoint.y - centerY)
abs(positiveModulo(angle - angle0, TwoPi) - m)
}
diff --git a/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/RoundedPolygon.kt b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/RoundedPolygon.kt
new file mode 100644
index 0000000..a872789
--- /dev/null
+++ b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/RoundedPolygon.kt
@@ -0,0 +1,543 @@
+/*
+ * Copyright 2022 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.graphics.shapes
+
+import androidx.annotation.IntRange
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.sqrt
+
+/**
+ * The RoundedPolygon class allows simple construction of polygonal shapes with optional rounding
+ * at the vertices. Polygons can be constructed with either the number of vertices
+ * desired or an ordered list of vertices.
+ *
+ */
+class RoundedPolygon internal constructor(
+ internal val features: List<Feature>,
+ val centerX: Float,
+ val centerY: Float
+) {
+ /**
+ * A flattened version of the [Feature]s, as a List<Cubic>.
+ */
+ val cubics = features.flatMap { it.cubics }
+
+ init {
+ var prevCubic = cubics[cubics.size - 1]
+ debugLog("RoundedPolygon") { "Cubic-1 = $prevCubic" }
+ cubics.forEachIndexed { index, cubic ->
+ if (abs(cubic.anchor0X - prevCubic.anchor1X) > DistanceEpsilon ||
+ abs(cubic.anchor0Y - prevCubic.anchor1Y) > DistanceEpsilon) {
+ debugLog("RoundedPolygon") { "Cubic = $cubic" }
+ debugLog("RoundedPolygon") {
+ "Ix: $index | (${cubic.anchor0X},${cubic.anchor0Y}) vs " +
+ "$prevCubic"
+ }
+ throw IllegalArgumentException("RoundedPolygon must be contiguous, with the " +
+ "anchor points of all curves matching the anchor points of the preceding " +
+ "and succeeding cubics")
+ }
+ prevCubic = cubic
+ }
+ }
+
+ /**
+ * Transforms (scales/translates/etc.) this [RoundedPolygon] with the given [PointTransformer]
+ * and returns a new [RoundedPolygon].
+ * This is a low level API and there should be more platform idiomatic ways to transform
+ * a [RoundedPolygon] provided by the platform specific wrapper.
+ *
+ * @param f The [PointTransformer] used to transform this [RoundedPolygon]
+ */
+ fun transformed(f: PointTransformer): RoundedPolygon {
+ val center = Point(centerX, centerY).transformed(f)
+ return RoundedPolygon(features.map { it.transformed(f) }, center.x, center.y)
+ }
+
+ /**
+ * Creates a new RoundedPolygon, moving and resizing this one, so it's completely inside the
+ * (0, 0) -> (1, 1) square, centered if there extra space in one direction
+ */
+ fun normalized(): RoundedPolygon {
+ val bounds = calculateBounds()
+ val width = bounds[2] - bounds[0]
+ val height = bounds[3] - bounds[1]
+ val side = max(width, height)
+ // Center the shape if bounds are not a square
+ val offsetX = (side - width) / 2 - bounds[0] /* left */
+ val offsetY = (side - height) / 2 - bounds[1] /* top */
+ return transformed {
+ x = (x + offsetX) / side
+ y = (y + offsetY) / side
+ }
+ }
+
+ override fun toString(): String = "[RoundedPolygon." +
+ " Cubics = " + cubics.joinToString() +
+ " || Features = " + features.joinToString() +
+ " || Center = ($centerX, $centerY)]"
+
+ /**
+ * Calculates estimated bounds of the object, using the min/max bounding box of
+ * all points in the cubics that make up the shape.
+ * This is a library-internal API, prefer the appropriate wrapper in your platform.
+ */
+ fun calculateBounds(bounds: FloatArray = FloatArray(4)): FloatArray {
+ require(bounds.size >= 4)
+ var minX = Float.MAX_VALUE
+ var minY = Float.MAX_VALUE
+ var maxX = Float.MIN_VALUE
+ var maxY = Float.MIN_VALUE
+ for (bezier in cubics) {
+ if (bezier.anchor0X < minX) minX = bezier.anchor0X
+ if (bezier.anchor0Y < minY) minY = bezier.anchor0Y
+ if (bezier.anchor0X > maxX) maxX = bezier.anchor0X
+ if (bezier.anchor0Y > maxY) maxY = bezier.anchor0Y
+
+ if (bezier.control0X < minX) minX = bezier.control0X
+ if (bezier.control0Y < minY) minY = bezier.control0Y
+ if (bezier.control0X > maxX) maxX = bezier.control0X
+ if (bezier.control0Y > maxY) maxY = bezier.control0Y
+
+ if (bezier.control1X < minX) minX = bezier.control1X
+ if (bezier.control1Y < minY) minY = bezier.control1Y
+ if (bezier.control1X > maxX) maxX = bezier.control1X
+ if (bezier.control1Y > maxY) maxY = bezier.control1Y
+ // No need to use x3/y3, since it is already taken into account in the next
+ // curve's x0/y0 point.
+ }
+ bounds[0] = minX
+ bounds[1] = minY
+ bounds[2] = maxX
+ bounds[3] = maxY
+ return bounds
+ }
+
+ companion object {}
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is RoundedPolygon) return false
+
+ return features == other.features
+ }
+
+ override fun hashCode(): Int {
+ return features.hashCode()
+ }
+}
+
+/**
+ * This constructor takes the number of vertices in the resulting polygon. These vertices are
+ * positioned on a virtual circle around a given center with each vertex positioned [radius]
+ * distance from that center, equally spaced (with equal angles between them). If no radius
+ * is supplied, the shape will be created with a default radius of 1, resulting in a shape
+ * whose vertices lie on a unit circle, with width/height of 2. That default polygon will
+ * probably need to be rescaled using [transformed] into the appropriate size for the UI in
+ * which it will be drawn.
+ *
+ * The [rounding] and [perVertexRounding] parameters are optional. If not supplied, the result
+ * will be a regular polygon with straight edges and unrounded corners.
+ *
+ * @param numVertices The number of vertices in this polygon.
+ * @param radius The radius of the polygon, in pixels. This radius determines the
+ * initial size of the object, but it can be transformed later by using the [transformed] function.
+ * @param centerX The X coordinate of the center of the polygon, around which all vertices
+ * will be placed. The default center is at (0,0).
+ * @param centerY The Y coordinate of the center of the polygon, around which all vertices
+ * will be placed. The default center is at (0,0).
+ * @param rounding The [CornerRounding] properties of all vertices. If some vertices should
+ * have different rounding properties, then use [perVertexRounding] instead. The default
+ * rounding value is [CornerRounding.Unrounded], meaning that the polygon will use the vertices
+ * themselves in the final shape and not curves rounded around the vertices.
+ * @param perVertexRounding The [CornerRounding] properties of every vertex. If this
+ * parameter is not null, then it must have [numVertices] elements. If this parameter
+ * is null, then the polygon will use the [rounding] parameter for every vertex instead. The
+ * default value is null.
+ *
+ * @throws IllegalArgumentException If [perVertexRounding] is not null and its size is not
+ * equal to [numVertices].
+ * @throws IllegalArgumentException [numVertices] must be at least 3.
+ */
+@JvmOverloads
+fun RoundedPolygon(
+ @IntRange(from = 3) numVertices: Int,
+ radius: Float = 1f,
+ centerX: Float = 0f,
+ centerY: Float = 0f,
+ rounding: CornerRounding = CornerRounding.Unrounded,
+ perVertexRounding: List<CornerRounding>? = null
+) = RoundedPolygon(
+ verticesFromNumVerts(numVertices, radius, centerX, centerY),
+ rounding = rounding,
+ perVertexRounding = perVertexRounding,
+ centerX = centerX,
+ centerY = centerY)
+
+/**
+ * Creates a copy of the given [RoundedPolygon]
+ */
+fun RoundedPolygon(source: RoundedPolygon) =
+ RoundedPolygon(source.features, source.centerX, source.centerY)
+
+/**
+ * This function takes the vertices (either supplied or calculated, depending on the
+ * constructor called), plus [CornerRounding] parameters, and creates the actual
+ * [RoundedPolygon] shape, rounding around the vertices (or not) as specified. The result
+ * is a list of [Cubic] curves which represent the geometry of the final shape.
+ *
+ * @param vertices The list of vertices in this polygon specified as pairs of x/y coordinates in
+ * this FloatArray. This should be an ordered list (with the outline of the shape going from each
+ * vertex to the next in order of this list), otherwise the results will be undefined.
+ * @param rounding The [CornerRounding] properties of all vertices. If some vertices should
+ * have different rounding properties, then use [perVertexRounding] instead. The default
+ * rounding value is [CornerRounding.Unrounded], meaning that the polygon will use the vertices
+ * themselves in the final shape and not curves rounded around the vertices.
+ * @param perVertexRounding The [CornerRounding] properties of all vertices. If this
+ * parameter is not null, then it must have the same size as [vertices]. If this parameter
+ * is null, then the polygon will use the [rounding] parameter for every vertex instead. The
+ * default value is null.
+ * @param centerX The X coordinate of the center of the polygon, around which all vertices
+ * will be placed. The default center is at (0,0).
+ * @param centerY The Y coordinate of the center of the polygon, around which all vertices
+ * will be placed. The default center is at (0,0).
+ * @throws IllegalArgumentException if the number of vertices is less than 3 (the [vertices]
+ * parameter has less than 6 Floats). Or if the [perVertexRounding] parameter is not null and the
+ * size doesn't match the number vertices.
+ */
+@JvmOverloads
+fun RoundedPolygon(
+ vertices: FloatArray,
+ rounding: CornerRounding = CornerRounding.Unrounded,
+ perVertexRounding: List<CornerRounding>? = null,
+ centerX: Float = Float.MIN_VALUE,
+ centerY: Float = Float.MIN_VALUE
+): RoundedPolygon {
+ if (vertices.size < 6) {
+ throw IllegalArgumentException("Polygons must have at least 3 vertices")
+ }
+ if (vertices.size % 2 == 1) {
+ throw IllegalArgumentException("The vertices array should have even size")
+ }
+ if (perVertexRounding != null && perVertexRounding.size * 2 != vertices.size) {
+ throw IllegalArgumentException("perVertexRounding list should be either null or " +
+ "the same size as the number of vertices (vertices.size / 2)")
+ }
+ val corners = mutableListOf<List<Cubic>>()
+ val n = vertices.size / 2
+ val roundedCorners = mutableListOf<RoundedCorner>()
+ for (i in 0 until n) {
+ val vtxRounding = perVertexRounding?.get(i) ?: rounding
+ val prevIndex = ((i + n - 1) % n) * 2
+ val nextIndex = ((i + 1) % n) * 2
+ roundedCorners.add(
+ RoundedCorner(
+ Point(vertices[prevIndex], vertices[prevIndex + 1]),
+ Point(vertices[i * 2], vertices[i * 2 + 1]),
+ Point(vertices[nextIndex], vertices[nextIndex + 1]),
+ vtxRounding
+ )
+ )
+ }
+
+ // For each side, check if we have enough space to do the cuts needed, and if not split
+ // the available space, first for round cuts, then for smoothing if there is space left.
+ // Each element in this list is a pair, that represent how much we can do of the cut for
+ // the given side (side i goes from corner i to corner i+1), the elements of the pair are:
+ // first is how much we can use of expectedRoundCut, second how much of expectedCut
+ val cutAdjusts = (0 until n).map { ix ->
+ val expectedRoundCut = roundedCorners[ix].expectedRoundCut +
+ roundedCorners[(ix + 1) % n].expectedRoundCut
+ val expectedCut = roundedCorners[ix].expectedCut +
+ roundedCorners[(ix + 1) % n].expectedCut
+ val vtxX = vertices[ix * 2]
+ val vtxY = vertices[ix * 2 + 1]
+ val nextVtxX = vertices[((ix + 1) % n) * 2]
+ val nextVtxY = vertices[((ix + 1) % n) * 2 + 1]
+ val sideSize = distance(vtxX - nextVtxX, vtxY - nextVtxY)
+
+ // Check expectedRoundCut first, and ensure we fulfill rounding needs first for
+ // both corners before using space for smoothing
+ if (expectedRoundCut > sideSize) {
+ // Not enough room for fully rounding, see how much we can actually do.
+ sideSize / expectedRoundCut to 0f
+ } else if (expectedCut > sideSize) {
+ // We can do full rounding, but not full smoothing.
+ 1f to (sideSize - expectedRoundCut) / (expectedCut - expectedRoundCut)
+ } else {
+ // There is enough room for rounding & smoothing.
+ 1f to 1f
+ }
+ }
+ // Create and store list of beziers for each [potentially] rounded corner
+ for (i in 0 until n) {
+ // allowedCuts[0] is for the side from the previous corner to this one,
+ // allowedCuts[1] is for the side from this corner to the next one.
+ val allowedCuts = (0..1).map { delta ->
+ val (roundCutRatio, cutRatio) = cutAdjusts[(i + n - 1 + delta) % n]
+ roundedCorners[i].expectedRoundCut * roundCutRatio +
+ (roundedCorners[i].expectedCut - roundedCorners[i].expectedRoundCut) * cutRatio
+ }
+ corners.add(
+ roundedCorners[i].getCubics(
+ allowedCut0 = allowedCuts[0],
+ allowedCut1 = allowedCuts[1]
+ )
+ )
+ }
+ // Finally, store the calculated cubics. This includes all of the rounded corners
+ // from above, along with new cubics representing the edges between those corners.
+ val tempFeatures = mutableListOf<Feature>()
+ for (i in 0 until n) {
+ // Determine whether corner at this vertex is concave or convex, based on the
+ // relationship of the prev->curr/curr->next vectors
+ // Note that these indices are for pairs of values (points), they need to be
+ // doubled to access the xy values in the vertices float array
+ val prevVtxIndex = (i + n - 1) % n
+ val nextVtxIndex = (i + 1) % n
+ val currVertex = Point(vertices[i * 2], vertices[i * 2 + 1])
+ val prevVertex = Point(vertices[prevVtxIndex * 2], vertices[prevVtxIndex * 2 + 1])
+ val nextVertex = Point(vertices[nextVtxIndex * 2], vertices[nextVtxIndex * 2 + 1])
+ val convex = (currVertex - prevVertex).clockwise(nextVertex - currVertex)
+ tempFeatures.add(
+ Feature.Corner(
+ corners[i], currVertex, roundedCorners[i].center,
+ convex
+ )
+ )
+ tempFeatures.add(
+ Feature.Edge(
+ listOf(
+ Cubic.straightLine(
+ corners[i].last().anchor1X, corners[i].last().anchor1Y,
+ corners[(i + 1) % n].first().anchor0X, corners[(i + 1) % n].first().anchor0Y
+ )
+ )
+ )
+ )
+ }
+
+ val (cx, cy) = if (centerX == Float.MIN_VALUE || centerY == Float.MIN_VALUE) {
+ calculateCenter(vertices)
+ } else {
+ Point(centerX, centerY)
+ }
+ return RoundedPolygon(tempFeatures, cx, cy)
+}
+
+/**
+ * Calculates an estimated center position for the polygon, returning it.
+ * This function should only be called if the center is not already calculated or provided.
+ * The Polygon constructor which takes `numVertices` calculates its own center, since it
+ * knows exactly where it is centered, at (0, 0).
+ *
+ * Note that this center will be transformed whenever the shape itself is transformed.
+ * Any transforms that occur before the center is calculated will be taken into account
+ * automatically since the center calculation is an average of the current location of
+ * all cubic anchor points.
+ */
+private fun calculateCenter(vertices: FloatArray): Point {
+ var cumulativeX = 0f
+ var cumulativeY = 0f
+ var index = 0
+ while (index < vertices.size) {
+ cumulativeX += vertices[index++]
+ cumulativeY += vertices[index++]
+ }
+ return Point(cumulativeX / vertices.size / 2, cumulativeY / vertices.size / 2)
+}
+
+/**
+ * Private utility class that holds the information about each corner in a polygon. The shape
+ * of the corner can be returned by calling the [getCubics] function, which will return a list
+ * of curves representing the corner geometry. The shape of the corner depends on the [rounding]
+ * constructor parameter.
+ *
+ * If rounding is null, there is no rounding; the corner will simply be a single point at [p1].
+ * This point will be represented by a [Cubic] of length 0 at that point.
+ *
+ * If rounding is not null, the corner will be rounded either with a curve approximating a circular
+ * arc of the radius specified in [rounding], or with three curves if [rounding] has a nonzero
+ * smoothing parameter. These three curves are a circular arc in the middle and two symmetrical
+ * flanking curves on either side. The smoothing parameter determines the curvature of the
+ * flanking curves.
+ *
+ * This is a class because we usually need to do the work in 2 steps, and prefer to keep state
+ * between: first we determine how much we want to cut to comply with the parameters, then we are
+ * given how much we can actually cut (because of space restrictions outside this corner)
+ *
+ * @param p0 the vertex before the one being rounded
+ * @param p1 the vertex of this rounded corner
+ * @param p2 the vertex after the one being rounded
+ * @param rounding the optional parameters specifying how this corner should be rounded
+ */
+private class RoundedCorner(
+ val p0: Point,
+ val p1: Point,
+ val p2: Point,
+ val rounding: CornerRounding? = null
+) {
+ val d1 = (p0 - p1).getDirection()
+ val d2 = (p2 - p1).getDirection()
+ val cornerRadius = rounding?.radius ?: 0f
+ val smoothing = rounding?.smoothing ?: 0f
+
+ // cosine of angle at p1 is dot product of unit vectors to the other two vertices
+ val cosAngle = d1.dotProduct(d2)
+ // identity: sin^2 + cos^2 = 1
+ // sinAngle gives us the intersection
+ val sinAngle = sqrt(1 - square(cosAngle))
+ // How much we need to cut, as measured on a side, to get the required radius
+ // calculating where the rounding circle hits the edge
+ // This uses the identity of tan(A/2) = sinA/(1 + cosA), where tan(A/2) = radius/cut
+ val expectedRoundCut =
+ if (sinAngle > 1e-3) { cornerRadius * (cosAngle + 1) / sinAngle } else { 0f }
+ // smoothing changes the actual cut. 0 is same as expectedRoundCut, 1 doubles it
+ val expectedCut: Float
+ get() = ((1 + smoothing) * expectedRoundCut)
+ // the center of the circle approximated by the rounding curve (or the middle of the three
+ // curves if smoothing is requested). The center is the same as p0 if there is no rounding.
+ var center: Point = Point()
+
+ @JvmOverloads
+ fun getCubics(allowedCut0: Float, allowedCut1: Float = allowedCut0):
+ List<Cubic> {
+ // We use the minimum of both cuts to determine the radius, but if there is more space
+ // in one side we can use it for smoothing.
+ val allowedCut = min(allowedCut0, allowedCut1)
+ // Nothing to do, just use lines, or a point
+ if (expectedRoundCut < DistanceEpsilon ||
+ allowedCut < DistanceEpsilon ||
+ cornerRadius < DistanceEpsilon
+ ) {
+ center = p1
+ return listOf(Cubic.straightLine(p1.x, p1.y, p1.x, p1.y))
+ }
+ // How much of the cut is required for the rounding part.
+ val actualRoundCut = min(allowedCut, expectedRoundCut)
+ // We have two smoothing values, one for each side of the vertex
+ // Space is used for rounding values first. If there is space left over, then we
+ // apply smoothing, if it was requested
+ val actualSmoothing0 = calculateActualSmoothingValue(allowedCut0)
+ val actualSmoothing1 = calculateActualSmoothingValue(allowedCut1)
+ // Scale the radius if needed
+ val actualR = cornerRadius * actualRoundCut / expectedRoundCut
+ // Distance from the corner (p1) to the center
+ val centerDistance = sqrt(square(actualR) + square(actualRoundCut))
+ // Center of the arc we will use for rounding
+ center = p1 + ((d1 + d2) / 2f).getDirection() * centerDistance
+ val circleIntersection0 = p1 + d1 * actualRoundCut
+ val circleIntersection2 = p1 + d2 * actualRoundCut
+ val flanking0 = computeFlankingCurve(
+ actualRoundCut, actualSmoothing0, p1, p0,
+ circleIntersection0, circleIntersection2, center, actualR
+ )
+ val flanking2 = computeFlankingCurve(
+ actualRoundCut, actualSmoothing1, p1, p2,
+ circleIntersection2, circleIntersection0, center, actualR
+ ).reverse()
+ return listOf(
+ flanking0,
+ Cubic.circularArc(center.x, center.y, flanking0.anchor1X, flanking0.anchor1Y,
+ flanking2.anchor0X, flanking2.anchor0Y),
+ flanking2
+ )
+ }
+
+ /**
+ * If allowedCut (the amount we are able to cut) is greater than the expected cut
+ * (without smoothing applied yet), then there is room to apply smoothing and we
+ * calculate the actual smoothing value here.
+ */
+ private fun calculateActualSmoothingValue(allowedCut: Float): Float {
+ return if (allowedCut > expectedCut) {
+ smoothing
+ } else if (allowedCut > expectedRoundCut) {
+ smoothing * (allowedCut - expectedRoundCut) / (expectedCut - expectedRoundCut)
+ } else {
+ 0f
+ }
+ }
+
+ /**
+ * Compute a Bezier to connect the linear segment defined by corner and sideStart
+ * with the circular segment defined by circleCenter, circleSegmentIntersection,
+ * otherCircleSegmentIntersection and actualR.
+ * The bezier will start at the linear segment and end on the circular segment.
+ *
+ * @param actualRoundCut How much we are cutting of the corner to add the circular segment
+ * (this is before smoothing, that will cut some more).
+ * @param actualSmoothingValues How much we want to smooth (this is the smooth parameter,
+ * adjusted down if there is not enough room).
+ * @param corner The point at which the linear side ends
+ * @param sideStart The point at which the linear side starts
+ * @param circleSegmentIntersection The point at which the linear side and the circle intersect.
+ * @param otherCircleSegmentIntersection The point at which the opposing linear side and the
+ * circle intersect.
+ * @param circleCenter The center of the circle.
+ * @param actualR The radius of the circle.
+ *
+ * @return a Bezier cubic curve that connects from the (cut) linear side and the (cut) circular
+ * segment in a smooth way.
+ */
+ private fun computeFlankingCurve(
+ actualRoundCut: Float,
+ actualSmoothingValues: Float,
+ corner: Point,
+ sideStart: Point,
+ circleSegmentIntersection: Point,
+ otherCircleSegmentIntersection: Point,
+ circleCenter: Point,
+ actualR: Float
+ ): Cubic {
+ // sideStart is the anchor, 'anchor' is actual control point
+ val sideDirection = (sideStart - corner).getDirection()
+ val curveStart = corner + sideDirection * actualRoundCut * (1 + actualSmoothingValues)
+ // We use an approximation to cut a part of the circle section proportional to 1 - smooth,
+ // When smooth = 0, we take the full section, when smooth = 1, we take nothing.
+ // TODO: revisit this, it can be problematic as it approaches 180 degrees
+ val p = interpolate(circleSegmentIntersection,
+ (circleSegmentIntersection + otherCircleSegmentIntersection) / 2f,
+ actualSmoothingValues)
+ // The flanking curve ends on the circle
+ val curveEnd = circleCenter +
+ directionVector(p.x - circleCenter.x, p.y - circleCenter.y) * actualR
+ // The anchor on the circle segment side is in the intersection between the tangent to the
+ // circle in the circle/flanking curve boundary and the linear segment.
+ val circleTangent = (curveEnd - circleCenter).rotate90()
+ val anchorEnd = lineIntersection(sideStart, sideDirection, curveEnd, circleTangent)
+ ?: circleSegmentIntersection
+ // From what remains, we pick a point for the start anchor.
+ // 2/3 seems to come from design tools?
+ val anchorStart = (curveStart + anchorEnd * 2f) / 3f
+ return Cubic(curveStart, anchorStart, anchorEnd, curveEnd)
+ }
+
+ /**
+ * Returns the intersection point of the two lines d0->d1 and p0->p1, or null if the
+ * lines do not intersect
+ */
+ private fun lineIntersection(p0: Point, d0: Point, p1: Point, d1: Point): Point? {
+ val rotatedD1 = d1.rotate90()
+ val den = d0.dotProduct(rotatedD1)
+ if (abs(den) < AngleEpsilon) return null
+ val k = (p1 - p0).dotProduct(rotatedD1) / den
+ return p0 + d0 * k
+ }
+}
diff --git a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Shapes.kt b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Shapes.kt
similarity index 98%
rename from graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Shapes.kt
rename to graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Shapes.kt
index 24c1e9c..ef4d25b 100644
--- a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Shapes.kt
+++ b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Shapes.kt
@@ -57,7 +57,7 @@
*
* As with all [RoundedPolygon] objects, if this shape is created with default dimensions and
* center, it is sized to fit within the 2x2 bounding box around a center of (0, 0) and will
- * need to be scaled and moved using [RoundedPolygon.transform] to fit the intended area
+ * need to be scaled and moved using [RoundedPolygon.transformed] to fit the intended area
* in a UI.
*
* @param width The width of the rectangle, default value is 2
diff --git a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Utils.kt b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Utils.kt
similarity index 69%
rename from graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Utils.kt
rename to graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Utils.kt
index 9ed7748..0c3b2b0 100644
--- a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Utils.kt
+++ b/graphics/graphics-shapes/src/commonMain/kotlin/androidx/graphics/shapes/Utils.kt
@@ -18,11 +18,6 @@
package androidx.graphics.shapes
-import android.graphics.PointF
-import android.util.Log
-import androidx.core.graphics.div
-import androidx.core.graphics.plus
-import androidx.core.graphics.times
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
@@ -32,40 +27,24 @@
* This class has all internal methods, used by Polygon, Morph, etc.
*/
-internal fun interpolate(start: Float, stop: Float, fraction: Float) =
- (start * (1 - fraction) + stop * fraction)
-
-internal fun PointF.getDistance() = sqrt(x * x + y * y)
-
-internal fun PointF.dotProduct(other: PointF) = x * other.x + y * other.y
-internal fun PointF.dotProduct(otherX: Float, otherY: Float) = x * otherX + y * otherY
-
-/**
- * Compute the Z coordinate of the cross product of two vectors, to check if the second vector is
- * going clockwise ( > 0 ) or counterclockwise (< 0) compared with the first one.
- * It could also be 0, if the vectors are co-linear.
- */
-internal fun PointF.clockwise(other: PointF) = x * other.y - y * other.x > 0
-
-/**
- * Returns unit vector representing the direction to this point from (0, 0)
- */
-internal fun PointF.getDirection() = run {
- val d = this.getDistance()
- require(d > 0f)
- this / d
-}
-
internal fun distance(x: Float, y: Float) = sqrt(x * x + y * y)
/**
* Returns unit vector representing the direction to this point from (0, 0)
*/
-internal fun directionVector(x: Float, y: Float): PointF {
+internal fun directionVector(x: Float, y: Float): Point {
val d = distance(x, y)
require(d > 0f)
- return PointF(x / d, y / d)
+ return Point(x / d, y / d)
}
+
+internal fun directionVector(angleRadians: Float) = Point(cos(angleRadians), sin(angleRadians))
+
+internal fun angle(x: Float, y: Float) = ((atan2(y, x) + TwoPi) % TwoPi)
+
+internal fun radialToCartesian(radius: Float, angleRadians: Float, center: Point = Zero) =
+ directionVector(angleRadians) * radius + center
+
/**
* These epsilon values are used internally to determine when two points are the same, within
* some reasonable roundoff error. The distance epsilon is smaller, with the intention that the
@@ -74,27 +53,22 @@
internal const val DistanceEpsilon = 1e-4f
internal const val AngleEpsilon = 1e-6f
-internal fun PointF.rotate90() = PointF(-y, x)
+internal fun Point.rotate90() = Point(-y, x)
-internal val Zero = PointF(0f, 0f)
+internal val Zero = Point(0f, 0f)
internal val FloatPi = Math.PI.toFloat()
internal val TwoPi: Float = 2 * Math.PI.toFloat()
-internal fun directionVector(angleRadians: Float) = PointF(cos(angleRadians), sin(angleRadians))
-
internal fun square(x: Float) = x * x
-internal fun PointF.copy(x: Float = Float.NaN, y: Float = Float.NaN) =
- PointF(if (x.isNaN()) this.x else x, if (y.isNaN()) this.y else y)
-
-internal fun PointF.angle() = ((atan2(y, x) + TwoPi) % TwoPi)
-
-internal fun angle(x: Float, y: Float) = ((atan2(y, x) + TwoPi) % TwoPi)
-
-internal fun radialToCartesian(radius: Float, angleRadians: Float, center: PointF = Zero) =
- directionVector(angleRadians) * radius + center
+/**
+ * Linearly interpolate between [start] and [stop] with [fraction] fraction between them.
+ */
+internal fun interpolate(start: Float, stop: Float, fraction: Float): Float {
+ return (1 - fraction) * start + fraction * stop
+}
internal fun positiveModulo(num: Float, mod: Float) = (num % mod + mod) % mod
@@ -135,7 +109,7 @@
var arrayIndex = 0
for (i in 0 until numVertices) {
val vertex = radialToCartesian(radius, (FloatPi / numVertices * 2 * i)) +
- PointF(centerX, centerY)
+ Point(centerX, centerY)
result[arrayIndex++] = vertex.x
result[arrayIndex++] = vertex.y
}
@@ -153,20 +127,22 @@
var arrayIndex = 0
for (i in 0 until numVerticesPerRadius) {
var vertex = radialToCartesian(radius, (FloatPi / numVerticesPerRadius * 2 * i)) +
- PointF(centerX, centerY)
+ Point(centerX, centerY)
result[arrayIndex++] = vertex.x
result[arrayIndex++] = vertex.y
vertex = radialToCartesian(innerRadius, (FloatPi / numVerticesPerRadius * (2 * i + 1))) +
- PointF(centerX, centerY)
+ Point(centerX, centerY)
result[arrayIndex++] = vertex.x
result[arrayIndex++] = vertex.y
}
return result
}
-// Used to enable debug logging in the library
-internal val DEBUG = false
+internal const val DEBUG = false
internal inline fun debugLog(tag: String, messageFactory: () -> String) {
- if (DEBUG) messageFactory().split("\n").forEach { Log.d(tag, it) }
+ // TODO: Re-implement properly when the library goes KMP using expect/actual
+ if (DEBUG) {
+ println("$tag: ${messageFactory()}")
+ }
}
diff --git a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Cubic.kt b/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Cubic.kt
deleted file mode 100644
index a99d457..0000000
--- a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Cubic.kt
+++ /dev/null
@@ -1,332 +0,0 @@
-/*
- * Copyright 2022 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.graphics.shapes
-
-import android.graphics.Matrix
-import android.graphics.PointF
-import kotlin.math.sqrt
-
-/**
- * This class holds the anchor and control point data for a single cubic Bézier curve,
- * with anchor points ([anchor0X], [anchor0Y]) and ([anchor1X], [anchor1Y]) at either end
- * and control points ([control0X], [control0Y]) and ([control1X], [control1Y]) determining
- * the slope of the curve between the anchor points.
- *
- * @param anchor0X the first anchor point x coordinate
- * @param anchor0Y the first anchor point y coordinate
- * @param control0X the first control point x coordinate
- * @param control0Y the first control point y coordinate
- * @param control1X the second control point x coordinate
- * @param control1Y the second control point y coordinate
- * @param anchor1X the second anchor point x coordinate
- * @param anchor1Y the second anchor point y coordinate
- */
-class Cubic(
- anchor0X: Float,
- anchor0Y: Float,
- control0X: Float,
- control0Y: Float,
- control1X: Float,
- control1Y: Float,
- anchor1X: Float,
- anchor1Y: Float
-) {
-
- /**
- * The first anchor point x coordinate
- */
- var anchor0X: Float = anchor0X
- private set
-
- /**
- * The first anchor point y coordinate
- */
- var anchor0Y: Float = anchor0Y
- private set
-
- /**
- * The first control point x coordinate
- */
- var control0X: Float = control0X
- private set
-
- /**
- * The first control point y coordinate
- */
- var control0Y: Float = control0Y
- private set
-
- /**
- * The second control point x coordinate
- */
- var control1X: Float = control1X
- private set
-
- /**
- * The second control point y coordinate
- */
- var control1Y: Float = control1Y
- private set
-
- /**
- * The second anchor point x coordinate
- */
- var anchor1X: Float = anchor1X
- private set
-
- /**
- * The second anchor point y coordinate
- */
- var anchor1Y: Float = anchor1Y
- private set
-
- internal constructor(p0: PointF, p1: PointF, p2: PointF, p3: PointF) :
- this(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)
-
- /**
- * Copy constructor which creates a copy of the given object.
- */
- constructor(cubic: Cubic) : this(
- cubic.anchor0X, cubic.anchor0Y, cubic.control0X, cubic.control0Y,
- cubic.control1X, cubic.control1Y, cubic.anchor1X, cubic.anchor1Y,
- )
-
- override fun toString(): String {
- return "p0: ($anchor0X, $anchor0Y) p1: ($control0X, $control0Y), " +
- "p2: ($control1X, $control1Y), p3: ($anchor1X, $anchor1Y)"
- }
-
- /**
- * Returns a point on the curve for parameter t, representing the proportional distance
- * along the curve between its starting ([anchor0X], [anchor0Y]) and ending
- * ([anchor1X], [anchor1Y]) anchor points.
- *
- * @param t The distance along the curve between the anchor points, where 0 is at
- * ([anchor0X], [anchor0Y]) and 1 is at ([control0X], [control0Y])
- * @param result Optional object to hold the result, can be passed in to avoid allocating a
- * new PointF object.
- */
- @JvmOverloads
- fun pointOnCurve(t: Float, result: PointF = PointF()): PointF {
- val u = 1 - t
- result.x = anchor0X * (u * u * u) + control0X * (3 * t * u * u) +
- control1X * (3 * t * t * u) + anchor1X * (t * t * t)
- result.y = anchor0Y * (u * u * u) + control0Y * (3 * t * u * u) +
- control1Y * (3 * t * t * u) + anchor1Y * (t * t * t)
- return result
- }
-
- /**
- * Returns two Cubics, created by splitting this curve at the given
- * distance of [t] between the original starting and ending anchor points.
- */
- // TODO: cartesian optimization?
- fun split(t: Float): Pair<Cubic, Cubic> {
- val u = 1 - t
- val pointOnCurve = pointOnCurve(t)
- return Cubic(
- anchor0X, anchor0Y,
- anchor0X * u + control0X * t, anchor0Y * u + control0Y * t,
- anchor0X * (u * u) + control0X * (2 * u * t) + control1X * (t * t),
- anchor0Y * (u * u) + control0Y * (2 * u * t) + control1Y * (t * t),
- pointOnCurve.x, pointOnCurve.y
- ) to Cubic(
- // TODO: should calculate once and share the result
- pointOnCurve.x, pointOnCurve.y,
- control0X * (u * u) + control1X * (2 * u * t) + anchor1X * (t * t),
- control0Y * (u * u) + control1Y * (2 * u * t) + anchor1Y * (t * t),
- control1X * u + anchor1X * t, control1Y * u + anchor1Y * t,
- anchor1X, anchor1Y
- )
- }
-
- /**
- * Utility function to reverse the control/anchor points for this curve.
- */
- fun reverse() = Cubic(anchor1X, anchor1Y, control1X, control1Y, control0X, control0Y,
- anchor0X, anchor0Y)
-
- /**
- * Operator overload to enable adding Cubic objects together, like "c0 + c1"
- */
- operator fun plus(o: Cubic) = Cubic(
- anchor0X + o.anchor0X, anchor0Y + o.anchor0Y,
- control0X + o.control0X, control0Y + o.control0Y,
- control1X + o.control1X, control1Y + o.control1Y,
- anchor1X + o.anchor1X, anchor1Y + o.anchor1Y
- )
-
- /**
- * Operator overload to enable multiplying Cubics by a scalar value x, like "c0 * x"
- */
- operator fun times(x: Float) = Cubic(
- anchor0X * x, anchor0Y * x,
- control0X * x, control0Y * x,
- control1X * x, control1Y * x,
- anchor1X * x, anchor1Y * x
- )
-
- /**
- * Operator overload to enable multiplying Cubics by an Int scalar value x, like "c0 * x"
- */
- operator fun times(x: Int) = times(x.toFloat())
-
- /**
- * Operator overload to enable dividing Cubics by a scalar value x, like "c0 / x"
- */
- operator fun div(x: Float) = times(1f / x)
-
- /**
- * Operator overload to enable dividing Cubics by a scalar value x, like "c0 / x"
- */
- operator fun div(x: Int) = div(x.toFloat())
-
- /**
- * This function transforms this curve (its anchor and control points) with the given
- * Matrix.
- *
- * @param matrix The matrix used to transform the curve
- * @param points Optional array of Floats used internally. Supplying this array of floats saves
- * allocating the array internally when not provided. Must have size equal to or larger than 8.
- * @throws IllegalArgumentException if [points] is provided but is not large enough to
- * hold 8 values.
- */
- @JvmOverloads
- fun transform(matrix: Matrix, points: FloatArray = FloatArray(8)) {
- if (points.size < 8) {
- throw IllegalArgumentException("points array must be of size >= 8")
- }
- points[0] = anchor0X
- points[1] = anchor0Y
- points[2] = control0X
- points[3] = control0Y
- points[4] = control1X
- points[5] = control1Y
- points[6] = anchor1X
- points[7] = anchor1Y
- matrix.mapPoints(points)
- anchor0X = points[0]
- anchor0Y = points[1]
- control0X = points[2]
- control0Y = points[3]
- control1X = points[4]
- control1Y = points[5]
- anchor1X = points[6]
- anchor1Y = points[7]
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as Cubic
-
- if (anchor0X != other.anchor0X) return false
- if (anchor0Y != other.anchor0Y) return false
- if (control0X != other.control0X) return false
- if (control0Y != other.control0Y) return false
- if (control1X != other.control1X) return false
- if (control1Y != other.control1Y) return false
- if (anchor1X != other.anchor1X) return false
- if (anchor1Y != other.anchor1Y) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = anchor0X.hashCode()
- result = 31 * result + anchor0Y.hashCode()
- result = 31 * result + control0X.hashCode()
- result = 31 * result + control0Y.hashCode()
- result = 31 * result + control1X.hashCode()
- result = 31 * result + control1Y.hashCode()
- result = 31 * result + anchor1X.hashCode()
- result = 31 * result + anchor1Y.hashCode()
- return result
- }
-
- companion object {
- /**
- * Generates a bezier curve that is a straight line between the given anchor points.
- * The control points lie 1/3 of the distance from their respective anchor points.
- */
- @JvmStatic
- fun straightLine(x0: Float, y0: Float, x1: Float, y1: Float): Cubic {
- return Cubic(
- x0, y0,
- interpolate(x0, x1, 1f / 3f),
- interpolate(y0, y1, 1f / 3f),
- interpolate(x0, x1, 2f / 3f),
- interpolate(y0, y1, 2f / 3f),
- x1, y1
- )
- }
-
- // TODO: consider a more general function (maybe in addition to this) that allows
- // caller to get a list of curves surpassing 180 degrees
- /**
- * Generates a bezier curve that approximates a circular arc, with p0 and p1 as
- * the starting and ending anchor points. The curve generated is the smallest of
- * the two possible arcs around the entire 360-degree circle. Arcs of greater than 180
- * degrees should use more than one arc together. Note that p0 and p1 should be
- * equidistant from the center.
- */
- @JvmStatic
- fun circularArc(
- centerX: Float,
- centerY: Float,
- x0: Float,
- y0: Float,
- x1: Float,
- y1: Float
- ): Cubic {
- val p0d = directionVector(x0 - centerX, y0 - centerY)
- val p1d = directionVector(x1 - centerX, y1 - centerY)
- val rotatedP0 = p0d.rotate90()
- val rotatedP1 = p1d.rotate90()
- val clockwise = rotatedP0.dotProduct(x1 - centerX, y1 - centerY) >= 0
- val cosa = p0d.dotProduct(p1d)
- if (cosa > 0.999f) /* p0 ~= p1 */ return straightLine(x0, y0, x1, y1)
- val k = distance(x0 - centerX, y0 - centerY) * 4f / 3f *
- (sqrt(2 * (1 - cosa)) - sqrt(1 - cosa * cosa)) / (1 - cosa) *
- if (clockwise) 1f else -1f
- return Cubic(
- x0, y0, x0 + rotatedP0.x * k, y0 + rotatedP0.y * k,
- x1 - rotatedP1.x * k, y1 - rotatedP1.y * k, x1, y1
- )
- }
-
- /**
- * Creates and returns a new Cubic which is a linear interpolation between
- * [start] AND [end]. This can be used, for example, in animations to smoothly animate a
- * curve from one location and size to another.
- */
- @JvmStatic
- fun interpolate(start: Cubic, end: Cubic, t: Float): Cubic {
- return (Cubic(
- interpolate(start.anchor0X, end.anchor0X, t),
- interpolate(start.anchor0Y, end.anchor0Y, t),
- interpolate(start.control0X, end.control0X, t),
- interpolate(start.control0Y, end.control0Y, t),
- interpolate(start.control1X, end.control1X, t),
- interpolate(start.control1Y, end.control1Y, t),
- interpolate(start.anchor1X, end.anchor1X, t),
- interpolate(start.anchor1Y, end.anchor1Y, t),
- ))
- }
- }
-}
diff --git a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/CubicShape.kt b/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/CubicShape.kt
deleted file mode 100644
index 84d5007..0000000
--- a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/CubicShape.kt
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * Copyright 2022 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.graphics.shapes
-
-import android.graphics.Canvas
-import android.graphics.Matrix
-import android.graphics.Paint
-import android.graphics.Path
-import android.graphics.RectF
-
-/**
- * This shape is defined by the list of [Cubic] curves with which it is created.
- * The list is contiguous. That is, a path based on this list
- * starts at the first anchor point of the first cubic, with each new cubic starting
- * at the end of each current cubic (i.e., the second anchor point of each cubic
- * is the same as the first anchor point of the next cubic). The final
- * cubic ends at the first anchor point of the initial cubic.
- */
-class CubicShape internal constructor() {
-
- /**
- * Constructs a [CubicShape] with the given list of [Cubic]s. The list is copied
- * internally to ensure immutability of this shape.
- * @throws IllegalArgumentException The last point of each cubic must match the
- * first point of the next cubic (with the final cubic's last point matching
- * the first point of the first cubic in the list).
- */
- constructor(cubics: List<Cubic>) : this() {
- val copy = mutableListOf<Cubic>()
- var prevCubic = cubics[cubics.size - 1]
- for (cubic in cubics) {
- if (cubic.anchor0X != prevCubic.anchor1X || cubic.anchor0Y != prevCubic.anchor1Y) {
- throw IllegalArgumentException("CubicShapes must be contiguous, with the anchor " +
- "points of all curves matching the anchor points of the preceding and " +
- "succeeding cubics")
- }
- prevCubic = cubic
- copy.add(Cubic(cubic))
- }
- updateCubics(copy)
- }
-
- constructor(sourceShape: CubicShape) : this(sourceShape.cubics)
-
- /**
- * The ordered list of cubic curves that define this shape.
- */
- lateinit var cubics: List<Cubic>
- private set
-
- /**
- * The bounds of a shape are a simple min/max bounding box of the points in all of
- * the [Cubic] objects. Note that this is not the same as the bounds of the resulting
- * shape, but is a reasonable (and cheap) way to estimate the bounds. These bounds
- * can be used to, for example, determine the size to scale the object when drawing it.
- */
- var bounds: RectF = RectF()
- internal set
-
- /**
- * This path object is used for drawing the shape. Callers can retrieve a copy of it with
- * the [toPath] function. The path is updated automatically whenever the shape's
- * [cubics] are updated.
- */
- private val path: Path = Path()
-
- /**
- * Transforms (scales, rotates, and translates) the shape by the given matrix.
- * Note that this operation alters the points in the shape directly; the original
- * points are not retained, nor is the matrix itself. Thus calling this function
- * twice with the same matrix will composite the effect. For example, a matrix which
- * scales by 2 will scale the shape by 2. Calling transform twice with that matrix
- * will have the effect os scaling the shape size by 4.
- *
- * @param matrix The matrix used to transform the curve
- * @param points Optional array of Floats used internally. Supplying this array of floats saves
- * allocating the array internally when not provided. Must have size equal to or larger than 8.
- * @throws IllegalArgumentException if [points] is provided but is not large enough to
- * hold 8 values.
- */
- @JvmOverloads
- fun transform(matrix: Matrix, points: FloatArray = FloatArray(8)) {
- if (points.size < 8) {
- throw IllegalArgumentException("points array must be of size >= 8")
- }
- for (cubic in cubics) {
- cubic.transform(matrix, points)
- }
- updateCubics(cubics)
- }
-
- /**
- * This is called by Polygon's constructor. It should not generally be called later;
- * CubicShape should be immutable.
- */
- internal fun updateCubics(cubics: List<Cubic>) {
- this.cubics = cubics
- calculateBounds()
- updatePath()
- }
-
- /**
- * A CubicShape is rendered as a [Path]. A copy of the underlying [Path] object can be
- * retrieved for use outside of this class. Note that this function returns a copy of
- * the internal [Path] to maintain immutability, thus there is some overhead in retrieving
- * and using the path with this function.
- */
- fun toPath(): Path {
- return Path(path)
- }
-
- /**
- * Internal function to update the Path object whenever the cubics are updated.
- * The Path should not be needed until drawing (or being retrieved via [toPath]),
- * but might as well update it immediately since the cubics should not change
- * in the meantime.
- */
- private fun updatePath() {
- path.rewind()
- if (cubics.isNotEmpty()) {
- path.moveTo(cubics[0].anchor0X, cubics[0].anchor0Y)
- for (bezier in cubics) {
- path.cubicTo(
- bezier.control0X, bezier.control0Y,
- bezier.control1X, bezier.control1Y,
- bezier.anchor1X, bezier.anchor1Y
- )
- }
- }
- path.close()
- }
-
- internal fun draw(canvas: Canvas, paint: Paint) {
- canvas.drawPath(path, paint)
- }
-
- /**
- * Calculates estimated bounds of the object, using the min/max bounding box of
- * all points in the cubics that make up the shape.
- */
- private fun calculateBounds() {
- var minX = Float.MAX_VALUE
- var minY = Float.MAX_VALUE
- var maxX = Float.MIN_VALUE
- var maxY = Float.MIN_VALUE
- for (bezier in cubics) {
- if (bezier.anchor0X < minX) minX = bezier.anchor0X
- if (bezier.anchor0Y < minY) minY = bezier.anchor0Y
- if (bezier.anchor0X > maxX) maxX = bezier.anchor0X
- if (bezier.anchor0Y > maxY) maxY = bezier.anchor0Y
-
- if (bezier.control0X < minX) minX = bezier.control0X
- if (bezier.control0Y < minY) minY = bezier.control0Y
- if (bezier.control0X > maxX) maxX = bezier.control0X
- if (bezier.control0Y > maxY) maxY = bezier.control0Y
-
- if (bezier.control1X < minX) minX = bezier.control1X
- if (bezier.control1Y < minY) minY = bezier.control1Y
- if (bezier.control1X > maxX) maxX = bezier.control1X
- if (bezier.control1Y > maxY) maxY = bezier.control1Y
- // No need to use x3/y3, since it is already taken into account in the next
- // curve's x0/y0 point.
- }
- bounds.set(minX, minY, maxX, maxY)
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- return cubics == (other as CubicShape).cubics
- }
-
- override fun hashCode(): Int {
- return cubics.hashCode()
- }
-}
-
-/**
- * Extension function which draws the given [CubicShape] object into this [Canvas]. Rendering
- * occurs by drawing the underlying path for the object; callers can optionally retrieve the
- * path and draw it directly via [CubicShape.toPath] (though that function copies the underlying
- * path. This extension function avoids that overhead when rendering).
- *
- * @param shape The object to be drawn
- * @param paint The attributes
- */
-fun Canvas.drawCubicShape(shape: CubicShape, paint: Paint) {
- shape.draw(this, paint)
-}
diff --git a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Morph.kt b/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Morph.kt
deleted file mode 100644
index 54452d9..0000000
--- a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Morph.kt
+++ /dev/null
@@ -1,387 +0,0 @@
-/*
- * Copyright 2022 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.graphics.shapes
-
-import android.graphics.Canvas
-import android.graphics.Matrix
-import android.graphics.Paint
-import android.graphics.Path
-import android.graphics.PointF
-import android.graphics.RectF
-import kotlin.math.min
-
-/**
- * This class is used to animate between start and end polygons objects.
- *
- * Morphing between arbitrary objects can be problematic because it can be difficult to
- * determine how the points of a given shape map to the points of some other shape.
- * [Morph] simplifies the problem by only operating on [RoundedPolygon] objects, which
- * are known to have similar, contiguous structures. For one thing, the shape of a polygon
- * is contiguous from start to end (compared to an arbitrary [Path] object, which could have
- * one or more `moveTo` operations in the shape). Also, all edges of a polygon shape are
- * represented by [Cubic] objects, thus the start and end shapes use similar operations. Two
- * Polygon shapes then only differ in the quantity and placement of their curves.
- * The morph works by determining how to map the curves of the two shapes together (based on
- * proximity and other information, such as distance to polygon vertices and concavity),
- * and splitting curves when the shapes do not have the same number of curves or when the
- * curve placement within the shapes is very different.
- */
-class Morph(
- start: RoundedPolygon,
- end: RoundedPolygon
-) {
- // morphMatch is the structure which holds the actual shape being morphed. It contains
- // all cubics necessary to represent the start and end shapes (the original cubics in the
- // shapes may be cut to align the start/end shapes)
- private var morphMatch = match(start, end)
-
- // path is used to draw the object
- // It is cached to avoid recalculating it if the progress has not changed
- private val path = Path()
-
- // last value for which the cached path was constructed. We cache this and the path
- // to avoid recreating the path for the same progress value
- private var currentPathProgress: Float = Float.MIN_VALUE
-
- /**
- * The bounds of the morph object are estimated by control and anchor points of all cubic curves
- * representing the shape.
- */
- val bounds = RectF()
-
- init {
- calculateBounds(bounds)
- }
-
- /**
- * Rough bounds of the object, based on the min/max bounds of all cubics points in morphMatch
- */
- private fun calculateBounds(bounds: RectF) {
- // TODO: Maybe using just the anchors (p0 and p3) is sufficient and more correct than
- // also using the control points (p1 and p2)
- var minX = Float.MAX_VALUE
- var minY = Float.MAX_VALUE
- var maxX = Float.MIN_VALUE
- var maxY = Float.MIN_VALUE
- for (pair in morphMatch) {
- if (pair.first.anchor0X < minX) minX = pair.first.anchor0X
- if (pair.first.anchor0Y < minY) minY = pair.first.anchor0Y
- if (pair.first.anchor0X > maxX) maxX = pair.first.anchor0X
- if (pair.first.anchor0Y > maxY) maxY = pair.first.anchor0Y
-
- if (pair.second.anchor0X < minX) minX = pair.second.anchor0X
- if (pair.second.anchor0Y < minY) minY = pair.second.anchor0Y
- if (pair.second.anchor0X > maxX) maxX = pair.second.anchor0X
- if (pair.second.anchor0Y > maxY) maxY = pair.second.anchor0Y
-
- if (pair.first.control0X < minX) minX = pair.first.control0X
- if (pair.first.control0Y < minY) minY = pair.first.control0Y
- if (pair.first.control0X > maxX) maxX = pair.first.control0X
- if (pair.first.control0Y > maxY) maxY = pair.first.control0Y
-
- if (pair.second.control0X < minX) minX = pair.second.control0X
- if (pair.second.control0Y < minY) minY = pair.second.control0Y
- if (pair.second.control0X > maxX) maxX = pair.second.control0X
- if (pair.second.control0Y > maxY) maxY = pair.second.control0Y
-
- if (pair.first.control1X < minX) minX = pair.first.control1X
- if (pair.first.control1Y < minY) minY = pair.first.control1Y
- if (pair.first.control1X > maxX) maxX = pair.first.control1X
- if (pair.first.control1Y > maxY) maxY = pair.first.control1Y
-
- if (pair.second.control1X < minX) minX = pair.second.control1X
- if (pair.second.control1Y < minY) minY = pair.second.control1Y
- if (pair.second.control1X > maxX) maxX = pair.second.control1X
- if (pair.second.control1Y > maxY) maxY = pair.second.control1Y
- // Skip x3/y3 since every last point is the next curve's first point
- }
- bounds.set(minX, minY, maxX, maxY)
- }
-
- /**
- * This function updates the [path] object which holds the rendering information for the
- * morph shape, using the current [progress] property for the morph.
- */
- private fun getPath(progress: Float): Path {
- // Noop if we have already
- if (progress == currentPathProgress) return path
-
- // In a future release, Path interpolation may be possible through the Path API
- // itself. Until then, we have to rewind and repopulate with the new/interpolated
- // values
- path.rewind()
-
- // If the list is not empty, do an initial moveTo using the first element of the match.
- morphMatch.firstOrNull()?. let { first ->
- path.moveTo(
- interpolate(first.first.anchor0X, first.second.anchor0X, progress),
- interpolate(first.first.anchor0Y, first.second.anchor0Y, progress)
- )
- }
-
- // And one cubicTo for each element, including the first.
- for (i in 0..morphMatch.lastIndex) {
- val element = morphMatch[i]
- path.cubicTo(
- interpolate(element.first.control0X, element.second.control0X, progress),
- interpolate(element.first.control0Y, element.second.control0Y, progress),
- interpolate(element.first.control1X, element.second.control1X, progress),
- interpolate(element.first.control1Y, element.second.control1Y, progress),
- interpolate(element.first.anchor1X, element.second.anchor1X, progress),
- interpolate(element.first.anchor1Y, element.second.anchor1Y, progress),
- )
- }
- path.close()
- currentPathProgress = progress
- return path
- }
-
- /**
- * Transforms (scales, rotates, and translates) the shape by the given matrix.
- * Note that this operation alters the points in the shape directly; the original
- * points are not retained, nor is the matrix itself. Thus calling this function
- * twice with the same matrix will composite the effect. For example, a matrix which
- * scales by 2 will scale the shape by 2. Calling transform twice with that matrix
- * will have the effect of scaling the original shape by 4.
- */
- fun transform(matrix: Matrix) {
- for (pair in morphMatch) {
- pair.first.transform(matrix)
- pair.second.transform(matrix)
- }
- calculateBounds(bounds)
- // Reset cached progress value to force recalculation due to transform change
- currentPathProgress = Float.MIN_VALUE
- }
-
- /**
- * Morph is rendered as a [Path]. A copy of the underlying [Path] object can be
- * retrieved for use outside of this class. Note that this function returns a copy of
- * the internal [Path] to maintain immutability, thus there is some overhead in retrieving
- * the path with this function.
- *
- * @param progress a value from 0 to 1 that determines the morph's current
- * shape, between the start and end shapes provided at construction time. A value of 0 results
- * in the start shape, a value of 1 results in the end shape, and any value in between
- * results in a shape which is a linear interpolation between those two shapes.
- * The range is generally [0..1] and values outside could result in undefined shapes, but
- * values close to (but outside) the range can be used to get an exaggerated effect
- * (e.g., for a bounce or overshoot animation).
- * @param path optional Path object to be used to hold the resulting Path data. If provided,
- * that Path's data will be replaced with the internal Path data for the Morph. If none
- * is provided, new Path object will be created and used instead.
- */
- @JvmOverloads
- fun asPath(progress: Float, path: Path = Path()): Path {
- path.set(getPath(progress))
- return path
- }
-
- /**
- * Returns a representation of the morph object at a given [progress] value as a list of Cubics.
- * Note that this function causes a new list to be created and populated, so there is some
- * overhead.
- *
- * @param progress a value from 0 to 1 that determines the morph's current
- * shape, between the start and end shapes provided at construction time. A value of 0 results
- * in the start shape, a value of 1 results in the end shape, and any value in between
- * results in a shape which is a linear interpolation between those two shapes.
- * The range is generally [0..1] and values outside could result in undefined shapes, but
- * values close to (but outside) the range can be used to get an exaggerated effect
- * (e.g., for a bounce or overshoot animation).
- */
- fun asCubics(progress: Float) =
- mutableListOf<Cubic>().apply {
- clear()
- for (pair in morphMatch) {
- add(Cubic.interpolate(pair.first, pair.second, progress))
- }
- }
-
- internal companion object {
- /**
- * [match], called at Morph construction time, creates the structure used to animate between
- * the start and end shapes. The technique is to match geometry (curves) between the shapes
- * when and where possible, and to create new/placeholder curves when necessary (when
- * one of the shapes has more curves than the other). The result is a list of pairs of
- * Cubic curves. Those curves are the matched pairs: the first of each pair holds the
- * geometry of the start shape, the second holds the geometry for the end shape.
- * Changing the progress of a Morph object simply interpolates between all pairs of
- * curves for the morph shape.
- *
- * Curves on both shapes are matched by running the [Measurer] to determine where
- * the points are in each shape (proportionally, along the outline), and then running
- * [featureMapper] which decides how to map (match) all of the curves with each other.
- */
- @JvmStatic
- internal fun match(
- p1: RoundedPolygon,
- p2: RoundedPolygon
- ): List<Pair<Cubic, Cubic>> {
- if (DEBUG) {
- repeat(2) { polyIndex ->
- debugLog(LOG_TAG) {
- listOf("Initial start:\n", "Initial end:\n")[polyIndex] +
- listOf(p1, p2)[polyIndex].features.joinToString("\n") { feature ->
- "${feature.javaClass.name.split("$").last()} - " +
- ((feature as? RoundedPolygon.Corner)?.convex?.let {
- if (it) "Convex - " else "Concave - " } ?: "") +
- feature.cubics.joinToString("|")
- }
- }
- }
- }
-
- // Measure polygons, returns lists of measured cubics for each polygon, which
- // we then use to match start/end curves
- val measuredPolygon1 = MeasuredPolygon.measurePolygon(
- AngleMeasurer(p1.centerX, p1.centerY), p1)
- val measuredPolygon2 = MeasuredPolygon.measurePolygon(
- AngleMeasurer(p2.centerX, p2.centerY), p2)
-
- // features1 and 2 will contain the list of corners (just the inner circular curve)
- // along with the progress at the middle of those corners. These measurement values
- // are then used to compare and match between the two polygons
- val features1 = measuredPolygon1.features
- val features2 = measuredPolygon2.features
-
- // Map features: doubleMapper is the result of mapping the features in each shape to the
- // closest feature in the other shape.
- // Given a progress in one of the shapes it can be used to find the corresponding
- // progress in the other shape (in both directions)
- val doubleMapper = featureMapper(features1, features2)
-
- // cut point on poly2 is the mapping of the 0 point on poly1
- val polygon2CutPoint = doubleMapper.map(0f)
- debugLog(LOG_TAG) { "polygon2CutPoint = $polygon2CutPoint" }
-
- // Cut and rotate.
- // Polygons start at progress 0, and the featureMapper has decided that we want to match
- // progress 0 in the first polygon to `polygon2CutPoint` on the second polygon.
- // So we need to cut the second polygon there and "rotate it", so as we walk through
- // both polygons we can find the matching.
- // The resulting bs1/2 are MeasuredPolygons, whose MeasuredCubics start from
- // outlineProgress=0 and increasing until outlineProgress=1
- val bs1 = measuredPolygon1
- val bs2 = measuredPolygon2.cutAndShift(polygon2CutPoint)
-
- if (DEBUG) {
- (0 until bs1.size).forEach { index ->
- debugLog(LOG_TAG) { "start $index: ${bs1.getOrNull(index)}" }
- }
- (0 until bs2.size).forEach { index ->
- debugLog(LOG_TAG) { "End $index: ${bs2.getOrNull(index)}" }
- }
- }
-
- // Match
- // Now we can compare the two lists of measured cubics and create a list of pairs
- // of cubics [ret], which are the start/end curves that represent the Morph object
- // and the start and end shapes, and which can be interpolated to animate the
- // between those shapes.
- val ret = mutableListOf<Pair<Cubic, Cubic>>()
- // i1/i2 are the indices of the current cubic on the start (1) and end (2) shapes
- var i1 = 0
- var i2 = 0
- // b1, b2 are the current measured cubic for each polygon
- var b1 = bs1.getOrNull(i1++)
- var b2 = bs2.getOrNull(i2++)
- // Iterate until all curves are accounted for and matched
- while (b1 != null && b2 != null) {
- // Progresses are in shape1's perspective
- // b1a, b2a are ending progress values of current measured cubics in [0,1] range
- val b1a = if (i1 == bs1.size) 1f else b1.endOutlineProgress
- val b2a = if (i2 == bs2.size) 1f else doubleMapper.mapBack(
- positiveModulo(b2.endOutlineProgress + polygon2CutPoint, 1f)
- )
- val minb = min(b1a, b2a)
- debugLog(LOG_TAG) { "$b1a $b2a | $minb" }
- // minb is the progress at which the curve that ends first ends.
- // If both curves ends roughly there, no cutting is needed, we have a match.
- // If one curve extends beyond, we need to cut it.
- val (seg1, newb1) = if (b1a > minb + AngleEpsilon) {
- debugLog(LOG_TAG) { "Cut 1" }
- b1.cutAtProgress(minb)
- } else {
- b1 to bs1.getOrNull(i1++)
- }
- val (seg2, newb2) = if (b2a > minb + AngleEpsilon) {
- debugLog(LOG_TAG) { "Cut 2" }
- b2.cutAtProgress(positiveModulo(doubleMapper.map(minb) - polygon2CutPoint, 1f))
- } else {
- b2 to bs2.getOrNull(i2++)
- }
- debugLog(LOG_TAG) { "Match: $seg1 -> $seg2" }
- ret.add(Cubic(seg1.cubic) to Cubic(seg2.cubic))
- b1 = newb1
- b2 = newb2
- }
- require(b1 == null && b2 == null)
-
- if (DEBUG) {
- // Export as SVG path
- val showPoint: (PointF) -> String = {
- "%.3f %.3f".format(it.x * 100, it.y * 100)
- }
- repeat(2) { listIx ->
- val points = ret.map { if (listIx == 0) it.first else it.second }
- debugLog(LOG_TAG) {
- "M " + showPoint(PointF(points.first().anchor0X,
- points.first().anchor0Y)) + " " +
- points.joinToString(" ") {
- "C " + showPoint(PointF(it.control0X, it.control0Y)) + ", " +
- showPoint(PointF(it.control1X, it.control1Y)) + ", " +
- showPoint(PointF(it.anchor1X, it.anchor1Y))
- } + " Z"
- }
- }
- }
- return ret
- }
- }
-
- /**
- * Draws the Morph object. This is called by the public extension function
- * [Canvas.drawMorph]. By default, it simply calls [Canvas.drawPath].
- */
- internal fun draw(canvas: Canvas, paint: Paint, progress: Float) {
- val path = getPath(progress)
- canvas.drawPath(path, paint)
- }
-}
-
-/**
- * Extension function which draws the given [Morph] object into this [Canvas]. Rendering
- * occurs by drawing the underlying path for the object; callers can optionally retrieve the
- * path and draw it directly via [Morph.asPath] (though that function copies the underlying
- * path. This extension function avoids that overhead when rendering).
- *
- * @param morph The object to be drawn
- * @param paint The drawing attributes to be used when rendering the morph object
- * @param progress a value from 0 to 1 that determines the morph's current
- * shape, between the start and end shapes provided at construction time. A value of 0 results
- * in the start shape, a value of 1 results in the end shape, and any value in between
- * results in a shape which is a linear interpolation between those two shapes.
- * The range is generally [0..1] and values outside could result in undefined shapes, but
- * values close to (but outside) the range can be used to get an exaggerated effect
- * (e.g., for a bounce or overshoot animation).
- */
-fun Canvas.drawMorph(morph: Morph, paint: Paint, progress: Float = 0f) {
- morph.draw(this, paint, progress)
-}
-
-private val LOG_TAG = "Morph"
diff --git a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/RoundedPolygon.kt b/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/RoundedPolygon.kt
deleted file mode 100644
index 18af8d2..0000000
--- a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/RoundedPolygon.kt
+++ /dev/null
@@ -1,667 +0,0 @@
-/*
- * Copyright 2022 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.graphics.shapes
-
-import android.graphics.Canvas
-import android.graphics.Matrix
-import android.graphics.Paint
-import android.graphics.Path
-import android.graphics.PointF
-import android.graphics.RectF
-import androidx.annotation.IntRange
-import androidx.core.graphics.div
-import androidx.core.graphics.minus
-import androidx.core.graphics.plus
-import androidx.core.graphics.times
-import kotlin.math.abs
-import kotlin.math.min
-import kotlin.math.sqrt
-
-/**
- * The RoundedPolygon class allows simple construction of polygonal shapes with optional rounding
- * at the vertices. Polygons can be constructed with either the number of vertices
- * desired or an ordered list of vertices.
- */
-class RoundedPolygon {
-
- /**
- * A RoundedPolygon is essentially a CubicShape, which handles all of the functionality around
- * cubic Beziers that are used to create and render the geometry. But subclassing from
- * CubicShape causes a bit of naming confusion, since an actual polygon, in geometry,
- * is a shape with straight edges and hard corners, whereas CubicShape obviously allows for
- * more general, curved shapes. Therefore, we delegate to CubicShape as an internal
- * implementation detail, and RoundedPolygon has no superclass.
- */
- private val cubicShape = CubicShape()
-
- /**
- * Features are the corners (rounded or not) and edges of a polygon. Retaining the list of
- * per-vertex corner (and the edges between them) allows manipulation of a RoundedPolygon with
- * more context for the structure of that polygon, rather than just the list of cubic beziers
- * which are calculated for rendering purposes.
- */
- internal lateinit var features: List<Feature>
- private set
-
- // TODO center point should not be mutable
- /**
- * The X coordinated of the center of this polygon.
- * The center is determined at construction time, either calculated
- * to be an average of all of the vertices of the polygon, or passed in as a parameter. This
- * center may be used in later operations, to help determine (for example) the relative
- * placement of points along the perimeter of the polygon.
- */
- var centerX: Float
- private set
-
- /**
- * The Y coordinated of the center of this polygon.
- * The center is determined at construction time, either calculated
- * to be an average of all of the vertices of the polygon, or passed in as a parameter. This
- * center may be used in later operations, to help determine (for example) the relative
- * placement of points along the perimeter of the polygon.
- */
- var centerY: Float
- private set
-
- /**
- * The bounds of a shape are a simple min/max bounding box of the points in all of
- * the [Cubic] objects. Note that this is not the same as the bounds of the resulting
- * shape, but is a reasonable (and cheap) way to estimate the bounds. These bounds
- * can be used to, for example, determine the size to scale the object when drawing it.
- */
- var bounds: RectF by cubicShape::bounds
-
- companion object {}
-
- /**
- * Constructs a RoundedPolygon object from a given list of vertices, with optional
- * corner-rounding parameters for all corners or per-corner.
- *
- * A RoundedPolygon without any rounding parameters is equivalent to a [RoundedPolygon]
- * constructed with the same [vertices] and ([centerX], [centerY]) values.
- *
- * @param vertices The list of vertices in this polygon. This should be an ordered list
- * (with the outline of the shape going from each vertex to the next in order of this
- * list), otherwise the results will be undefined.
- * @param rounding The [CornerRounding] properties of every vertex. If some vertices should
- * have different rounding properties, then use [perVertexRounding] instead. The default
- * rounding value is [CornerRounding.Unrounded], meaning that the polygon will use the vertices
- * themselves in the final shape and not curves rounded around the vertices.
- * @param perVertexRounding The [CornerRounding] properties of every vertex. If this
- * parameter is not null, then it must have the same size as [vertices]. If this parameter
- * is null, then the polygon will use the [rounding] parameter for every vertex instead. The
- * default value is null.
- * @param centerX The X coordinate of an optionally declared center of the polygon. If either
- * [centerX] or [centerY] is not supplied, both will be calculated based on the supplied
- * vertices.
- * @param centerY The Y coordinate of an optionally declared center of the polygon. If either
- * [centerX] or [centerY] is not supplied, both will be calculated based on the supplied
- * vertices.
- *
- * @throws IllegalArgumentException If [perVertexRounding] is not null, it must be
- * the same size as the [vertices] list.
- * @throws IllegalArgumentException [vertices] must have a size of at least three.
- */
- constructor(
- vertices: FloatArray,
- rounding: CornerRounding = CornerRounding.Unrounded,
- perVertexRounding: List<CornerRounding>? = null,
- centerX: Float = Float.MIN_VALUE,
- centerY: Float = Float.MIN_VALUE
- ) {
- if (centerX == Float.MIN_VALUE || centerY == Float.MIN_VALUE) {
- val center = PointF()
- calculateCenter(vertices, center)
- this.centerX = center.x
- this.centerY = center.y
- } else {
- this.centerX = centerX
- this.centerY = centerY
- }
- setupPolygon(vertices, rounding, perVertexRounding)
- }
-
- /**
- * This constructor takes the number of vertices in the resulting polygon. These vertices are
- * positioned on a virtual circle around a given center with each vertex positioned [radius]
- * distance from that center, equally spaced (with equal angles between them). If no radius
- * is supplied, the shape will be created with a default radius of 1, resulting in a shape
- * whose vertices lie on a unit circle, with width/height of 2. That default polygon will
- * probably need to be rescaled using [transform] into the appropriate size for the UI in
- * which it will be drawn.
- *
- * The [rounding] and [perVertexRounding] parameters are optional. If not supplied, the result
- * will be a regular polygon with straight edges and unrounded corners.
- *
- * @param numVertices The number of vertices in this polygon.
- * @param radius The radius of the polygon, in pixels. This radius determines the
- * initial size of the object, but it can be transformed later by setting
- * a matrix on it.
- * @param centerX The X coordinate of the center of the polygon, around which all vertices
- * will be placed. The default center is at (0,0).
- * @param centerY The Y coordinate of the center of the polygon, around which all vertices
- * will be placed. The default center is at (0,0).
- * @param rounding The [CornerRounding] properties of every vertex. If some vertices should
- * have different rounding properties, then use [perVertexRounding] instead. The default
- * rounding value is [CornerRounding.Unrounded], meaning that the polygon will use the vertices
- * themselves in the final shape and not curves rounded around the vertices.
- * @param perVertexRounding The [CornerRounding] properties of every vertex. If this
- * parameter is not null, then it must have [numVertices] elements. If this parameter
- * is null, then the polygon will use the [rounding] parameter for every vertex instead. The
- * default value is null.
- *
- * @throws IllegalArgumentException If [perVertexRounding] is not null, it must have
- * [numVertices] elements.
- * @throws IllegalArgumentException [numVertices] must be at least 3.
- */
- constructor(
- @IntRange(from = 3) numVertices: Int,
- radius: Float = 1f,
- centerX: Float = 0f,
- centerY: Float = 0f,
- rounding: CornerRounding = CornerRounding.Unrounded,
- perVertexRounding: List<CornerRounding>? = null
- ) : this(
- verticesFromNumVerts(numVertices, radius, centerX, centerY),
- rounding = rounding,
- perVertexRounding = perVertexRounding,
- centerX = centerX,
- centerY = centerY)
-
- constructor(source: RoundedPolygon) {
- val newCubics = mutableListOf<Cubic>()
- for (cubic in source.cubicShape.cubics) {
- newCubics.add(Cubic(cubic))
- }
- val tempFeatures = mutableListOf<Feature>()
- for (feature in source.features) {
- if (feature is Edge) {
- tempFeatures.add(Edge(feature))
- } else {
- tempFeatures.add(Corner(feature as Corner))
- }
- }
- features = tempFeatures
- centerX = source.centerX
- centerY = source.centerY
- cubicShape.updateCubics(newCubics)
- }
-
- /**
- * This function takes the vertices (either supplied or calculated, depending on the
- * constructor called), plus [CornerRounding] parameters, and creates the actual
- * [RoundedPolygon] shape, rounding around the vertices (or not) as specified. The result
- * is a list of [Cubic] curves which represent the geometry of the final shape.
- *
- * @param vertices The list of vertices in this polygon. This should be an ordered list
- * (with the outline of the shape going from each vertex to the next in order of this
- * list), otherwise the results will be undefined.
- * @param rounding The [CornerRounding] properties of every vertex. If some vertices should
- * have different rounding properties, then use [perVertexRounding] instead. The default
- * rounding value is [CornerRounding.Unrounded], meaning that the polygon will use the vertices
- * themselves in the final shape and not curves rounded around the vertices.
- * @param perVertexRounding The [CornerRounding] properties of every vertex. If this
- * parameter is not null, then it must have the same size as [vertices]. If this parameter
- * is null, then the polygon will use the [rounding] parameter for every vertex instead. The
- * default value is null.
- */
- private fun setupPolygon(
- vertices: FloatArray,
- rounding: CornerRounding = CornerRounding.Unrounded,
- perVertexRounding: List<CornerRounding>? = null
- ) {
- if (vertices.size < 6) {
- throw IllegalArgumentException("Polygons must have at least 3 vertices")
- }
- if (perVertexRounding != null && perVertexRounding.size != vertices.size / 2) {
- throw IllegalArgumentException("perVertexRounding list should be either null or " +
- "the same size as the number of vertices (2 * vertices.size)")
- }
- val cubics = mutableListOf<Cubic>()
- val corners = mutableListOf<List<Cubic>>()
- val n = vertices.size / 2
- val roundedCorners = mutableListOf<RoundedCorner>()
- for (i in 0 until n) {
- val vtxRounding = perVertexRounding?.get(i) ?: rounding
- val prevIndex = ((i + n - 1) % n) * 2
- val nextIndex = ((i + 1) % n) * 2
- roundedCorners.add(
- RoundedCorner(
- PointF(vertices[prevIndex], vertices[prevIndex + 1]),
- PointF(vertices[i * 2], vertices[i * 2 + 1]),
- PointF(vertices[nextIndex], vertices[nextIndex + 1]),
- vtxRounding
- )
- )
- }
-
- // For each side, check if we have enough space to do the cuts needed, and if not split
- // the available space, first for round cuts, then for smoothing if there is space left.
- // Each element in this list is a pair, that represent how much we can do of the cut for
- // the given side (side i goes from corner i to corner i+1), the elements of the pair are:
- // first is how much we can use of expectedRoundCut, second how much of expectedCut
- val cutAdjusts = (0 until n).map { ix ->
- val expectedRoundCut = roundedCorners[ix].expectedRoundCut +
- roundedCorners[(ix + 1) % n].expectedRoundCut
- val expectedCut = roundedCorners[ix].expectedCut +
- roundedCorners[(ix + 1) % n].expectedCut
- val vtxX = vertices[ix * 2]
- val vtxY = vertices[ix * 2 + 1]
- val nextVtxX = vertices[((ix + 1) % n) * 2]
- val nextVtxY = vertices[((ix + 1) % n) * 2 + 1]
- val sideSize = distance(vtxX - nextVtxX, vtxY - nextVtxY)
-
- // Check expectedRoundCut first, and ensure we fulfill rounding needs first for
- // both corners before using space for smoothing
- if (expectedRoundCut > sideSize) {
- // Not enough room for fully rounding, see how much we can actually do.
- sideSize / expectedRoundCut to 0f
- } else if (expectedCut > sideSize) {
- // We can do full rounding, but not full smoothing.
- 1f to (sideSize - expectedRoundCut) / (expectedCut - expectedRoundCut)
- } else {
- // There is enough room for rounding & smoothing.
- 1f to 1f
- }
- }
- // Create and store list of beziers for each [potentially] rounded corner
- for (i in 0 until n) {
- // allowedCuts[0] is for the side from the previous corner to this one,
- // allowedCuts[1] is for the side from this corner to the next one.
- val allowedCuts = (0..1).map { delta ->
- val (roundCutRatio, cutRatio) = cutAdjusts[(i + n - 1 + delta) % n]
- roundedCorners[i].expectedRoundCut * roundCutRatio +
- (roundedCorners[i].expectedCut - roundedCorners[i].expectedRoundCut) * cutRatio
- }
- corners.add(
- roundedCorners[i].getCubics(
- allowedCut0 = allowedCuts[0],
- allowedCut1 = allowedCuts[1]
- )
- )
- }
- // Finally, store the calculated cubics. This includes all of the rounded corners
- // from above, along with new cubics representing the edges between those corners.
- val tempFeatures = mutableListOf<Feature>()
- for (i in 0 until n) {
- val cornerIndices = mutableListOf<Int>()
- for (cubic in corners[i]) {
- cornerIndices.add(cubics.size)
- cubics.add(cubic)
- }
- // Determine whether corner at this vertex is concave or convex, based on the
- // relationship of the prev->curr/curr->next vectors
- // Note that these indices are for pairs of values (points), they need to be
- // doubled to access the xy values in the vertices float array
- val prevVtxIndex = (i + n - 1) % n
- val nextVtxIndex = (i + 1) % n
- val currVertex = PointF(vertices[i * 2], vertices[i * 2 + 1])
- val prevVertex = PointF(vertices[prevVtxIndex * 2], vertices[prevVtxIndex * 2 + 1])
- val nextVertex = PointF(vertices[nextVtxIndex * 2], vertices[nextVtxIndex * 2 + 1])
- val convex = (currVertex - prevVertex).clockwise(nextVertex - currVertex)
- tempFeatures.add(Corner(cornerIndices, currVertex, roundedCorners[i].center,
- convex))
- tempFeatures.add(Edge(listOf(cubics.size)))
- cubics.add(Cubic.straightLine(corners[i].last().anchor1X, corners[i].last().anchor1Y,
- corners[(i + 1) % n].first().anchor0X, corners[(i + 1) % n].first().anchor0Y))
- }
- features = tempFeatures
- cubicShape.updateCubics(cubics)
- }
-
- /**
- * Transforms (scales, rotates, and translates) the polygon by the given matrix.
- * Note that this operation alters the points in the polygon directly; the original
- * points are not retained, nor is the matrix itself. Thus calling this function
- * twice with the same matrix will composite the effect. For example, a matrix which
- * scales by 2 will scale the polygon by 2. Calling transform twice with that matrix
- * will have the effect os scaling the shape size by 4.
- *
- * Note that [RoundedPolygon] objects created with default radius and center values will
- * probably need to be scaled and repositioned using [transform] to be displayed correctly
- * in the UI. Polygons are created by default on the unit circle around a center
- * of (0, 0), so the resulting geometry has a bounding box width and height of 2x2; It should
- * be resized to fit where it will be displayed appropriately.
- *
- * @param matrix The matrix used to transform the polygon
- */
- fun transform(matrix: Matrix) {
- cubicShape.transform(matrix)
- val point = scratchTransformPoint
- point[0] = centerX
- point[1] = centerY
- matrix.mapPoints(point)
- centerX = point[0]
- centerY = point[1]
- for (feature in features) {
- feature.transform(matrix)
- }
- }
-
- /**
- * Internally, the Polygon is stored as a [CubicShape] object. This function returns a copy
- * of that object.
- */
- fun toCubicShape(): CubicShape {
- return CubicShape(cubicShape)
- }
-
- /**
- * A Polygon is rendered as a [Path]. A copy of the underlying [Path] object can be
- * retrieved for use outside of this class. Note that this function returns a copy of
- * the internal [Path] to maintain immutability, thus there is some overhead in retrieving
- * and using the path with this function.
- */
- fun toPath(): Path {
- return cubicShape.toPath()
- }
-
- internal fun draw(canvas: Canvas, paint: Paint) {
- cubicShape.draw(canvas, paint)
- }
-
- /**
- * Calculates an estimated center position for the polygon, storing it in the [centerX]
- * and [centerY] properties.
- * This function should only be called if the center is not already calculated or provided.
- * The Polygon constructor which takes `numVertices` calculates its own center, since it
- * knows exactly where it is centered, at (0, 0).
- *
- * Note that this center will be transformed whenever the shape itself is transformed.
- * Any transforms that occur before the center is calculated will be taken into account
- * automatically since the center calculation is an average of the current location of
- * all cubic anchor points.
- */
- private fun calculateCenter(vertices: FloatArray, result: PointF) {
- var cumulativeX = 0f
- var cumulativeY = 0f
- var index = 0
- while (index < vertices.size) {
- cumulativeX += vertices[index++]
- cumulativeY += vertices[index++]
- }
- result.x = cumulativeX / vertices.size / 2
- result.y = cumulativeY / vertices.size / 2
- }
-
- /**
- * This class holds information about a corner (rounded or not) or an edge of a given
- * polygon. The features of a Polygon can be used to manipulate the shape with more context
- * of what the shape actually is, rather than simply manipulating the raw curves and lines
- * which describe it.
- */
- internal open inner class Feature(protected val cubicIndices: List<Int>) {
- val cubics: List<Cubic>
- get() = cubicIndices.map { toCubicShape().cubics[it] }
-
- open fun transform(matrix: Matrix) {}
- }
- /**
- * Edges have only a list of the cubic curves which make up the edge. Edges lie between
- * corners and have no vertex or concavity; the curves are simply straight lines (represented
- * by Cubic curves).
- */
- internal inner class Edge(indices: List<Int>) : Feature(indices) {
- constructor(source: Edge) : this(source.cubicIndices)
- }
-
- /**
- * Corners contain the list of cubic curves which describe how the corner is rounded (or
- * not), plus the vertex at the corner (which the cubics may or may not pass through, depending
- * on whether the corner is rounded) and a flag indicating whether the corner is convex.
- * A regular polygon has all convex corners, while a star polygon generally (but not
- * necessarily) has both convex (outer) and concave (inner) corners.
- */
- internal inner class Corner(
- cubicIndices: List<Int>,
- // TODO: parameters here should be immutable
- val vertex: PointF,
- val roundedCenter: PointF,
- val convex: Boolean = true
- ) : Feature(cubicIndices) {
- constructor(source: Corner) : this(
- source.cubicIndices,
- source.vertex,
- source.roundedCenter,
- source.convex
- )
-
- override fun transform(matrix: Matrix) {
- val tempPoints = floatArrayOf(vertex.x, vertex.y, roundedCenter.x, roundedCenter.y)
- matrix.mapPoints(tempPoints)
- vertex.set(tempPoints[0], tempPoints[1])
- roundedCenter.set(tempPoints[2], tempPoints[3])
- }
-
- override fun toString(): String {
- return "Corner: vtx, center, convex = $vertex, $roundedCenter, $convex"
- }
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is RoundedPolygon) return false
-
- if (cubicShape != other.cubicShape) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- return cubicShape.hashCode()
- }
-}
-
-/**
- * Private utility class that holds the information about each corner in a polygon. The shape
- * of the corner can be returned by calling the [getCubics] function, which will return a list
- * of curves representing the corner geometry. The shape of the corner depends on the [rounding]
- * constructor parameter.
- *
- * If rounding is null, there is no rounding; the corner will simply be a single point at [p1].
- * This point will be represented by a [Cubic] of length 0 at that point.
- *
- * If rounding is not null, the corner will be rounded either with a curve approximating a circular
- * arc of the radius specified in [rounding], or with three curves if [rounding] has a nonzero
- * smoothing parameter. These three curves are a circular arc in the middle and two symmetrical
- * flanking curves on either side. The smoothing parameter determines the curvature of the
- * flanking curves.
- *
- * This is a class because we usually need to do the work in 2 steps, and prefer to keep state
- * between: first we determine how much we want to cut to comply with the parameters, then we are
- * given how much we can actually cut (because of space restrictions outside this corner)
- *
- * @param p0 the vertex before the one being rounded
- * @param p1 the vertex of this rounded corner
- * @param p2 the vertex after the one being rounded
- * @param rounding the optional parameters specifying how this corner should be rounded
- */
-private class RoundedCorner(
- val p0: PointF,
- val p1: PointF,
- val p2: PointF,
- val rounding: CornerRounding? = null
-) {
- val d1 = (p0 - p1).getDirection()
- val d2 = (p2 - p1).getDirection()
- val cornerRadius = rounding?.radius ?: 0f
- val smoothing = rounding?.smoothing ?: 0f
-
- // cosine of angle at p1 is dot product of unit vectors to the other two vertices
- val cosAngle = d1.dotProduct(d2)
- // identity: sin^2 + cos^2 = 1
- // sinAngle gives us the intersection
- val sinAngle = sqrt(1 - square(cosAngle))
- // How much we need to cut, as measured on a side, to get the required radius
- // calculating where the rounding circle hits the edge
- // This uses the identity of tan(A/2) = sinA/(1 + cosA), where tan(A/2) = radius/cut
- val expectedRoundCut =
- if (sinAngle > 1e-3) { cornerRadius * (cosAngle + 1) / sinAngle } else { 0f }
- // smoothing changes the actual cut. 0 is same as expectedRoundCut, 1 doubles it
- val expectedCut: Float
- get() = ((1 + smoothing) * expectedRoundCut)
- // the center of the circle approximated by the rounding curve (or the middle of the three
- // curves if smoothing is requested). The center is the same as p0 if there is no rounding.
- lateinit var center: PointF
-
- @JvmOverloads
- fun getCubics(allowedCut0: Float, allowedCut1: Float = allowedCut0):
- List<Cubic> {
- // We use the minimum of both cuts to determine the radius, but if there is more space
- // in one side we can use it for smoothing.
- val allowedCut = min(allowedCut0, allowedCut1)
- // Nothing to do, just use lines, or a point
- if (expectedRoundCut < DistanceEpsilon ||
- allowedCut < DistanceEpsilon ||
- cornerRadius < DistanceEpsilon
- ) {
- center = p1
- return listOf(Cubic.straightLine(p1.x, p1.y, p1.x, p1.y))
- }
- // How much of the cut is required for the rounding part.
- val actualRoundCut = min(allowedCut, expectedRoundCut)
- // We have two smoothing values, one for each side of the vertex
- // Space is used for rounding values first. If there is space left over, then we
- // apply smoothing, if it was requested
- val actualSmoothing0 = calculateActualSmoothingValue(allowedCut0)
- val actualSmoothing1 = calculateActualSmoothingValue(allowedCut1)
- // Scale the radius if needed
- val actualR = cornerRadius * actualRoundCut / expectedRoundCut
- // Distance from the corner (p1) to the center
- val centerDistance = sqrt(square(actualR) + square(actualRoundCut))
- // Center of the arc we will use for rounding
- center = p1 + ((d1 + d2) / 2f).getDirection() * centerDistance
- val circleIntersection0 = p1 + d1 * actualRoundCut
- val circleIntersection2 = p1 + d2 * actualRoundCut
- val flanking0 = computeFlankingCurve(
- actualRoundCut, actualSmoothing0, p1, p0,
- circleIntersection0, circleIntersection2, center, actualR
- )
- val flanking2 = computeFlankingCurve(
- actualRoundCut, actualSmoothing1, p1, p2,
- circleIntersection2, circleIntersection0, center, actualR
- ).reverse()
- return listOf(
- flanking0,
- Cubic.circularArc(center.x, center.y, flanking0.anchor1X, flanking0.anchor1Y,
- flanking2.anchor0X, flanking2.anchor0Y),
- flanking2
- )
- }
-
- /**
- * If allowedCut (the amount we are able to cut) is greater than the expected cut
- * (without smoothing applied yet), then there is room to apply smoothing and we
- * calculate the actual smoothing value here.
- */
- private fun calculateActualSmoothingValue(allowedCut: Float): Float {
- return if (allowedCut > expectedCut) {
- smoothing
- } else if (allowedCut > expectedRoundCut) {
- smoothing * (allowedCut - expectedRoundCut) / (expectedCut - expectedRoundCut)
- } else {
- 0f
- }
- }
-
- /**
- * Compute a Bezier to connect the linear segment defined by corner and sideStart
- * with the circular segment defined by circleCenter, circleSegmentIntersection,
- * otherCircleSegmentIntersection and actualR.
- * The bezier will start at the linear segment and end on the circular segment.
- *
- * @param actualRoundCut How much we are cutting of the corner to add the circular segment
- * (this is before smoothing, that will cut some more).
- * @param actualSmoothingValues How much we want to smooth (this is the smooth parameter,
- * adjusted down if there is not enough room).
- * @param corner The point at which the linear side ends
- * @param sideStart The point at which the linear side starts
- * @param circleSegmentIntersection The point at which the linear side and the circle intersect.
- * @param otherCircleSegmentIntersection The point at which the opposing linear side and the
- * circle intersect.
- * @param circleCenter The center of the circle.
- * @param actualR The radius of the circle.
- *
- * @return a Bezier cubic curve that connects from the (cut) linear side and the (cut) circular
- * segment in a smooth way.
- */
- private fun computeFlankingCurve(
- actualRoundCut: Float,
- actualSmoothingValues: Float,
- corner: PointF,
- sideStart: PointF,
- circleSegmentIntersection: PointF,
- otherCircleSegmentIntersection: PointF,
- circleCenter: PointF,
- actualR: Float
- ): Cubic {
- // sideStart is the anchor, 'anchor' is actual control point
- val sideDirection = (sideStart - corner).getDirection()
- val curveStart = corner + sideDirection * actualRoundCut * (1 + actualSmoothingValues)
- // We use an approximation to cut a part of the circle section proportional to 1 - smooth,
- // When smooth = 0, we take the full section, when smooth = 1, we take nothing.
- // TODO: revisit this, it can be problematic as it approaches 19- degrees
- val px = interpolate(circleSegmentIntersection.x,
- (circleSegmentIntersection.x + otherCircleSegmentIntersection.x) / 2f,
- actualSmoothingValues)
- val py = interpolate(circleSegmentIntersection.y,
- (circleSegmentIntersection.y + otherCircleSegmentIntersection.y) / 2f,
- actualSmoothingValues)
- // The flanking curve ends on the circle
- val curveEnd = circleCenter +
- directionVector(px - circleCenter.x, py - circleCenter.y) * actualR
- // The anchor on the circle segment side is in the intersection between the tangent to the
- // circle in the circle/flanking curve boundary and the linear segment.
- val circleTangent = (curveEnd - circleCenter).rotate90()
- val anchorEnd = lineIntersection(sideStart, sideDirection, curveEnd, circleTangent)
- ?: circleSegmentIntersection
- // From what remains, we pick a point for the start anchor.
- // 2/3 seems to come from design tools?
- val anchorStart = (curveStart + anchorEnd * 2f) / 3f
- return Cubic(curveStart, anchorStart, anchorEnd, curveEnd)
- }
-
- /**
- * Returns the intersection point of the two lines d0->d1 and p0->p1, or null if the
- * lines do not intersect
- */
- private fun lineIntersection(p0: PointF, d0: PointF, p1: PointF, d1: PointF): PointF? {
- val rotatedD1 = d1.rotate90()
- val den = d0.dotProduct(rotatedD1)
- if (abs(den) < AngleEpsilon) return null
- val k = (p1 - p0).dotProduct(rotatedD1) / den
- return p0 + d0 * k
- }
-}
-
-/**
- * Extension function which draws the given [RoundedPolygon] object into this [Canvas]. Rendering
- * occurs by drawing the underlying path for the object; callers can optionally retrieve the
- * path and draw it directly via [RoundedPolygon.toPath] (though that function copies the underlying
- * path. This extension function avoids that overhead when rendering).
- *
- * @param polygon The object to be drawn
- * @param paint The attributes
- */
-fun Canvas.drawPolygon(polygon: RoundedPolygon, paint: Paint) {
- polygon.draw(this, paint)
-}
-
-private val scratchTransformPoint = floatArrayOf(0f, 0f)
-
-private val LOG_TAG = "Polygon"
diff --git a/graphics/integration-tests/testapp-compose/src/main/AndroidManifest.xml b/graphics/integration-tests/testapp-compose/src/main/AndroidManifest.xml
index 91ec203..4652d53 100644
--- a/graphics/integration-tests/testapp-compose/src/main/AndroidManifest.xml
+++ b/graphics/integration-tests/testapp-compose/src/main/AndroidManifest.xml
@@ -18,7 +18,7 @@
<application>
<activity android:name=".MainActivity"
- android:label="Graphics Shapes Test"
+ android:label="Graphics Shapes Test - Compose"
android:exported="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<intent-filter>
diff --git a/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/Compose.kt b/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/Compose.kt
new file mode 100644
index 0000000..8d75ae8
--- /dev/null
+++ b/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/Compose.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.graphics.shapes.testcompose
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.graphics.Path
+import androidx.graphics.shapes.Cubic
+import androidx.graphics.shapes.MutableCubic
+import androidx.graphics.shapes.RoundedPolygon
+
+/**
+ * Utility functions providing more idiomatic ways of transforming RoundedPolygons and
+ * transforming shapes into a compose Path, for drawing them.
+ *
+ * This should in the future move into the compose library, maybe with additional API that makes
+ * it easier to create, draw, and animate from Compose apps.
+ *
+ * This code is just here for now prior to integration into compose
+ */
+
+/**
+ * Scales a shape (given as a Sequence) in place.
+ * As this works in Sequences, it doesn't create the whole list at any point, only one
+ * MutableCubic is (re)used.
+ */
+fun Sequence<MutableCubic>.scaled(scale: Float) = map {
+ it.transform {
+ x *= scale
+ y *= scale
+ }
+ it
+}
+
+/**
+ * Scales a shape (given as a List), creating a new List.
+ */
+fun List<Cubic>.scaled(scale: Float) = map {
+ it.transformed {
+ x *= scale
+ y *= scale
+ }
+}
+
+/**
+ * Transforms a [RoundedPolygon] with the given [Matrix]
+ */
+fun RoundedPolygon.transformed(matrix: Matrix): RoundedPolygon =
+ transformed {
+ val transformedPoint = matrix.map(Offset(x, y))
+ x = transformedPoint.x
+ y = transformedPoint.y
+ }
+
+/**
+ * Calculates and returns the bounds of this [RoundedPolygon] as a [Rect]
+ */
+fun RoundedPolygon.getBounds() = calculateBounds().let { Rect(it[0], it[1], it[2], it[3]) }
+
+/**
+ * Function used to create a Path from some Cubics.
+ * Note that this takes an Iterator, so it could be used on Lists, Sequences, etc.
+ */
+fun Iterator<Cubic>.toPath(path: Path = Path()): Path {
+ path.reset()
+ var first = true
+ while (hasNext()) {
+ var bezier = next()
+ if (first) {
+ path.moveTo(bezier.anchor0X, bezier.anchor0Y)
+ first = false
+ }
+ path.cubicTo(
+ bezier.control0X, bezier.control0Y,
+ bezier.control1X, bezier.control1Y,
+ bezier.anchor1X, bezier.anchor1Y
+ )
+ }
+ path.close()
+ return path
+}
+
+/**
+ * Transforms the Sequence into a [Path].
+ */
+fun Sequence<Cubic>.toPath(path: Path = Path()) = iterator().toPath(path)
+
+internal const val DEBUG = false
+
+internal inline fun debugLog(message: String) {
+ if (DEBUG) {
+ println(message)
+ }
+}
diff --git a/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/DebugDraw.kt b/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/DebugDraw.kt
index 62c3fdc..e7e55d7 100644
--- a/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/DebugDraw.kt
+++ b/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/DebugDraw.kt
@@ -16,79 +16,33 @@
package androidx.graphics.shapes.testcompose
-import android.graphics.Path
-import android.graphics.PointF
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.asComposePath
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.graphics.shapes.Cubic
-import androidx.graphics.shapes.CubicShape
-import androidx.graphics.shapes.Morph
-internal fun DrawScope.debugDraw(morph: Morph, progress: Float) =
- debugDraw(morph.asCubics(progress), morph.asPath(progress))
+internal fun DrawScope.debugDraw(shape: Sequence<Cubic>) {
+ drawPath(shape.toPath(), Color.Green, style = Stroke(2f))
-internal fun DrawScope.debugDraw(cubicShape: CubicShape) =
- debugDraw(cubicShape.cubics, cubicShape.toPath())
-
-internal fun DrawScope.debugDraw(cubics: List<Cubic>, path: Path) {
- drawPath(path.asComposePath(), Color.Green, style = Stroke(2f))
-
- for (bezier in cubics) {
+ for (bezier in shape) {
// Draw red circles for start and end.
- drawCircle(bezier.anchor0X, bezier.anchor0Y, 6f, Color.Red, strokeWidth = 2f)
- drawCircle(bezier.anchor1X, bezier.anchor1Y, 8f, Color.Magenta, strokeWidth = 2f)
+ drawCircle(Color.Red, radius = 6f, center = bezier.anchor0(), style = Stroke(2f))
+ drawCircle(Color.Magenta, radius = 8f, center = bezier.anchor1(), style = Stroke(2f))
+
// Draw a circle for the first control point, and a line from start to it.
// The curve will start in this direction
+ drawLine(Color.Yellow, bezier.anchor0(), bezier.control0(), strokeWidth = 0f)
+ drawCircle(Color.Yellow, radius = 4f, center = bezier.control0(), style = Stroke(2f))
- drawLine(bezier.anchor0X, bezier.anchor0Y, bezier.control0X, bezier.control0Y, Color.Yellow,
- strokeWidth = 0f)
- drawCircle(bezier.control0X, bezier.control0Y, 4f, Color.Yellow, strokeWidth = 2f)
// Draw a circle for the second control point, and a line from it to the end.
// The curve will end in this direction
- drawLine(bezier.control1X, bezier.control1Y, bezier.anchor1X, bezier.anchor1Y, Color.Yellow,
- strokeWidth = 0f)
- drawCircle(bezier.control1X, bezier.control1Y, 4f, Color.Yellow, strokeWidth = 2f)
+ drawLine(Color.Yellow, bezier.control1(), bezier.anchor1(), strokeWidth = 0f)
+ drawCircle(Color.Yellow, radius = 4f, center = bezier.control1(), style = Stroke(2f))
}
}
-/**
- * Utility extension functions to bridge OffsetF as points to Compose's Offsets.
- */
-private fun PointF.asOffset() = Offset(x, y)
-
-private fun DrawScope.drawCircle(
- center: PointF,
- radius: Float,
- color: Color,
- strokeWidth: Float = 2f
-) {
- drawCircle(color, radius, center.asOffset(), style = Stroke(strokeWidth))
-}
-
-private fun DrawScope.drawCircle(
- centerX: Float,
- centerY: Float,
- radius: Float,
- color: Color,
- strokeWidth: Float = 2f
-) {
- drawCircle(color, radius, Offset(centerX, centerY), style = Stroke(strokeWidth))
-}
-
-private fun DrawScope.drawLine(start: PointF, end: PointF, color: Color, strokeWidth: Float = 2f) {
- drawLine(color, start.asOffset(), end.asOffset(), strokeWidth = strokeWidth)
-}
-
-private fun DrawScope.drawLine(
- startX: Float,
- startY: Float,
- endX: Float,
- endY: Float,
- color: Color,
- strokeWidth: Float = 2f
-) {
- drawLine(color, Offset(startX, startY), Offset(endX, endY), strokeWidth = strokeWidth)
-}
+private fun Cubic.anchor0() = Offset(anchor0X, anchor0Y)
+private fun Cubic.control0() = Offset(control0X, control0Y)
+private fun Cubic.control1() = Offset(control1X, control1Y)
+private fun Cubic.anchor1() = Offset(anchor1X, anchor1Y)
diff --git a/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/MainActivity.kt b/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/MainActivity.kt
index f174290..a88e2c4 100644
--- a/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/MainActivity.kt
+++ b/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/MainActivity.kt
@@ -16,9 +16,6 @@
package androidx.graphics.shapes.testcompose
-import android.graphics.Matrix
-import android.graphics.PointF
-import android.graphics.RectF
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.animation.core.Animatable
@@ -52,12 +49,11 @@
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.asComposePath
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
+import androidx.graphics.shapes.Cubic
import androidx.graphics.shapes.Morph
import androidx.graphics.shapes.RoundedPolygon
-import kotlin.math.abs
import kotlin.math.min
import kotlinx.coroutines.launch
@@ -67,56 +63,15 @@
@Composable
private fun MorphComposable(
- sizedMorph: SizedMorph,
+ morph: Morph,
progress: Float,
modifier: Modifier = Modifier,
isDebug: Boolean = false
-) = MorphComposableImpl(sizedMorph, modifier, isDebug, progress)
-
-internal fun calculateMatrix(bounds: RectF, width: Float, height: Float): Matrix {
- val originalWidth = bounds.right - bounds.left
- val originalHeight = bounds.bottom - bounds.top
- val scale = min(width / originalWidth, height / originalHeight)
- val newLeft = bounds.left - (width / scale - originalWidth) / 2
- val newTop = bounds.top - (height / scale - originalHeight) / 2
- val matrix = Matrix()
- matrix.setTranslate(-newLeft, -newTop)
- matrix.postScale(scale, scale)
- return matrix
-}
-
-internal fun PointF.transform(
- matrix: Matrix,
- dst: PointF = PointF(),
- floatArray: FloatArray = FloatArray(2)
-): PointF {
- floatArray[0] = x
- floatArray[1] = y
- matrix.mapPoints(floatArray)
- dst.x = floatArray[0]
- dst.y = floatArray[1]
- return dst
-}
-
-private val TheBounds = RectF(0f, 0f, 1f, 1f)
-
-private class SizedMorph(val morph: Morph) {
- var width = 1f
- var height = 1f
-
- fun resizeMaybe(newWidth: Float, newHeight: Float) {
- if (abs(width - newWidth) > 1e-4 || abs(height - newHeight) > 1e-4) {
- val matrix = calculateMatrix(RectF(0f, 0f, width, height), newWidth, newHeight)
- morph.transform(matrix)
- width = newWidth
- height = newHeight
- }
- }
-}
+) = MorphComposableImpl(morph, modifier, isDebug, progress)
@Composable
private fun MorphComposableImpl(
- sizedMorph: SizedMorph,
+ morph: Morph,
modifier: Modifier = Modifier,
isDebug: Boolean = false,
progress: Float
@@ -126,37 +81,42 @@
.fillMaxSize()
.drawWithContent {
drawContent()
- sizedMorph.resizeMaybe(size.width, size.height)
+ val scale = min(size.width, size.height)
+ val shape = morph
+ .asMutableCubics(progress)
+ .scaled(scale)
+
if (isDebug) {
- debugDraw(sizedMorph.morph, progress = progress)
+ debugDraw(shape)
} else {
- drawPath(sizedMorph.morph.asPath(progress).asComposePath(), Color.White)
+ drawPath(shape.toPath(), Color.White)
}
})
}
@Composable
internal fun PolygonComposableImpl(
- shape: RoundedPolygon,
+ polygon: RoundedPolygon,
modifier: Modifier = Modifier,
debug: Boolean = false
) {
- val sizedPolygonCache = remember(shape) {
- mutableMapOf<Size, RoundedPolygon>()
- }
+ val sizedShapes = remember(polygon) { mutableMapOf<Size, Sequence<Cubic>>() }
Box(
modifier
.fillMaxSize()
.drawWithContent {
+ // TODO: Can we use drawWithCache to simplify this?
drawContent()
- val sizedPolygon = sizedPolygonCache.getOrPut(size) {
- val matrix = calculateMatrix(TheBounds, size.width, size.height)
- RoundedPolygon(shape).apply { transform(matrix) }
+ val scale = min(size.width, size.height)
+ val shape = sizedShapes.getOrPut(size) {
+ polygon.cubics
+ .scaled(scale)
+ .asSequence()
}
if (debug) {
- debugDraw(sizedPolygon.toCubicShape())
+ debugDraw(shape)
} else {
- drawPath(sizedPolygon.toPath().asComposePath(), Color.White)
+ drawPath(shape.toPath(), Color.White)
}
})
}
@@ -288,13 +248,9 @@
) {
val shapes = remember {
shapeParams.map { sp ->
- sp.genShape().also { poly ->
- val matrix = calculateMatrix(poly.bounds, 1f, 1f)
- poly.transform(matrix)
- }
+ sp.genShape().let { poly -> poly.normalized() }
}
}
-
var currShape by remember { mutableStateOf(selectedShape.value) }
val progress = remember { Animatable(0f) }
@@ -304,11 +260,9 @@
derivedStateOf {
// NOTE: We need to access this variable to ensure we recalculate the morph !
debugLog("Re-computing morph / $debug")
- SizedMorph(
- Morph(
- shapes[currShape],
- shapes[selectedShape.value]
- )
+ Morph(
+ shapes[currShape],
+ shapes[selectedShape.value]
)
}
}
diff --git a/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/ShapeEditor.kt b/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/ShapeEditor.kt
index abe70b1c..d86c557 100644
--- a/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/ShapeEditor.kt
+++ b/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/ShapeEditor.kt
@@ -16,9 +16,6 @@
package androidx.graphics.shapes.testcompose
-import android.graphics.Matrix
-import android.graphics.PointF
-import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
@@ -43,26 +40,20 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.unit.dp
-import androidx.core.graphics.plus
import androidx.graphics.shapes.CornerRounding
import androidx.graphics.shapes.RoundedPolygon
import androidx.graphics.shapes.circle
import androidx.graphics.shapes.rectangle
import androidx.graphics.shapes.star
-import kotlin.math.cos
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
-import kotlin.math.sin
private val LOG_TAG = "ShapeEditor"
-private val DEBUG = false
-
-internal fun debugLog(message: String) {
- if (DEBUG) Log.d(LOG_TAG, message)
-}
data class ShapeItem(
val name: String,
@@ -74,8 +65,6 @@
val usesInnerParameters: Boolean = true
)
-private val PointZero = PointF(0f, 0f)
-
class ShapeParameters(
sides: Int = 5,
innerRadius: Float = 0.5f,
@@ -114,8 +103,8 @@
private fun radialToCartesian(
radius: Float,
angleRadians: Float,
- center: PointF = PointZero
- ) = directionVectorPointF(angleRadians) * radius + center
+ center: Offset = Offset.Zero
+ ) = directionVector(angleRadians) * radius + center
private fun rotationAsString() =
if (this.rotation.value != 0f)
@@ -180,7 +169,8 @@
RoundedPolygon(
points,
CornerRounding(this.roundness.value, this.smooth.value),
- centerX = PointZero.x, centerY = PointZero.y
+ centerX = 0f,
+ centerY = 0f
)
},
debugDump = {
@@ -227,7 +217,8 @@
CornerRounding(1f),
CornerRounding(1f)
),
- centerX = PointZero.x, centerY = PointZero.y
+ centerX = 0f,
+ centerY = 0f
)
},
debugDump = {
@@ -286,18 +277,22 @@
fun selectedShape() = derivedStateOf { shapes[shapeIx] }
- fun genShape(autoSize: Boolean = true) = selectedShape().value.shapegen().apply {
- transform(Matrix().apply {
+ fun genShape(autoSize: Boolean = true) = selectedShape().value.shapegen().let { poly ->
+ poly.transformed(Matrix().apply {
if (autoSize) {
+ val bounds = poly.getBounds()
// Move the center to the origin.
- postTranslate(-(bounds.left + bounds.right) / 2, -(bounds.top + bounds.bottom) / 2)
+ translate(
+ x = -(bounds.left + bounds.right) / 2,
+ y = -(bounds.top + bounds.bottom) / 2
+ )
// Scale to the [-1, 1] range
- val scale = 2f / max(bounds.width(), bounds.height())
- postScale(scale, scale)
+ val scale = 2f / max(bounds.width, bounds.height)
+ scale(x = scale, y = scale)
}
// Apply the needed rotation
- postRotate(rotation.value)
+ rotateZ(rotation.value)
})
}
}
@@ -349,10 +344,11 @@
.border(1.dp, Color.White)
.padding(2.dp)
) {
- PolygonComposableImpl(params.genShape(autoSize = autoSize).also { poly ->
+ PolygonComposableImpl(params.genShape(autoSize = autoSize).let { poly ->
if (autoSize) {
- val matrix = calculateMatrix(poly.bounds, 1f, 1f)
- poly.transform(matrix)
+ poly.normalized()
+ } else {
+ poly
}
}, debug = debug)
}
@@ -417,12 +413,4 @@
}
}
-// TODO: remove this when it is integrated into Ktx
-operator fun PointF.times(factor: Float): PointF {
- return PointF(this.x * factor, this.y * factor)
-}
-
private fun squarePoints() = floatArrayOf(1f, 1f, -1f, 1f, -1f, -1f, 1f, -1f)
-
-internal fun directionVectorPointF(angleRadians: Float) =
- PointF(cos(angleRadians), sin(angleRadians))
diff --git a/graphics/integration-tests/testapp/src/main/AndroidManifest.xml b/graphics/integration-tests/testapp/src/main/AndroidManifest.xml
index c29b15e..47fbd2d 100644
--- a/graphics/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/graphics/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -18,7 +18,7 @@
<application>
<activity android:name=".ShapeActivity"
- android:label="Graphics Shapes Test"
+ android:label="Graphics Shapes Test - Views"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
diff --git a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/Android.kt b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/Android.kt
new file mode 100644
index 0000000..ff68f78
--- /dev/null
+++ b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/Android.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.graphics.shapes.test
+
+import android.graphics.Canvas
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.Path
+import androidx.core.graphics.scaleMatrix
+import androidx.graphics.shapes.Cubic
+import androidx.graphics.shapes.RoundedPolygon
+
+fun RoundedPolygon.transformed(matrix: Matrix, tmp: FloatArray = FloatArray(2)):
+ RoundedPolygon = transformed {
+ // TODO: Should we have a fast path for when the MutablePoint is array-backed?
+ tmp[0] = x
+ tmp[1] = y
+ matrix.mapPoints(tmp)
+ x = tmp[0]
+ x = tmp[1]
+ }
+
+/**
+ * Function used to create a Path from this CubicShape.
+ * This usually should only be called once and cached, since CubicShape is immutable.
+ */
+fun Iterator<Cubic>.toPath(path: Path = Path()): Path {
+ path.rewind()
+ var first = true
+ for (bezier in this) {
+ if (first) {
+ path.moveTo(bezier.anchor0X, bezier.anchor0Y)
+ first = false
+ }
+ path.cubicTo(
+ bezier.control0X, bezier.control0Y,
+ bezier.control1X, bezier.control1Y,
+ bezier.anchor1X, bezier.anchor1Y
+ )
+ }
+ path.close()
+ return path
+}
+
+fun Canvas.drawPolygon(shape: RoundedPolygon, scale: Int, paint: Paint) =
+ drawPath(shape.cubics.iterator().toPath().apply {
+ transform(scaleMatrix(scale.toFloat(), scale.toFloat()))
+}, paint)
diff --git a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeActivity.kt b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeActivity.kt
index bff8b6b..ed34b57 100644
--- a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeActivity.kt
+++ b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeActivity.kt
@@ -59,13 +59,12 @@
}
private fun setupShapes() {
+ val tmp = FloatArray(2)
// Note: all RoundedPolygon(4) shapes are placeholders for shapes not yet handled
val matrix1 = Matrix().apply { setRotate(-45f) }
val matrix2 = Matrix().apply { setRotate(45f) }
- val blobR1 = MaterialShapes.blobR(.19f, .86f)
- blobR1.transform(matrix1)
- val blobR2 = MaterialShapes.blobR(.19f, .86f)
- blobR2.transform(matrix2)
+ val blobR1 = MaterialShapes.blobR(.19f, .86f).transformed(matrix1, tmp)
+ val blobR2 = MaterialShapes.blobR(.19f, .86f).transformed(matrix2, tmp)
// "Circle" to DefaultShapes.star(4, 1f, 1f),
shapes.add(RoundedPolygon.circle())
diff --git a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeView.kt b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeView.kt
index e93bc68..6039e79 100644
--- a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeView.kt
+++ b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeView.kt
@@ -19,49 +19,25 @@
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
-import android.graphics.Matrix
import android.graphics.Paint
-import android.graphics.RectF
import android.view.View
import androidx.graphics.shapes.RoundedPolygon
-import androidx.graphics.shapes.drawPolygon
import kotlin.math.min
-class ShapeView(context: Context, var shape: RoundedPolygon) : View(context) {
-
+class ShapeView(context: Context, shape: RoundedPolygon) : View(context) {
val paint = Paint()
+ val shape = shape.normalized()
+ var scale = 1
init {
paint.setColor(Color.WHITE)
}
- private fun calculateScale(bounds: RectF): Float {
- val scaleX = width / (bounds.right - bounds.left)
- val scaleY = height / (bounds.bottom - bounds.top)
- val scaleFactor = min(scaleX, scaleY)
- return scaleFactor
- }
-
- private fun calculateMatrix(bounds: RectF): Matrix {
- val scale = calculateScale(bounds)
- val scaledLeft = scale * bounds.left
- val scaledTop = scale * bounds.top
- val scaledWidth = scale * bounds.right - scaledLeft
- val scaledHeight = scale * bounds.bottom - scaledTop
- val newLeft = scaledLeft - (width - scaledWidth) / 2
- val newTop = scaledTop - (height - scaledHeight) / 2
- val matrix = Matrix()
- matrix.preTranslate(-newLeft, -newTop)
- matrix.preScale(scale, scale)
- return matrix
- }
-
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
- val matrix = calculateMatrix(shape.bounds)
- shape.transform(matrix)
+ scale = min(w, h)
}
override fun onDraw(canvas: Canvas) {
- canvas.drawPolygon(shape, paint)
+ canvas.drawPolygon(shape, scale, paint)
}
}
diff --git a/libraryversions.toml b/libraryversions.toml
index 3a94f1b..6cf5da4 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -21,7 +21,7 @@
COLLECTION = "1.4.0-alpha01"
COMPOSE = "1.6.0-alpha04"
COMPOSE_COMPILER = "1.5.2"
-COMPOSE_MATERIAL3 = "1.2.0-alpha06"
+COMPOSE_MATERIAL3 = "1.2.0-alpha07"
COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha01"
COMPOSE_RUNTIME_TRACING = "1.0.0-alpha04"
CONSTRAINTLAYOUT = "2.2.0-alpha13"
@@ -162,7 +162,7 @@
WINDOW_EXTENSIONS_CORE = "1.1.0-alpha01"
WINDOW_SIDECAR = "1.0.0-rc01"
# Do not remove comment
-WORK = "2.9.0-alpha03"
+WORK = "2.9.0-beta01"
[groups]
ACTIVITY = { group = "androidx.activity", atomicGroupVersion = "versions.ACTIVITY" }
diff --git a/lifecycle/lifecycle-livedata-ktx/api/current.ignore b/lifecycle/lifecycle-livedata-ktx/api/current.ignore
index d7e55f2..01ef7e3 100644
--- a/lifecycle/lifecycle-livedata-ktx/api/current.ignore
+++ b/lifecycle/lifecycle-livedata-ktx/api/current.ignore
@@ -1,3 +1,3 @@
// Baseline format: 1.0
-InvalidNullConversion: androidx.lifecycle.LiveDataScope#emit(T, kotlin.coroutines.Continuation<? super kotlin.Unit>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter value in androidx.lifecycle.LiveDataScope.emit(T value, kotlin.coroutines.Continuation<? super kotlin.Unit> arg2)
+RemovedPackage: androidx.lifecycle:
+ Removed package androidx.lifecycle
diff --git a/lifecycle/lifecycle-livedata-ktx/api/current.txt b/lifecycle/lifecycle-livedata-ktx/api/current.txt
index d871976..e6f50d0 100644
--- a/lifecycle/lifecycle-livedata-ktx/api/current.txt
+++ b/lifecycle/lifecycle-livedata-ktx/api/current.txt
@@ -1,25 +1 @@
// Signature format: 4.0
-package androidx.lifecycle {
-
- public final class CoroutineLiveDataKt {
- method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> liveData(optional kotlin.coroutines.CoroutineContext context, java.time.Duration timeout, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
- method public static <T> androidx.lifecycle.LiveData<T> liveData(optional kotlin.coroutines.CoroutineContext context, optional long timeoutInMs, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
- }
-
- public final class FlowLiveDataConversions {
- method public static <T> kotlinx.coroutines.flow.Flow<T> asFlow(androidx.lifecycle.LiveData<T>);
- method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>);
- method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
- method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context, java.time.Duration timeout);
- method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context, optional long timeoutInMs);
- }
-
- public interface LiveDataScope<T> {
- method public suspend Object? emit(T value, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public suspend Object? emitSource(androidx.lifecycle.LiveData<T> source, kotlin.coroutines.Continuation<? super kotlinx.coroutines.DisposableHandle>);
- method public T? getLatestValue();
- property public abstract T? latestValue;
- }
-
-}
-
diff --git a/lifecycle/lifecycle-livedata-ktx/api/restricted_current.ignore b/lifecycle/lifecycle-livedata-ktx/api/restricted_current.ignore
index d7e55f2..01ef7e3 100644
--- a/lifecycle/lifecycle-livedata-ktx/api/restricted_current.ignore
+++ b/lifecycle/lifecycle-livedata-ktx/api/restricted_current.ignore
@@ -1,3 +1,3 @@
// Baseline format: 1.0
-InvalidNullConversion: androidx.lifecycle.LiveDataScope#emit(T, kotlin.coroutines.Continuation<? super kotlin.Unit>) parameter #0:
- Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter value in androidx.lifecycle.LiveDataScope.emit(T value, kotlin.coroutines.Continuation<? super kotlin.Unit> arg2)
+RemovedPackage: androidx.lifecycle:
+ Removed package androidx.lifecycle
diff --git a/lifecycle/lifecycle-livedata-ktx/api/restricted_current.txt b/lifecycle/lifecycle-livedata-ktx/api/restricted_current.txt
index d871976..e6f50d0 100644
--- a/lifecycle/lifecycle-livedata-ktx/api/restricted_current.txt
+++ b/lifecycle/lifecycle-livedata-ktx/api/restricted_current.txt
@@ -1,25 +1 @@
// Signature format: 4.0
-package androidx.lifecycle {
-
- public final class CoroutineLiveDataKt {
- method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> liveData(optional kotlin.coroutines.CoroutineContext context, java.time.Duration timeout, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
- method public static <T> androidx.lifecycle.LiveData<T> liveData(optional kotlin.coroutines.CoroutineContext context, optional long timeoutInMs, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
- }
-
- public final class FlowLiveDataConversions {
- method public static <T> kotlinx.coroutines.flow.Flow<T> asFlow(androidx.lifecycle.LiveData<T>);
- method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>);
- method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
- method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context, java.time.Duration timeout);
- method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context, optional long timeoutInMs);
- }
-
- public interface LiveDataScope<T> {
- method public suspend Object? emit(T value, kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method public suspend Object? emitSource(androidx.lifecycle.LiveData<T> source, kotlin.coroutines.Continuation<? super kotlinx.coroutines.DisposableHandle>);
- method public T? getLatestValue();
- property public abstract T? latestValue;
- }
-
-}
-
diff --git a/lifecycle/lifecycle-livedata/api/current.txt b/lifecycle/lifecycle-livedata/api/current.txt
index da5dac9..06cef73 100644
--- a/lifecycle/lifecycle-livedata/api/current.txt
+++ b/lifecycle/lifecycle-livedata/api/current.txt
@@ -1,6 +1,29 @@
// Signature format: 4.0
package androidx.lifecycle {
+ public final class CoroutineLiveDataKt {
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> liveData(java.time.Duration timeout, optional kotlin.coroutines.CoroutineContext context, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> liveData(java.time.Duration timeout, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method public static <T> androidx.lifecycle.LiveData<T> liveData(optional kotlin.coroutines.CoroutineContext context, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method public static <T> androidx.lifecycle.LiveData<T> liveData(optional kotlin.coroutines.CoroutineContext context, optional long timeoutInMs, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method public static <T> androidx.lifecycle.LiveData<T> liveData(kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ }
+
+ public final class FlowLiveDataConversions {
+ method public static <T> kotlinx.coroutines.flow.Flow<T> asFlow(androidx.lifecycle.LiveData<T>);
+ method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>);
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, java.time.Duration timeout, optional kotlin.coroutines.CoroutineContext context);
+ method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
+ method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context, optional long timeoutInMs);
+ }
+
+ public interface LiveDataScope<T> {
+ method public suspend Object? emit(T value, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? emitSource(androidx.lifecycle.LiveData<T> source, kotlin.coroutines.Continuation<? super kotlinx.coroutines.DisposableHandle>);
+ method public T? getLatestValue();
+ property public abstract T? latestValue;
+ }
+
public class MediatorLiveData<T> extends androidx.lifecycle.MutableLiveData<T> {
ctor public MediatorLiveData();
ctor public MediatorLiveData(T!);
diff --git a/lifecycle/lifecycle-livedata/api/restricted_current.txt b/lifecycle/lifecycle-livedata/api/restricted_current.txt
index ea55d7e..356d2ef 100644
--- a/lifecycle/lifecycle-livedata/api/restricted_current.txt
+++ b/lifecycle/lifecycle-livedata/api/restricted_current.txt
@@ -10,6 +10,29 @@
property public androidx.lifecycle.LiveData<T> liveData;
}
+ public final class CoroutineLiveDataKt {
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> liveData(java.time.Duration timeout, optional kotlin.coroutines.CoroutineContext context, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> liveData(java.time.Duration timeout, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method public static <T> androidx.lifecycle.LiveData<T> liveData(optional kotlin.coroutines.CoroutineContext context, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method public static <T> androidx.lifecycle.LiveData<T> liveData(optional kotlin.coroutines.CoroutineContext context, optional long timeoutInMs, kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method public static <T> androidx.lifecycle.LiveData<T> liveData(kotlin.jvm.functions.Function2<? super androidx.lifecycle.LiveDataScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ }
+
+ public final class FlowLiveDataConversions {
+ method public static <T> kotlinx.coroutines.flow.Flow<T> asFlow(androidx.lifecycle.LiveData<T>);
+ method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>);
+ method @RequiresApi(android.os.Build.VERSION_CODES.O) public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, java.time.Duration timeout, optional kotlin.coroutines.CoroutineContext context);
+ method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context);
+ method public static <T> androidx.lifecycle.LiveData<T> asLiveData(kotlinx.coroutines.flow.Flow<? extends T>, optional kotlin.coroutines.CoroutineContext context, optional long timeoutInMs);
+ }
+
+ public interface LiveDataScope<T> {
+ method public suspend Object? emit(T value, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public suspend Object? emitSource(androidx.lifecycle.LiveData<T> source, kotlin.coroutines.Continuation<? super kotlinx.coroutines.DisposableHandle>);
+ method public T? getLatestValue();
+ property public abstract T? latestValue;
+ }
+
public class MediatorLiveData<T> extends androidx.lifecycle.MutableLiveData<T> {
ctor public MediatorLiveData();
ctor public MediatorLiveData(T!);
diff --git a/lifecycle/lifecycle-livedata/build.gradle b/lifecycle/lifecycle-livedata/build.gradle
index 898e7b1..123b06b 100644
--- a/lifecycle/lifecycle-livedata/build.gradle
+++ b/lifecycle/lifecycle-livedata/build.gradle
@@ -24,9 +24,12 @@
dependencies {
api(libs.kotlinStdlib)
- implementation("androidx.arch.core:core-common:2.2.0")
+ api(libs.kotlinCoroutinesCore)
api("androidx.arch.core:core-runtime:2.2.0")
api(project(":lifecycle:lifecycle-livedata-core"))
+ api(project(":lifecycle:lifecycle-livedata-core-ktx"))
+
+ implementation("androidx.arch.core:core-common:2.2.0")
testImplementation(project(":lifecycle:lifecycle-runtime-testing"))
testImplementation("androidx.arch.core:core-testing:2.2.0")
@@ -34,6 +37,14 @@
testImplementation(libs.junit)
testImplementation(libs.mockitoCore4)
testImplementation(libs.truth)
+
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(libs.kotlinCoroutinesTest)
+ androidTestImplementation(libs.kotlinCoroutinesAndroid)
}
androidx {
diff --git a/lifecycle/lifecycle-livedata-ktx/src/androidTest/java/androidx.lifecycle/FlowAsLiveDataIntegrationTest.kt b/lifecycle/lifecycle-livedata/src/androidTest/java/androidx.lifecycle/FlowAsLiveDataIntegrationTest.kt
similarity index 100%
rename from lifecycle/lifecycle-livedata-ktx/src/androidTest/java/androidx.lifecycle/FlowAsLiveDataIntegrationTest.kt
rename to lifecycle/lifecycle-livedata/src/androidTest/java/androidx.lifecycle/FlowAsLiveDataIntegrationTest.kt
diff --git a/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/CoroutineLiveData.kt b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/CoroutineLiveData.kt
similarity index 99%
rename from lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/CoroutineLiveData.kt
rename to lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/CoroutineLiveData.kt
index 49a3ef3..1cdc303 100644
--- a/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/CoroutineLiveData.kt
+++ b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/CoroutineLiveData.kt
@@ -351,6 +351,7 @@
* ([LiveData.hasActiveObservers]. Defaults to [DEFAULT_TIMEOUT].
* @param block The block to run when the [LiveData] has active observers.
*/
+@JvmOverloads
public fun <T> liveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
@@ -462,9 +463,10 @@
* @param block The block to run when the [LiveData] has active observers.
*/
@RequiresApi(Build.VERSION_CODES.O)
+@JvmOverloads
public fun <T> liveData(
- context: CoroutineContext = EmptyCoroutineContext,
timeout: Duration,
+ context: CoroutineContext = EmptyCoroutineContext,
block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, Api26Impl.toMillis(timeout), block)
diff --git a/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/FlowLiveData.kt b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/FlowLiveData.kt
similarity index 97%
rename from lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/FlowLiveData.kt
rename to lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/FlowLiveData.kt
index 3b05ffd..72d3ef7 100644
--- a/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/FlowLiveData.kt
+++ b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/FlowLiveData.kt
@@ -18,6 +18,7 @@
package androidx.lifecycle
+import android.annotation.SuppressLint
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.arch.core.executor.ArchTaskExecutor
@@ -83,6 +84,7 @@
}.also { liveData ->
val flow = this
if (flow is StateFlow<T>) {
+ @SuppressLint("RestrictedApi")
if (ArchTaskExecutor.getInstance().isMainThread) {
liveData.value = flow.value
} else {
@@ -154,6 +156,6 @@
*/
@RequiresApi(Build.VERSION_CODES.O)
public fun <T> Flow<T>.asLiveData(
- context: CoroutineContext = EmptyCoroutineContext,
- timeout: Duration
+ timeout: Duration,
+ context: CoroutineContext = EmptyCoroutineContext
): LiveData<T> = asLiveData(context, Api26Impl.toMillis(timeout))
diff --git a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/BuildLiveDataTest.kt b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/BuildLiveDataTest.kt
similarity index 99%
rename from lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/BuildLiveDataTest.kt
rename to lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/BuildLiveDataTest.kt
index fbb5f3d..8f25ee8 100644
--- a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/BuildLiveDataTest.kt
+++ b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/BuildLiveDataTest.kt
@@ -19,6 +19,8 @@
package androidx.lifecycle
import androidx.annotation.RequiresApi
+import androidx.lifecycle.util.ScopesRule
+import androidx.lifecycle.util.addObserver
import com.google.common.truth.Truth.assertThat
import java.time.Duration
import java.util.concurrent.atomic.AtomicBoolean
diff --git a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/FlowAsLiveDataTest.kt b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/FlowAsLiveDataTest.kt
similarity index 98%
rename from lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/FlowAsLiveDataTest.kt
rename to lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/FlowAsLiveDataTest.kt
index c1e21b0..c168fdc 100644
--- a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/FlowAsLiveDataTest.kt
+++ b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/FlowAsLiveDataTest.kt
@@ -19,6 +19,8 @@
package androidx.lifecycle
import androidx.annotation.RequiresApi
+import androidx.lifecycle.util.ScopesRule
+import androidx.lifecycle.util.addObserver
import com.google.common.truth.Truth.assertThat
import java.time.Duration
import java.util.concurrent.atomic.AtomicBoolean
diff --git a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/LiveDataAsFlowTest.kt b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/LiveDataAsFlowTest.kt
similarity index 98%
rename from lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/LiveDataAsFlowTest.kt
rename to lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/LiveDataAsFlowTest.kt
index 5bbe0b0..53806ab 100644
--- a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/LiveDataAsFlowTest.kt
+++ b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/LiveDataAsFlowTest.kt
@@ -18,9 +18,9 @@
package androidx.lifecycle
+import androidx.lifecycle.util.ScopesRule
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
diff --git a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/LiveDataFlowJavaTest.java b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/LiveDataFlowJavaTest.java
similarity index 100%
rename from lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/LiveDataFlowJavaTest.java
rename to lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/LiveDataFlowJavaTest.java
diff --git a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/ScopesRule.kt b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/util/ScopesRule.kt
similarity index 95%
rename from lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/ScopesRule.kt
rename to lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/util/ScopesRule.kt
index 63cd8a1..9ad1b95 100644
--- a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/ScopesRule.kt
+++ b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/util/ScopesRule.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,10 +16,12 @@
@file:OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
-package androidx.lifecycle
+package androidx.lifecycle.util
import androidx.arch.core.executor.ArchTaskExecutor
import androidx.arch.core.executor.TaskExecutor
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
import com.google.common.truth.Truth
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
index 2370b740..6be73f6 100644
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
@@ -139,11 +139,22 @@
/**
* Add a new {@link Closeable} object that will be closed directly before
* {@link #onCleared()} is called.
- *
+ * <p>
+ * If onCleared() has already been called, the closeable will not be added,
+ * and will instead be closed immediately.
+ * <p>
* @param closeable The object that should be {@link Closeable#close() closed} directly before
* {@link #onCleared()} is called.
*/
public void addCloseable(@NonNull Closeable closeable) {
+ // Although no logic should be done after user calls onCleared(), we will
+ // ensure that if it has already been called, the closeable attempting to
+ // be added will be closed immediately to ensure there will be no leaks.
+ if (mCleared) {
+ closeWithRuntimeException(closeable);
+ return;
+ }
+
// As this method is final, it will still be called on mock objects even
// though mCloseables won't actually be created...we'll just not do anything
// in that case.
@@ -186,6 +197,7 @@
closeWithRuntimeException(closeable);
}
}
+ mCloseables.clear();
}
onCleared();
}
diff --git a/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelTest.kt b/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelTest.kt
index 6e68344..5e97e09 100644
--- a/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelTest.kt
+++ b/lifecycle/lifecycle-viewmodel/src/test/java/androidx/lifecycle/ViewModelTest.kt
@@ -80,6 +80,16 @@
}
@Test
+ fun testAddCloseableAlreadyClearedVM() {
+ val vm = ViewModel()
+ vm.clear()
+ val impl = CloseableImpl()
+ // This shouldn't crash, even though vm already cleared
+ vm.addCloseable(impl)
+ assertTrue(impl.wasClosed)
+ }
+
+ @Test
fun testConstructorCloseable() {
val impl = CloseableImpl()
val vm = ConstructorArgViewModel(impl)
diff --git a/paging/paging-common/build.gradle b/paging/paging-common/build.gradle
index 3368c0b..ffd08fb 100644
--- a/paging/paging-common/build.gradle
+++ b/paging/paging-common/build.gradle
@@ -18,13 +18,16 @@
import androidx.build.PlatformIdentifier
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import androidx.build.Publish
+import org.jetbrains.kotlin.konan.target.Family
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
}
-def enableNative = KmpPlatformsKt.enableNative(project)
+def macEnabled = KmpPlatformsKt.enableMac(project)
+def linuxEnabled = KmpPlatformsKt.enableLinux(project)
androidXMultiplatform {
jvm()
@@ -36,14 +39,11 @@
defaultPlatform(PlatformIdentifier.JVM)
sourceSets {
-
commonMain {
dependencies {
api(libs.kotlinStdlib)
api(libs.kotlinCoroutinesCore)
api("androidx.annotation:annotation:1.7.0-alpha02")
- implementation(libs.statelyConcurrency)
- implementation(libs.statelyConcurrentCollections)
}
}
@@ -95,19 +95,40 @@
}
}
- if (enableNative) {
+ if (macEnabled || linuxEnabled) {
nativeMain {
dependsOn(commonMain)
+ dependencies {
+ implementation(libs.atomicFu)
+ }
}
nativeTest {
dependsOn(commonTest)
}
}
+ if (macEnabled) {
+ darwinMain {
+ dependsOn(nativeMain)
+ }
+ }
+
+ if (linuxEnabled) {
+ linuxMain {
+ dependsOn(nativeMain)
+ }
+ }
targets.all { target ->
if (target.platformType == KotlinPlatformType.native) {
target.compilations["main"].defaultSourceSet {
- dependsOn(nativeMain)
+ def konanTargetFamily = target.konanTarget.family
+ if (konanTargetFamily == Family.OSX || konanTargetFamily == Family.IOS) {
+ dependsOn(darwinMain)
+ } else if (konanTargetFamily == Family.LINUX) {
+ dependsOn(linuxMain)
+ } else {
+ throw new GradleException("unknown native target ${target}")
+ }
}
target.compilations["test"].defaultSourceSet {
dependsOn(nativeTest)
diff --git a/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/LegacyPageFetcher.jvm.kt b/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/LegacyPageFetcher.jvm.kt
index 9717bdd..cc919f4 100644
--- a/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/LegacyPageFetcher.jvm.kt
+++ b/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/LegacyPageFetcher.jvm.kt
@@ -19,7 +19,7 @@
import androidx.paging.LoadState.Loading
import androidx.paging.LoadState.NotLoading
import androidx.paging.PagingSource.LoadParams
-import co.touchlab.stately.concurrency.AtomicBoolean
+import androidx.paging.internal.AtomicBoolean
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -45,7 +45,7 @@
}
val isDetached
- get() = detached.value
+ get() = detached.get()
private fun scheduleLoad(type: LoadType, params: LoadParams<K>) {
// Listen on the BG thread if the paged source is invalid, since it can be expensive.
@@ -155,7 +155,7 @@
}
fun detach() {
- detached.value = true
+ detached.set(true)
}
internal interface PageConsumer<V : Any> {
diff --git a/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/internal/Atomics.jvm.kt b/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/internal/Atomics.jvm.kt
new file mode 100644
index 0000000..58b53e9
--- /dev/null
+++ b/paging/paging-common/src/commonJvmAndroidMain/kotlin/androidx/paging/internal/Atomics.jvm.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316
+package androidx.paging.internal
+
+internal actual typealias ReentrantLock = java.util.concurrent.locks.ReentrantLock
+
+internal actual typealias AtomicInt = java.util.concurrent.atomic.AtomicInteger
+
+internal actual typealias AtomicBoolean = java.util.concurrent.atomic.AtomicBoolean
+
+internal actual typealias CopyOnWriteArrayList<T> = java.util.concurrent.CopyOnWriteArrayList<T>
diff --git a/paging/paging-common/src/commonJvmAndroidTest/kotlin/androidx/paging/GarbageCollectionTestHelper.kt b/paging/paging-common/src/commonJvmAndroidTest/kotlin/androidx/paging/GarbageCollectionTestHelper.kt
index 940fd38..d13dd42 100644
--- a/paging/paging-common/src/commonJvmAndroidTest/kotlin/androidx/paging/GarbageCollectionTestHelper.kt
+++ b/paging/paging-common/src/commonJvmAndroidTest/kotlin/androidx/paging/GarbageCollectionTestHelper.kt
@@ -17,7 +17,7 @@
package androidx.paging
import androidx.kruth.assertWithMessage
-import co.touchlab.stately.concurrency.AtomicBoolean
+import androidx.paging.internal.AtomicBoolean
import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference
import kotlin.concurrent.thread
@@ -45,7 +45,7 @@
val arraySize = Random.nextInt(1000)
leak.add(ByteArray(arraySize))
System.gc()
- } while (continueTriggeringGc.value)
+ } while (continueTriggeringGc.get())
}
var collectedItemCount = 0
val expectedItemCount = size - expected.sumOf { it.second }
@@ -54,7 +54,7 @@
) {
collectedItemCount++
}
- continueTriggeringGc.value = false
+ continueTriggeringGc.set(false)
val leakedObjects = countLiveObjects()
val leakedObjectToStrings = references.mapNotNull {
it.get()
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/FlowExt.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/FlowExt.kt
index beea20a6..a1e281c 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/FlowExt.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/FlowExt.kt
@@ -22,13 +22,12 @@
import androidx.paging.CombineSource.INITIAL
import androidx.paging.CombineSource.OTHER
import androidx.paging.CombineSource.RECEIVER
-import co.touchlab.stately.concurrency.AtomicInt
+import androidx.paging.internal.AtomicInt
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/HintHandler.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/HintHandler.kt
index 4d35dd4..8f48b22 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/HintHandler.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/HintHandler.kt
@@ -21,8 +21,8 @@
import androidx.annotation.RestrictTo
import androidx.paging.LoadType.APPEND
import androidx.paging.LoadType.PREPEND
-import co.touchlab.stately.concurrency.Lock
-import co.touchlab.stately.concurrency.withLock
+import androidx.paging.internal.ReentrantLock
+import androidx.paging.internal.withLock
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -106,7 +106,7 @@
get() = prepend.flow
val appendFlow
get() = append.flow
- private val lock = Lock()
+ private val lock = ReentrantLock()
/**
* Modifies the state inside a lock where it gets access to the mutable values.
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/InvalidateCallbackTracker.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/InvalidateCallbackTracker.kt
index fea2249..7c83d58 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/InvalidateCallbackTracker.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/InvalidateCallbackTracker.kt
@@ -17,8 +17,8 @@
package androidx.paging
import androidx.annotation.VisibleForTesting
-import co.touchlab.stately.concurrency.Lock
-import co.touchlab.stately.concurrency.withLock
+import androidx.paging.internal.ReentrantLock
+import androidx.paging.internal.withLock
/**
* Helper class for thread-safe invalidation callback tracking + triggering on registration.
@@ -30,7 +30,7 @@
*/
private val invalidGetter: (() -> Boolean)? = null,
) {
- private val lock = Lock()
+ private val lock = ReentrantLock()
private val callbacks = mutableListOf<T>()
internal var invalid = false
private set
@@ -51,12 +51,12 @@
return
}
- var callImmediately = false
- lock.withLock {
+ val callImmediately = lock.withLock {
if (invalid) {
- callImmediately = true
+ true // call immediately
} else {
callbacks.add(callback)
+ false // don't call, not invalid yet.
}
}
@@ -74,16 +74,16 @@
internal fun invalidate(): Boolean {
if (invalid) return false
- var callbacksToInvoke: List<T>? = null
- lock.withLock {
+ val callbacksToInvoke = lock.withLock {
if (invalid) return false
invalid = true
- callbacksToInvoke = callbacks.toList()
- callbacks.clear()
+ callbacks.toList().also {
+ callbacks.clear()
+ }
}
- callbacksToInvoke?.forEach(callbackInvoker)
+ callbacksToInvoke.forEach(callbackInvoker)
return true
}
}
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
index b8cf8c9..21f520e 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
@@ -17,7 +17,8 @@
package androidx.paging
import androidx.annotation.VisibleForTesting
-import co.touchlab.stately.collections.ConcurrentMutableList
+import androidx.paging.internal.ReentrantLock
+import androidx.paging.internal.withLock
/**
* Wrapper class for a [PagingSource] factory intended for usage in [Pager] construction.
@@ -25,24 +26,31 @@
* Calling [invalidate] on this [InvalidatingPagingSourceFactory] will forward invalidate signals
* to all active [PagingSource]s that were produced by calling [invoke].
*
- * This class is backed by a [ConcurrentMutableList], which is thread-safe for concurrent calls to
- * any mutative operations including both [invoke] and [invalidate].
+ * This class is thread-safe for concurrent calls to any mutative operations including both
+ * [invoke] and [invalidate].
*
* @param pagingSourceFactory The [PagingSource] factory that returns a PagingSource when called
*/
public class InvalidatingPagingSourceFactory<Key : Any, Value : Any>(
private val pagingSourceFactory: () -> PagingSource<Key, Value>
) : PagingSourceFactory<Key, Value> {
+ private val lock = ReentrantLock()
+
+ private var pagingSources: List<PagingSource<Key, Value>> = emptyList()
@VisibleForTesting
- internal val pagingSources = ConcurrentMutableList<PagingSource<Key, Value>>()
+ internal fun pagingSources() = pagingSources
/**
* @return [PagingSource] which will be invalidated when this factory's [invalidate] method
* is called
*/
override fun invoke(): PagingSource<Key, Value> {
- return pagingSourceFactory().also { pagingSources.add(it) }
+ return pagingSourceFactory().also {
+ lock.withLock {
+ pagingSources = pagingSources + it
+ }
+ }
}
/**
@@ -50,10 +58,15 @@
* [InvalidatingPagingSourceFactory]
*/
public fun invalidate() {
- pagingSources
- .filterNot { it.invalid }
- .forEach { it.invalidate() }
-
- pagingSources.removeAll { it.invalid }
+ val previousList = lock.withLock {
+ pagingSources.also {
+ pagingSources = emptyList()
+ }
+ }
+ for (pagingSource in previousList) {
+ if (!pagingSource.invalid) {
+ pagingSource.invalidate()
+ }
+ }
}
}
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/MutableCombinedLoadStateCollection.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/MutableCombinedLoadStateCollection.kt
index b622a16..755c80d 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/MutableCombinedLoadStateCollection.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/MutableCombinedLoadStateCollection.kt
@@ -19,7 +19,7 @@
import androidx.paging.LoadState.Error
import androidx.paging.LoadState.Loading
import androidx.paging.LoadState.NotLoading
-import co.touchlab.stately.collections.ConcurrentMutableList
+import androidx.paging.internal.CopyOnWriteArrayList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -35,7 +35,7 @@
*/
internal class MutableCombinedLoadStateCollection {
- private val listeners = ConcurrentMutableList<(CombinedLoadStates) -> Unit>()
+ private val listeners = CopyOnWriteArrayList<(CombinedLoadStates) -> Unit>()
private val _stateFlow = MutableStateFlow<CombinedLoadStates?>(null)
public val stateFlow = _stateFlow.asStateFlow()
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageFetcherSnapshot.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageFetcherSnapshot.kt
index 769adc3..2ca05b6 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageFetcherSnapshot.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageFetcherSnapshot.kt
@@ -31,7 +31,7 @@
import androidx.paging.PagingSource.LoadResult
import androidx.paging.PagingSource.LoadResult.Page
import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
-import co.touchlab.stately.concurrency.AtomicBoolean
+import androidx.paging.internal.AtomicBoolean
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataDiffer.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataDiffer.kt
index 6ce5871..4cb2a1f 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataDiffer.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataDiffer.kt
@@ -26,8 +26,8 @@
import androidx.paging.PageEvent.StaticList
import androidx.paging.PagePresenter.ProcessPageEventCallback
import androidx.paging.internal.BUGANIZER_URL
+import androidx.paging.internal.CopyOnWriteArrayList
import androidx.paging.internal.appendMediatorStatesIfNotNull
-import co.touchlab.stately.collections.ConcurrentMutableList
import kotlin.coroutines.CoroutineContext
import kotlin.jvm.Volatile
import kotlinx.coroutines.Dispatchers
@@ -51,7 +51,7 @@
private val combinedLoadStatesCollection = MutableCombinedLoadStateCollection().apply {
cachedPagingData?.cachedEvent()?.let { set(it.sourceLoadStates, it.mediatorLoadStates) }
}
- private val onPagesUpdatedListeners = ConcurrentMutableList<() -> Unit>()
+ private val onPagesUpdatedListeners = CopyOnWriteArrayList<() -> Unit>()
private val collectFromRunner = SingleRunner()
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/RemoteMediatorAccessor.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/RemoteMediatorAccessor.kt
index f2725a1..60e1cd7 100644
--- a/paging/paging-common/src/commonMain/kotlin/androidx/paging/RemoteMediatorAccessor.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/RemoteMediatorAccessor.kt
@@ -20,8 +20,8 @@
import androidx.paging.AccessorState.BlockState.REQUIRES_REFRESH
import androidx.paging.AccessorState.BlockState.UNBLOCKED
import androidx.paging.RemoteMediator.MediatorResult
-import co.touchlab.stately.concurrency.Lock
-import co.touchlab.stately.concurrency.withLock
+import androidx.paging.internal.ReentrantLock
+import androidx.paging.internal.withLock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -67,7 +67,7 @@
* Simple wrapper around the local state of accessor to ensure we don't concurrently change it.
*/
private class AccessorStateHolder<Key : Any, Value : Any> {
- private val lock = Lock()
+ private val lock = ReentrantLock()
private val _loadStates = MutableStateFlow(LoadStates.IDLE)
val loadStates
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/internal/Atomics.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/internal/Atomics.kt
new file mode 100644
index 0000000..1231856
--- /dev/null
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/internal/Atomics.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.paging.internal
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+
+internal expect class CopyOnWriteArrayList<T>() : Iterable<T> {
+ fun add(value: T): Boolean
+ fun remove(value: T): Boolean
+}
+
+internal expect class ReentrantLock constructor() {
+ fun lock()
+ fun unlock()
+}
+
+internal expect class AtomicInt {
+ constructor(initialValue: Int)
+
+ fun getAndIncrement(): Int
+ fun incrementAndGet(): Int
+ fun decrementAndGet(): Int
+ fun get(): Int
+}
+
+internal expect class AtomicBoolean {
+ constructor(initialValue: Boolean)
+
+ fun get(): Boolean
+ fun set(value: Boolean)
+ fun compareAndSet(expect: Boolean, update: Boolean): Boolean
+}
+
+@OptIn(ExperimentalContracts::class)
+@Suppress("BanInlineOptIn") // b/296638070
+internal inline fun <T> ReentrantLock.withLock(block: () -> T): T {
+ contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
+ try {
+ lock()
+ return block()
+ } finally {
+ unlock()
+ }
+}
diff --git a/paging/paging-common/src/commonTest/kotlin/androidx/paging/CachingTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/CachingTest.kt
index b739e28..e7112cb 100644
--- a/paging/paging-common/src/commonTest/kotlin/androidx/paging/CachingTest.kt
+++ b/paging/paging-common/src/commonTest/kotlin/androidx/paging/CachingTest.kt
@@ -19,7 +19,7 @@
import androidx.paging.ActiveFlowTracker.FlowType
import androidx.paging.ActiveFlowTracker.FlowType.PAGED_DATA_FLOW
import androidx.paging.ActiveFlowTracker.FlowType.PAGE_EVENT_FLOW
-import co.touchlab.stately.concurrency.AtomicInt
+import androidx.paging.internal.AtomicInt
import kotlin.test.Test
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
diff --git a/paging/paging-common/src/commonTest/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt
index 28b9cb4..647d7c8 100644
--- a/paging/paging-common/src/commonTest/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt
+++ b/paging/paging-common/src/commonTest/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt
@@ -19,6 +19,10 @@
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.runBlocking
class InvalidatingPagingSourceFactoryTest {
@@ -26,17 +30,17 @@
fun getPagingSource() {
val testFactory = InvalidatingPagingSourceFactory { TestPagingSource() }
repeat(4) { testFactory() }
- assertEquals(4, testFactory.pagingSources.size)
+ assertEquals(4, testFactory.pagingSources().size)
}
@Test
fun invalidateRemoveFromList() {
val testFactory = InvalidatingPagingSourceFactory { TestPagingSource() }
repeat(4) { testFactory() }
- assertEquals(4, testFactory.pagingSources.size)
+ assertEquals(4, testFactory.pagingSources().size)
testFactory.invalidate()
- assertEquals(0, testFactory.pagingSources.size)
+ assertEquals(0, testFactory.pagingSources().size)
}
@Test
@@ -44,7 +48,7 @@
val invalidateCalls = Array(4) { false }
val testFactory = InvalidatingPagingSourceFactory { TestPagingSource() }
repeat(4) { testFactory() }
- testFactory.pagingSources.forEachIndexed { index, pagingSource ->
+ testFactory.pagingSources().forEachIndexed { index, pagingSource ->
pagingSource.registerInvalidatedCallback {
invalidateCalls[index] = true
}
@@ -58,14 +62,14 @@
val testFactory = InvalidatingPagingSourceFactory { TestPagingSource() }
repeat(4) { testFactory() }
- val pagingSource = testFactory.pagingSources[0]
+ val pagingSource = testFactory.pagingSources()[0]
pagingSource.invalidate()
assertTrue(pagingSource.invalid)
var invalidateCount = 0
- testFactory.pagingSources.forEach {
+ testFactory.pagingSources().forEach {
it.registerInvalidatedCallback {
invalidateCount++
}
@@ -92,17 +96,15 @@
}
@Test
- fun invalidate_threadSafe() {
+ fun invalidate_threadSafe() = runBlocking<Unit> {
val factory = InvalidatingPagingSourceFactory { TestPagingSource() }
-
- // Check for concurrent modification when invalidating paging sources.
- repeat(2) {
- factory().registerInvalidatedCallback {
- factory()
+ (0 until 100).map {
+ async(Dispatchers.Default) {
+ factory().registerInvalidatedCallback {
+ factory().invalidate()
+ }
factory.invalidate()
- factory()
}
- }
- factory.invalidate()
+ }.awaitAll()
}
}
diff --git a/paging/paging-common/src/commonTest/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt
index 7cc69b1..2c9ebf2 100644
--- a/paging/paging-common/src/commonTest/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt
+++ b/paging/paging-common/src/commonTest/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt
@@ -25,7 +25,7 @@
import androidx.paging.RemoteMediator.InitializeAction.SKIP_INITIAL_REFRESH
import androidx.paging.RemoteMediatorMock.LoadEvent
import androidx.paging.TestPagingSource.Companion.LOAD_ERROR
-import co.touchlab.stately.concurrency.AtomicBoolean
+import androidx.paging.internal.AtomicBoolean
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.fail
@@ -500,7 +500,7 @@
return try {
super.load(loadType, state)
} finally {
- loading.value = false
+ loading.set(false)
}
}
}
@@ -583,7 +583,7 @@
return try {
super.load(loadType, state)
} finally {
- loading.value = false
+ loading.set(false)
}
}
}
diff --git a/compose/material/material-icons-extended-filled/build.gradle b/paging/paging-common/src/darwinMain/kotlin/androidx/paging/internal/Atomics.darwin.kt
similarity index 75%
rename from compose/material/material-icons-extended-filled/build.gradle
rename to paging/paging-common/src/darwinMain/kotlin/androidx/paging/internal/Atomics.darwin.kt
index 5933def..2f7f7fc 100644
--- a/compose/material/material-icons-extended-filled/build.gradle
+++ b/paging/paging-common/src/darwinMain/kotlin/androidx/paging/internal/Atomics.darwin.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * Copyright 2022 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.
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-apply from: "../material-icons-extended/generate.gradle"
+package androidx.paging.internal
-android {
- namespace "androidx.compose.material.icons.extended"
-}
+internal actual val PTHREAD_MUTEX_RECURSIVE: Int = platform.posix.PTHREAD_MUTEX_RECURSIVE
diff --git a/compose/material/material-icons-extended-filled/build.gradle b/paging/paging-common/src/linuxMain/kotlin/androidx/paging/internal/Atomics.linux.kt
similarity index 75%
copy from compose/material/material-icons-extended-filled/build.gradle
copy to paging/paging-common/src/linuxMain/kotlin/androidx/paging/internal/Atomics.linux.kt
index 5933def..22b6f1f 100644
--- a/compose/material/material-icons-extended-filled/build.gradle
+++ b/paging/paging-common/src/linuxMain/kotlin/androidx/paging/internal/Atomics.linux.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-apply from: "../material-icons-extended/generate.gradle"
+package androidx.paging.internal
-android {
- namespace "androidx.compose.material.icons.extended"
-}
+internal actual val PTHREAD_MUTEX_RECURSIVE: Int = platform.posix.PTHREAD_MUTEX_RECURSIVE.toInt()
diff --git a/paging/paging-common/src/nativeMain/kotlin/androidx/paging/internal/Atomics.native.kt b/paging/paging-common/src/nativeMain/kotlin/androidx/paging/internal/Atomics.native.kt
new file mode 100644
index 0000000..a550430
--- /dev/null
+++ b/paging/paging-common/src/nativeMain/kotlin/androidx/paging/internal/Atomics.native.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalForeignApi::class)
+
+package androidx.paging.internal
+
+import kotlin.native.internal.createCleaner
+import kotlinx.atomicfu.AtomicBoolean as AtomicFuAtomicBoolean
+import kotlinx.atomicfu.AtomicInt as AtomicFuAtomicInt
+import kotlinx.atomicfu.atomic
+import kotlinx.cinterop.Arena
+import kotlinx.cinterop.ExperimentalForeignApi
+import kotlinx.cinterop.alloc
+import kotlinx.cinterop.ptr
+import platform.posix.pthread_mutex_destroy
+import platform.posix.pthread_mutex_init
+import platform.posix.pthread_mutex_lock
+import platform.posix.pthread_mutex_t
+import platform.posix.pthread_mutex_unlock
+import platform.posix.pthread_mutexattr_destroy
+import platform.posix.pthread_mutexattr_init
+import platform.posix.pthread_mutexattr_settype
+import platform.posix.pthread_mutexattr_t
+
+/**
+ * Wrapper for platform.posix.PTHREAD_MUTEX_RECURSIVE which
+ * is represented as kotlin.Int on darwin platforms and kotlin.UInt on linuxX64
+ * See: // https://youtrack.jetbrains.com/issue/KT-41509
+ */
+internal expect val PTHREAD_MUTEX_RECURSIVE: Int
+
+@Suppress("ACTUAL_WITHOUT_EXPECT") // https://youtrack.jetbrains.com/issue/KT-37316
+internal actual class ReentrantLock actual constructor() {
+
+ private val resources = Resources()
+
+ @Suppress("unused") // The returned Cleaner must be assigned to a property
+ @ExperimentalStdlibApi
+ private val cleaner = createCleaner(resources, Resources::destroy)
+
+ actual fun lock() {
+ pthread_mutex_lock(resources.mutex.ptr)
+ }
+
+ actual fun unlock() {
+ pthread_mutex_unlock(resources.mutex.ptr)
+ }
+
+ private class Resources {
+ private val arena = Arena()
+ private val attr: pthread_mutexattr_t = arena.alloc()
+ val mutex: pthread_mutex_t = arena.alloc()
+
+ init {
+ pthread_mutexattr_init(attr.ptr)
+ pthread_mutexattr_settype(attr.ptr, PTHREAD_MUTEX_RECURSIVE)
+ pthread_mutex_init(mutex.ptr, attr.ptr)
+ }
+
+ fun destroy() {
+ pthread_mutex_destroy(mutex.ptr)
+ pthread_mutexattr_destroy(attr.ptr)
+ arena.clear()
+ }
+ }
+}
+
+internal actual class AtomicInt actual constructor(initialValue: Int) {
+ private var delegate: AtomicFuAtomicInt = atomic(initialValue)
+ private var property by delegate
+
+ actual fun getAndIncrement(): Int {
+ return delegate.getAndIncrement()
+ }
+
+ actual fun decrementAndGet(): Int {
+ return delegate.decrementAndGet()
+ }
+
+ actual fun get(): Int = property
+
+ actual fun incrementAndGet(): Int {
+ return delegate.incrementAndGet()
+ }
+}
+
+internal actual class AtomicBoolean actual constructor(initialValue: Boolean) {
+ private var delegate: AtomicFuAtomicBoolean = atomic(initialValue)
+ private var property by delegate
+
+ actual fun get(): Boolean = property
+
+ actual fun set(value: Boolean) {
+ property = value
+ }
+
+ actual fun compareAndSet(expect: Boolean, update: Boolean): Boolean {
+ return delegate.compareAndSet(expect, update)
+ }
+}
+
+internal actual class CopyOnWriteArrayList<T> : Iterable<T> {
+ private var data: List<T> = emptyList()
+ private val lock = ReentrantLock()
+ override fun iterator(): Iterator<T> {
+ return data.iterator()
+ }
+
+ actual fun add(value: T) = lock.withLock {
+ data = data + value
+ true
+ }
+
+ actual fun remove(value: T): Boolean = lock.withLock {
+ val newList = data.toMutableList()
+ val result = newList.remove(value)
+ data = newList
+ result
+ }
+}
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/PoolingContainerRecyclerViewTest.kt b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/PoolingContainerRecyclerViewTest.kt
index d21ab11..cc6560b 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/PoolingContainerRecyclerViewTest.kt
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/PoolingContainerRecyclerViewTest.kt
@@ -338,10 +338,11 @@
assertThat(adapter1.creations).isEqualTo(10)
// Scroll to put some views into the shared pool
- instrumentation.runOnMainSync {
- rv1.smoothScrollBy(0, 100)
+ repeat(10) {
+ instrumentation.runOnMainSync {
+ rv1.scrollBy(0, 10)
+ }
}
- waitForIdleScroll(rv1)
// The RV keeps a couple items in its view cache before returning them to the pool
val expectedRecycledItems = 10 - itemViewCacheSize
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java
index 94676b4..fff9c40 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/RoutesManager.java
@@ -109,13 +109,13 @@
}
/**
- * Gets the route with the passed id or null if not exists.
+ * Gets the route with the passed id, or null if no route with the given id exists.
*
* @param id of the route to search for.
- * @return the route with the passed id or null if not exists.
+ * @return the route with the passed id, or null if it does not exist.
*/
@Nullable
- public RouteItem getRouteWithId(@NonNull String id) {
+ public RouteItem getRouteWithId(@Nullable String id) {
return mRouteItems.get(id);
}
diff --git a/settings.gradle b/settings.gradle
index 0a3a4d9..7f6500a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -593,6 +593,7 @@
includeProject(":compose:material3:material3", [BuildType.COMPOSE])
includeProject(":compose:material3:benchmark", [BuildType.COMPOSE])
includeProject(":compose:material3:material3-adaptive", [BuildType.COMPOSE])
+includeProject(":compose:material3:material3-adaptive:material3-adaptive-samples", "compose/material3/material3-adaptive/samples", [BuildType.COMPOSE])
includeProject(":compose:material3:material3-lint", [BuildType.COMPOSE])
includeProject(":compose:material3:material3-window-size-class", [BuildType.COMPOSE])
includeProject(":compose:material3:material3-window-size-class:material3-window-size-class-samples", "compose/material3/material3-window-size-class/samples", [BuildType.COMPOSE])
@@ -602,11 +603,6 @@
includeProject(":compose:material:material-icons-core", [BuildType.COMPOSE])
includeProject(":compose:material:material-icons-core:material-icons-core-samples", "compose/material/material-icons-core/samples", [BuildType.COMPOSE])
includeProject(":compose:material:material-icons-extended", [BuildType.COMPOSE])
-includeProject(":compose:material:material-icons-extended-filled", [BuildType.COMPOSE])
-includeProject(":compose:material:material-icons-extended-outlined", [BuildType.COMPOSE])
-includeProject(":compose:material:material-icons-extended-rounded", [BuildType.COMPOSE])
-includeProject(":compose:material:material-icons-extended-sharp", [BuildType.COMPOSE])
-includeProject(":compose:material:material-icons-extended-twotone", [BuildType.COMPOSE])
includeProject(":compose:material:material-ripple", [BuildType.COMPOSE])
includeProject(":compose:material:material-ripple:material-ripple-benchmark", "compose/material/material-ripple/benchmark", [BuildType.COMPOSE])
includeProject(":compose:material:material:icons:generator", [BuildType.COMPOSE])
@@ -767,7 +763,9 @@
includeProject(":glance:glance-appwidget", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget-preview", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget-proto", [BuildType.GLANCE])
+includeProject(":glance:glance-appwidget-testing", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget:glance-appwidget-samples", "glance/glance-appwidget/samples", [BuildType.GLANCE])
+includeProject(":glance:glance-appwidget-testing:glance-appwidget-testing-samples", "glance/glance-appwidget-testing/samples", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget:integration-tests:demos", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget:integration-tests:macrobenchmark", [BuildType.GLANCE])
includeProject(":glance:glance-appwidget:integration-tests:macrobenchmark-target", [BuildType.GLANCE])
diff --git a/wear/watchface/watchface-complications/api/1.2.0-beta01.txt b/wear/watchface/watchface-complications/api/1.2.0-beta01.txt
index ebc68f2..cb7f48e 100644
--- a/wear/watchface/watchface-complications/api/1.2.0-beta01.txt
+++ b/wear/watchface/watchface-complications/api/1.2.0-beta01.txt
@@ -90,7 +90,6 @@
field public static final int DATA_SOURCE_TIME_AND_DATE = 3; // 0x3
field public static final int DATA_SOURCE_UNREAD_NOTIFICATION_COUNT = 7; // 0x7
field public static final int DATA_SOURCE_WATCH_BATTERY = 1; // 0x1
- field @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static final int DATA_SOURCE_WEATHER = 17; // 0x11
field public static final int DATA_SOURCE_WORLD_CLOCK = 5; // 0x5
field public static final int NO_DATA_SOURCE = -1; // 0xffffffff
}
diff --git a/wear/watchface/watchface-complications/api/current.ignore b/wear/watchface/watchface-complications/api/current.ignore
new file mode 100644
index 0000000..069625a
--- /dev/null
+++ b/wear/watchface/watchface-complications/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedField: androidx.wear.watchface.complications.SystemDataSources#DATA_SOURCE_WEATHER:
+ Removed field androidx.wear.watchface.complications.SystemDataSources.DATA_SOURCE_WEATHER
diff --git a/wear/watchface/watchface-complications/api/current.txt b/wear/watchface/watchface-complications/api/current.txt
index ebc68f2..cb7f48e 100644
--- a/wear/watchface/watchface-complications/api/current.txt
+++ b/wear/watchface/watchface-complications/api/current.txt
@@ -90,7 +90,6 @@
field public static final int DATA_SOURCE_TIME_AND_DATE = 3; // 0x3
field public static final int DATA_SOURCE_UNREAD_NOTIFICATION_COUNT = 7; // 0x7
field public static final int DATA_SOURCE_WATCH_BATTERY = 1; // 0x1
- field @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static final int DATA_SOURCE_WEATHER = 17; // 0x11
field public static final int DATA_SOURCE_WORLD_CLOCK = 5; // 0x5
field public static final int NO_DATA_SOURCE = -1; // 0xffffffff
}
diff --git a/wear/watchface/watchface-complications/api/restricted_1.2.0-beta01.txt b/wear/watchface/watchface-complications/api/restricted_1.2.0-beta01.txt
index ebc68f2..cb7f48e 100644
--- a/wear/watchface/watchface-complications/api/restricted_1.2.0-beta01.txt
+++ b/wear/watchface/watchface-complications/api/restricted_1.2.0-beta01.txt
@@ -90,7 +90,6 @@
field public static final int DATA_SOURCE_TIME_AND_DATE = 3; // 0x3
field public static final int DATA_SOURCE_UNREAD_NOTIFICATION_COUNT = 7; // 0x7
field public static final int DATA_SOURCE_WATCH_BATTERY = 1; // 0x1
- field @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static final int DATA_SOURCE_WEATHER = 17; // 0x11
field public static final int DATA_SOURCE_WORLD_CLOCK = 5; // 0x5
field public static final int NO_DATA_SOURCE = -1; // 0xffffffff
}
diff --git a/wear/watchface/watchface-complications/api/restricted_current.ignore b/wear/watchface/watchface-complications/api/restricted_current.ignore
new file mode 100644
index 0000000..069625a
--- /dev/null
+++ b/wear/watchface/watchface-complications/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedField: androidx.wear.watchface.complications.SystemDataSources#DATA_SOURCE_WEATHER:
+ Removed field androidx.wear.watchface.complications.SystemDataSources.DATA_SOURCE_WEATHER
diff --git a/wear/watchface/watchface-complications/api/restricted_current.txt b/wear/watchface/watchface-complications/api/restricted_current.txt
index ebc68f2..cb7f48e 100644
--- a/wear/watchface/watchface-complications/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications/api/restricted_current.txt
@@ -90,7 +90,6 @@
field public static final int DATA_SOURCE_TIME_AND_DATE = 3; // 0x3
field public static final int DATA_SOURCE_UNREAD_NOTIFICATION_COUNT = 7; // 0x7
field public static final int DATA_SOURCE_WATCH_BATTERY = 1; // 0x1
- field @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static final int DATA_SOURCE_WEATHER = 17; // 0x11
field public static final int DATA_SOURCE_WORLD_CLOCK = 5; // 0x5
field public static final int NO_DATA_SOURCE = -1; // 0xffffffff
}
diff --git a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/DefaultComplicationDataSourcePolicy.kt b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/DefaultComplicationDataSourcePolicy.kt
index c142a0d2..1ae59f1 100644
--- a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/DefaultComplicationDataSourcePolicy.kt
+++ b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/DefaultComplicationDataSourcePolicy.kt
@@ -355,11 +355,6 @@
}
val systemDataSourceFallback =
parser.getAttributeIntValue(NAMESPACE_APP, "systemDataSourceFallback", 0)
- require(SystemDataSources.isAllowedOnDevice(systemDataSourceFallback)) {
- "$nodeName at line ${parser.lineNumber} cannot have the supplied " +
- "systemDataSourceFallback value at the current API level."
- }
-
require(parser.hasValue("systemDataSourceFallbackDefaultType")) {
"A $nodeName must have a systemDataSourceFallbackDefaultType attribute"
}
diff --git a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/SystemDataSources.kt b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/SystemDataSources.kt
index d0053b3..b3d620f 100644
--- a/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/SystemDataSources.kt
+++ b/wear/watchface/watchface-complications/src/main/java/androidx/wear/watchface/complications/SystemDataSources.kt
@@ -15,9 +15,7 @@
*/
package androidx.wear.watchface.complications
-import android.os.Build
import androidx.annotation.IntDef
-import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.wear.watchface.complications.data.ComplicationType
@@ -27,7 +25,7 @@
*/
public class SystemDataSources private constructor() {
public companion object {
- // NEXT AVAILABLE DATA SOURCE ID: 18
+ // NEXT AVAILABLE DATA SOURCE ID: 17
/** Specifies that no complication data source should be used. */
public const val NO_DATA_SOURCE: Int = -1
@@ -179,29 +177,6 @@
* This complication data source supports only [ComplicationType.SHORT_TEXT].
*/
public const val DATA_SOURCE_DAY_AND_DATE: Int = 16
-
- /**
- * Id for the 'weather' complication complication data source.
- *
- * This is a safe complication data source, so if a watch face uses this as a default it
- * will be able to receive data from it even before the RECEIVE_COMPLICATION_DATA permission
- * has been granted.
- *
- * This complication data source supports the following types:
- * [ComplicationType.SHORT_TEXT], [ComplicationType.LONG_TEXT],
- * [ComplicationType.SMALL_IMAGE].
- */
- @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
- public const val DATA_SOURCE_WEATHER: Int = 17
-
- /** Checks if the given data source is implemented by the device. */
- internal fun isAllowedOnDevice(@DataSourceId systemDataSourceFallback: Int): Boolean {
- return when {
- systemDataSourceFallback == DATA_SOURCE_WEATHER &&
- Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> false
- else -> true
- }
- }
}
/** System complication data source id as defined in [SystemDataSources]. */
@@ -218,8 +193,7 @@
DATA_SOURCE_SUNRISE_SUNSET,
DATA_SOURCE_DAY_OF_WEEK,
DATA_SOURCE_FAVORITE_CONTACT,
- DATA_SOURCE_DAY_AND_DATE,
- DATA_SOURCE_WEATHER,
+ DATA_SOURCE_DAY_AND_DATE
)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Retention(AnnotationRetention.SOURCE)
diff --git a/wear/watchface/watchface/build.gradle b/wear/watchface/watchface/build.gradle
index 97122f9..ee3f5e0 100644
--- a/wear/watchface/watchface/build.gradle
+++ b/wear/watchface/watchface/build.gradle
@@ -45,7 +45,6 @@
androidTestImplementation(libs.mockitoKotlin)
androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it"s own MockMaker
androidTestImplementation(libs.truth)
- androidTestImplementation(libs.kotlinTest)
testImplementation(project(":wear:watchface:watchface-complications-rendering"))
testImplementation(libs.testExtJunit)
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/XmlDefinedUserStyleSchemaAndComplicationSlotsTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/XmlDefinedUserStyleSchemaAndComplicationSlotsTest.kt
index e47d35f..1973bc0 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/XmlDefinedUserStyleSchemaAndComplicationSlotsTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/XmlDefinedUserStyleSchemaAndComplicationSlotsTest.kt
@@ -59,11 +59,9 @@
import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption
import androidx.wear.watchface.style.data.UserStyleWireFormat
import com.google.common.truth.Truth.assertThat
-import java.lang.IllegalArgumentException
import java.time.ZonedDateTime
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
-import kotlin.test.assertFailsWith
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert
@@ -84,14 +82,13 @@
class TestXmlWatchFaceService(
testContext: Context,
- private var surfaceHolderOverride: SurfaceHolder,
- private var xmlWatchFaceResourceId: Int,
+ private var surfaceHolderOverride: SurfaceHolder
) : WatchFaceService() {
init {
attachBaseContext(testContext)
}
- override fun getXmlWatchFaceResourceId() = xmlWatchFaceResourceId
+ override fun getXmlWatchFaceResourceId() = R.xml.xml_watchface
override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
@@ -159,7 +156,7 @@
@RunWith(AndroidJUnit4::class)
@MediumTest
-class XmlDefinedUserStyleSchemaAndComplicationSlotsTest {
+public class XmlDefinedUserStyleSchemaAndComplicationSlotsTest {
@get:Rule
val mocks = MockitoJUnit.rule()
@@ -174,25 +171,25 @@
private lateinit var interactiveWatchFaceInstance: IInteractiveWatchFace
@Before
- fun setUp() {
+ public fun setUp() {
Assume.assumeTrue("This test suite assumes API 29", Build.VERSION.SDK_INT >= 29)
}
@After
- fun tearDown() {
+ public fun tearDown() {
InteractiveInstanceManager.setParameterlessEngine(null)
if (this::interactiveWatchFaceInstance.isInitialized) {
interactiveWatchFaceInstance.release()
}
}
- private fun setPendingWallpaperInteractiveWatchFaceInstance(instanceId: String) {
+ private fun setPendingWallpaperInteractiveWatchFaceInstance() {
val existingInstance =
InteractiveInstanceManager
.getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance(
WallpaperInteractiveWatchFaceInstanceParams(
- instanceId,
+ INTERACTIVE_INSTANCE_ID,
DeviceConfig(false, false, 0, 0),
WatchUiState(false, 0),
UserStyleWireFormat(emptyMap()),
@@ -221,14 +218,13 @@
assertThat(existingInstance).isNull()
}
- private fun createAndMountTestService(
- xmlWatchFaceResourceId: Int = R.xml.xml_watchface,
- ): WatchFaceService.EngineWrapper {
+ @Test
+ @Suppress("Deprecation", "NewApi") // userStyleSettings
+ public fun staticSchemaAndComplicationsRead() {
val service =
TestXmlWatchFaceService(
- ApplicationProvider.getApplicationContext(),
- surfaceHolder,
- xmlWatchFaceResourceId
+ ApplicationProvider.getApplicationContext<Context>(),
+ surfaceHolder
)
Mockito.`when`(surfaceHolder.surfaceFrame)
@@ -237,21 +233,11 @@
Mockito.`when`(surfaceHolder.surface).thenReturn(surface)
Mockito.`when`(surface.isValid).thenReturn(false)
- setPendingWallpaperInteractiveWatchFaceInstance(
- "${INTERACTIVE_INSTANCE_ID}_$xmlWatchFaceResourceId"
- )
+ setPendingWallpaperInteractiveWatchFaceInstance()
val wrapper = service.onCreateEngine() as WatchFaceService.EngineWrapper
assertThat(initLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue()
- return wrapper
- }
-
- @Test
- @Suppress("Deprecation", "NewApi") // userStyleSettings
- fun staticSchemaAndComplicationsRead() {
- val wrapper = createAndMountTestService()
-
runBlocking {
val watchFaceImpl = wrapper.deferredWatchFaceImpl.await()
val schema = watchFaceImpl.currentUserStyleRepository.schema
@@ -409,42 +395,4 @@
)
}
}
-
- @Test
- fun staticSchemaAndComplicationsRead_invalidXml() {
- // test that when the xml cannot be parsed, the error is propagated and that
- // the deferred values of the engine wrapper do not hang indefinitely
- val wrapper = createAndMountTestService(R.xml.xml_watchface_invalid)
- runBlocking {
- val exception =
- assertFailsWith<IllegalArgumentException> { wrapper.deferredValidation.await() }
- assertThat(exception.message).contains("must have a systemDataSourceFallback attribute")
- assertThat(wrapper.deferredWatchFaceImpl.isCancelled)
- }
- }
-
- @Test
- fun readsComplicationWithWeatherDefaultOnApi34() {
- Assume.assumeTrue("This test runs only on API >= 34", Build.VERSION.SDK_INT >= 34)
- val wrapper = createAndMountTestService(R.xml.xml_watchface_weather)
- runBlocking {
- val watchFaceImpl = wrapper.deferredWatchFaceImpl.await()
- val complicationSlot = watchFaceImpl.complicationSlotsManager.complicationSlots[10]!!
- assertThat(complicationSlot.defaultDataSourcePolicy.systemDataSourceFallback)
- .isEqualTo(SystemDataSources.DATA_SOURCE_WEATHER)
- }
- }
-
- @Test
- fun throwsExceptionOnReadingComplicationWithWeatherDefaultOnApiBelow34() {
- Assume.assumeTrue("This test runs only on API < 34", Build.VERSION.SDK_INT < 34)
- val wrapper = createAndMountTestService(R.xml.xml_watchface_weather)
-
- runBlocking {
- val exception =
- assertFailsWith<IllegalArgumentException> { wrapper.deferredValidation.await() }
- assertThat(exception.message)
- .contains("cannot have the supplied systemDataSourceFallback value")
- }
- }
}
diff --git a/wear/watchface/watchface/src/androidTest/res/xml/xml_watchface_invalid.xml b/wear/watchface/watchface/src/androidTest/res/xml/xml_watchface_invalid.xml
deleted file mode 100644
index 7a1fe31..0000000
--- a/wear/watchface/watchface/src/androidTest/res/xml/xml_watchface_invalid.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- Copyright 2021 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-<XmlWatchFace xmlns:app="http://schemas.android.com/apk/res-auto"
- app:complicationScaleX="10.0"
- app:complicationScaleY="100.0">
- <ComplicationSlot
- app:slotId="@integer/complication_slot_10"
- app:name="@string/complication_name_one"
- app:screenReaderName="@string/complication_screen_reader_name_one"
- app:boundsType="ROUND_RECT"
- app:supportedTypes="RANGED_VALUE|SHORT_TEXT|SMALL_IMAGE">
- <ComplicationSlotBounds app:left="3" app:top="70" app:right="7" app:bottom="90"/>
- </ComplicationSlot>
-</XmlWatchFace>
diff --git a/wear/watchface/watchface/src/androidTest/res/xml/xml_watchface_weather.xml b/wear/watchface/watchface/src/androidTest/res/xml/xml_watchface_weather.xml
deleted file mode 100644
index fdcf220..0000000
--- a/wear/watchface/watchface/src/androidTest/res/xml/xml_watchface_weather.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- Copyright 2021 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-<XmlWatchFace xmlns:app="http://schemas.android.com/apk/res-auto"
- app:complicationScaleX="10.0"
- app:complicationScaleY="100.0">
- <ComplicationSlot
- app:slotId="@integer/complication_slot_10"
- app:name="@string/complication_name_one"
- app:screenReaderName="@string/complication_screen_reader_name_one"
- app:boundsType="ROUND_RECT"
- app:supportedTypes="RANGED_VALUE|SHORT_TEXT|SMALL_IMAGE"
- app:systemDataSourceFallback="DATA_SOURCE_WEATHER"
- app:systemDataSourceFallbackDefaultType="SHORT_TEXT">
- <ComplicationSlotBounds app:left="3" app:top="70" app:right="7" app:bottom="90"/>
- </ComplicationSlot>
-</XmlWatchFace>
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 19fd292..7ff9161 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -521,7 +521,9 @@
internal open fun createComplicationSlotsManagerInternal(
currentUserStyleRepository: CurrentUserStyleRepository,
resourceOnlyWatchFacePackageName: String?
- ): ComplicationSlotsManager = createComplicationSlotsManager(currentUserStyleRepository)
+ ): ComplicationSlotsManager = createComplicationSlotsManager(
+ currentUserStyleRepository
+ )
/**
* Used when inflating [ComplicationSlot]s from XML to provide a
@@ -592,8 +594,10 @@
currentUserStyleRepository: CurrentUserStyleRepository,
complicationSlotsManager: ComplicationSlotsManager,
resourceOnlyWatchFacePackageName: String?
- ): UserStyleFlavors =
- createUserStyleFlavors(currentUserStyleRepository, complicationSlotsManager)
+ ): UserStyleFlavors = createUserStyleFlavors(
+ currentUserStyleRepository,
+ complicationSlotsManager
+ )
/**
* Override this factory method to create your WatchFaceImpl. This method will be called by the
@@ -627,13 +631,12 @@
complicationSlotsManager: ComplicationSlotsManager,
currentUserStyleRepository: CurrentUserStyleRepository,
resourceOnlyWatchFacePackageName: String?
- ): WatchFace =
- createWatchFace(
- surfaceHolder,
- watchState,
- complicationSlotsManager,
- currentUserStyleRepository,
- )
+ ): WatchFace = createWatchFace(
+ surfaceHolder,
+ watchState,
+ complicationSlotsManager,
+ currentUserStyleRepository,
+ )
/** Creates an interactive engine for WallpaperService. */
final override fun onCreateEngine(): Engine =
@@ -1982,9 +1985,8 @@
@WorkerThread
internal fun getComplicationSlotMetadataWireFormats() =
createComplicationSlotsManagerInternal(
- CurrentUserStyleRepository(
- createUserStyleSchemaInternal(resourceOnlyWatchFacePackageName)
- ),
+ CurrentUserStyleRepository(
+ createUserStyleSchemaInternal(resourceOnlyWatchFacePackageName)),
resourceOnlyWatchFacePackageName
)
.complicationSlots
@@ -2186,81 +2188,68 @@
}
backgroundThreadCoroutineScope.launch {
- // deferred objects used to signal to the UI thread that some init steps have
- // finished
+ val timeBefore = System.currentTimeMillis()
+ val currentUserStyleRepository =
+ TraceEvent("WatchFaceService.createUserStyleSchema").use {
+ CurrentUserStyleRepository(
+ createUserStyleSchemaInternal(resourceOnlyWatchFacePackageName)
+ )
+ }
+ initStyle(currentUserStyleRepository)
+
+ val complicationSlotsManager =
+ TraceEvent("WatchFaceService.createComplicationsManager").use {
+ createComplicationSlotsManagerInternal(
+ currentUserStyleRepository,
+ resourceOnlyWatchFacePackageName
+ )
+ }
+ complicationSlotsManager.watchFaceHostApi = this@EngineWrapper
+ complicationSlotsManager.watchState = watchState
+ complicationSlotsManager.listenForStyleChanges(uiThreadCoroutineScope)
+ listenForComplicationChanges(complicationSlotsManager)
+ if (!watchState.isHeadless) {
+ periodicallyWriteComplicationDataCache(
+ _context,
+ watchState.watchFaceInstanceId.value,
+ complicationsFlow
+ )
+ }
+
+ val userStyleFlavors =
+ TraceEvent("WatchFaceService.createUserStyleFlavors").use {
+ createUserStyleFlavorsInternal(
+ currentUserStyleRepository,
+ complicationSlotsManager,
+ resourceOnlyWatchFacePackageName
+ )
+ }
+
+ deferredEarlyInitDetails.complete(
+ EarlyInitDetails(
+ complicationSlotsManager,
+ currentUserStyleRepository,
+ userStyleFlavors
+ )
+ )
+
val deferredWatchFace = CompletableDeferred<WatchFace>()
val initComplicationsDone = CompletableDeferred<Unit>()
- // add here all the deferred values completed in the background thread
- val futuresToCancelOnError =
- listOf(
+ // WatchFaceImpl (which registers broadcast observers) needs to be constructed
+ // on the UIThread. Part of this process can be done in parallel with
+ // createWatchFace.
+ uiThreadCoroutineScope.launch {
+ createWatchFaceImpl(
+ complicationSlotsManager,
+ currentUserStyleRepository,
deferredWatchFace,
initComplicationsDone,
- deferredEarlyInitDetails,
- [email protected],
- deferredWatchFaceImpl,
- deferredValidation,
+ watchState
)
+ }
try {
- val timeBefore = System.currentTimeMillis()
- val currentUserStyleRepository =
- TraceEvent("WatchFaceService.createUserStyleSchema").use {
- CurrentUserStyleRepository(
- createUserStyleSchemaInternal(resourceOnlyWatchFacePackageName)
- )
- }
- initStyle(currentUserStyleRepository)
-
- val complicationSlotsManager =
- TraceEvent("WatchFaceService.createComplicationsManager").use {
- createComplicationSlotsManagerInternal(
- currentUserStyleRepository,
- resourceOnlyWatchFacePackageName
- )
- }
- complicationSlotsManager.watchFaceHostApi = this@EngineWrapper
- complicationSlotsManager.watchState = watchState
- complicationSlotsManager.listenForStyleChanges(uiThreadCoroutineScope)
- listenForComplicationChanges(complicationSlotsManager)
- if (!watchState.isHeadless) {
- periodicallyWriteComplicationDataCache(
- _context,
- watchState.watchFaceInstanceId.value,
- complicationsFlow
- )
- }
-
- val userStyleFlavors =
- TraceEvent("WatchFaceService.createUserStyleFlavors").use {
- createUserStyleFlavorsInternal(
- currentUserStyleRepository,
- complicationSlotsManager,
- resourceOnlyWatchFacePackageName
- )
- }
-
- deferredEarlyInitDetails.complete(
- EarlyInitDetails(
- complicationSlotsManager,
- currentUserStyleRepository,
- userStyleFlavors
- )
- )
-
- // WatchFaceImpl (which registers broadcast observers) needs to be constructed
- // on the UIThread. Part of this process can be done in parallel with
- // createWatchFace.
- uiThreadCoroutineScope.launch {
- createWatchFaceImpl(
- complicationSlotsManager,
- currentUserStyleRepository,
- deferredWatchFace,
- initComplicationsDone,
- watchState
- )
- }
-
val surfaceHolder = overrideSurfaceHolder ?: deferredSurfaceHolder.await()
val watchFace =
@@ -2311,11 +2300,7 @@
throw e
} catch (e: Exception) {
Log.e(TAG, "WatchFace crashed during init", e)
- futuresToCancelOnError.forEach {
- if (!it.isCompleted) {
- it.completeExceptionally(e)
- }
- }
+ deferredValidation.completeExceptionally(e)
}
deferredValidation.complete(Unit)
@@ -2942,10 +2927,12 @@
* WatchFaceRuntimeService is a special kind of [WatchFaceService], which loads the watch face
* definition from another resource only watch face package (see the
* `resourceOnlyWatchFacePackageName` parameter passed to [createUserStyleSchema],
- * [createComplicationSlotsManager], [createUserStyleFlavors] and [createWatchFace]).
+ * [createComplicationSlotsManager], [createUserStyleFlavors] and
+ * [createWatchFace]).
*
* Note because a WatchFaceRuntimeService loads it's resources from another package, it will need
* the following permission:
+ *
* ```
* <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
* tools:ignore="QueryAllPackagesPermission" />
@@ -3006,11 +2993,10 @@
internal override fun createComplicationSlotsManagerInternal(
currentUserStyleRepository: CurrentUserStyleRepository,
resourceOnlyWatchFacePackageName: String?
- ): ComplicationSlotsManager =
- createComplicationSlotsManager(
- currentUserStyleRepository,
- resourceOnlyWatchFacePackageName!!
- )
+ ): ComplicationSlotsManager = createComplicationSlotsManager(
+ currentUserStyleRepository,
+ resourceOnlyWatchFacePackageName!!
+ )
@Suppress("DocumentExceptions") // NB this method isn't expected to be called from user code.
final override fun createComplicationSlotsManager(
@@ -3046,12 +3032,11 @@
currentUserStyleRepository: CurrentUserStyleRepository,
complicationSlotsManager: ComplicationSlotsManager,
resourceOnlyWatchFacePackageName: String?
- ): UserStyleFlavors =
- createUserStyleFlavors(
- currentUserStyleRepository,
- complicationSlotsManager,
- resourceOnlyWatchFacePackageName!!
- )
+ ): UserStyleFlavors = createUserStyleFlavors(
+ currentUserStyleRepository,
+ complicationSlotsManager,
+ resourceOnlyWatchFacePackageName!!
+ )
@Suppress("DocumentExceptions") // NB this method isn't expected to be called from user code.
final override fun createUserStyleFlavors(
@@ -3096,14 +3081,13 @@
complicationSlotsManager: ComplicationSlotsManager,
currentUserStyleRepository: CurrentUserStyleRepository,
resourceOnlyWatchFacePackageName: String?
- ): WatchFace =
- createWatchFace(
- surfaceHolder,
- watchState,
- complicationSlotsManager,
- currentUserStyleRepository,
- resourceOnlyWatchFacePackageName!!
- )
+ ): WatchFace = createWatchFace(
+ surfaceHolder,
+ watchState,
+ complicationSlotsManager,
+ currentUserStyleRepository,
+ resourceOnlyWatchFacePackageName!!
+ )
@Suppress("DocumentExceptions") // NB this method isn't expected to be called from user code.
final override suspend fun createWatchFace(
diff --git a/wear/watchface/watchface/src/main/res/values/attrs.xml b/wear/watchface/watchface/src/main/res/values/attrs.xml
index 02dacba..3e8d8b2 100644
--- a/wear/watchface/watchface/src/main/res/values/attrs.xml
+++ b/wear/watchface/watchface/src/main/res/values/attrs.xml
@@ -80,7 +80,6 @@
<enum name="DATA_SOURCE_DAY_OF_WEEK" value="13" />
<enum name="DATA_SOURCE_FAVORITE_CONTACT" value="14" />
<enum name="DATA_SOURCE_DAY_AND_DATE" value="16" />
- <enum name="DATA_SOURCE_WEATHER" value="17" />
</attr>
<!-- Required. The default [ComplicationType] for the default complication data source.
diff --git a/work/work-gcm/api/2.9.0-beta01.txt b/work/work-gcm/api/2.9.0-beta01.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/work-gcm/api/2.9.0-beta01.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/work-gcm/api/res-2.9.0-beta01.txt b/work/work-gcm/api/res-2.9.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-gcm/api/res-2.9.0-beta01.txt
diff --git a/work/work-gcm/api/restricted_2.9.0-beta01.txt b/work/work-gcm/api/restricted_2.9.0-beta01.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/work-gcm/api/restricted_2.9.0-beta01.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/work-multiprocess/api/2.9.0-beta01.txt b/work/work-multiprocess/api/2.9.0-beta01.txt
new file mode 100644
index 0000000..bd27cfb
--- /dev/null
+++ b/work/work-multiprocess/api/2.9.0-beta01.txt
@@ -0,0 +1,26 @@
+// Signature format: 4.0
+package androidx.work.multiprocess {
+
+ public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
+ ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
+ method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+ method public final void onStopped();
+ method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
+ }
+
+ public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
+ ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
+ method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+ field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+ field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+ }
+
+ public class RemoteWorkerService extends android.app.Service {
+ ctor public RemoteWorkerService();
+ method public android.os.IBinder? onBind(android.content.Intent);
+ }
+
+}
+
diff --git a/work/work-multiprocess/api/res-2.9.0-beta01.txt b/work/work-multiprocess/api/res-2.9.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-multiprocess/api/res-2.9.0-beta01.txt
diff --git a/work/work-multiprocess/api/restricted_2.9.0-beta01.txt b/work/work-multiprocess/api/restricted_2.9.0-beta01.txt
new file mode 100644
index 0000000..bd27cfb
--- /dev/null
+++ b/work/work-multiprocess/api/restricted_2.9.0-beta01.txt
@@ -0,0 +1,26 @@
+// Signature format: 4.0
+package androidx.work.multiprocess {
+
+ public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
+ ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
+ method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+ method public final void onStopped();
+ method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
+ }
+
+ public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
+ ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
+ method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+ field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+ field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+ }
+
+ public class RemoteWorkerService extends android.app.Service {
+ ctor public RemoteWorkerService();
+ method public android.os.IBinder? onBind(android.content.Intent);
+ }
+
+}
+
diff --git a/work/work-runtime-ktx/api/2.9.0-beta01.txt b/work/work-runtime-ktx/api/2.9.0-beta01.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/work-runtime-ktx/api/2.9.0-beta01.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/work-runtime-ktx/api/res-2.9.0-beta01.txt b/work/work-runtime-ktx/api/res-2.9.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-runtime-ktx/api/res-2.9.0-beta01.txt
diff --git a/work/work-runtime-ktx/api/restricted_2.9.0-beta01.txt b/work/work-runtime-ktx/api/restricted_2.9.0-beta01.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/work-runtime-ktx/api/restricted_2.9.0-beta01.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/work-runtime/api/2.9.0-beta01.txt b/work/work-runtime/api/2.9.0-beta01.txt
new file mode 100644
index 0000000..4c1e9d7
--- /dev/null
+++ b/work/work-runtime/api/2.9.0-beta01.txt
@@ -0,0 +1,606 @@
+// Signature format: 4.0
+package androidx.work {
+
+ public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
+ ctor public ArrayCreatingInputMerger();
+ method public androidx.work.Data merge(java.util.List<androidx.work.Data> inputs);
+ }
+
+ public enum BackoffPolicy {
+ method public static androidx.work.BackoffPolicy valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.work.BackoffPolicy[] values();
+ enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
+ enum_constant public static final androidx.work.BackoffPolicy LINEAR;
+ }
+
+ public interface Clock {
+ method public long currentTimeMillis();
+ }
+
+ public final class Configuration {
+ method public androidx.work.Clock getClock();
+ method public int getContentUriTriggerWorkersLimit();
+ method public String? getDefaultProcessName();
+ method public java.util.concurrent.Executor getExecutor();
+ method public androidx.core.util.Consumer<java.lang.Throwable>? getInitializationExceptionHandler();
+ method public androidx.work.InputMergerFactory getInputMergerFactory();
+ method public int getMaxJobSchedulerId();
+ method public int getMinJobSchedulerId();
+ method public androidx.work.RunnableScheduler getRunnableScheduler();
+ method public androidx.core.util.Consumer<java.lang.Throwable>? getSchedulingExceptionHandler();
+ method public java.util.concurrent.Executor getTaskExecutor();
+ method public androidx.work.WorkerFactory getWorkerFactory();
+ property public final androidx.work.Clock clock;
+ property public final int contentUriTriggerWorkersLimit;
+ property public final String? defaultProcessName;
+ property public final java.util.concurrent.Executor executor;
+ property public final androidx.core.util.Consumer<java.lang.Throwable>? initializationExceptionHandler;
+ property public final androidx.work.InputMergerFactory inputMergerFactory;
+ property public final int maxJobSchedulerId;
+ property public final int minJobSchedulerId;
+ property public final androidx.work.RunnableScheduler runnableScheduler;
+ property public final androidx.core.util.Consumer<java.lang.Throwable>? schedulingExceptionHandler;
+ property public final java.util.concurrent.Executor taskExecutor;
+ property public final androidx.work.WorkerFactory workerFactory;
+ field public static final androidx.work.Configuration.Companion Companion;
+ field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
+ }
+
+ public static final class Configuration.Builder {
+ ctor public Configuration.Builder();
+ method public androidx.work.Configuration build();
+ method public androidx.work.Configuration.Builder setClock(androidx.work.Clock clock);
+ method public androidx.work.Configuration.Builder setContentUriTriggerWorkersLimit(int contentUriTriggerWorkersLimit);
+ method public androidx.work.Configuration.Builder setDefaultProcessName(String processName);
+ method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor executor);
+ method public androidx.work.Configuration.Builder setInitializationExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable> exceptionHandler);
+ method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory inputMergerFactory);
+ method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int minJobSchedulerId, int maxJobSchedulerId);
+ method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int maxSchedulerLimit);
+ method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int loggingLevel);
+ method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler runnableScheduler);
+ method public androidx.work.Configuration.Builder setSchedulingExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable> schedulingExceptionHandler);
+ method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor taskExecutor);
+ method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory workerFactory);
+ }
+
+ public static final class Configuration.Companion {
+ }
+
+ public static interface Configuration.Provider {
+ method public androidx.work.Configuration getWorkManagerConfiguration();
+ property public abstract androidx.work.Configuration workManagerConfiguration;
+ }
+
+ public final class Constraints {
+ ctor public Constraints(androidx.work.Constraints other);
+ ctor @androidx.room.Ignore public Constraints(optional androidx.work.NetworkType requiredNetworkType, optional boolean requiresCharging, optional boolean requiresBatteryNotLow, optional boolean requiresStorageNotLow);
+ ctor @RequiresApi(23) @androidx.room.Ignore public Constraints(optional androidx.work.NetworkType requiredNetworkType, optional boolean requiresCharging, optional boolean requiresDeviceIdle, optional boolean requiresBatteryNotLow, optional boolean requiresStorageNotLow);
+ ctor @RequiresApi(24) public Constraints(optional androidx.work.NetworkType requiredNetworkType, optional boolean requiresCharging, optional boolean requiresDeviceIdle, optional boolean requiresBatteryNotLow, optional boolean requiresStorageNotLow, optional long contentTriggerUpdateDelayMillis, optional long contentTriggerMaxDelayMillis, optional java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers);
+ method @RequiresApi(24) public long getContentTriggerMaxDelayMillis();
+ method @RequiresApi(24) public long getContentTriggerUpdateDelayMillis();
+ method @RequiresApi(24) public java.util.Set<androidx.work.Constraints.ContentUriTrigger> getContentUriTriggers();
+ method public androidx.work.NetworkType getRequiredNetworkType();
+ method public boolean requiresBatteryNotLow();
+ method public boolean requiresCharging();
+ method @RequiresApi(23) public boolean requiresDeviceIdle();
+ method public boolean requiresStorageNotLow();
+ property @RequiresApi(24) public final long contentTriggerMaxDelayMillis;
+ property @RequiresApi(24) public final long contentTriggerUpdateDelayMillis;
+ property @RequiresApi(24) public final java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers;
+ property public final androidx.work.NetworkType requiredNetworkType;
+ field public static final androidx.work.Constraints.Companion Companion;
+ field public static final androidx.work.Constraints NONE;
+ }
+
+ public static final class Constraints.Builder {
+ ctor public Constraints.Builder();
+ method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri uri, boolean triggerForDescendants);
+ method public androidx.work.Constraints build();
+ method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType networkType);
+ method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean requiresBatteryNotLow);
+ method public androidx.work.Constraints.Builder setRequiresCharging(boolean requiresCharging);
+ method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean requiresDeviceIdle);
+ method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean requiresStorageNotLow);
+ method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration duration);
+ method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+ method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration duration);
+ method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+ }
+
+ public static final class Constraints.Companion {
+ }
+
+ public static final class Constraints.ContentUriTrigger {
+ ctor public Constraints.ContentUriTrigger(android.net.Uri uri, boolean isTriggeredForDescendants);
+ method public android.net.Uri getUri();
+ method public boolean isTriggeredForDescendants();
+ property public final boolean isTriggeredForDescendants;
+ property public final android.net.Uri uri;
+ }
+
+ public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
+ ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
+ method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+ method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
+ method public suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo>);
+ method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo> getForegroundInfoAsync();
+ method public final void onStopped();
+ method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
+ property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
+ }
+
+ public final class Data {
+ ctor public Data(androidx.work.Data);
+ method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
+ method public boolean getBoolean(String, boolean);
+ method public boolean[]? getBooleanArray(String);
+ method public byte getByte(String, byte);
+ method public byte[]? getByteArray(String);
+ method public double getDouble(String, double);
+ method public double[]? getDoubleArray(String);
+ method public float getFloat(String, float);
+ method public float[]? getFloatArray(String);
+ method public int getInt(String, int);
+ method public int[]? getIntArray(String);
+ method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
+ method public long getLong(String, long);
+ method public long[]? getLongArray(String);
+ method public String? getString(String);
+ method public String![]? getStringArray(String);
+ method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
+ method public byte[] toByteArray();
+ field public static final androidx.work.Data EMPTY;
+ field public static final int MAX_DATA_BYTES = 10240; // 0x2800
+ }
+
+ public static final class Data.Builder {
+ ctor public Data.Builder();
+ method public androidx.work.Data build();
+ method public androidx.work.Data.Builder putAll(androidx.work.Data);
+ method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
+ method public androidx.work.Data.Builder putBoolean(String, boolean);
+ method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
+ method public androidx.work.Data.Builder putByte(String, byte);
+ method public androidx.work.Data.Builder putByteArray(String, byte[]);
+ method public androidx.work.Data.Builder putDouble(String, double);
+ method public androidx.work.Data.Builder putDoubleArray(String, double[]);
+ method public androidx.work.Data.Builder putFloat(String, float);
+ method public androidx.work.Data.Builder putFloatArray(String, float[]);
+ method public androidx.work.Data.Builder putInt(String, int);
+ method public androidx.work.Data.Builder putIntArray(String, int[]);
+ method public androidx.work.Data.Builder putLong(String, long);
+ method public androidx.work.Data.Builder putLongArray(String, long[]);
+ method public androidx.work.Data.Builder putString(String, String?);
+ method public androidx.work.Data.Builder putStringArray(String, String![]);
+ }
+
+ public final class DataKt {
+ method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
+ method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
+ }
+
+ public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
+ ctor public DelegatingWorkerFactory();
+ method public final void addFactory(androidx.work.WorkerFactory);
+ method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+ }
+
+ public enum ExistingPeriodicWorkPolicy {
+ method public static androidx.work.ExistingPeriodicWorkPolicy valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.work.ExistingPeriodicWorkPolicy[] values();
+ enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy CANCEL_AND_REENQUEUE;
+ enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
+ enum_constant @Deprecated public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
+ enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy UPDATE;
+ }
+
+ public enum ExistingWorkPolicy {
+ method public static androidx.work.ExistingWorkPolicy valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.work.ExistingWorkPolicy[] values();
+ enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
+ enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
+ enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
+ enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
+ }
+
+ public final class ForegroundInfo {
+ ctor public ForegroundInfo(int, android.app.Notification);
+ ctor public ForegroundInfo(int, android.app.Notification, int);
+ method public int getForegroundServiceType();
+ method public android.app.Notification getNotification();
+ method public int getNotificationId();
+ }
+
+ public interface ForegroundUpdater {
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
+ }
+
+ public abstract class InputMerger {
+ ctor public InputMerger();
+ method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data> inputs);
+ }
+
+ public abstract class InputMergerFactory {
+ ctor public InputMergerFactory();
+ method public abstract androidx.work.InputMerger? createInputMerger(String className);
+ }
+
+ public abstract class ListenableWorker {
+ ctor public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+ method public final android.content.Context getApplicationContext();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo!> getForegroundInfoAsync();
+ method public final java.util.UUID getId();
+ method public final androidx.work.Data getInputData();
+ method @RequiresApi(28) public final android.net.Network? getNetwork();
+ method @IntRange(from=0) public final int getRunAttemptCount();
+ method @RequiresApi(31) public final int getStopReason();
+ method public final java.util.Set<java.lang.String!> getTags();
+ method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+ method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
+ method public final boolean isStopped();
+ method public void onStopped();
+ method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
+ method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+ }
+
+ public abstract static class ListenableWorker.Result {
+ method public static androidx.work.ListenableWorker.Result failure();
+ method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
+ method public abstract androidx.work.Data getOutputData();
+ method public static androidx.work.ListenableWorker.Result retry();
+ method public static androidx.work.ListenableWorker.Result success();
+ method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
+ }
+
+ public enum NetworkType {
+ method public static androidx.work.NetworkType valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.work.NetworkType[] values();
+ enum_constant public static final androidx.work.NetworkType CONNECTED;
+ enum_constant public static final androidx.work.NetworkType METERED;
+ enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
+ enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+ enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
+ enum_constant public static final androidx.work.NetworkType UNMETERED;
+ }
+
+ public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
+ method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+ method public static java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+ field public static final androidx.work.OneTimeWorkRequest.Companion Companion;
+ }
+
+ public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
+ ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass);
+ method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger> inputMerger);
+ }
+
+ public static final class OneTimeWorkRequest.Companion {
+ method public androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+ method public java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+ }
+
+ public final class OneTimeWorkRequestKt {
+ method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder OneTimeWorkRequestBuilder();
+ method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
+ }
+
+ public interface Operation {
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
+ method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
+ }
+
+ public abstract static class Operation.State {
+ }
+
+ public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
+ ctor public Operation.State.FAILURE(Throwable);
+ method public Throwable getThrowable();
+ }
+
+ public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
+ }
+
+ public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
+ }
+
+ public final class OperationKt {
+ method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS>);
+ }
+
+ public enum OutOfQuotaPolicy {
+ method public static androidx.work.OutOfQuotaPolicy valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.work.OutOfQuotaPolicy[] values();
+ enum_constant public static final androidx.work.OutOfQuotaPolicy DROP_WORK_REQUEST;
+ enum_constant public static final androidx.work.OutOfQuotaPolicy RUN_AS_NON_EXPEDITED_WORK_REQUEST;
+ }
+
+ public final class OverwritingInputMerger extends androidx.work.InputMerger {
+ ctor public OverwritingInputMerger();
+ method public androidx.work.Data merge(java.util.List<androidx.work.Data> inputs);
+ }
+
+ public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
+ field public static final androidx.work.PeriodicWorkRequest.Companion Companion;
+ field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
+ field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
+ }
+
+ public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
+ ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval);
+ ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval, java.time.Duration flexInterval);
+ ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+ ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexInterval, java.util.concurrent.TimeUnit flexIntervalTimeUnit);
+ method public androidx.work.PeriodicWorkRequest.Builder clearNextScheduleTimeOverride();
+ method public androidx.work.PeriodicWorkRequest.Builder setNextScheduleTimeOverride(long nextScheduleTimeOverrideMillis);
+ }
+
+ public static final class PeriodicWorkRequest.Companion {
+ }
+
+ public final class PeriodicWorkRequestKt {
+ method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
+ method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
+ method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+ method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
+ }
+
+ public interface ProgressUpdater {
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
+ }
+
+ public interface RunnableScheduler {
+ method public void cancel(Runnable);
+ method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
+ }
+
+ public abstract class WorkContinuation {
+ ctor public WorkContinuation();
+ method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
+ method public abstract androidx.work.Operation enqueue();
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
+ method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
+ method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
+ method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+ }
+
+ public final class WorkInfo {
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress, optional int runAttemptCount);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress, optional int runAttemptCount, optional int generation);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress, optional int runAttemptCount, optional int generation, optional androidx.work.Constraints constraints);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress, optional int runAttemptCount, optional int generation, optional androidx.work.Constraints constraints, optional long initialDelayMillis);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress, optional int runAttemptCount, optional int generation, optional androidx.work.Constraints constraints, optional long initialDelayMillis, optional androidx.work.WorkInfo.PeriodicityInfo? periodicityInfo);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress, optional int runAttemptCount, optional int generation, optional androidx.work.Constraints constraints, optional long initialDelayMillis, optional androidx.work.WorkInfo.PeriodicityInfo? periodicityInfo, optional long nextScheduleTimeMillis);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress, optional int runAttemptCount, optional int generation, optional androidx.work.Constraints constraints, optional long initialDelayMillis, optional androidx.work.WorkInfo.PeriodicityInfo? periodicityInfo, optional long nextScheduleTimeMillis, optional int stopReason);
+ method public androidx.work.Constraints getConstraints();
+ method public int getGeneration();
+ method public java.util.UUID getId();
+ method public long getInitialDelayMillis();
+ method public long getNextScheduleTimeMillis();
+ method public androidx.work.Data getOutputData();
+ method public androidx.work.WorkInfo.PeriodicityInfo? getPeriodicityInfo();
+ method public androidx.work.Data getProgress();
+ method @IntRange(from=0L) public int getRunAttemptCount();
+ method public androidx.work.WorkInfo.State getState();
+ method @RequiresApi(31) public int getStopReason();
+ method public java.util.Set<java.lang.String> getTags();
+ property public final androidx.work.Constraints constraints;
+ property public final int generation;
+ property public final java.util.UUID id;
+ property public final long initialDelayMillis;
+ property public final long nextScheduleTimeMillis;
+ property public final androidx.work.Data outputData;
+ property public final androidx.work.WorkInfo.PeriodicityInfo? periodicityInfo;
+ property public final androidx.work.Data progress;
+ property @IntRange(from=0L) public final int runAttemptCount;
+ property public final androidx.work.WorkInfo.State state;
+ property @RequiresApi(31) public final int stopReason;
+ property public final java.util.Set<java.lang.String> tags;
+ field public static final androidx.work.WorkInfo.Companion Companion;
+ field public static final int STOP_REASON_APP_STANDBY = 12; // 0xc
+ field public static final int STOP_REASON_BACKGROUND_RESTRICTION = 11; // 0xb
+ field public static final int STOP_REASON_CANCELLED_BY_APP = 1; // 0x1
+ field public static final int STOP_REASON_CONSTRAINT_BATTERY_NOT_LOW = 5; // 0x5
+ field public static final int STOP_REASON_CONSTRAINT_CHARGING = 6; // 0x6
+ field public static final int STOP_REASON_CONSTRAINT_CONNECTIVITY = 7; // 0x7
+ field public static final int STOP_REASON_CONSTRAINT_DEVICE_IDLE = 8; // 0x8
+ field public static final int STOP_REASON_CONSTRAINT_STORAGE_NOT_LOW = 9; // 0x9
+ field public static final int STOP_REASON_DEVICE_STATE = 4; // 0x4
+ field public static final int STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED = 15; // 0xf
+ field public static final int STOP_REASON_NOT_STOPPED = -256; // 0xffffff00
+ field public static final int STOP_REASON_PREEMPT = 2; // 0x2
+ field public static final int STOP_REASON_QUOTA = 10; // 0xa
+ field public static final int STOP_REASON_SYSTEM_PROCESSING = 14; // 0xe
+ field public static final int STOP_REASON_TIMEOUT = 3; // 0x3
+ field public static final int STOP_REASON_UNKNOWN = -512; // 0xfffffe00
+ field public static final int STOP_REASON_USER = 13; // 0xd
+ }
+
+ public static final class WorkInfo.Companion {
+ }
+
+ public static final class WorkInfo.PeriodicityInfo {
+ ctor public WorkInfo.PeriodicityInfo(long repeatIntervalMillis, long flexIntervalMillis);
+ method public long getFlexIntervalMillis();
+ method public long getRepeatIntervalMillis();
+ property public final long flexIntervalMillis;
+ property public final long repeatIntervalMillis;
+ }
+
+ public enum WorkInfo.State {
+ method public final boolean isFinished();
+ method public static androidx.work.WorkInfo.State valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.work.WorkInfo.State[] values();
+ property public final boolean isFinished;
+ enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
+ enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
+ enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
+ enum_constant public static final androidx.work.WorkInfo.State FAILED;
+ enum_constant public static final androidx.work.WorkInfo.State RUNNING;
+ enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
+ }
+
+ public abstract class WorkManager {
+ method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+ method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+ method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+ method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+ method public abstract androidx.work.Operation cancelAllWork();
+ method public abstract androidx.work.Operation cancelAllWorkByTag(String);
+ method public abstract androidx.work.Operation cancelUniqueWork(String);
+ method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
+ method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
+ method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
+ method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
+ method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+ method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+ method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+ method public abstract androidx.work.Configuration getConfiguration();
+ method @Deprecated public static androidx.work.WorkManager getInstance();
+ method public static androidx.work.WorkManager getInstance(android.content.Context);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
+ method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
+ method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
+ method public abstract kotlinx.coroutines.flow.Flow<androidx.work.WorkInfo!> getWorkInfoByIdFlow(java.util.UUID);
+ method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
+ method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagFlow(String);
+ method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
+ method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosFlow(androidx.work.WorkQuery);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
+ method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkFlow(String);
+ method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
+ method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
+ method public static void initialize(android.content.Context, androidx.work.Configuration);
+ method public static boolean isInitialized();
+ method public abstract androidx.work.Operation pruneWork();
+ method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkManager.UpdateResult!> updateWork(androidx.work.WorkRequest);
+ }
+
+ public enum WorkManager.UpdateResult {
+ enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_FOR_NEXT_RUN;
+ enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_IMMEDIATELY;
+ enum_constant public static final androidx.work.WorkManager.UpdateResult NOT_APPLIED;
+ }
+
+ public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+ ctor public WorkManagerInitializer();
+ method public androidx.work.WorkManager create(android.content.Context);
+ method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+ }
+
+ public final class WorkQuery {
+ method public static androidx.work.WorkQuery fromIds(java.util.List<java.util.UUID!>);
+ method public static androidx.work.WorkQuery fromIds(java.util.UUID!...);
+ method public static androidx.work.WorkQuery fromStates(androidx.work.WorkInfo.State!...);
+ method public static androidx.work.WorkQuery fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+ method public static androidx.work.WorkQuery fromTags(java.lang.String!...);
+ method public static androidx.work.WorkQuery fromTags(java.util.List<java.lang.String!>);
+ method public static androidx.work.WorkQuery fromUniqueWorkNames(java.lang.String!...);
+ method public static androidx.work.WorkQuery fromUniqueWorkNames(java.util.List<java.lang.String!>);
+ method public java.util.List<java.util.UUID!> getIds();
+ method public java.util.List<androidx.work.WorkInfo.State!> getStates();
+ method public java.util.List<java.lang.String!> getTags();
+ method public java.util.List<java.lang.String!> getUniqueWorkNames();
+ }
+
+ public static final class WorkQuery.Builder {
+ method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
+ method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
+ method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
+ method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
+ method public androidx.work.WorkQuery build();
+ method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
+ method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+ method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
+ method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
+ }
+
+ public abstract class WorkRequest {
+ method public java.util.UUID getId();
+ property public java.util.UUID id;
+ field public static final androidx.work.WorkRequest.Companion Companion;
+ field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
+ field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
+ field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
+ }
+
+ public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<B, ?>, W extends androidx.work.WorkRequest> {
+ method public final B addTag(String tag);
+ method public final W build();
+ method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration duration);
+ method public final B keepResultsForAtLeast(long duration, java.util.concurrent.TimeUnit timeUnit);
+ method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, java.time.Duration duration);
+ method public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, long backoffDelay, java.util.concurrent.TimeUnit timeUnit);
+ method public final B setConstraints(androidx.work.Constraints constraints);
+ method public B setExpedited(androidx.work.OutOfQuotaPolicy policy);
+ method public final B setId(java.util.UUID id);
+ method @RequiresApi(26) public B setInitialDelay(java.time.Duration duration);
+ method public B setInitialDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+ method public final B setInputData(androidx.work.Data inputData);
+ }
+
+ public static final class WorkRequest.Companion {
+ }
+
+ public abstract class Worker extends androidx.work.ListenableWorker {
+ ctor public Worker(android.content.Context, androidx.work.WorkerParameters);
+ method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
+ method @WorkerThread public androidx.work.ForegroundInfo getForegroundInfo();
+ method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+ }
+
+ public abstract class WorkerFactory {
+ ctor public WorkerFactory();
+ method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+ }
+
+ public final class WorkerParameters {
+ method @IntRange(from=0) public int getGeneration();
+ method public java.util.UUID getId();
+ method public androidx.work.Data getInputData();
+ method @RequiresApi(28) public android.net.Network? getNetwork();
+ method @IntRange(from=0) public int getRunAttemptCount();
+ method public java.util.Set<java.lang.String!> getTags();
+ method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+ method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
+ }
+
+}
+
+package androidx.work.multiprocess {
+
+ public abstract class RemoteWorkContinuation {
+ method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
+ method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
+ method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+ }
+
+ public abstract class RemoteWorkManager {
+ method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+ method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+ method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+ method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+ method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+ method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+ }
+
+}
+
diff --git a/work/work-runtime/api/res-2.9.0-beta01.txt b/work/work-runtime/api/res-2.9.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-runtime/api/res-2.9.0-beta01.txt
diff --git a/work/work-runtime/api/restricted_2.9.0-beta01.txt b/work/work-runtime/api/restricted_2.9.0-beta01.txt
new file mode 100644
index 0000000..4c1e9d7
--- /dev/null
+++ b/work/work-runtime/api/restricted_2.9.0-beta01.txt
@@ -0,0 +1,606 @@
+// Signature format: 4.0
+package androidx.work {
+
+ public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
+ ctor public ArrayCreatingInputMerger();
+ method public androidx.work.Data merge(java.util.List<androidx.work.Data> inputs);
+ }
+
+ public enum BackoffPolicy {
+ method public static androidx.work.BackoffPolicy valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.work.BackoffPolicy[] values();
+ enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
+ enum_constant public static final androidx.work.BackoffPolicy LINEAR;
+ }
+
+ public interface Clock {
+ method public long currentTimeMillis();
+ }
+
+ public final class Configuration {
+ method public androidx.work.Clock getClock();
+ method public int getContentUriTriggerWorkersLimit();
+ method public String? getDefaultProcessName();
+ method public java.util.concurrent.Executor getExecutor();
+ method public androidx.core.util.Consumer<java.lang.Throwable>? getInitializationExceptionHandler();
+ method public androidx.work.InputMergerFactory getInputMergerFactory();
+ method public int getMaxJobSchedulerId();
+ method public int getMinJobSchedulerId();
+ method public androidx.work.RunnableScheduler getRunnableScheduler();
+ method public androidx.core.util.Consumer<java.lang.Throwable>? getSchedulingExceptionHandler();
+ method public java.util.concurrent.Executor getTaskExecutor();
+ method public androidx.work.WorkerFactory getWorkerFactory();
+ property public final androidx.work.Clock clock;
+ property public final int contentUriTriggerWorkersLimit;
+ property public final String? defaultProcessName;
+ property public final java.util.concurrent.Executor executor;
+ property public final androidx.core.util.Consumer<java.lang.Throwable>? initializationExceptionHandler;
+ property public final androidx.work.InputMergerFactory inputMergerFactory;
+ property public final int maxJobSchedulerId;
+ property public final int minJobSchedulerId;
+ property public final androidx.work.RunnableScheduler runnableScheduler;
+ property public final androidx.core.util.Consumer<java.lang.Throwable>? schedulingExceptionHandler;
+ property public final java.util.concurrent.Executor taskExecutor;
+ property public final androidx.work.WorkerFactory workerFactory;
+ field public static final androidx.work.Configuration.Companion Companion;
+ field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
+ }
+
+ public static final class Configuration.Builder {
+ ctor public Configuration.Builder();
+ method public androidx.work.Configuration build();
+ method public androidx.work.Configuration.Builder setClock(androidx.work.Clock clock);
+ method public androidx.work.Configuration.Builder setContentUriTriggerWorkersLimit(int contentUriTriggerWorkersLimit);
+ method public androidx.work.Configuration.Builder setDefaultProcessName(String processName);
+ method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor executor);
+ method public androidx.work.Configuration.Builder setInitializationExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable> exceptionHandler);
+ method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory inputMergerFactory);
+ method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int minJobSchedulerId, int maxJobSchedulerId);
+ method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int maxSchedulerLimit);
+ method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int loggingLevel);
+ method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler runnableScheduler);
+ method public androidx.work.Configuration.Builder setSchedulingExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable> schedulingExceptionHandler);
+ method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor taskExecutor);
+ method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory workerFactory);
+ }
+
+ public static final class Configuration.Companion {
+ }
+
+ public static interface Configuration.Provider {
+ method public androidx.work.Configuration getWorkManagerConfiguration();
+ property public abstract androidx.work.Configuration workManagerConfiguration;
+ }
+
+ public final class Constraints {
+ ctor public Constraints(androidx.work.Constraints other);
+ ctor @androidx.room.Ignore public Constraints(optional androidx.work.NetworkType requiredNetworkType, optional boolean requiresCharging, optional boolean requiresBatteryNotLow, optional boolean requiresStorageNotLow);
+ ctor @RequiresApi(23) @androidx.room.Ignore public Constraints(optional androidx.work.NetworkType requiredNetworkType, optional boolean requiresCharging, optional boolean requiresDeviceIdle, optional boolean requiresBatteryNotLow, optional boolean requiresStorageNotLow);
+ ctor @RequiresApi(24) public Constraints(optional androidx.work.NetworkType requiredNetworkType, optional boolean requiresCharging, optional boolean requiresDeviceIdle, optional boolean requiresBatteryNotLow, optional boolean requiresStorageNotLow, optional long contentTriggerUpdateDelayMillis, optional long contentTriggerMaxDelayMillis, optional java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers);
+ method @RequiresApi(24) public long getContentTriggerMaxDelayMillis();
+ method @RequiresApi(24) public long getContentTriggerUpdateDelayMillis();
+ method @RequiresApi(24) public java.util.Set<androidx.work.Constraints.ContentUriTrigger> getContentUriTriggers();
+ method public androidx.work.NetworkType getRequiredNetworkType();
+ method public boolean requiresBatteryNotLow();
+ method public boolean requiresCharging();
+ method @RequiresApi(23) public boolean requiresDeviceIdle();
+ method public boolean requiresStorageNotLow();
+ property @RequiresApi(24) public final long contentTriggerMaxDelayMillis;
+ property @RequiresApi(24) public final long contentTriggerUpdateDelayMillis;
+ property @RequiresApi(24) public final java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers;
+ property public final androidx.work.NetworkType requiredNetworkType;
+ field public static final androidx.work.Constraints.Companion Companion;
+ field public static final androidx.work.Constraints NONE;
+ }
+
+ public static final class Constraints.Builder {
+ ctor public Constraints.Builder();
+ method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri uri, boolean triggerForDescendants);
+ method public androidx.work.Constraints build();
+ method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType networkType);
+ method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean requiresBatteryNotLow);
+ method public androidx.work.Constraints.Builder setRequiresCharging(boolean requiresCharging);
+ method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean requiresDeviceIdle);
+ method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean requiresStorageNotLow);
+ method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration duration);
+ method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+ method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration duration);
+ method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+ }
+
+ public static final class Constraints.Companion {
+ }
+
+ public static final class Constraints.ContentUriTrigger {
+ ctor public Constraints.ContentUriTrigger(android.net.Uri uri, boolean isTriggeredForDescendants);
+ method public android.net.Uri getUri();
+ method public boolean isTriggeredForDescendants();
+ property public final boolean isTriggeredForDescendants;
+ property public final android.net.Uri uri;
+ }
+
+ public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
+ ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
+ method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+ method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
+ method public suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo>);
+ method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo> getForegroundInfoAsync();
+ method public final void onStopped();
+ method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
+ property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
+ }
+
+ public final class Data {
+ ctor public Data(androidx.work.Data);
+ method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
+ method public boolean getBoolean(String, boolean);
+ method public boolean[]? getBooleanArray(String);
+ method public byte getByte(String, byte);
+ method public byte[]? getByteArray(String);
+ method public double getDouble(String, double);
+ method public double[]? getDoubleArray(String);
+ method public float getFloat(String, float);
+ method public float[]? getFloatArray(String);
+ method public int getInt(String, int);
+ method public int[]? getIntArray(String);
+ method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
+ method public long getLong(String, long);
+ method public long[]? getLongArray(String);
+ method public String? getString(String);
+ method public String![]? getStringArray(String);
+ method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
+ method public byte[] toByteArray();
+ field public static final androidx.work.Data EMPTY;
+ field public static final int MAX_DATA_BYTES = 10240; // 0x2800
+ }
+
+ public static final class Data.Builder {
+ ctor public Data.Builder();
+ method public androidx.work.Data build();
+ method public androidx.work.Data.Builder putAll(androidx.work.Data);
+ method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
+ method public androidx.work.Data.Builder putBoolean(String, boolean);
+ method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
+ method public androidx.work.Data.Builder putByte(String, byte);
+ method public androidx.work.Data.Builder putByteArray(String, byte[]);
+ method public androidx.work.Data.Builder putDouble(String, double);
+ method public androidx.work.Data.Builder putDoubleArray(String, double[]);
+ method public androidx.work.Data.Builder putFloat(String, float);
+ method public androidx.work.Data.Builder putFloatArray(String, float[]);
+ method public androidx.work.Data.Builder putInt(String, int);
+ method public androidx.work.Data.Builder putIntArray(String, int[]);
+ method public androidx.work.Data.Builder putLong(String, long);
+ method public androidx.work.Data.Builder putLongArray(String, long[]);
+ method public androidx.work.Data.Builder putString(String, String?);
+ method public androidx.work.Data.Builder putStringArray(String, String![]);
+ }
+
+ public final class DataKt {
+ method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
+ method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
+ }
+
+ public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
+ ctor public DelegatingWorkerFactory();
+ method public final void addFactory(androidx.work.WorkerFactory);
+ method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+ }
+
+ public enum ExistingPeriodicWorkPolicy {
+ method public static androidx.work.ExistingPeriodicWorkPolicy valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.work.ExistingPeriodicWorkPolicy[] values();
+ enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy CANCEL_AND_REENQUEUE;
+ enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
+ enum_constant @Deprecated public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
+ enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy UPDATE;
+ }
+
+ public enum ExistingWorkPolicy {
+ method public static androidx.work.ExistingWorkPolicy valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.work.ExistingWorkPolicy[] values();
+ enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
+ enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
+ enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
+ enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
+ }
+
+ public final class ForegroundInfo {
+ ctor public ForegroundInfo(int, android.app.Notification);
+ ctor public ForegroundInfo(int, android.app.Notification, int);
+ method public int getForegroundServiceType();
+ method public android.app.Notification getNotification();
+ method public int getNotificationId();
+ }
+
+ public interface ForegroundUpdater {
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
+ }
+
+ public abstract class InputMerger {
+ ctor public InputMerger();
+ method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data> inputs);
+ }
+
+ public abstract class InputMergerFactory {
+ ctor public InputMergerFactory();
+ method public abstract androidx.work.InputMerger? createInputMerger(String className);
+ }
+
+ public abstract class ListenableWorker {
+ ctor public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+ method public final android.content.Context getApplicationContext();
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo!> getForegroundInfoAsync();
+ method public final java.util.UUID getId();
+ method public final androidx.work.Data getInputData();
+ method @RequiresApi(28) public final android.net.Network? getNetwork();
+ method @IntRange(from=0) public final int getRunAttemptCount();
+ method @RequiresApi(31) public final int getStopReason();
+ method public final java.util.Set<java.lang.String!> getTags();
+ method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+ method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
+ method public final boolean isStopped();
+ method public void onStopped();
+ method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
+ method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+ }
+
+ public abstract static class ListenableWorker.Result {
+ method public static androidx.work.ListenableWorker.Result failure();
+ method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
+ method public abstract androidx.work.Data getOutputData();
+ method public static androidx.work.ListenableWorker.Result retry();
+ method public static androidx.work.ListenableWorker.Result success();
+ method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
+ }
+
+ public enum NetworkType {
+ method public static androidx.work.NetworkType valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.work.NetworkType[] values();
+ enum_constant public static final androidx.work.NetworkType CONNECTED;
+ enum_constant public static final androidx.work.NetworkType METERED;
+ enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
+ enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+ enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
+ enum_constant public static final androidx.work.NetworkType UNMETERED;
+ }
+
+ public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
+ method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+ method public static java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+ field public static final androidx.work.OneTimeWorkRequest.Companion Companion;
+ }
+
+ public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
+ ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass);
+ method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger> inputMerger);
+ }
+
+ public static final class OneTimeWorkRequest.Companion {
+ method public androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+ method public java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+ }
+
+ public final class OneTimeWorkRequestKt {
+ method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder OneTimeWorkRequestBuilder();
+ method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
+ }
+
+ public interface Operation {
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
+ method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
+ }
+
+ public abstract static class Operation.State {
+ }
+
+ public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
+ ctor public Operation.State.FAILURE(Throwable);
+ method public Throwable getThrowable();
+ }
+
+ public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
+ }
+
+ public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
+ }
+
+ public final class OperationKt {
+ method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS>);
+ }
+
+ public enum OutOfQuotaPolicy {
+ method public static androidx.work.OutOfQuotaPolicy valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.work.OutOfQuotaPolicy[] values();
+ enum_constant public static final androidx.work.OutOfQuotaPolicy DROP_WORK_REQUEST;
+ enum_constant public static final androidx.work.OutOfQuotaPolicy RUN_AS_NON_EXPEDITED_WORK_REQUEST;
+ }
+
+ public final class OverwritingInputMerger extends androidx.work.InputMerger {
+ ctor public OverwritingInputMerger();
+ method public androidx.work.Data merge(java.util.List<androidx.work.Data> inputs);
+ }
+
+ public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
+ field public static final androidx.work.PeriodicWorkRequest.Companion Companion;
+ field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
+ field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
+ }
+
+ public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
+ ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval);
+ ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval, java.time.Duration flexInterval);
+ ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+ ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexInterval, java.util.concurrent.TimeUnit flexIntervalTimeUnit);
+ method public androidx.work.PeriodicWorkRequest.Builder clearNextScheduleTimeOverride();
+ method public androidx.work.PeriodicWorkRequest.Builder setNextScheduleTimeOverride(long nextScheduleTimeOverrideMillis);
+ }
+
+ public static final class PeriodicWorkRequest.Companion {
+ }
+
+ public final class PeriodicWorkRequestKt {
+ method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
+ method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
+ method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+ method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
+ }
+
+ public interface ProgressUpdater {
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
+ }
+
+ public interface RunnableScheduler {
+ method public void cancel(Runnable);
+ method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
+ }
+
+ public abstract class WorkContinuation {
+ ctor public WorkContinuation();
+ method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
+ method public abstract androidx.work.Operation enqueue();
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
+ method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
+ method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
+ method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+ }
+
+ public final class WorkInfo {
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress, optional int runAttemptCount);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress, optional int runAttemptCount, optional int generation);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress, optional int runAttemptCount, optional int generation, optional androidx.work.Constraints constraints);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress, optional int runAttemptCount, optional int generation, optional androidx.work.Constraints constraints, optional long initialDelayMillis);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress, optional int runAttemptCount, optional int generation, optional androidx.work.Constraints constraints, optional long initialDelayMillis, optional androidx.work.WorkInfo.PeriodicityInfo? periodicityInfo);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress, optional int runAttemptCount, optional int generation, optional androidx.work.Constraints constraints, optional long initialDelayMillis, optional androidx.work.WorkInfo.PeriodicityInfo? periodicityInfo, optional long nextScheduleTimeMillis);
+ ctor public WorkInfo(java.util.UUID id, androidx.work.WorkInfo.State state, java.util.Set<java.lang.String> tags, optional androidx.work.Data outputData, optional androidx.work.Data progress, optional int runAttemptCount, optional int generation, optional androidx.work.Constraints constraints, optional long initialDelayMillis, optional androidx.work.WorkInfo.PeriodicityInfo? periodicityInfo, optional long nextScheduleTimeMillis, optional int stopReason);
+ method public androidx.work.Constraints getConstraints();
+ method public int getGeneration();
+ method public java.util.UUID getId();
+ method public long getInitialDelayMillis();
+ method public long getNextScheduleTimeMillis();
+ method public androidx.work.Data getOutputData();
+ method public androidx.work.WorkInfo.PeriodicityInfo? getPeriodicityInfo();
+ method public androidx.work.Data getProgress();
+ method @IntRange(from=0L) public int getRunAttemptCount();
+ method public androidx.work.WorkInfo.State getState();
+ method @RequiresApi(31) public int getStopReason();
+ method public java.util.Set<java.lang.String> getTags();
+ property public final androidx.work.Constraints constraints;
+ property public final int generation;
+ property public final java.util.UUID id;
+ property public final long initialDelayMillis;
+ property public final long nextScheduleTimeMillis;
+ property public final androidx.work.Data outputData;
+ property public final androidx.work.WorkInfo.PeriodicityInfo? periodicityInfo;
+ property public final androidx.work.Data progress;
+ property @IntRange(from=0L) public final int runAttemptCount;
+ property public final androidx.work.WorkInfo.State state;
+ property @RequiresApi(31) public final int stopReason;
+ property public final java.util.Set<java.lang.String> tags;
+ field public static final androidx.work.WorkInfo.Companion Companion;
+ field public static final int STOP_REASON_APP_STANDBY = 12; // 0xc
+ field public static final int STOP_REASON_BACKGROUND_RESTRICTION = 11; // 0xb
+ field public static final int STOP_REASON_CANCELLED_BY_APP = 1; // 0x1
+ field public static final int STOP_REASON_CONSTRAINT_BATTERY_NOT_LOW = 5; // 0x5
+ field public static final int STOP_REASON_CONSTRAINT_CHARGING = 6; // 0x6
+ field public static final int STOP_REASON_CONSTRAINT_CONNECTIVITY = 7; // 0x7
+ field public static final int STOP_REASON_CONSTRAINT_DEVICE_IDLE = 8; // 0x8
+ field public static final int STOP_REASON_CONSTRAINT_STORAGE_NOT_LOW = 9; // 0x9
+ field public static final int STOP_REASON_DEVICE_STATE = 4; // 0x4
+ field public static final int STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED = 15; // 0xf
+ field public static final int STOP_REASON_NOT_STOPPED = -256; // 0xffffff00
+ field public static final int STOP_REASON_PREEMPT = 2; // 0x2
+ field public static final int STOP_REASON_QUOTA = 10; // 0xa
+ field public static final int STOP_REASON_SYSTEM_PROCESSING = 14; // 0xe
+ field public static final int STOP_REASON_TIMEOUT = 3; // 0x3
+ field public static final int STOP_REASON_UNKNOWN = -512; // 0xfffffe00
+ field public static final int STOP_REASON_USER = 13; // 0xd
+ }
+
+ public static final class WorkInfo.Companion {
+ }
+
+ public static final class WorkInfo.PeriodicityInfo {
+ ctor public WorkInfo.PeriodicityInfo(long repeatIntervalMillis, long flexIntervalMillis);
+ method public long getFlexIntervalMillis();
+ method public long getRepeatIntervalMillis();
+ property public final long flexIntervalMillis;
+ property public final long repeatIntervalMillis;
+ }
+
+ public enum WorkInfo.State {
+ method public final boolean isFinished();
+ method public static androidx.work.WorkInfo.State valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+ method public static androidx.work.WorkInfo.State[] values();
+ property public final boolean isFinished;
+ enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
+ enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
+ enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
+ enum_constant public static final androidx.work.WorkInfo.State FAILED;
+ enum_constant public static final androidx.work.WorkInfo.State RUNNING;
+ enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
+ }
+
+ public abstract class WorkManager {
+ method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+ method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+ method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+ method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+ method public abstract androidx.work.Operation cancelAllWork();
+ method public abstract androidx.work.Operation cancelAllWorkByTag(String);
+ method public abstract androidx.work.Operation cancelUniqueWork(String);
+ method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
+ method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
+ method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
+ method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
+ method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+ method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+ method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+ method public abstract androidx.work.Configuration getConfiguration();
+ method @Deprecated public static androidx.work.WorkManager getInstance();
+ method public static androidx.work.WorkManager getInstance(android.content.Context);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
+ method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
+ method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
+ method public abstract kotlinx.coroutines.flow.Flow<androidx.work.WorkInfo!> getWorkInfoByIdFlow(java.util.UUID);
+ method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
+ method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagFlow(String);
+ method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
+ method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosFlow(androidx.work.WorkQuery);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
+ method public abstract kotlinx.coroutines.flow.Flow<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkFlow(String);
+ method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
+ method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
+ method public static void initialize(android.content.Context, androidx.work.Configuration);
+ method public static boolean isInitialized();
+ method public abstract androidx.work.Operation pruneWork();
+ method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkManager.UpdateResult!> updateWork(androidx.work.WorkRequest);
+ }
+
+ public enum WorkManager.UpdateResult {
+ enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_FOR_NEXT_RUN;
+ enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_IMMEDIATELY;
+ enum_constant public static final androidx.work.WorkManager.UpdateResult NOT_APPLIED;
+ }
+
+ public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+ ctor public WorkManagerInitializer();
+ method public androidx.work.WorkManager create(android.content.Context);
+ method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+ }
+
+ public final class WorkQuery {
+ method public static androidx.work.WorkQuery fromIds(java.util.List<java.util.UUID!>);
+ method public static androidx.work.WorkQuery fromIds(java.util.UUID!...);
+ method public static androidx.work.WorkQuery fromStates(androidx.work.WorkInfo.State!...);
+ method public static androidx.work.WorkQuery fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+ method public static androidx.work.WorkQuery fromTags(java.lang.String!...);
+ method public static androidx.work.WorkQuery fromTags(java.util.List<java.lang.String!>);
+ method public static androidx.work.WorkQuery fromUniqueWorkNames(java.lang.String!...);
+ method public static androidx.work.WorkQuery fromUniqueWorkNames(java.util.List<java.lang.String!>);
+ method public java.util.List<java.util.UUID!> getIds();
+ method public java.util.List<androidx.work.WorkInfo.State!> getStates();
+ method public java.util.List<java.lang.String!> getTags();
+ method public java.util.List<java.lang.String!> getUniqueWorkNames();
+ }
+
+ public static final class WorkQuery.Builder {
+ method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
+ method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
+ method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
+ method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
+ method public androidx.work.WorkQuery build();
+ method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
+ method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+ method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
+ method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
+ }
+
+ public abstract class WorkRequest {
+ method public java.util.UUID getId();
+ property public java.util.UUID id;
+ field public static final androidx.work.WorkRequest.Companion Companion;
+ field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
+ field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
+ field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
+ }
+
+ public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<B, ?>, W extends androidx.work.WorkRequest> {
+ method public final B addTag(String tag);
+ method public final W build();
+ method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration duration);
+ method public final B keepResultsForAtLeast(long duration, java.util.concurrent.TimeUnit timeUnit);
+ method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, java.time.Duration duration);
+ method public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, long backoffDelay, java.util.concurrent.TimeUnit timeUnit);
+ method public final B setConstraints(androidx.work.Constraints constraints);
+ method public B setExpedited(androidx.work.OutOfQuotaPolicy policy);
+ method public final B setId(java.util.UUID id);
+ method @RequiresApi(26) public B setInitialDelay(java.time.Duration duration);
+ method public B setInitialDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+ method public final B setInputData(androidx.work.Data inputData);
+ }
+
+ public static final class WorkRequest.Companion {
+ }
+
+ public abstract class Worker extends androidx.work.ListenableWorker {
+ ctor public Worker(android.content.Context, androidx.work.WorkerParameters);
+ method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
+ method @WorkerThread public androidx.work.ForegroundInfo getForegroundInfo();
+ method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+ }
+
+ public abstract class WorkerFactory {
+ ctor public WorkerFactory();
+ method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+ }
+
+ public final class WorkerParameters {
+ method @IntRange(from=0) public int getGeneration();
+ method public java.util.UUID getId();
+ method public androidx.work.Data getInputData();
+ method @RequiresApi(28) public android.net.Network? getNetwork();
+ method @IntRange(from=0) public int getRunAttemptCount();
+ method public java.util.Set<java.lang.String!> getTags();
+ method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+ method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
+ }
+
+}
+
+package androidx.work.multiprocess {
+
+ public abstract class RemoteWorkContinuation {
+ method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
+ method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
+ method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+ }
+
+ public abstract class RemoteWorkManager {
+ method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+ method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+ method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+ method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+ method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+ method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+ }
+
+}
+
diff --git a/work/work-rxjava2/api/2.9.0-beta01.txt b/work/work-rxjava2/api/2.9.0-beta01.txt
new file mode 100644
index 0000000..1cca40e
--- /dev/null
+++ b/work/work-rxjava2/api/2.9.0-beta01.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.work {
+
+ public abstract class RxWorker extends androidx.work.ListenableWorker {
+ ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+ method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
+ method protected io.reactivex.Scheduler getBackgroundScheduler();
+ method public io.reactivex.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+ method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
+ method public final io.reactivex.Completable setForeground(androidx.work.ForegroundInfo);
+ method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+ }
+
+}
+
diff --git a/work/work-rxjava2/api/res-2.9.0-beta01.txt b/work/work-rxjava2/api/res-2.9.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-rxjava2/api/res-2.9.0-beta01.txt
diff --git a/work/work-rxjava2/api/restricted_2.9.0-beta01.txt b/work/work-rxjava2/api/restricted_2.9.0-beta01.txt
new file mode 100644
index 0000000..1cca40e
--- /dev/null
+++ b/work/work-rxjava2/api/restricted_2.9.0-beta01.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.work {
+
+ public abstract class RxWorker extends androidx.work.ListenableWorker {
+ ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+ method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
+ method protected io.reactivex.Scheduler getBackgroundScheduler();
+ method public io.reactivex.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+ method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
+ method public final io.reactivex.Completable setForeground(androidx.work.ForegroundInfo);
+ method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+ }
+
+}
+
diff --git a/work/work-rxjava3/api/2.9.0-beta01.txt b/work/work-rxjava3/api/2.9.0-beta01.txt
new file mode 100644
index 0000000..0983052
--- /dev/null
+++ b/work/work-rxjava3/api/2.9.0-beta01.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.work.rxjava3 {
+
+ public abstract class RxWorker extends androidx.work.ListenableWorker {
+ ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+ method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
+ method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
+ method public io.reactivex.rxjava3.core.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+ method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
+ method public final io.reactivex.rxjava3.core.Completable setForeground(androidx.work.ForegroundInfo);
+ method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+ }
+
+}
+
diff --git a/work/work-rxjava3/api/res-2.9.0-beta01.txt b/work/work-rxjava3/api/res-2.9.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-rxjava3/api/res-2.9.0-beta01.txt
diff --git a/work/work-rxjava3/api/restricted_2.9.0-beta01.txt b/work/work-rxjava3/api/restricted_2.9.0-beta01.txt
new file mode 100644
index 0000000..0983052
--- /dev/null
+++ b/work/work-rxjava3/api/restricted_2.9.0-beta01.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.work.rxjava3 {
+
+ public abstract class RxWorker extends androidx.work.ListenableWorker {
+ ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+ method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
+ method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
+ method public io.reactivex.rxjava3.core.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+ method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
+ method public final io.reactivex.rxjava3.core.Completable setForeground(androidx.work.ForegroundInfo);
+ method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+ }
+
+}
+
diff --git a/work/work-testing/api/2.9.0-beta01.txt b/work/work-testing/api/2.9.0-beta01.txt
new file mode 100644
index 0000000..2812b61
--- /dev/null
+++ b/work/work-testing/api/2.9.0-beta01.txt
@@ -0,0 +1,61 @@
+// Signature format: 4.0
+package androidx.work.testing {
+
+ public class SynchronousExecutor implements java.util.concurrent.Executor {
+ ctor public SynchronousExecutor();
+ method public void execute(Runnable);
+ }
+
+ public interface TestDriver {
+ method public void setAllConstraintsMet(java.util.UUID);
+ method public void setInitialDelayMet(java.util.UUID);
+ method public void setPeriodDelayMet(java.util.UUID);
+ }
+
+ public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
+ method public W build();
+ method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
+ method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
+ method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
+ method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
+ method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
+ method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
+ method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
+ method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
+ method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
+ method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
+ method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
+ method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
+ }
+
+ public final class TestListenableWorkerBuilderKt {
+ method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W> TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<java.lang.String> triggeredContentAuthorities);
+ }
+
+ public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
+ method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
+ method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
+ }
+
+ public final class TestWorkerBuilderKt {
+ method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W> TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<java.lang.String> triggeredContentAuthorities);
+ }
+
+ public final class WorkManagerTestInitHelper {
+ method public static void closeWorkDatabase();
+ method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
+ method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
+ method public static void initializeTestWorkManager(android.content.Context);
+ method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
+ method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration, androidx.work.testing.WorkManagerTestInitHelper.ExecutorsMode);
+ method public static void initializeTestWorkManager(android.content.Context, androidx.work.testing.WorkManagerTestInitHelper.ExecutorsMode);
+ }
+
+ public enum WorkManagerTestInitHelper.ExecutorsMode {
+ enum_constant public static final androidx.work.testing.WorkManagerTestInitHelper.ExecutorsMode LEGACY_OVERRIDE_WITH_SYNCHRONOUS_EXECUTORS;
+ enum_constant public static final androidx.work.testing.WorkManagerTestInitHelper.ExecutorsMode PRESERVE_EXECUTORS;
+ enum_constant public static final androidx.work.testing.WorkManagerTestInitHelper.ExecutorsMode USE_TIME_BASED_SCHEDULING;
+ }
+
+}
+
diff --git a/work/work-testing/api/res-2.9.0-beta01.txt b/work/work-testing/api/res-2.9.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-testing/api/res-2.9.0-beta01.txt
diff --git a/work/work-testing/api/restricted_2.9.0-beta01.txt b/work/work-testing/api/restricted_2.9.0-beta01.txt
new file mode 100644
index 0000000..2812b61
--- /dev/null
+++ b/work/work-testing/api/restricted_2.9.0-beta01.txt
@@ -0,0 +1,61 @@
+// Signature format: 4.0
+package androidx.work.testing {
+
+ public class SynchronousExecutor implements java.util.concurrent.Executor {
+ ctor public SynchronousExecutor();
+ method public void execute(Runnable);
+ }
+
+ public interface TestDriver {
+ method public void setAllConstraintsMet(java.util.UUID);
+ method public void setInitialDelayMet(java.util.UUID);
+ method public void setPeriodDelayMet(java.util.UUID);
+ }
+
+ public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
+ method public W build();
+ method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
+ method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
+ method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
+ method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
+ method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
+ method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
+ method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
+ method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
+ method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
+ method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
+ method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
+ method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
+ }
+
+ public final class TestListenableWorkerBuilderKt {
+ method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W> TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<java.lang.String> triggeredContentAuthorities);
+ }
+
+ public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
+ method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
+ method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
+ }
+
+ public final class TestWorkerBuilderKt {
+ method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W> TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<java.lang.String> triggeredContentAuthorities);
+ }
+
+ public final class WorkManagerTestInitHelper {
+ method public static void closeWorkDatabase();
+ method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
+ method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
+ method public static void initializeTestWorkManager(android.content.Context);
+ method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
+ method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration, androidx.work.testing.WorkManagerTestInitHelper.ExecutorsMode);
+ method public static void initializeTestWorkManager(android.content.Context, androidx.work.testing.WorkManagerTestInitHelper.ExecutorsMode);
+ }
+
+ public enum WorkManagerTestInitHelper.ExecutorsMode {
+ enum_constant public static final androidx.work.testing.WorkManagerTestInitHelper.ExecutorsMode LEGACY_OVERRIDE_WITH_SYNCHRONOUS_EXECUTORS;
+ enum_constant public static final androidx.work.testing.WorkManagerTestInitHelper.ExecutorsMode PRESERVE_EXECUTORS;
+ enum_constant public static final androidx.work.testing.WorkManagerTestInitHelper.ExecutorsMode USE_TIME_BASED_SCHEDULING;
+ }
+
+}
+