Merge "Change flans to ToT dependencies" into androidx-main
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
index 814c66f..4d722e5e 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
@@ -196,21 +196,17 @@
}
withActivity {
addOnNewIntentListener(listener)
- startActivity(
- Intent(this, SingleTopActivity::class.java).apply {
- putExtra("newExtra", 5)
- }
- )
+ onNewIntent(Intent(this, SingleTopActivity::class.java).apply {
+ putExtra("newExtra", 5)
+ })
}
- withActivity {
- assertWithMessage("Should have received one intent")
- .that(receivedIntents)
- .hasSize(1)
- val receivedIntent = receivedIntents.first()
- assertThat(receivedIntent.getIntExtra("newExtra", -1))
- .isEqualTo(5)
- }
+ assertWithMessage("Should have received one intent")
+ .that(receivedIntents)
+ .hasSize(1)
+ val receivedIntent = receivedIntents.first()
+ assertThat(receivedIntent.getIntExtra("newExtra", -1))
+ .isEqualTo(5)
}
}
@@ -224,36 +220,28 @@
}
withActivity {
addOnNewIntentListener(listener)
- startActivity(
- Intent(this, SingleTopActivity::class.java).apply {
- putExtra("newExtra", 5)
- }
- )
+ onNewIntent(Intent(this, SingleTopActivity::class.java).apply {
+ putExtra("newExtra", 5)
+ })
}
- withActivity {
- assertWithMessage("Should have received one intent")
- .that(receivedIntents)
- .hasSize(1)
- val receivedIntent = receivedIntents.first()
- assertThat(receivedIntent.getIntExtra("newExtra", -1))
- .isEqualTo(5)
- }
+ assertWithMessage("Should have received one intent")
+ .that(receivedIntents)
+ .hasSize(1)
+ val receivedIntent = receivedIntents.first()
+ assertThat(receivedIntent.getIntExtra("newExtra", -1))
+ .isEqualTo(5)
withActivity {
removeOnNewIntentListener(listener)
- startActivity(
- Intent(this, SingleTopActivity::class.java).apply {
- putExtra("newExtra", 10)
- }
- )
+ onNewIntent(Intent(this, SingleTopActivity::class.java).apply {
+ putExtra("newExtra", 5)
+ })
}
- withActivity {
- assertWithMessage("Should have received only one intent")
- .that(receivedIntents)
- .hasSize(1)
- }
+ assertWithMessage("Should have received only one intent")
+ .that(receivedIntents)
+ .hasSize(1)
}
}
@@ -274,32 +262,27 @@
// Add a second listener to force a ConcurrentModificationException
// if not properly handled by ComponentActivity
addOnNewIntentListener { }
- startActivity(
- Intent(this, SingleTopActivity::class.java).apply {
- putExtra("newExtra", 5)
- }
- )
+ onNewIntent(Intent(this, SingleTopActivity::class.java).apply {
+ putExtra("newExtra", 5)
+ })
+ onNewIntent(Intent(this, SingleTopActivity::class.java).apply {
+ putExtra("newExtra", 10)
+ })
}
- withActivity {
- startActivity(
- Intent(this, SingleTopActivity::class.java).apply {
- putExtra("newExtra", 10)
- }
- )
- }
-
- withActivity {
- // Only the first Intent should be received
- assertWithMessage("Should have received only one intent")
- .that(receivedIntents)
- .hasSize(1)
- val receivedIntent = receivedIntents.first()
- assertThat(receivedIntent.getIntExtra("newExtra", -1))
- .isEqualTo(5)
- }
+ // Only the first Intent should be received
+ assertWithMessage("Should have received only one intent")
+ .that(receivedIntents)
+ .hasSize(1)
+ val receivedIntent = receivedIntents.first()
+ assertThat(receivedIntent.getIntExtra("newExtra", -1))
+ .isEqualTo(5)
}
}
}
-class SingleTopActivity : ComponentActivity()
+class SingleTopActivity : ComponentActivity() {
+ public override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ }
+}
diff --git a/benchmark/benchmark-macro-junit4/build.gradle b/benchmark/benchmark-macro-junit4/build.gradle
index 75fe717..75bc6d4 100644
--- a/benchmark/benchmark-macro-junit4/build.gradle
+++ b/benchmark/benchmark-macro-junit4/build.gradle
@@ -42,7 +42,6 @@
implementation("androidx.test.uiautomator:uiautomator:2.2.0")
androidTestImplementation(project(":internal-testutils-ktx"))
- androidTestImplementation(project(":tracing:tracing-ktx"))
androidTestImplementation("androidx.test:rules:1.3.0")
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
diff --git a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt
index 58ced3f..1bec5eb 100644
--- a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt
+++ b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt
@@ -53,7 +53,7 @@
* @param packageName Package name of the app being measured.
* @param metrics List of metrics to measure.
* @param compilationMode Mode of compilation used before capturing measurement, such as
- * [CompilationMode.SpeedProfile].
+ * [CompilationMode.Partial], defaults to [CompilationMode.DEFAULT].
* @param startupMode Optional mode to force app launches performed with
* [MacrobenchmarkScope.startActivityAndWait] (and similar variants) to be of the assigned
* type. For example, `COLD` launches kill the process before the measureBlock, to ensure
@@ -70,7 +70,7 @@
public fun measureRepeated(
packageName: String,
metrics: List<Metric>,
- compilationMode: CompilationMode = CompilationMode.SpeedProfile(),
+ compilationMode: CompilationMode = CompilationMode.DEFAULT,
startupMode: StartupMode? = null,
@IntRange(from = 1)
iterations: Int,
diff --git a/benchmark/benchmark-macro/api/current.txt b/benchmark/benchmark-macro/api/current.txt
index 6327890..0d971ac 100644
--- a/benchmark/benchmark-macro/api/current.txt
+++ b/benchmark/benchmark-macro/api/current.txt
@@ -4,27 +4,52 @@
@RequiresApi(29) public final class Api29Kt {
}
+ public enum BaselineProfileMode {
+ enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Disable;
+ enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Require;
+ enum_constant public static final androidx.benchmark.macro.BaselineProfileMode UseIfAvailable;
+ }
+
public final class BaselineProfilesKt {
}
public abstract sealed class CompilationMode {
+ field public static final androidx.benchmark.macro.CompilationMode.Companion Companion;
+ field public static final androidx.benchmark.macro.CompilationMode DEFAULT;
}
- public static final class CompilationMode.BaselineProfile extends androidx.benchmark.macro.CompilationMode {
- field public static final androidx.benchmark.macro.CompilationMode.BaselineProfile INSTANCE;
+ @Deprecated public static final class CompilationMode.BaselineProfile extends androidx.benchmark.macro.CompilationMode {
+ field @Deprecated public static final androidx.benchmark.macro.CompilationMode.BaselineProfile INSTANCE;
}
- public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
- field public static final androidx.benchmark.macro.CompilationMode.None INSTANCE;
+ public static final class CompilationMode.Companion {
}
- public static final class CompilationMode.Speed extends androidx.benchmark.macro.CompilationMode {
- field public static final androidx.benchmark.macro.CompilationMode.Speed INSTANCE;
+ public static final class CompilationMode.Full extends androidx.benchmark.macro.CompilationMode {
+ ctor public CompilationMode.Full();
}
- public static final class CompilationMode.SpeedProfile extends androidx.benchmark.macro.CompilationMode {
- ctor public CompilationMode.SpeedProfile(optional int warmupIterations);
+ @RequiresApi(24) public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
+ ctor public CompilationMode.None();
+ }
+
+ @RequiresApi(24) public static final class CompilationMode.Partial extends androidx.benchmark.macro.CompilationMode {
+ ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode, optional @IntRange(from=0) int warmupIterations);
+ ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode);
+ ctor public CompilationMode.Partial();
+ method public androidx.benchmark.macro.BaselineProfileMode getBaselineProfileMode();
method public int getWarmupIterations();
+ property public final androidx.benchmark.macro.BaselineProfileMode baselineProfileMode;
+ property public final int warmupIterations;
+ }
+
+ @Deprecated public static final class CompilationMode.Speed extends androidx.benchmark.macro.CompilationMode {
+ field @Deprecated public static final androidx.benchmark.macro.CompilationMode.Speed INSTANCE;
+ }
+
+ @Deprecated public static final class CompilationMode.SpeedProfile extends androidx.benchmark.macro.CompilationMode {
+ ctor @Deprecated public CompilationMode.SpeedProfile(optional int warmupIterations);
+ method @Deprecated public int getWarmupIterations();
property public final int warmupIterations;
}
diff --git a/benchmark/benchmark-macro/api/public_plus_experimental_current.txt b/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
index 35e822e..4e4fde3 100644
--- a/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
@@ -4,27 +4,52 @@
@RequiresApi(29) public final class Api29Kt {
}
+ public enum BaselineProfileMode {
+ enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Disable;
+ enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Require;
+ enum_constant public static final androidx.benchmark.macro.BaselineProfileMode UseIfAvailable;
+ }
+
public final class BaselineProfilesKt {
}
public abstract sealed class CompilationMode {
+ field public static final androidx.benchmark.macro.CompilationMode.Companion Companion;
+ field public static final androidx.benchmark.macro.CompilationMode DEFAULT;
}
- public static final class CompilationMode.BaselineProfile extends androidx.benchmark.macro.CompilationMode {
- field public static final androidx.benchmark.macro.CompilationMode.BaselineProfile INSTANCE;
+ @Deprecated public static final class CompilationMode.BaselineProfile extends androidx.benchmark.macro.CompilationMode {
+ field @Deprecated public static final androidx.benchmark.macro.CompilationMode.BaselineProfile INSTANCE;
}
- public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
- field public static final androidx.benchmark.macro.CompilationMode.None INSTANCE;
+ public static final class CompilationMode.Companion {
}
- public static final class CompilationMode.Speed extends androidx.benchmark.macro.CompilationMode {
- field public static final androidx.benchmark.macro.CompilationMode.Speed INSTANCE;
+ public static final class CompilationMode.Full extends androidx.benchmark.macro.CompilationMode {
+ ctor public CompilationMode.Full();
}
- public static final class CompilationMode.SpeedProfile extends androidx.benchmark.macro.CompilationMode {
- ctor public CompilationMode.SpeedProfile(optional int warmupIterations);
+ @RequiresApi(24) public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
+ ctor public CompilationMode.None();
+ }
+
+ @RequiresApi(24) public static final class CompilationMode.Partial extends androidx.benchmark.macro.CompilationMode {
+ ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode, optional @IntRange(from=0) int warmupIterations);
+ ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode);
+ ctor public CompilationMode.Partial();
+ method public androidx.benchmark.macro.BaselineProfileMode getBaselineProfileMode();
method public int getWarmupIterations();
+ property public final androidx.benchmark.macro.BaselineProfileMode baselineProfileMode;
+ property public final int warmupIterations;
+ }
+
+ @Deprecated public static final class CompilationMode.Speed extends androidx.benchmark.macro.CompilationMode {
+ field @Deprecated public static final androidx.benchmark.macro.CompilationMode.Speed INSTANCE;
+ }
+
+ @Deprecated public static final class CompilationMode.SpeedProfile extends androidx.benchmark.macro.CompilationMode {
+ ctor @Deprecated public CompilationMode.SpeedProfile(optional int warmupIterations);
+ method @Deprecated public int getWarmupIterations();
property public final int warmupIterations;
}
diff --git a/benchmark/benchmark-macro/api/restricted_current.txt b/benchmark/benchmark-macro/api/restricted_current.txt
index ee02155..3292f02 100644
--- a/benchmark/benchmark-macro/api/restricted_current.txt
+++ b/benchmark/benchmark-macro/api/restricted_current.txt
@@ -4,31 +4,56 @@
@RequiresApi(29) public final class Api29Kt {
}
+ public enum BaselineProfileMode {
+ enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Disable;
+ enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Require;
+ enum_constant public static final androidx.benchmark.macro.BaselineProfileMode UseIfAvailable;
+ }
+
public final class BaselineProfilesKt {
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectBaselineProfile(String uniqueName, String packageName, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+ method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectBaselineProfile(String uniqueName, String packageName, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
}
public abstract sealed class CompilationMode {
+ field public static final androidx.benchmark.macro.CompilationMode.Companion Companion;
+ field public static final androidx.benchmark.macro.CompilationMode DEFAULT;
}
- public static final class CompilationMode.BaselineProfile extends androidx.benchmark.macro.CompilationMode {
- field public static final androidx.benchmark.macro.CompilationMode.BaselineProfile INSTANCE;
+ @Deprecated public static final class CompilationMode.BaselineProfile extends androidx.benchmark.macro.CompilationMode {
+ field @Deprecated public static final androidx.benchmark.macro.CompilationMode.BaselineProfile INSTANCE;
+ }
+
+ public static final class CompilationMode.Companion {
+ }
+
+ public static final class CompilationMode.Full extends androidx.benchmark.macro.CompilationMode {
+ ctor public CompilationMode.Full();
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class CompilationMode.Interpreted extends androidx.benchmark.macro.CompilationMode {
}
- public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
- field public static final androidx.benchmark.macro.CompilationMode.None INSTANCE;
+ @RequiresApi(24) public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
+ ctor public CompilationMode.None();
}
- public static final class CompilationMode.Speed extends androidx.benchmark.macro.CompilationMode {
- field public static final androidx.benchmark.macro.CompilationMode.Speed INSTANCE;
- }
-
- public static final class CompilationMode.SpeedProfile extends androidx.benchmark.macro.CompilationMode {
- ctor public CompilationMode.SpeedProfile(optional int warmupIterations);
+ @RequiresApi(24) public static final class CompilationMode.Partial extends androidx.benchmark.macro.CompilationMode {
+ ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode, optional @IntRange(from=0) int warmupIterations);
+ ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode);
+ ctor public CompilationMode.Partial();
+ method public androidx.benchmark.macro.BaselineProfileMode getBaselineProfileMode();
method public int getWarmupIterations();
+ property public final androidx.benchmark.macro.BaselineProfileMode baselineProfileMode;
+ property public final int warmupIterations;
+ }
+
+ @Deprecated public static final class CompilationMode.Speed extends androidx.benchmark.macro.CompilationMode {
+ field @Deprecated public static final androidx.benchmark.macro.CompilationMode.Speed INSTANCE;
+ }
+
+ @Deprecated public static final class CompilationMode.SpeedProfile extends androidx.benchmark.macro.CompilationMode {
+ ctor @Deprecated public CompilationMode.SpeedProfile(optional int warmupIterations);
+ method @Deprecated public int getWarmupIterations();
property public final int warmupIterations;
}
diff --git a/benchmark/benchmark-macro/build.gradle b/benchmark/benchmark-macro/build.gradle
index 13147d6..d16e515 100644
--- a/benchmark/benchmark-macro/build.gradle
+++ b/benchmark/benchmark-macro/build.gradle
@@ -48,7 +48,7 @@
implementation(project(":benchmark:benchmark-common"))
implementation("androidx.profileinstaller:profileinstaller:1.0.3")
- implementation(project(":tracing:tracing-ktx"))
+ implementation("androidx.tracing:tracing-ktx:1.1.0-beta01")
implementation(libs.testCore)
implementation(libs.testUiautomator)
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt
index a09cfeb..4cafd89 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt
@@ -16,8 +16,10 @@
package androidx.benchmark.macro
+import android.os.Build
import androidx.benchmark.Shell
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -26,10 +28,12 @@
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
+import kotlin.test.assertFailsWith
+@Suppress("DEPRECATION")
@RunWith(AndroidJUnit4::class)
@SmallTest
-public class CompilationModeTest {
+class CompilationModeTest {
private val vmRunningInterpretedOnly: Boolean
init {
@@ -37,33 +41,76 @@
vmRunningInterpretedOnly = getProp.contains("-Xusejit:false")
}
+ @SdkSuppress(minSdkVersion = 24)
@Test
- public fun names() {
+ fun partial() {
+ assertFailsWith<IllegalArgumentException> { // can't ignore with 0 iters
+ CompilationMode.Partial(BaselineProfileMode.Disable, warmupIterations = 0)
+ }
+ assertFailsWith<java.lang.IllegalArgumentException> { // can't set negative iters
+ CompilationMode.Partial(BaselineProfileMode.Require, warmupIterations = -1)
+ }
+ }
+
+ @Test
+ fun names() {
// We test these names, as they're likely built into parameterized
// test strings, so stability/brevity are important
- assertEquals("None", CompilationMode.None.toString())
- assertEquals("SpeedProfile(iterations=123)", CompilationMode.SpeedProfile(123).toString())
- assertEquals("Speed", CompilationMode.Speed.toString())
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ assertEquals("None", CompilationMode.None().toString())
+ assertEquals("BaselineProfile", CompilationMode.Partial().toString())
+ assertEquals(
+ "WarmupProfile(iterations=3)",
+ CompilationMode.Partial(
+ BaselineProfileMode.Disable,
+ warmupIterations = 3
+ ).toString()
+ )
+ assertEquals(
+ "Partial(baselineProfile=Require,iterations=3)",
+ CompilationMode.Partial(warmupIterations = 3).toString()
+ )
+ assertEquals("Full", CompilationMode.Full().toString())
+ }
assertEquals("Interpreted", CompilationMode.Interpreted.toString())
+
+ // deprecated
+ assertEquals(
+ "SpeedProfile(iterations=123)",
+ CompilationMode.SpeedProfile(123).toString()
+ )
+ assertEquals("Speed", CompilationMode.Speed.toString())
}
@Test
- public fun isSupportedWithVmSettings_jitEnabled() {
+ fun isSupportedWithVmSettings_jitEnabled() {
assumeFalse(vmRunningInterpretedOnly)
- assertTrue(CompilationMode.None.isSupportedWithVmSettings())
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ assertTrue(CompilationMode.None().isSupportedWithVmSettings())
+ assertTrue(CompilationMode.Partial().isSupportedWithVmSettings())
+ assertTrue(CompilationMode.Full().isSupportedWithVmSettings())
+ }
+ assertFalse(CompilationMode.Interpreted.isSupportedWithVmSettings())
+
+ // deprecated
assertTrue(CompilationMode.SpeedProfile().isSupportedWithVmSettings())
assertTrue(CompilationMode.Speed.isSupportedWithVmSettings())
- assertFalse(CompilationMode.Interpreted.isSupportedWithVmSettings())
}
@Test
- public fun isSupportedWithVmSettings_jitDisabled() {
+ fun isSupportedWithVmSettings_jitDisabled() {
assumeTrue(vmRunningInterpretedOnly)
- assertFalse(CompilationMode.None.isSupportedWithVmSettings())
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ assertFalse(CompilationMode.None().isSupportedWithVmSettings())
+ assertFalse(CompilationMode.Partial().isSupportedWithVmSettings())
+ assertFalse(CompilationMode.Full().isSupportedWithVmSettings())
+ }
+ assertTrue(CompilationMode.Interpreted.isSupportedWithVmSettings())
+
+ // deprecated
assertFalse(CompilationMode.SpeedProfile().isSupportedWithVmSettings())
assertFalse(CompilationMode.Speed.isSupportedWithVmSettings())
- assertTrue(CompilationMode.Interpreted.isSupportedWithVmSettings())
}
}
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 d60f101..ffac1fa 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
@@ -19,6 +19,7 @@
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
+import androidx.annotation.RequiresApi
import androidx.benchmark.Shell
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -40,7 +41,6 @@
@LargeTest
class MacrobenchmarkScopeTest {
private val instrumentation = InstrumentationRegistry.getInstrumentation()
- private val device = UiDevice.getInstance(instrumentation)
@Before
fun setup() {
@@ -66,14 +66,17 @@
assertFalse(Shell.isPackageAlive(Packages.TARGET))
}
- @SdkSuppress(minSdkVersion = 24) // TODO: define behavior for older platforms
+ @SdkSuppress(minSdkVersion = 24)
@Test
fun compile_speedProfile() {
val scope = MacrobenchmarkScope(Packages.TARGET, launchWithClearTask = true)
val iterations = 1
var executions = 0
- val compilation = CompilationMode.SpeedProfile(warmupIterations = iterations)
- compilation.compile(Packages.TARGET) {
+ val compilation = CompilationMode.Partial(
+ baselineProfileMode = BaselineProfileMode.Disable,
+ warmupIterations = iterations
+ )
+ compilation.resetAndCompile(Packages.TARGET) {
executions += 1
scope.pressHome()
scope.startActivityAndWait()
@@ -81,11 +84,10 @@
assertEquals(iterations, executions)
}
- @SdkSuppress(minSdkVersion = 24) // TODO: define behavior for older platforms
@Test
- fun compile_speed() {
- val compilation = CompilationMode.Speed
- compilation.compile(Packages.TARGET) {
+ fun compile_full() {
+ val compilation = CompilationMode.Full()
+ compilation.resetAndCompile(Packages.TARGET) {
fail("Should never be called for $compilation")
}
}
@@ -119,6 +121,7 @@
}
}
+ @RequiresApi(24) // Test flakes locally on API 23, appears to be UiAutomation issue
@Test
fun startActivityAndWait_invalidActivity() {
val scope = MacrobenchmarkScope(Packages.TARGET, launchWithClearTask = true)
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
index c7b5398..de2131e 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
@@ -42,7 +42,7 @@
testName = "testName",
packageName = "com.ignored",
metrics = emptyList(), // invalid
- compilationMode = CompilationMode.None,
+ compilationMode = CompilationMode.noop,
iterations = 1,
startupMode = null,
setupBlock = {},
@@ -61,7 +61,7 @@
testName = "testName",
packageName = "com.ignored",
metrics = listOf(FrameTimingMetric()),
- compilationMode = CompilationMode.None,
+ compilationMode = CompilationMode.noop,
iterations = 0, // invalid
startupMode = null,
setupBlock = {},
@@ -87,7 +87,7 @@
testName = "validateCallbackBehavior",
packageName = Packages.TARGET,
metrics = listOf(TraceSectionMetric(TRACE_LABEL)),
- compilationMode = CompilationMode.None,
+ compilationMode = CompilationMode.DEFAULT,
iterations = 2,
startupMode = startupMode,
setupBlock = {
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfileMode.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfileMode.kt
new file mode 100644
index 0000000..66626cc
--- /dev/null
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfileMode.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.macro
+
+/**
+ * Choice of how the Baseline Profile in a target application should be included or ignored during pre-compilation.
+ */
+enum class BaselineProfileMode {
+ /**
+ * Require the BaselineProfile methods/classes from the target app to be pre-compiled.
+ *
+ * If the ProfileInstaller library or Baseline Profile isn't present in the target app, an
+ * exception will be thrown at compilation time.
+ */
+ Require,
+
+ /**
+ * Include the BaselineProfile methods/classes from the target app into the compilation step if
+ * a Baseline Profile and the ProfileInstaller library are both present in the target.
+ *
+ * This is the same as [Require], except it logs instead of throwing when the
+ * Baseline Profile or ProfileInstaller library aren't present in the target application.
+ */
+ UseIfAvailable,
+
+ /**
+ * Do not include the Baseline Profile, if present, in the compilation of the target app.
+ */
+ Disable
+}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
index 20694f0..1712f72 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
@@ -18,6 +18,7 @@
import android.os.Build
import android.util.Log
+import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.benchmark.InstrumentationResults
import androidx.benchmark.Outputs
@@ -30,6 +31,7 @@
* @suppress
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@RequiresApi(28)
fun collectBaselineProfile(
uniqueName: String,
packageName: String,
@@ -46,14 +48,21 @@
}
val startTime = System.nanoTime()
- val scope = MacrobenchmarkScope(packageName, /* launchWithClearTask */ true)
- val speedProfile = CompilationMode.SpeedProfile(warmupIterations = 3)
+ val scope = MacrobenchmarkScope(packageName, launchWithClearTask = true)
+
+ // Ignore because we're *creating* a baseline profile, not using it yet
+ val compilationMode = CompilationMode.Partial(
+ baselineProfileMode = BaselineProfileMode.Disable,
+ warmupIterations = 3
+ )
// always kill the process at beginning of a collection.
scope.killProcess()
try {
userspaceTrace("compile $packageName") {
- speedProfile.compile(packageName) {
+ compilationMode.resetAndCompile(
+ packageName = packageName
+ ) {
setupBlock(scope)
profileBlock(scope)
}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
index 19cc93a..135615c 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
@@ -18,11 +18,11 @@
import android.os.Build
import android.util.Log
+import androidx.annotation.IntRange
+import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.benchmark.DeviceInfo
import androidx.benchmark.Shell
-import androidx.benchmark.macro.CompilationMode.BaselineProfile
-import androidx.benchmark.macro.CompilationMode.SpeedProfile
import androidx.profileinstaller.ProfileInstallReceiver
import androidx.profileinstaller.ProfileInstaller
import org.junit.AssumptionViolatedException
@@ -34,42 +34,225 @@
* with the next. This compilation mode dictates any pre-compilation that occurs before repeatedly
* running the setup / measure blocks of the benchmark.
*
- * If [SpeedProfile] is used, the following occur before measurement:
- * 1. Compilation is reset
- * 2. The setup / measure loop will be run a configurable number of profiling iterations to capture
- * a profile
- * 3. The app is compiled with `cmd package compile -f -m speed-profile <package>`
+ * On Android N+ (API 24+), there are different levels of compilation supported:
*
- * * [None] skips steps 2 and 3 above.
- * * [BaselineProfile] has an alternate implementation of 2, where it installs profile information
- * bundled within the APK.
- * * [Speed] skips 2 (since it AOT compiles the full app), and uses `speed` instead of
- * `speed-profile` for 3.
+ * * [Partial] - the default configuration of [Partial] will partially pre-compile your application,
+ * if a Baseline Profile is included in your app. This represents the most realistic fresh-install
+ * experience on an end-user's device. You can additionally or instead use
+ * [Partial.warmupIterations] to use Profile Guided Optimization, using the benchmark content to
+ * guide pre-compilation to mimic an application's performance after some, and JIT-ing has occurred.
*
- * While some of these modes directly map to
+ * * [Full] - the app is fully pre-compiled. This is generally not representative of real user
+ * experience, as apps are not fully pre-compiled on user devices, but this can be used to either
+ * illustrate ideal performance, or to reduce noise/inconsistency from just-in-time compilation
+ * while the benchmark runs.
+ *
+ * * [None] - the app isn't pre-compiled at all, bypassing the default compilation that should
+ * generally be done at install time, e.g. by the Play Store. This will illustrate worst case
+ * performance, and will show you performance of your app if you do not enable baseline profiles,
+ * useful for judging the performance impact of the baseline profiles included in your application.
+ *
+ * On Android M (API 23), only [Full] is supported, as all apps are always fully compiled.
+ *
+ * To understand more how these modes work, you can see comments for each class, and also see the
* [Android Runtime compilation modes](https://source.android.com/devices/tech/dalvik/configure#compilation_options)
- * (which can be passed to
- * [`cmd compile`](https://source.android.com/devices/tech/dalvik/jit-compiler#force-compilation-of-a-specific-package)),
- * there isn't a direct mapping.
+ * (which are passed by benchmark into
+ * [`cmd compile`](https://source.android.com/devices/tech/dalvik/jit-compiler#force-compilation-of-a-specific-package)
+ * to compile the target app).
*/
-public sealed class CompilationMode(
- // for modes other than [None], is argument passed `cmd package compile`
- private val compileArgument: String?
-) {
- internal fun compileArgument(): String {
- if (compileArgument == null) {
- throw UnsupportedOperationException("No compileArgument for mode $this")
- }
- return compileArgument
+sealed class CompilationMode {
+ internal fun resetAndCompile(packageName: String, warmupBlock: () -> Unit) {
+ Log.d(TAG, "Clearing profiles for $packageName")
+ Shell.executeCommand("cmd package compile --reset $packageName")
+ compileImpl(packageName, warmupBlock)
}
+ internal fun cmdPackageCompile(packageName: String, compileArgument: String) {
+ Shell.executeCommand("cmd package compile -f -m $compileArgument $packageName")
+ }
+
+ internal abstract fun compileImpl(packageName: String, warmupBlock: () -> Unit)
+
/**
* No pre-compilation - entire app will be allowed to Just-In-Time compile as it runs.
*
* Note that later iterations may perform differently, as app code is jitted.
*/
- public object None : CompilationMode(null) {
- public override fun toString(): String = "None"
+ // Leaving possibility for future configuration (such as interpreted = true)
+ @Suppress("CanSealedSubClassBeObject")
+ @RequiresApi(24)
+ class None : CompilationMode() {
+ override fun toString(): String = "None"
+
+ override fun compileImpl(packageName: String, warmupBlock: () -> Unit) {
+ // nothing to do!
+ }
+ }
+
+ /**
+ * Partial ahead-of-time app compilation.
+ *
+ * The default parameters for this mimic the default state of an app partially pre-compiled by
+ * the installer - such as via Google Play.
+ *
+ * Either [baselineProfileMode] must be set to non-[BaselineProfileMode.Disable], or
+ * [warmupIterations] must be set to a non-`0` value.
+ *
+ * Note: `[baselineProfileMode] = [BaselineProfileMode.Require]` is only supported for APKs that
+ * have the ProfileInstaller library included, and have been built by AGP 7.0+ to package the
+ * baseline profile in the APK.
+ */
+ @RequiresApi(24)
+ class Partial @JvmOverloads constructor(
+ /**
+ * Controls whether a Baseline Profile should be used to partially pre compile the app.
+ *
+ * Defaults to [BaselineProfileMode.Require]
+ *
+ * @see BaselineProfileMode
+ */
+ val baselineProfileMode: BaselineProfileMode = BaselineProfileMode.Require,
+
+ /**
+ * If greater than 0, your macrobenchmark will run an extra [warmupIterations] times before
+ * compilation, to prepare
+ */
+ @IntRange(from = 0)
+ val warmupIterations: Int = 0
+ ) : CompilationMode() {
+ init {
+ require(warmupIterations >= 0) {
+ "warmupIterations must be non-negative, was $warmupIterations"
+ }
+ require(
+ baselineProfileMode != BaselineProfileMode.Disable || warmupIterations > 0
+ ) {
+ "Must set baselineProfileMode != Ignore, or warmup iterations > 0 to define" +
+ " which portion of the app to pre-compile."
+ }
+ }
+
+ override fun toString(): String {
+ return if (
+ baselineProfileMode == BaselineProfileMode.Require && warmupIterations == 0
+ ) {
+ "BaselineProfile"
+ } else if (baselineProfileMode == BaselineProfileMode.Disable && warmupIterations > 0) {
+ "WarmupProfile(iterations=$warmupIterations)"
+ } else {
+ "Partial(baselineProfile=$baselineProfileMode,iterations=$warmupIterations)"
+ }
+ }
+
+ /**
+ * Returns null on success, or an error string otherwise.
+ *
+ * Returned error strings aren't thrown, to let the calling function decide strictness.
+ */
+ private fun broadcastBaselineProfileInstall(packageName: String): String? {
+ // For baseline profiles, we trigger this broadcast to force the baseline profile to be
+ // installed synchronously
+ val action = ProfileInstallReceiver.ACTION_INSTALL_PROFILE
+ // Use an explicit broadcast given the app was force-stopped.
+ val name = ProfileInstallReceiver::class.java.name
+ val result = Shell.executeCommand("am broadcast -a $action $packageName/$name")
+ .substringAfter("Broadcast completed: result=")
+ .trim()
+ .toIntOrNull()
+ when (result) {
+ null,
+ // 0 is returned by the platform by default, and also if no broadcast receiver
+ // receives the broadcast.
+ 0 -> {
+ return "The baseline profile install broadcast was not received. " +
+ "This most likely means that the profileinstaller library is missing " +
+ "from the target apk."
+ }
+ ProfileInstaller.RESULT_INSTALL_SUCCESS -> {
+ return null // success!
+ }
+ ProfileInstaller.RESULT_ALREADY_INSTALLED -> {
+ throw RuntimeException(
+ "Unable to install baseline profile. This most likely means that the " +
+ "latest version of the profileinstaller library is not being used. " +
+ "Please use the latest profileinstaller library version " +
+ "in the target app."
+ )
+ }
+ ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION -> {
+ throw RuntimeException(
+ "Baseline profiles aren't supported on this device version"
+ )
+ }
+ ProfileInstaller.RESULT_BASELINE_PROFILE_NOT_FOUND -> {
+ return "No baseline profile was found in the target apk."
+ }
+ ProfileInstaller.RESULT_NOT_WRITABLE,
+ ProfileInstaller.RESULT_DESIRED_FORMAT_UNSUPPORTED,
+ ProfileInstaller.RESULT_IO_EXCEPTION,
+ ProfileInstaller.RESULT_PARSE_EXCEPTION -> {
+ throw RuntimeException("Baseline Profile wasn't successfully installed")
+ }
+ else -> {
+ throw RuntimeException(
+ "unrecognized ProfileInstaller result code: $result"
+ )
+ }
+ }
+ }
+
+ override fun compileImpl(packageName: String, warmupBlock: () -> Unit) {
+ if (baselineProfileMode != BaselineProfileMode.Disable) {
+ val installErrorString = broadcastBaselineProfileInstall(packageName)
+ if (installErrorString == null) {
+ // baseline profile install success, kill process before compiling
+ Log.d(TAG, "Killing process $packageName")
+ Shell.executeCommand("am force-stop $packageName")
+ cmdPackageCompile(packageName, "speed-profile")
+ } else {
+ if (baselineProfileMode == BaselineProfileMode.Require) {
+ throw RuntimeException(installErrorString)
+ } else {
+ Log.d(TAG, installErrorString)
+ }
+ }
+ }
+ if (warmupIterations > 0) {
+ repeat(this.warmupIterations) {
+ warmupBlock()
+ }
+ // For speed profile compilation, ART team recommended to wait for 5 secs when app
+ // is in the foreground, dump the profile, wait for another 5 secs before
+ // speed-profile compilation.
+ Thread.sleep(5000)
+ val response = Shell.executeCommand("killall -s SIGUSR1 $packageName")
+ if (response.isNotBlank()) {
+ Log.d(TAG, "Received dump profile response $response")
+ throw RuntimeException("Failed to dump profile for $packageName ($response)")
+ }
+ cmdPackageCompile(packageName, "speed-profile")
+ }
+ }
+ }
+
+ /**
+ * Full ahead-of-time compilation.
+ *
+ * Equates to `cmd package compile -f -m speed <package>` on API 24+.
+ *
+ * On Android M (API 23), this is the only supported compilation mode, as all apps are
+ * fully compiled ahead-of-time.
+ */
+ @Suppress("CanSealedSubClassBeObject") // Leaving possibility for future configuration
+ class Full : CompilationMode() {
+ override fun toString(): String = "Full"
+
+ override fun compileImpl(packageName: String, warmupBlock: () -> Unit) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ cmdPackageCompile(packageName, "speed")
+ }
+ // Noop on older versions: apps are fully compiled at install time on API 23 and below
+ }
}
/**
@@ -77,10 +260,28 @@
*
* The compilation itself is performed with `cmd package compile -f -m speed-profile <package>`
*/
- public class SpeedProfile(
- public val warmupIterations: Int = 3
- ) : CompilationMode("speed-profile") {
- public override fun toString(): String = "SpeedProfile(iterations=$warmupIterations)"
+ @Deprecated(
+ message = "Use CompilationMode.Partial to partially pre-compile the target app",
+ replaceWith = ReplaceWith(
+ expression = "CompilationMode.Partial(baselineProfileMode=" +
+ "BaselineProfileMode.Ignore, warmupIterations=warmupIterations)",
+ imports = ["androidx.benchmark.macro.BaselineProfileMode"]
+ )
+ )
+ class SpeedProfile(
+ val warmupIterations: Int = 3
+ ) : CompilationMode() {
+ override fun toString(): String = "SpeedProfile(iterations=$warmupIterations)"
+
+ override fun compileImpl(packageName: String, warmupBlock: () -> Unit) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ Partial(BaselineProfileMode.Disable, warmupIterations).compileImpl(
+ packageName,
+ warmupBlock
+ )
+ }
+ // Noop on older versions: this is compat behavior with previous library releases
+ }
}
/**
@@ -91,8 +292,22 @@
*
* The compilation itself is performed with `cmd package compile -f -m speed-profile <package>`
*/
- public object BaselineProfile : CompilationMode("speed-profile") {
- public override fun toString(): String = "BaselineProfile"
+ @Deprecated(
+ message = "Use CompilationMode.Partial to partially pre-compile the target app",
+ replaceWith = ReplaceWith("CompilationMode.Partial()")
+ )
+ object BaselineProfile : CompilationMode() {
+ override fun toString(): String = "BaselineProfile"
+
+ override fun compileImpl(packageName: String, warmupBlock: () -> Unit) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ Partial(BaselineProfileMode.Require, 0).compileImpl(
+ packageName,
+ warmupBlock
+ )
+ }
+ // Noop on older versions: this is compat behavior with previous library releases
+ }
}
/**
@@ -100,8 +315,16 @@
*
* Equates to `cmd package compile -f -m speed <package>`
*/
- public object Speed : CompilationMode("speed") {
- public override fun toString(): String = "Speed"
+ @Deprecated(
+ message = "Use CompilationMode.Full to fully compile the target app",
+ replaceWith = ReplaceWith("CompilationMode.Full")
+ )
+ object Speed : CompilationMode() {
+ override fun toString(): String = "Speed"
+
+ override fun compileImpl(packageName: String, warmupBlock: () -> Unit) {
+ Full().compileImpl(packageName, warmupBlock)
+ }
}
/**
@@ -110,99 +333,47 @@
* Note: this mode will only be supported on rooted devices with jit disabled. For this reason,
* it's only available for internal benchmarking.
*
+ * TODO: migrate this to an internal-only flag on [None] instead
+ *
* @suppress
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- public object Interpreted : CompilationMode(null) {
- public override fun toString(): String = "Interpreted"
- }
-}
+ object Interpreted : CompilationMode() {
+ override fun toString(): String = "Interpreted"
-/**
- * Compiles the application with the given mode.
- *
- * For more information: https://source.android.com/devices/tech/dalvik/jit-compiler
- */
-internal fun CompilationMode.compile(packageName: String, block: () -> Unit) {
- if (Build.VERSION.SDK_INT < 24) {
- // All supported versions prior to 24 were full AOT
- // TODO: clarify this with CompilationMode errors on these versions
- return
+ override fun compileImpl(packageName: String, warmupBlock: () -> Unit) {
+ // Nothing to do - handled externally
+ }
}
- // Clear profile between runs.
- Log.d(TAG, "Clearing profiles for $packageName")
- Shell.executeCommand("cmd package compile --reset $packageName")
- if (this == CompilationMode.None || this == CompilationMode.Interpreted) {
- return // nothing to do
- } else if (this == CompilationMode.BaselineProfile) {
- // For baseline profiles, if the ProfileInstaller library is included in the APK, then we
- // triggering this broadcast will cause the baseline profile to get installed
- // synchronously, instead of waiting for the
- val action = ProfileInstallReceiver.ACTION_INSTALL_PROFILE
- // Use an explicit broadcast given the app was force-stopped.
- val name = ProfileInstallReceiver::class.java.name
- val result = Shell.executeCommand("am broadcast -a $action $packageName/$name")
- .substringAfter("Broadcast completed: result=")
- .trim()
- .toIntOrNull()
- when (result) {
- null,
- // 0 is returned by the platform by default, and also if no broadcast receiver
- // receives the broadcast.
- 0 -> {
- throw RuntimeException(
- "The baseline profile install broadcast was not received. This most likely " +
- "means that the profileinstaller library is not in the target APK. It " +
- "must be in order to use CompilationMode.BaselineProfile."
- )
- }
- ProfileInstaller.RESULT_INSTALL_SUCCESS -> {
- // success !
- }
- ProfileInstaller.RESULT_ALREADY_INSTALLED -> {
- throw RuntimeException(
- "Unable to install baseline profiles. This most likely means that the latest " +
- "version of the profileinstaller library is not being used. Please " +
- "use the latest profileinstaller library version."
- )
- }
- ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION -> {
- throw RuntimeException("Baseline profiles aren't supported on this device version")
- }
- ProfileInstaller.RESULT_BASELINE_PROFILE_NOT_FOUND -> {
- throw RuntimeException("No baseline profile was found in the target apk.")
- }
- ProfileInstaller.RESULT_NOT_WRITABLE,
- ProfileInstaller.RESULT_DESIRED_FORMAT_UNSUPPORTED,
- ProfileInstaller.RESULT_IO_EXCEPTION,
- ProfileInstaller.RESULT_PARSE_EXCEPTION -> {
- throw RuntimeException("Baseline Profile wasn't successfully installed")
- }
- else -> {
- throw RuntimeException(
- "unrecognized ProfileInstaller result code: $result"
- )
- }
- }
- // Kill process before compiling
- Log.d(TAG, "Killing process $packageName")
- Shell.executeCommand("am force-stop $packageName")
- } else if (this is SpeedProfile) {
- repeat(this.warmupIterations) {
- block()
- }
- // For speed profile compilation, ART team recommended to wait for 5 secs when app
- // is in the foreground, dump the profile, wait for another 5 secs before
- // speed-profile compilation.
- Thread.sleep(5000)
- val response = Shell.executeCommand("killall -s SIGUSR1 $packageName")
- if (response.isNotBlank()) {
- Log.d(TAG, "Received dump profile response $response")
- throw RuntimeException("Failed to dump profile for $packageName ($response)")
+ companion object {
+ internal val noop: CompilationMode = if (Build.VERSION.SDK_INT >= 24) None() else Full()
+
+ /**
+ * Represents the default compilation mode for the platform, on an end user's device.
+ *
+ * This is a post-store-install app configuration for this device's SDK
+ * version - [`Partial(BaselineProfileMode.IncludeIfAvailable)`][Partial] on
+ * API 24+, and [Full] prior to API 24 (where all apps are fully AOT compiled).
+ *
+ * On API 24+, Baseline Profile pre-compilation is used if possible, but no error will be
+ * thrown if installation fails.
+ *
+ * Generally, it is preferable to explicitly pass a compilation mode, such as
+ * [Partial(BaselineProfileMode.Include)][Partial] to avoid ambiguity, and e.g. validate an
+ * app's BaselineProfile can be correctly used.
+ */
+ @JvmField
+ val DEFAULT: CompilationMode = if (Build.VERSION.SDK_INT >= 24) {
+ Partial(
+ baselineProfileMode = BaselineProfileMode.UseIfAvailable,
+ warmupIterations = 0
+ )
+ } else {
+ // API 23 is always fully compiled
+ Full()
}
}
- compilePackage(packageName)
}
/**
@@ -213,7 +384,7 @@
* @suppress
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public fun CompilationMode.isSupportedWithVmSettings(): Boolean {
+fun CompilationMode.isSupportedWithVmSettings(): Boolean {
val getProp = Shell.executeCommand("getprop dalvik.vm.extra-opts")
val vmRunningInterpretedOnly = getProp.contains("-Xusejit:false")
@@ -245,17 +416,3 @@
)
}
}
-
-/**
- * Compiles the application.
- */
-internal fun CompilationMode.compilePackage(packageName: String) {
- Log.d(TAG, "Compiling $packageName ($this)")
- val response = Shell.executeCommand(
- "cmd package compile -f -m ${compileArgument()} $packageName"
- )
- if (!response.contains("Success")) {
- Log.d(TAG, "Received compile cmd response: $response")
- throw RuntimeException("Failed to compile $packageName ($response)")
- }
-}
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 c1ae94e..c603bad 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
@@ -105,7 +105,7 @@
testName: String,
packageName: String,
metrics: List<Metric>,
- compilationMode: CompilationMode = CompilationMode.SpeedProfile(),
+ compilationMode: CompilationMode,
iterations: Int,
launchWithClearTask: Boolean,
startupModeMetricHint: StartupMode?,
@@ -118,6 +118,7 @@
require(metrics.isNotEmpty()) {
"Empty list of metrics passed to metrics param, must pass at least one Metric"
}
+
// skip benchmark if not supported by vm settings
compilationMode.assumeSupportedWithVmSettings()
@@ -134,7 +135,7 @@
scope.killProcess()
userspaceTrace("compile $packageName") {
- compilationMode.compile(packageName) {
+ compilationMode.resetAndCompile(packageName) {
setupBlock(scope)
measureBlock(scope)
}
@@ -257,10 +258,10 @@
}
}
- val warmupIterations = if (compilationMode is CompilationMode.SpeedProfile) {
- compilationMode.warmupIterations
- } else {
- 0
+ val warmupIterations = @Suppress("DEPRECATION") when (compilationMode) {
+ is CompilationMode.SpeedProfile -> compilationMode.warmupIterations
+ is CompilationMode.Partial -> compilationMode.warmupIterations
+ else -> 0
}
ResultWriter.appendReport(
@@ -285,13 +286,13 @@
* @suppress
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun macrobenchmarkWithStartupMode(
+fun macrobenchmarkWithStartupMode(
uniqueName: String,
className: String,
testName: String,
packageName: String,
metrics: List<Metric>,
- compilationMode: CompilationMode = CompilationMode.SpeedProfile(),
+ compilationMode: CompilationMode,
iterations: Int,
startupMode: StartupMode?,
setupBlock: MacrobenchmarkScope.() -> Unit,
@@ -325,8 +326,8 @@
// Empirically, this is also the scenario most significantly affected by this
// JIT persistence, so we optimize specifically for measurement correctness in
// this scenario.
- if (compilationMode == CompilationMode.None) {
- compilationMode.compile(packageName) {
+ if (compilationMode is CompilationMode.None) {
+ compilationMode.resetAndCompile(packageName = packageName) {
// This is only compiling for Compilation.None
throw IllegalStateException("block never used for CompilationMode.None")
}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupFullyDrawnBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupFullyDrawnBenchmark.kt
index 436794b..172ebda 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupFullyDrawnBenchmark.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupFullyDrawnBenchmark.kt
@@ -17,6 +17,7 @@
package androidx.benchmark.integration.macrobenchmark
import android.content.Intent
+import android.os.Build
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
@@ -27,6 +28,7 @@
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import androidx.testutils.getStartupMetrics
+import org.junit.Assume.assumeFalse
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -45,7 +47,11 @@
val benchmarkRule = MacrobenchmarkRule()
private fun startup(startupMode: StartupMode) = benchmarkRule.measureRepeated(
- compilationMode = CompilationMode.None,
+ compilationMode = if (Build.VERSION.SDK_INT >= 24) {
+ CompilationMode.None()
+ } else {
+ CompilationMode.Full()
+ },
packageName = TARGET_PACKAGE_NAME,
metrics = getStartupMetrics(),
startupMode = startupMode,
@@ -64,7 +70,12 @@
}
@Test
- fun hot() = startup(StartupMode.HOT)
+ fun hot() {
+ // b/204572406 - HOT doesn't work on Angler API 23 in CI, but failure doesn't repro locally
+ assumeFalse(Build.VERSION.SDK_INT == 23 && Build.DEVICE == "angler")
+
+ startup(StartupMode.HOT)
+ }
@Test
fun warm() = startup(StartupMode.WARM)
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupJavaBenchmark.java b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupJavaBenchmark.java
index 3ee5bde..54baa7b 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupJavaBenchmark.java
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupJavaBenchmark.java
@@ -39,7 +39,7 @@
mBenchmarkRule.measureRepeated(
"androidx.benchmark.integration.macrobenchmark.target",
Collections.singletonList(new StartupTimingMetric()),
- new CompilationMode.SpeedProfile(),
+ new CompilationMode.Partial(),
StartupMode.COLD,
3,
scope -> {
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/public/src/main/kotlin/androidx/build/LibraryVersions.kt
index fcf6e5f..bf911bf 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -22,58 +22,58 @@
object LibraryVersions {
val ACTIVITY = Version("1.5.0-alpha01")
val ADS_IDENTIFIER = Version("1.0.0-alpha05")
- val ANNOTATION = Version("1.4.0-alpha01")
+ val ANNOTATION = Version("1.4.0-alpha02")
val ANNOTATION_EXPERIMENTAL = Version("1.3.0-alpha01")
val APPCOMPAT = Version("1.5.0-alpha01")
val APPSEARCH = Version("1.0.0-alpha05")
val ARCH_CORE = Version("2.2.0-alpha01")
val ASYNCLAYOUTINFLATER = Version("1.1.0-alpha01")
val AUTOFILL = Version("1.2.0-beta02")
- val BENCHMARK = Version("1.1.0-alpha13")
+ val BENCHMARK = Version("1.1.0-alpha14")
val BIOMETRIC = Version("1.2.0-alpha05")
val BROWSER = Version("1.5.0-alpha01")
val BUILDSRC_TESTS = Version("1.0.0-alpha01")
- val CAMERA = Version("1.1.0-alpha12")
- val CAMERA_EXTENSIONS = Version("1.0.0-alpha32")
+ val CAMERA = Version("1.1.0-alpha13")
+ val CAMERA_EXTENSIONS = Version("1.0.0-alpha33")
val CAMERA_PIPE = Version("1.0.0-alpha01")
- val CAMERA_VIEW = Version("1.0.0-alpha32")
+ val CAMERA_VIEW = Version("1.0.0-alpha33")
val CARDVIEW = Version("1.1.0-alpha01")
- val CAR_APP = Version("1.2.0-alpha02")
+ val CAR_APP = Version("1.2.0-alpha03")
val COLLECTION = Version("1.3.0-alpha01")
val COLLECTION2 = Version("1.3.0-alpha01")
val CONTENTPAGER = Version("1.1.0-alpha01")
val COMPOSE_MATERIAL3 = Version(System.getenv("COMPOSE_CUSTOM_VERSION") ?: "1.0.0-alpha03")
val COMPOSE = Version(System.getenv("COMPOSE_CUSTOM_VERSION") ?: "1.2.0-alpha01")
- val COORDINATORLAYOUT = Version("1.2.0-rc01")
- val CORE = Version("1.8.0-alpha02")
+ val COORDINATORLAYOUT = Version("1.3.0-alpha01")
+ val CORE = Version("1.8.0-alpha03")
val CORE_ANIMATION = Version("1.0.0-alpha03")
val CORE_ANIMATION_TESTING = Version("1.0.0-alpha03")
val CORE_APPDIGEST = Version("1.0.0-alpha01")
val CORE_GOOGLE_SHORTCUTS = Version("1.1.0-alpha02")
- val CORE_PERFORMANCE = Version("1.0.0-alpha01")
- val CORE_REMOTEVIEWS = Version("1.0.0-alpha01")
- val CORE_ROLE = Version("1.1.0-rc01")
+ val CORE_PERFORMANCE = Version("1.0.0-alpha02")
+ val CORE_REMOTEVIEWS = Version("1.0.0-alpha02")
+ val CORE_ROLE = Version("1.2.0-alpha01")
val CORE_SPLASHSCREEN = Version("1.0.0-alpha03")
val CURSORADAPTER = Version("1.1.0-alpha01")
val CUSTOMVIEW = Version("1.2.0-alpha01")
val DATASTORE = Version("1.1.0-alpha01")
val DOCUMENTFILE = Version("1.1.0-alpha02")
- val DRAGANDDROP = Version("1.0.0-alpha02")
+ val DRAGANDDROP = Version("1.0.0-alpha03")
val DRAWERLAYOUT = Version("1.2.0-alpha01")
val DYNAMICANIMATION = Version("1.1.0-alpha04")
val DYNAMICANIMATION_KTX = Version("1.0.0-alpha04")
val EMOJI = Version("1.2.0-alpha03")
- val EMOJI2 = Version("1.1.0-alpha01")
+ val EMOJI2 = Version("1.1.0-alpha02")
val ENTERPRISE = Version("1.1.0-rc01")
val EXIFINTERFACE = Version("1.4.0-alpha01")
val FRAGMENT = Version("1.5.0-alpha01")
val FUTURES = Version("1.2.0-alpha01")
- val GLANCE = Version("1.0.0-alpha01")
+ val GLANCE = Version("1.0.0-alpha02")
val GRIDLAYOUT = Version("1.1.0-alpha01")
val HEALTH_SERVICES_CLIENT = Version("1.0.0-alpha04")
val HEIFWRITER = Version("1.1.0-alpha02")
val HILT = Version("1.1.0-alpha01")
- val HILT_NAVIGATION_COMPOSE = Version("1.0.0-rc01")
+ val HILT_NAVIGATION_COMPOSE = Version("1.1.0-alpha01")
val INSPECTION = Version("1.0.0")
val INTERPOLATOR = Version("1.1.0-alpha01")
val JETIFIER = Version("1.0.0-beta11")
@@ -83,7 +83,7 @@
val LEANBACK_TAB = Version("1.1.0-beta01")
val LEANBACK_GRID = Version("1.0.0-alpha02")
val LEGACY = Version("1.1.0-alpha01")
- val LOCALBROADCASTMANAGER = Version("1.1.0-rc01")
+ val LOCALBROADCASTMANAGER = Version("1.2.0-alpha01")
val LIBYUV = Version("0.1.0-dev01")
val LIFECYCLE = Version("2.5.0-alpha01")
val LIFECYCLE_VIEWMODEL_COMPOSE = Version("2.5.0-alpha01")
@@ -91,7 +91,7 @@
val LOADER = Version("1.2.0-alpha01")
val MEDIA = Version("1.5.0-beta02")
val MEDIA2 = Version("1.3.0-alpha01")
- val MEDIAROUTER = Version("1.3.0-alpha01")
+ val MEDIAROUTER = Version("1.3.0-alpha02")
val METRICS = Version("1.0.0-alpha01")
val NAVIGATION = Version("2.5.0-alpha01")
val PAGING = Version("3.2.0-alpha01")
@@ -126,7 +126,7 @@
val TESTSCREENSHOT = Version("1.0.0-alpha01")
val TEXT = Version("1.0.0-alpha01")
val TEXTCLASSIFIER = Version("1.0.0-alpha03")
- val TRACING = Version("1.1.0-beta02")
+ val TRACING = Version("1.1.0-beta03")
val TRANSITION = Version("1.5.0-alpha01")
val TVPROVIDER = Version("1.1.0-alpha02")
val VECTORDRAWABLE = Version("1.2.0-alpha03")
@@ -136,14 +136,14 @@
val VIEWPAGER = Version("1.1.0-alpha02")
val VIEWPAGER2 = Version("1.1.0-beta02")
val WEAR = Version("1.3.0-alpha02")
- val WEAR_COMPOSE = Version("1.0.0-alpha13")
+ val WEAR_COMPOSE = Version("1.0.0-alpha14")
val WEAR_INPUT = Version("1.2.0-alpha03")
val WEAR_INPUT_TESTING = WEAR_INPUT
val WEAR_ONGOING = Version("1.1.0-alpha01")
- val WEAR_PHONE_INTERACTIONS = Version("1.1.0-alpha02")
+ val WEAR_PHONE_INTERACTIONS = Version("1.1.0-alpha03")
val WEAR_REMOTE_INTERACTIONS = Version("1.1.0-alpha01")
val WEAR_TILES = Version("1.1.0-alpha01")
- val WEAR_WATCHFACE = Version("1.1.0-alpha01")
+ val WEAR_WATCHFACE = Version("1.1.0-alpha02")
val WEBKIT = Version("1.5.0-alpha01")
val WINDOW = Version("1.1.0-alpha01")
val WINDOW_EXTENSIONS = Version("1.1.0-alpha01")
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
index 5cd5948..7102a4a 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
@@ -23,6 +23,7 @@
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.integration.adapter.CameraControlAdapter
import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageCapture
import androidx.camera.core.impl.CameraCaptureCallback
import androidx.camera.core.impl.CameraCaptureFailure
import androidx.camera.core.impl.CameraCaptureResult
@@ -138,7 +139,11 @@
}.build()
// Act
- cameraControl!!.submitStillCaptureRequests(listOf(captureConfig))
+ cameraControl!!.submitStillCaptureRequests(
+ listOf(captureConfig),
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ )
// Assert
Truth.assertThat(
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt
index 4c0462c..1422354 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt
@@ -29,7 +29,6 @@
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
-import androidx.camera.core.ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH
import androidx.camera.core.internal.CameraUseCaseAdapter
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraXUtil
@@ -162,40 +161,6 @@
}
@Test
- fun setExposureAndStartFlashSequence_theExposureSettingShouldApply() = runBlocking {
- val exposureState = camera.cameraInfo.exposureState
- Assume.assumeTrue(exposureState.isExposureCompensationSupported)
-
- bindUseCase()
-
- // Act. Set the exposure compensation
- val upper = exposureState.exposureCompensationRange.upper
- cameraControl.setExposureCompensationIndex(upper).get(3000, TimeUnit.MILLISECONDS)
- // Test the flash API after exposure changed.
- cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH).get(3000, TimeUnit.MILLISECONDS)
-
- // Assert. Verify the exposure compensation target result is in the capture result.
- registerListener().verifyCaptureResultParameter(CONTROL_AE_EXPOSURE_COMPENSATION, upper)
- }
-
- @Test
- fun setExposureAndTriggerAf_theExposureSettingShouldApply() = runBlocking {
- val exposureState = camera.cameraInfo.exposureState
- Assume.assumeTrue(exposureState.isExposureCompensationSupported)
-
- bindUseCase()
-
- // Act. Set the exposure compensation, and then use the AF API after the exposure is
- // changed.
- val upper = exposureState.exposureCompensationRange.upper
- cameraControl.setExposureCompensationIndex(upper).get(3000, TimeUnit.MILLISECONDS)
- cameraControl.triggerAf().get(3000, TimeUnit.MILLISECONDS)
-
- // Assert. Verify the exposure compensation target result is in the capture result.
- registerListener().verifyCaptureResultParameter(CONTROL_AE_EXPOSURE_COMPENSATION, upper)
- }
-
- @Test
fun setExposureAndZoomRatio_theExposureSettingShouldApply() = runBlocking {
val exposureState = camera.cameraInfo.exposureState
Assume.assumeTrue(exposureState.isExposureCompensationSupported)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
index 532cdb2..ab5babb 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
@@ -38,7 +38,6 @@
import androidx.camera.core.FocusMeteringResult
import androidx.camera.core.ImageCapture
import androidx.camera.core.TorchState
-import androidx.camera.core.impl.CameraCaptureResult
import androidx.camera.core.impl.CameraControlInternal
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.Config
@@ -48,6 +47,7 @@
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
+import java.util.Collections
import javax.inject.Inject
/**
@@ -158,34 +158,22 @@
this.imageCaptureFlashMode = flashMode
}
- override fun triggerAf(): ListenableFuture<CameraCaptureResult> {
- warn { "TODO: triggerAf is not yet supported" }
- return Futures.immediateFuture(CameraCaptureResult.EmptyCameraCaptureResult.create())
- }
-
- override fun startFlashSequence(
- @ImageCapture.FlashType flashType: Int
- ): ListenableFuture<Void> {
- warn { "TODO: startFlashSequence is not yet supported" }
- return Futures.immediateFuture(null)
- }
-
- override fun cancelAfAndFinishFlashSequence(
- cancelAfTrigger: Boolean,
- finishFlashSequence: Boolean
- ) {
- warn { "TODO: cancelAfAndFinishFlashSequence is not yet supported" }
- }
-
override fun setExposureCompensationIndex(exposure: Int): ListenableFuture<Int> =
Futures.nonCancellationPropagating(
evCompControl.updateAsync(exposure).asListenableFuture()
)
- override fun submitStillCaptureRequests(captureConfigs: List<CaptureConfig>) {
+ override fun submitStillCaptureRequests(
+ captureConfigs: List<CaptureConfig>,
+ captureMode: Int,
+ flashType: Int,
+ ): ListenableFuture<List<Void>> {
val camera = useCaseManager.camera
checkNotNull(camera) { "Attempted to issue capture requests while the camera isn't ready." }
camera.capture(captureConfigs)
+
+ // TODO(b/199813515) : implement the preCapture
+ return Futures.immediateFuture(Collections.emptyList())
}
override fun getSessionConfig(): SessionConfig {
diff --git a/camera/camera-camera2/build.gradle b/camera/camera-camera2/build.gradle
index d18a2f3..63b4d89 100644
--- a/camera/camera-camera2/build.gradle
+++ b/camera/camera-camera2/build.gradle
@@ -38,13 +38,15 @@
testImplementation(libs.testRunner)
testImplementation(libs.junit)
testImplementation(libs.truth)
- testImplementation("org.robolectric:robolectric:4.6.1") // TODO(b/209062465): fix tests to work with SDK 31 and robolectric 4.7
+ testImplementation(libs.robolectric)
testImplementation(libs.mockitoCore)
testImplementation(libs.kotlinCoroutinesTest)
testImplementation("androidx.annotation:annotation-experimental:1.1.0")
+ testImplementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
testImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
testImplementation(project(":camera:camera-testing"))
testImplementation("androidx.arch.core:core-testing:2.1.0")
+ testImplementation("junit:junit:4.13") // Needed for Assert.assertThrows
testImplementation("org.apache.maven:maven-ant-tasks:2.1.3")
androidTestImplementation(libs.multidex)
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
index f9d5b24..964ea8a 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
@@ -38,6 +38,7 @@
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;
@@ -73,6 +74,7 @@
import androidx.camera.testing.CameraUtil;
import androidx.camera.testing.CameraXUtil;
import androidx.camera.testing.HandlerUtil;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.core.os.HandlerCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -93,6 +95,7 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
+import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
@@ -354,29 +357,70 @@
public void triggerAf_futureSucceeds() throws Exception {
Camera2CameraControlImpl camera2CameraControlImpl =
createCamera2CameraControlWithPhysicalCamera();
- ListenableFuture<CameraCaptureResult> future = camera2CameraControlImpl.triggerAf();
+
+ ListenableFuture<CameraCaptureResult> future = CallbackToFutureAdapter.getFuture(c -> {
+ camera2CameraControlImpl.mExecutor.execute(() ->
+ camera2CameraControlImpl.getFocusMeteringControl().triggerAf(
+ c, /* overrideAeMode */ false));
+ return "triggerAf";
+ });
+
future.get(5, TimeUnit.SECONDS);
}
@Test
- @LargeTest
- public void startFlashSequence_futureSucceeds() throws Exception {
- Camera2CameraControlImpl camera2CameraControlImpl =
- createCamera2CameraControlWithPhysicalCamera();
- ListenableFuture<Void> future = camera2CameraControlImpl.startFlashSequence(
+ public void captureMaxQuality_shouldSuccess()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ captureTest(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY,
ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
- future.get(5, TimeUnit.SECONDS);
}
@Test
- @LargeTest
- public void setFlashModeAndStartFlashSequence_futureSucceeds() throws Exception {
- Camera2CameraControlImpl camera2CameraControlImpl =
- createCamera2CameraControlWithPhysicalCamera();
- camera2CameraControlImpl.setFlashMode(ImageCapture.FLASH_MODE_ON);
- ListenableFuture<Void> future = camera2CameraControlImpl.startFlashSequence(
+ public void captureMiniLatency_shouldSuccess()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ captureTest(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
- future.get(5, TimeUnit.SECONDS);
+ }
+
+ @Test
+ public void captureMaxQuality_torchAsFlash_shouldSuccess()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ captureTest(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY,
+ ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH);
+ }
+
+ @Test
+ public void captureMiniLatency_torchAsFlash_shouldSuccess()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ captureTest(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH);
+ }
+
+ private void captureTest(int captureMode, int flashType)
+ throws ExecutionException, InterruptedException, TimeoutException {
+ ImageCapture imageCapture = new ImageCapture.Builder().build();
+
+ mCamera = CameraUtil.createCameraAndAttachUseCase(
+ ApplicationProvider.getApplicationContext(), CameraSelector.DEFAULT_BACK_CAMERA,
+ imageCapture);
+
+ Camera2CameraControlImpl camera2CameraControlImpl =
+ (Camera2CameraControlImpl) mCamera.getCameraControl();
+
+ CameraCaptureCallback captureCallback = mock(CameraCaptureCallback.class);
+ CaptureConfig.Builder captureConfigBuilder = new CaptureConfig.Builder();
+ captureConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_STILL_CAPTURE);
+ captureConfigBuilder.addSurface(imageCapture.getSessionConfig().getSurfaces().get(0));
+ captureConfigBuilder.addCameraCaptureCallback(captureCallback);
+
+ ListenableFuture<List<Void>> future = camera2CameraControlImpl.submitStillCaptureRequests(
+ Arrays.asList(captureConfigBuilder.build()), captureMode, flashType);
+
+ // The future should successfully complete
+ future.get(10, TimeUnit.SECONDS);
+ // CameraCaptureCallback.onCaptureCompleted() should be called to signal a capture attempt.
+ verify(captureCallback, timeout(3000).times(1))
+ .onCaptureCompleted(any(CameraCaptureResult.class));
}
private Camera2CameraControlImpl createCamera2CameraControlWithPhysicalCamera() {
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index e57c7a6..e207dbf 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -49,6 +49,7 @@
import androidx.camera.core.Camera;
import androidx.camera.core.CameraControl;
import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ImageCapture;
import androidx.camera.core.InitializationException;
import androidx.camera.core.UseCase;
import androidx.camera.core.impl.CameraCaptureCallback;
@@ -402,7 +403,9 @@
captureConfigBuilder.addCameraCaptureCallback(captureCallback);
mCamera2CameraImpl.getCameraControlInternal().submitStillCaptureRequests(
- Arrays.asList(captureConfigBuilder.build()));
+ Arrays.asList(captureConfigBuilder.build()),
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
UseCase useCase2 = createUseCase();
mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase2));
@@ -439,7 +442,9 @@
captureConfigBuilder.addCameraCaptureCallback(captureCallback);
mCamera2CameraImpl.getCameraControlInternal().submitStillCaptureRequests(
- Arrays.asList(captureConfigBuilder.build()));
+ Arrays.asList(captureConfigBuilder.build()),
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1));
// Unblock camera handle to make camera operation run quickly .
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
index d4caf95..9b8d57bd 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
@@ -286,70 +286,6 @@
}
@Test
- public void setExposureAndStartFlashSequence_theExposureSettingShouldApply()
- throws InterruptedException, ExecutionException, TimeoutException,
- CameraUseCaseAdapter.CameraException {
- ExposureState exposureState = mCameraInfoInternal.getExposureState();
- assumeTrue(exposureState.isExposureCompensationSupported());
-
- FakeTestUseCase useCase = openUseCase();
- ArgumentCaptor<TotalCaptureResult> captureResultCaptor = ArgumentCaptor.forClass(
- TotalCaptureResult.class);
- CameraCaptureSession.CaptureCallback callback = mock(
- CameraCaptureSession.CaptureCallback.class);
- useCase.setCameraCaptureCallback(callback);
-
- // Wait a little bit for the camera to open.
- assertTrue(mSessionStateCallback.waitForOnConfigured(1));
-
- // Set the exposure compensation
- int upper = exposureState.getExposureCompensationRange().getUpper();
- mCameraControlInternal.setExposureCompensationIndex(upper).get(3000, TimeUnit.MILLISECONDS);
- mCameraControlInternal.startFlashSequence(ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH).get(3000,
- TimeUnit.MILLISECONDS);
-
- // Verify the exposure compensation target result is in the capture result.
- verify(callback, timeout(3000).atLeastOnce()).onCaptureCompleted(
- any(CameraCaptureSession.class),
- any(CaptureRequest.class),
- captureResultCaptor.capture());
- List<TotalCaptureResult> totalCaptureResults = captureResultCaptor.getAllValues();
- TotalCaptureResult result = totalCaptureResults.get(totalCaptureResults.size() - 1);
- assertThat(result.get(CaptureResult.CONTROL_AE_EXPOSURE_COMPENSATION)).isEqualTo(upper);
- }
-
- @Test
- public void setExposureAndTriggerAf_theExposureSettingShouldApply()
- throws InterruptedException, ExecutionException, TimeoutException,
- CameraUseCaseAdapter.CameraException {
- ExposureState exposureState = mCameraInfoInternal.getExposureState();
- assumeTrue(exposureState.isExposureCompensationSupported());
-
- FakeTestUseCase useCase = openUseCase();
- ArgumentCaptor<TotalCaptureResult> captureResultCaptor = ArgumentCaptor.forClass(
- TotalCaptureResult.class);
- CameraCaptureSession.CaptureCallback callback = mock(
- CameraCaptureSession.CaptureCallback.class);
- useCase.setCameraCaptureCallback(callback);
-
- // Wait a little bit for the camera to open.
- assertTrue(mSessionStateCallback.waitForOnConfigured(1));
-
- int upper = exposureState.getExposureCompensationRange().getUpper();
- mCameraControlInternal.setExposureCompensationIndex(upper).get(3000, TimeUnit.MILLISECONDS);
- mCameraControlInternal.triggerAf().get(3000, TimeUnit.MILLISECONDS);
-
- // Verify the exposure compensation target result is in the capture result.
- verify(callback, timeout(3000).atLeastOnce()).onCaptureCompleted(
- any(CameraCaptureSession.class),
- any(CaptureRequest.class),
- captureResultCaptor.capture());
- List<TotalCaptureResult> totalCaptureResults = captureResultCaptor.getAllValues();
- TotalCaptureResult result = totalCaptureResults.get(totalCaptureResults.size() - 1);
- assertThat(result.get(CaptureResult.CONTROL_AE_EXPOSURE_COMPENSATION)).isEqualTo(upper);
- }
-
- @Test
public void setExposureAndZoomRatio_theExposureSettingShouldApply()
throws InterruptedException, ExecutionException, TimeoutException,
CameraUseCaseAdapter.CameraException {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
index 654e397..5cbc416 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
@@ -19,7 +19,6 @@
import static androidx.camera.core.ImageCapture.FLASH_MODE_AUTO;
import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
import static androidx.camera.core.ImageCapture.FLASH_MODE_ON;
-import static androidx.camera.core.ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH;
import android.graphics.Rect;
import android.hardware.camera2.CameraCaptureSession;
@@ -42,8 +41,6 @@
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.internal.compat.workaround.AeFpsRange;
import androidx.camera.camera2.internal.compat.workaround.AutoFlashAEModeDisabler;
-import androidx.camera.camera2.internal.compat.workaround.OverrideAeModeForStillCapture;
-import androidx.camera.camera2.internal.compat.workaround.UseTorchAsFlash;
import androidx.camera.camera2.interop.Camera2CameraControl;
import androidx.camera.camera2.interop.CaptureRequestOptions;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
@@ -128,19 +125,16 @@
private final TorchControl mTorchControl;
private final ExposureControl mExposureControl;
private final Camera2CameraControl mCamera2CameraControl;
+ private final Camera2CapturePipeline mCamera2CapturePipeline;
@GuardedBy("mLock")
private int mUseCount = 0;
// use volatile modifier to make these variables in sync in all threads.
private volatile boolean mIsTorchOn = false;
- private boolean mIsTorchEnabledByFlash = false;
- private boolean mIsAeTriggeredByFlash = false;
@ImageCapture.FlashMode
private volatile int mFlashMode = FLASH_MODE_OFF;
// Workarounds
private final AeFpsRange mAeFpsRange;
- private final UseTorchAsFlash mUseTorchAsFlash;
- private final OverrideAeModeForStillCapture mOverrideAeModeForStillCapture;
private final AutoFlashAEModeDisabler mAutoFlashAEModeDisabler = new AutoFlashAEModeDisabler();
static final String TAG_SESSION_UPDATE_ID = "CameraControlSessionUpdateId";
@@ -202,10 +196,9 @@
// Workarounds
mAeFpsRange = new AeFpsRange(cameraQuirks);
- mUseTorchAsFlash = new UseTorchAsFlash(cameraQuirks);
- mOverrideAeModeForStillCapture = new OverrideAeModeForStillCapture(cameraQuirks);
-
mCamera2CameraControl = new Camera2CameraControl(this, mExecutor);
+ mCamera2CapturePipeline = new Camera2CapturePipeline(this, mCameraCharacteristics,
+ cameraQuirks, mExecutor);
mExecutor.execute(
() -> addCaptureResultListener(mCamera2CameraControl.getCaptureRequestListener()));
}
@@ -387,27 +380,6 @@
return Futures.nonCancellationPropagating(mTorchControl.enableTorch(torch));
}
- /**
- * Issues a {@link CaptureRequest#CONTROL_AF_TRIGGER_START} request to start auto focus scan.
- *
- * @return a {@link ListenableFuture} which completes when the request is completed.
- * Cancelling the ListenableFuture is a no-op.
- */
- @Override
- @NonNull
- public ListenableFuture<CameraCaptureResult> triggerAf() {
- if (!isControlInUse()) {
- return Futures.immediateFailedFuture(
- new OperationCanceledException("Camera is not active."));
- }
- return Futures.nonCancellationPropagating(CallbackToFutureAdapter.getFuture(
- completer -> {
- mExecutor.execute(() -> mFocusMeteringControl.triggerAf(
- completer, /* overrideAeMode */ false));
- return "triggerAf";
- }));
- }
-
@ExecutedBy("mExecutor")
@NonNull
private ListenableFuture<Void> waitForSessionUpdateId(long sessionUpdateIdToWait) {
@@ -448,94 +420,6 @@
return false;
}
- /**
- * {@inheritDoc}
- *
- * <p>Issues a {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_START} request to start auto
- * exposure scan. In some cases, torch flash will be used instead of issuing
- * {@code CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START}.
- *
- * @param flashType Uses one shot flash or use torch as flash when taking a picture.
- * @return a {@link ListenableFuture} which completes when the request is completed.
- * Cancelling the ListenableFuture is a no-op.
- */
- @Override
- @NonNull
- public ListenableFuture<Void> startFlashSequence(@ImageCapture.FlashType int flashType) {
- if (!isControlInUse()) {
- return Futures.immediateFailedFuture(
- new OperationCanceledException("Camera is not active."));
- }
-
- // Prior to AE precapture, wait until pending flash mode session change is completed. On
- // some devices, AE precapture may not work properly if the repeating request to change
- // the flash mode is not completed.
- ListenableFuture<Void> future = FutureChain.from(mFlashModeChangeSessionUpdateFuture)
- .transformAsync(v -> {
- return CallbackToFutureAdapter.getFuture(
- completer -> {
- if (mUseTorchAsFlash.shouldUseTorchAsFlash()
- || flashType == FLASH_TYPE_USE_TORCH_AS_FLASH
- || mTemplate == CameraDevice.TEMPLATE_RECORD) {
- Logger.d(TAG, "startFlashSequence: Use torch");
- if (mIsTorchOn) {
- completer.set(null);
- } else {
- mTorchControl.enableTorchInternal(completer, true);
- mIsTorchEnabledByFlash = true;
- }
- } else {
- Logger.d(TAG, "startFlashSequence: use triggerAePrecapture");
- mFocusMeteringControl.triggerAePrecapture(completer);
- mIsAeTriggeredByFlash = true;
- mOverrideAeModeForStillCapture.onAePrecaptureStarted();
- }
- return "startFlashSequence";
- });
- }, mExecutor);
-
- return Futures.nonCancellationPropagating(future);
- }
-
- /**
- * {@inheritDoc}
- *
- * <p>Issues {@link CaptureRequest#CONTROL_AF_TRIGGER_CANCEL} and/or {@link
- * CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL} request to cancel auto focus or auto
- * exposure scan.
- *
- * <p>When torch is used instead of issuing
- * {@code CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START} in
- * {@link #startFlashSequence(int)}, this method will close torch instead of issuing
- * {@code CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL}.
- */
- @Override
- public void cancelAfAndFinishFlashSequence(final boolean cancelAfTrigger,
- final boolean finishFlashSequence) {
- if (!isControlInUse()) {
- Logger.w(TAG, "Camera is not active.");
- return;
- }
- mExecutor.execute(() -> {
- boolean cancelAeTrigger = false;
- if (finishFlashSequence) {
- if (mIsTorchEnabledByFlash) {
- mIsTorchEnabledByFlash = false;
- mTorchControl.enableTorchInternal(null, false);
- }
- if (mIsAeTriggeredByFlash) {
- mIsAeTriggeredByFlash = false;
- cancelAeTrigger = true;
- mOverrideAeModeForStillCapture.onAePrecaptureFinished();
- }
- }
-
- if (cancelAfTrigger || cancelAeTrigger) {
- mFocusMeteringControl.cancelAfAeTrigger(cancelAfTrigger, cancelAeTrigger);
- }
- });
- }
-
@NonNull
@Override
public ListenableFuture<Integer> setExposureCompensationIndex(int exposure) {
@@ -547,45 +431,25 @@
}
/** {@inheritDoc} */
+ @NonNull
@Override
- public void submitStillCaptureRequests(@NonNull List<CaptureConfig> captureConfigs) {
+ public ListenableFuture<List<Void>> submitStillCaptureRequests(
+ @NonNull List<CaptureConfig> captureConfigs,
+ @ImageCapture.CaptureMode int captureMode,
+ @ImageCapture.FlashType int flashType) {
if (!isControlInUse()) {
Logger.w(TAG, "Camera is not active.");
- return;
+ return Futures.immediateFailedFuture(
+ new OperationCanceledException("Camera is not active."));
}
- mExecutor.execute(() -> {
- List<CaptureConfig> configsToSubmit = new ArrayList<>(captureConfigs);
- for (int i = 0; i < captureConfigs.size(); i++) {
- CaptureConfig captureConfig = captureConfigs.get(i);
- int templateToModify = CaptureConfig.TEMPLATE_TYPE_NONE;
- if (mTemplate == CameraDevice.TEMPLATE_RECORD && !isLegacyDevice()) {
- // Always override template by TEMPLATE_VIDEO_SNAPSHOT when repeating
- // template is TEMPLATE_RECORD. Note: TEMPLATE_VIDEO_SNAPSHOT is not
- // supported on legacy device.
- templateToModify = CameraDevice.TEMPLATE_VIDEO_SNAPSHOT;
- } else if (captureConfig.getTemplateType() == CaptureConfig.TEMPLATE_TYPE_NONE) {
- templateToModify = CameraDevice.TEMPLATE_STILL_CAPTURE;
- }
- if (templateToModify != CaptureConfig.TEMPLATE_TYPE_NONE
- || mOverrideAeModeForStillCapture.shouldSetAeModeAlwaysFlash(mFlashMode)) {
- CaptureConfig.Builder configBuilder = CaptureConfig.Builder.from(captureConfig);
- if (templateToModify != CaptureConfig.TEMPLATE_TYPE_NONE) {
- configBuilder.setTemplateType(templateToModify);
- }
-
- // Override AE Mode to ON_ALWAYS_FLASH if necessary.
- if (mOverrideAeModeForStillCapture.shouldSetAeModeAlwaysFlash(mFlashMode)) {
- Camera2ImplConfig.Builder impBuilder = new Camera2ImplConfig.Builder();
- impBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE,
- CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
- configBuilder.addImplementationOptions(impBuilder.build());
- }
- configsToSubmit.set(i, configBuilder.build());
- }
- }
- submitCaptureRequestsInternal(configsToSubmit);
- });
+ // Prior to submitStillCaptures, wait until the pending flash mode session change is
+ // completed. On some devices, AE precapture triggered in submitStillCaptures may not
+ // work properly if the repeating request to change the flash mode is not completed.
+ int flashMode = getFlashMode();
+ return FutureChain.from(mFlashModeChangeSessionUpdateFuture).transformAsync(
+ v -> mCamera2CapturePipeline.submitStillCaptures(
+ captureConfigs, captureMode, flashMode, flashType), mExecutor);
}
/** {@inheritDoc} */
@@ -608,6 +472,7 @@
mTemplate = template;
mFocusMeteringControl.setTemplate(mTemplate);
+ mCamera2CapturePipeline.setTemplate(mTemplate);
}
@ExecutedBy("mExecutor")
@@ -718,6 +583,10 @@
updateSessionConfigSynchronous();
}
+ @ExecutedBy("mExecutor")
+ boolean isTorchOn() {
+ return mIsTorchOn;
+ }
@ExecutedBy("mExecutor")
void submitCaptureRequestsInternal(final List<CaptureConfig> captureConfigs) {
@@ -904,12 +773,6 @@
return mCurrentSessionUpdateId;
}
- private boolean isLegacyDevice() {
- Integer level =
- mCameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
- return level != null && level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
- }
-
/** An interface to listen to camera capture results. */
public interface CaptureResultListener {
/**
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
new file mode 100644
index 0000000..43a658a
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
@@ -0,0 +1,656 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal;
+
+import static androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY;
+import static androidx.camera.core.ImageCapture.CaptureMode;
+import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
+import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
+import static androidx.camera.core.ImageCapture.FLASH_MODE_AUTO;
+import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
+import static androidx.camera.core.ImageCapture.FLASH_MODE_ON;
+import static androidx.camera.core.ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH;
+import static androidx.camera.core.ImageCapture.FlashMode;
+import static androidx.camera.core.ImageCapture.FlashType;
+import static androidx.camera.core.impl.CameraCaptureMetaData.AfMode;
+import static androidx.camera.core.impl.CameraCaptureMetaData.AfState;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.camera2.impl.Camera2ImplConfig;
+import androidx.camera.camera2.internal.annotation.CameraExecutor;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.workaround.OverrideAeModeForStillCapture;
+import androidx.camera.camera2.internal.compat.workaround.UseTorchAsFlash;
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.CameraCaptureCallback;
+import androidx.camera.core.impl.CameraCaptureFailure;
+import androidx.camera.core.impl.CameraCaptureMetaData.AeState;
+import androidx.camera.core.impl.CameraCaptureMetaData.AwbState;
+import androidx.camera.core.impl.CameraCaptureResult;
+import androidx.camera.core.impl.CaptureConfig;
+import androidx.camera.core.impl.Quirks;
+import androidx.camera.core.impl.annotation.ExecutedBy;
+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;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Implementation detail of the submitStillCaptures method.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class Camera2CapturePipeline {
+
+ private static final String TAG = "Camera2CapturePipeline";
+
+ @NonNull
+ private final Camera2CameraControlImpl mCameraControl;
+
+ @NonNull
+ private final UseTorchAsFlash mUseTorchAsFlash;
+
+ @NonNull
+ private final Quirks mCameraQuirk;
+
+ @NonNull
+ @CameraExecutor
+ private final Executor mExecutor;
+
+ private final boolean mIsLegacyDevice;
+
+ private int mTemplate = CameraDevice.TEMPLATE_PREVIEW;
+
+ /**
+ * Constructs a Camera2CapturePipeline for single capture use.
+ */
+ Camera2CapturePipeline(@NonNull Camera2CameraControlImpl cameraControl,
+ @NonNull CameraCharacteristicsCompat cameraCharacteristics,
+ @NonNull Quirks cameraQuirks,
+ @CameraExecutor @NonNull Executor executor) {
+ mCameraControl = cameraControl;
+ Integer level =
+ cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
+ mIsLegacyDevice = level != null
+ && level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
+ mExecutor = executor;
+ mCameraQuirk = cameraQuirks;
+ mUseTorchAsFlash = new UseTorchAsFlash(cameraQuirks);
+ }
+
+ @ExecutedBy("mExecutor")
+ public void setTemplate(int template) {
+ mTemplate = template;
+ }
+
+ /**
+ * Submit a list of capture configs to the camera, it returns a ListenableFuture
+ * which will be completed after all the captures were done.
+ *
+ * @return the future will be completed after all the captures are completed, It would
+ * fail with a {@link androidx.camera.core.ImageCapture#ERROR_CAMERA_CLOSED} when the
+ * capture was canceled, or {@link androidx.camera.core.ImageCapture#ERROR_CAPTURE_FAILED}
+ * when the capture was failed.
+ */
+ @ExecutedBy("mExecutor")
+ @NonNull
+ public ListenableFuture<List<Void>> submitStillCaptures(
+ @NonNull List<CaptureConfig> captureConfigs, @CaptureMode int captureMode,
+ @FlashMode int flashMode, @FlashType int flashType) {
+
+ OverrideAeModeForStillCapture aeQuirk = new OverrideAeModeForStillCapture(mCameraQuirk);
+ Pipeline pipeline = new Pipeline(mTemplate, mExecutor, mCameraControl, mIsLegacyDevice,
+ aeQuirk);
+
+ if (captureMode == CAPTURE_MODE_MAXIMIZE_QUALITY) {
+ pipeline.addTask(new AfTask(mCameraControl));
+ }
+
+ if (isTorchAsFlash(flashType)) {
+ pipeline.addTask(new TorchTask(mCameraControl, flashMode));
+ } else {
+ pipeline.addTask(new AePreCaptureTask(mCameraControl, flashMode, aeQuirk));
+ }
+
+ return Futures.nonCancellationPropagating(
+ pipeline.executeCapture(captureConfigs, flashMode));
+ }
+
+ /**
+ * The pipeline for single capturing.
+ */
+ @VisibleForTesting
+ static class Pipeline {
+ private static final long CHECK_3A_TIMEOUT_IN_NS = TimeUnit.SECONDS.toNanos(1);
+ private static final long CHECK_3A_WITH_FLASH_TIMEOUT_IN_NS = TimeUnit.SECONDS.toNanos(5);
+
+ private final int mTemplate;
+ private final Executor mExecutor;
+ private final Camera2CameraControlImpl mCameraControl;
+ private final OverrideAeModeForStillCapture mOverrideAeModeForStillCapture;
+ private final boolean mIsLegacyDevice;
+ private long mTimeout3A = CHECK_3A_TIMEOUT_IN_NS;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final List<PipelineTask> mTasks = new ArrayList<>();
+
+ private final PipelineTask mPipelineSubTask = new PipelineTask() {
+
+ @NonNull
+ @Override
+ public ListenableFuture<Boolean> preCapture(
+ @Nullable TotalCaptureResult captureResult) {
+ ArrayList<ListenableFuture<Boolean>> futures = new ArrayList<>();
+ for (PipelineTask task : mTasks) {
+ futures.add(task.preCapture(captureResult));
+ }
+ return Futures.transform(Futures.allAsList(futures),
+ results -> results.contains(true), CameraXExecutors.directExecutor());
+ }
+
+ @Override
+ public boolean isCaptureResultNeeded() {
+ for (PipelineTask task : mTasks) {
+ if (task.isCaptureResultNeeded()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void postCapture() {
+ for (PipelineTask task : mTasks) {
+ task.postCapture();
+ }
+ }
+ };
+
+ Pipeline(int template, @NonNull Executor executor,
+ @NonNull Camera2CameraControlImpl cameraControl, boolean isLegacyDevice,
+ @NonNull OverrideAeModeForStillCapture overrideAeModeForStillCapture) {
+ mTemplate = template;
+ mExecutor = executor;
+ mCameraControl = cameraControl;
+ mIsLegacyDevice = isLegacyDevice;
+ mOverrideAeModeForStillCapture = overrideAeModeForStillCapture;
+ }
+
+ /**
+ * Add the AE/AF/Torch tasks if required.
+ *
+ * @param task implements the PipelineTask interface
+ */
+ void addTask(@NonNull PipelineTask task) {
+ mTasks.add(task);
+ }
+
+ /**
+ * Set the timeout for the 3A converge.
+ *
+ * @param timeout3A in nano seconds
+ */
+ private void setTimeout3A(long timeout3A) {
+ mTimeout3A = timeout3A;
+ }
+
+ @SuppressWarnings("FutureReturnValueIgnored")
+ @ExecutedBy("mExecutor")
+ @NonNull
+ ListenableFuture<List<Void>> executeCapture(@NonNull List<CaptureConfig> captureConfigs,
+ @FlashMode int flashMode) {
+ ListenableFuture<TotalCaptureResult> preCapture = Futures.immediateFuture(null);
+ if (!mTasks.isEmpty()) {
+ ListenableFuture<TotalCaptureResult> getResult =
+ mPipelineSubTask.isCaptureResultNeeded() ? waitForResult(
+ ResultListener.NO_TIMEOUT, null) : Futures.immediateFuture(null);
+
+ preCapture = FutureChain.from(getResult).transformAsync(captureResult -> {
+ if (isFlashRequired(flashMode, captureResult)) {
+ setTimeout3A(CHECK_3A_WITH_FLASH_TIMEOUT_IN_NS);
+ }
+ return mPipelineSubTask.preCapture(captureResult);
+ }, mExecutor).transformAsync(is3aConvergeRequired -> {
+ if (is3aConvergeRequired) {
+ return waitForResult(mTimeout3A, this::is3AConverged);
+ }
+ return Futures.immediateFuture(null);
+ }, mExecutor);
+ }
+
+ ListenableFuture<List<Void>> future = FutureChain.from(preCapture).transformAsync(
+ v -> submitConfigsInternal(captureConfigs, flashMode), mExecutor);
+
+
+ /* Always call postCapture(), it will unlock3A if it was locked in preCapture.*/
+ future.addListener(() -> {
+ mPipelineSubTask.postCapture();
+ }, mExecutor);
+
+ return future;
+ }
+
+ @ExecutedBy("mExecutor")
+ @NonNull
+ ListenableFuture<List<Void>> submitConfigsInternal(
+ @NonNull List<CaptureConfig> captureConfigs, @FlashMode int flashMode) {
+ List<ListenableFuture<Void>> futureList = new ArrayList<>();
+ List<CaptureConfig> configsToSubmit = new ArrayList<>();
+ for (CaptureConfig captureConfig : captureConfigs) {
+ CaptureConfig.Builder configBuilder = CaptureConfig.Builder.from(captureConfig);
+ applyStillCaptureTemplate(configBuilder, captureConfig);
+ if (mOverrideAeModeForStillCapture.shouldSetAeModeAlwaysFlash(flashMode)) {
+ applyAeModeQuirk(configBuilder);
+ }
+
+ futureList.add(CallbackToFutureAdapter.getFuture(completer -> {
+ configBuilder.addCameraCaptureCallback(new CameraCaptureCallback() {
+ @Override
+ public void onCaptureCompleted(@NonNull CameraCaptureResult result) {
+ completer.set(null);
+ }
+
+ @Override
+ public void onCaptureFailed(@NonNull CameraCaptureFailure failure) {
+ String msg =
+ "Capture request failed with reason " + failure.getReason();
+ completer.setException(
+ new ImageCaptureException(ERROR_CAPTURE_FAILED, msg, null));
+ }
+
+ @Override
+ public void onCaptureCancelled() {
+ String msg = "Capture request is cancelled because camera is closed";
+ completer.setException(
+ new ImageCaptureException(ERROR_CAMERA_CLOSED, msg, null));
+ }
+ });
+ return "submitStillCapture";
+ }));
+ configsToSubmit.add(configBuilder.build());
+ }
+ mCameraControl.submitCaptureRequestsInternal(configsToSubmit);
+
+ return Futures.allAsList(futureList);
+ }
+
+ @ExecutedBy("mExecutor")
+ private void applyStillCaptureTemplate(@NonNull CaptureConfig.Builder configBuilder,
+ @NonNull CaptureConfig captureConfig) {
+ int templateToModify = CaptureConfig.TEMPLATE_TYPE_NONE;
+ if (mTemplate == CameraDevice.TEMPLATE_RECORD && !mIsLegacyDevice) {
+ // Always override template by TEMPLATE_VIDEO_SNAPSHOT when
+ // repeating template is TEMPLATE_RECORD. Note:
+ // TEMPLATE_VIDEO_SNAPSHOT is not supported on legacy device.
+ templateToModify = CameraDevice.TEMPLATE_VIDEO_SNAPSHOT;
+ } else if (captureConfig.getTemplateType() == CaptureConfig.TEMPLATE_TYPE_NONE) {
+ templateToModify = CameraDevice.TEMPLATE_STILL_CAPTURE;
+ }
+
+ if (templateToModify != CaptureConfig.TEMPLATE_TYPE_NONE) {
+ configBuilder.setTemplateType(templateToModify);
+ }
+ }
+
+ @ExecutedBy("mExecutor")
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
+ private void applyAeModeQuirk(@NonNull CaptureConfig.Builder configBuilder) {
+ Camera2ImplConfig.Builder impBuilder = new Camera2ImplConfig.Builder();
+ impBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
+ configBuilder.addImplementationOptions(impBuilder.build());
+ }
+
+ @ExecutedBy("mExecutor")
+ @NonNull
+ private ListenableFuture<TotalCaptureResult> waitForResult(long waitTimeout,
+ @Nullable ResultListener.Checker checker) {
+ ResultListener resultListener = new ResultListener(waitTimeout, checker);
+ mCameraControl.addCaptureResultListener(resultListener);
+ return resultListener.getFuture();
+ }
+
+ private boolean is3AConverged(@Nullable TotalCaptureResult totalCaptureResult) {
+ if (totalCaptureResult == null) {
+ return false;
+ }
+
+ Camera2CameraCaptureResult captureResult = new Camera2CameraCaptureResult(
+ totalCaptureResult);
+
+ // If afMode is OFF or UNKNOWN , no need for waiting.
+ // otherwise wait until af is locked or focused.
+ boolean isAfReady = captureResult.getAfMode() == AfMode.OFF
+ || captureResult.getAfMode() == AfMode.UNKNOWN
+ || captureResult.getAfState() == AfState.PASSIVE_FOCUSED
+ || captureResult.getAfState() == AfState.PASSIVE_NOT_FOCUSED
+ || captureResult.getAfState() == AfState.LOCKED_FOCUSED
+ || captureResult.getAfState() == AfState.LOCKED_NOT_FOCUSED;
+
+ // Unknown means cannot get valid state from CaptureResult
+ boolean isAeReady = captureResult.getAeState() == AeState.CONVERGED
+ || captureResult.getAeState() == AeState.FLASH_REQUIRED
+ || captureResult.getAeState() == AeState.UNKNOWN;
+
+ // Unknown means cannot get valid state from CaptureResult
+ boolean isAwbReady = captureResult.getAwbState() == AwbState.CONVERGED
+ || captureResult.getAwbState() == AwbState.UNKNOWN;
+
+ Logger.d(TAG, "checkCaptureResult, AE=" + captureResult.getAeState()
+ + " AF =" + captureResult.getAfState()
+ + " AWB=" + captureResult.getAwbState());
+ return isAfReady && isAeReady && isAwbReady;
+ }
+ }
+
+ interface PipelineTask {
+ /**
+ * @return A {@link ListenableFuture} that will be fulfilled with a Boolean result, the
+ * result true if it needs to wait for 3A converge after the task is executed, otherwise
+ * false.
+ */
+ @ExecutedBy("mExecutor")
+ @NonNull
+ ListenableFuture<Boolean> preCapture(@Nullable TotalCaptureResult captureResult);
+
+ /**
+ * @return true if the preCapture method requires a CaptureResult. When it return false,
+ * that means the {@link #preCapture(TotalCaptureResult)} ()} can accept a null input, we
+ * don't need to capture a CaptureResult for this task.
+ */
+ @ExecutedBy("mExecutor")
+ boolean isCaptureResultNeeded();
+
+ @ExecutedBy("mExecutor")
+ void postCapture();
+ }
+
+ /**
+ * Task to triggerAF preCapture if it is required
+ */
+ static class AfTask implements PipelineTask {
+
+ private final Camera2CameraControlImpl mCameraControl;
+ private boolean mIsExecuted = false;
+
+ AfTask(@NonNull Camera2CameraControlImpl cameraControl) {
+ mCameraControl = cameraControl;
+ }
+
+ @ExecutedBy("mExecutor")
+ @NonNull
+ @Override
+ public ListenableFuture<Boolean> preCapture(@Nullable TotalCaptureResult captureResult) {
+ // Always return true for this task since we always need to wait for the focused
+ // signal after the task is executed.
+ ListenableFuture<Boolean> ret = Futures.immediateFuture(true);
+
+ if (captureResult == null) {
+ return ret;
+ }
+
+ Integer afMode = captureResult.get(CaptureResult.CONTROL_AF_MODE);
+ if (afMode == null) {
+ return ret;
+ }
+ switch (afMode) {
+ case CaptureResult.CONTROL_AF_MODE_AUTO:
+ case CaptureResult.CONTROL_AF_MODE_MACRO:
+ Logger.d(TAG, "TriggerAf? AF mode auto");
+ Integer afState = captureResult.get(CaptureResult.CONTROL_AF_STATE);
+ if (afState != null && afState == CaptureResult.CONTROL_AF_STATE_INACTIVE) {
+ Logger.d(TAG, "Trigger AF");
+
+ mIsExecuted = true;
+ mCameraControl.getFocusMeteringControl().triggerAf(null, false);
+ return ret;
+ }
+ break;
+ default:
+ // fall out
+ }
+
+ return ret;
+ }
+
+ @ExecutedBy("mExecutor")
+ @Override
+ public boolean isCaptureResultNeeded() {
+ return true;
+ }
+
+ @ExecutedBy("mExecutor")
+ @Override
+ public void postCapture() {
+ if (mIsExecuted) {
+ Logger.d(TAG, "cancel TriggerAF");
+ mCameraControl.getFocusMeteringControl().cancelAfAeTrigger(true, false);
+ }
+ }
+ }
+
+ /**
+ * Task to open the Torch if flash is required.
+ */
+ static class TorchTask implements PipelineTask {
+
+ private final Camera2CameraControlImpl mCameraControl;
+ private final @FlashMode int mFlashMode;
+ private boolean mIsExecuted = false;
+
+ TorchTask(@NonNull Camera2CameraControlImpl cameraControl, @FlashMode int flashMode) {
+ mCameraControl = cameraControl;
+ mFlashMode = flashMode;
+ }
+
+ @ExecutedBy("mExecutor")
+ @NonNull
+ @Override
+ public ListenableFuture<Boolean> preCapture(@Nullable TotalCaptureResult captureResult) {
+ if (isFlashRequired(mFlashMode, captureResult)) {
+ if (mCameraControl.isTorchOn()) {
+ Logger.d(TAG, "Torch already on, not turn on");
+ } else {
+ Logger.d(TAG, "Turn on torch");
+ mIsExecuted = true;
+
+ ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(completer -> {
+ mCameraControl.getTorchControl().enableTorchInternal(completer, true);
+ return "TorchOn";
+ });
+ return FutureChain.from(future).transform(input -> true,
+ CameraXExecutors.directExecutor());
+ }
+ }
+
+ return Futures.immediateFuture(false);
+ }
+
+ @ExecutedBy("mExecutor")
+ @Override
+ public boolean isCaptureResultNeeded() {
+ return mFlashMode == FLASH_MODE_AUTO;
+ }
+
+ @ExecutedBy("mExecutor")
+ @Override
+ public void postCapture() {
+ if (mIsExecuted) {
+ mCameraControl.getTorchControl().enableTorchInternal(null, false);
+ Logger.d(TAG, "Turn off torch");
+ }
+ }
+ }
+
+ /**
+ * Task to trigger AePreCapture if flash is required.
+ */
+ static class AePreCaptureTask implements PipelineTask {
+
+ private final Camera2CameraControlImpl mCameraControl;
+ private final OverrideAeModeForStillCapture mOverrideAeModeForStillCapture;
+ private final @FlashMode int mFlashMode;
+ private boolean mIsExecuted = false;
+
+ AePreCaptureTask(@NonNull Camera2CameraControlImpl cameraControl, @FlashMode int flashMode,
+ @NonNull OverrideAeModeForStillCapture overrideAeModeForStillCapture) {
+ mCameraControl = cameraControl;
+ mFlashMode = flashMode;
+ mOverrideAeModeForStillCapture = overrideAeModeForStillCapture;
+ }
+
+ @ExecutedBy("mExecutor")
+ @NonNull
+ @Override
+ public ListenableFuture<Boolean> preCapture(@Nullable TotalCaptureResult captureResult) {
+ if (isFlashRequired(mFlashMode, captureResult)) {
+ Logger.d(TAG, "Trigger AE");
+ mIsExecuted = true;
+
+ ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(completer -> {
+ mCameraControl.getFocusMeteringControl().triggerAePrecapture(completer);
+ mOverrideAeModeForStillCapture.onAePrecaptureStarted();
+ return "AePreCapture";
+ });
+ return FutureChain.from(future).transform(input -> true,
+ CameraXExecutors.directExecutor());
+ }
+
+ return Futures.immediateFuture(false);
+ }
+
+ @ExecutedBy("mExecutor")
+ @Override
+ public boolean isCaptureResultNeeded() {
+ return mFlashMode == FLASH_MODE_AUTO;
+ }
+
+ @ExecutedBy("mExecutor")
+ @Override
+ public void postCapture() {
+ if (mIsExecuted) {
+ Logger.d(TAG, "cancel TriggerAePreCapture");
+ mCameraControl.getFocusMeteringControl().cancelAfAeTrigger(false, true);
+ mOverrideAeModeForStillCapture.onAePrecaptureFinished();
+ }
+ }
+ }
+
+ static boolean isFlashRequired(@FlashMode int flashMode, @Nullable TotalCaptureResult result) {
+ switch (flashMode) {
+ case FLASH_MODE_ON:
+ return true;
+ case FLASH_MODE_AUTO:
+ Integer aeState = (result != null) ? result.get(CaptureResult.CONTROL_AE_STATE)
+ : null;
+ return aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED;
+ case FLASH_MODE_OFF:
+ return false;
+ }
+ throw new AssertionError(flashMode);
+ }
+
+ /**
+ * A listener receives the result of the repeating request. The results will be sent to the
+ * Checker to identify if the mFuture can be completed.
+ */
+ static class ResultListener implements Camera2CameraControlImpl.CaptureResultListener {
+
+ /**
+ * The totalCaptureResults will be sent to the Checker#check() method, return true in the
+ * Checker#check() will complete the mFuture.
+ */
+ interface Checker {
+ boolean check(@NonNull TotalCaptureResult totalCaptureResult);
+ }
+
+ static final long NO_TIMEOUT = 0L;
+
+ private CallbackToFutureAdapter.Completer<TotalCaptureResult> mCompleter;
+ private final ListenableFuture<TotalCaptureResult> mFuture =
+ CallbackToFutureAdapter.getFuture(completer -> {
+ mCompleter = completer;
+ return "waitFor3AResult";
+ });
+ private final long mTimeLimitNs;
+ private final Checker mChecker;
+ private volatile Long mTimestampOfFirstUpdateNs = null;
+
+ /**
+ * @param timeLimitNs timeout threshold in Nanos
+ * @param checker the checker to define the condition to complete the mFuture, set null
+ * will complete the mFuture once it receives any totalCaptureResults.
+ */
+ ResultListener(long timeLimitNs, @Nullable Checker checker) {
+ mTimeLimitNs = timeLimitNs;
+ mChecker = checker;
+ }
+
+ @NonNull
+ public ListenableFuture<TotalCaptureResult> getFuture() {
+ return mFuture;
+ }
+
+ @Override
+ public boolean onCaptureResult(@NonNull TotalCaptureResult captureResult) {
+ Long currentTimestampNs = captureResult.get(CaptureResult.SENSOR_TIMESTAMP);
+ if (currentTimestampNs != null && mTimestampOfFirstUpdateNs == null) {
+ mTimestampOfFirstUpdateNs = currentTimestampNs;
+ }
+
+ Long timestampOfFirstUpdateNs = mTimestampOfFirstUpdateNs;
+ if (NO_TIMEOUT != mTimeLimitNs && timestampOfFirstUpdateNs != null
+ && currentTimestampNs != null
+ && currentTimestampNs - timestampOfFirstUpdateNs > mTimeLimitNs) {
+ mCompleter.set(null);
+ Logger.d(TAG, "Wait for capture result timeout, current:" + currentTimestampNs
+ + " first: " + timestampOfFirstUpdateNs);
+ return true;
+ }
+
+ if (mChecker != null && !mChecker.check(captureResult)) {
+ return false;
+ }
+
+ mCompleter.set(captureResult);
+ return true;
+ }
+ }
+
+ private boolean isTorchAsFlash(@FlashType int flashType) {
+ return mUseTorchAsFlash.shouldUseTorchAsFlash() || mTemplate == CameraDevice.TEMPLATE_RECORD
+ || flashType == FLASH_TYPE_USE_TORCH_AS_FLASH;
+ }
+
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompat.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompat.java
index fb4fa35..2f73e88 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompat.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompat.java
@@ -50,7 +50,9 @@
* @see android.hardware.camera2.CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP
*/
public InputConfigurationCompat(int width, int height, int format) {
- if (Build.VERSION.SDK_INT >= 23) {
+ if (Build.VERSION.SDK_INT >= 31) {
+ mImpl = new InputConfigurationCompatApi31Impl(width, height, format);
+ } else if (Build.VERSION.SDK_INT >= 23) {
mImpl = new InputConfigurationCompatApi23Impl(width, height, format);
} else {
mImpl = new InputConfigurationCompatBaseImpl(width, height, format);
@@ -80,6 +82,10 @@
if (Build.VERSION.SDK_INT < 23) {
return null;
}
+ if (Build.VERSION.SDK_INT >= 31) {
+ return new InputConfigurationCompat(
+ new InputConfigurationCompatApi31Impl(inputConfiguration));
+ }
return new InputConfigurationCompat(
new InputConfigurationCompatApi23Impl(inputConfiguration));
}
@@ -112,6 +118,18 @@
}
/**
+ * Whether this input configuration is of multi-resolution.
+ *
+ * <p>An multi-resolution InputConfiguration means that the reprocessing session created from it
+ * allows input images of different sizes.</p>
+ *
+ * @return this input configuration is multi-resolution or not.
+ */
+ public boolean isMultiResolution() {
+ return mImpl.isMultiResolution();
+ }
+
+ /**
* Check if this InputConfiguration is equal to another InputConfiguration.
*
* <p>Two input configurations are equal if and only if they have the same widths, heights, and
@@ -145,6 +163,7 @@
*
* @return string representation of {@link InputConfigurationCompat}
*/
+ @NonNull
@Override
public String toString() {
return mImpl.toString();
@@ -171,6 +190,8 @@
int getFormat();
+ boolean isMultiResolution();
+
@Nullable
Object getInputConfiguration();
}
@@ -205,6 +226,11 @@
}
@Override
+ public boolean isMultiResolution() {
+ return false;
+ }
+
+ @Override
public Object getInputConfiguration() {
return null;
}
@@ -234,6 +260,7 @@
return h;
}
+ @NonNull
@SuppressLint("DefaultLocale") // Implementation matches framework
@Override
public String toString() {
@@ -243,7 +270,7 @@
}
@RequiresApi(23)
- private static final class InputConfigurationCompatApi23Impl implements
+ private static class InputConfigurationCompatApi23Impl implements
InputConfigurationCompatImpl {
private final InputConfiguration mObject;
@@ -271,6 +298,11 @@
return mObject.getFormat();
}
+ @Override
+ public boolean isMultiResolution() {
+ return false;
+ }
+
@Nullable
@Override
public Object getInputConfiguration() {
@@ -291,10 +323,29 @@
return mObject.hashCode();
}
+ @NonNull
@Override
public String toString() {
return mObject.toString();
}
}
+ @RequiresApi(31)
+ private static final class InputConfigurationCompatApi31Impl extends
+ InputConfigurationCompatApi23Impl {
+
+ InputConfigurationCompatApi31Impl(@NonNull Object inputConfiguration) {
+ super(inputConfiguration);
+ }
+
+ InputConfigurationCompatApi31Impl(int width, int height, int format) {
+ super(width, height, format);
+ }
+
+ @Override
+ public boolean isMultiResolution() {
+ return ((InputConfiguration) getInputConfiguration()).isMultiResolution();
+ }
+ }
+
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraControlImplTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraControlImplTest.kt
deleted file mode 100644
index 4a735b2..0000000
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraControlImplTest.kt
+++ /dev/null
@@ -1,663 +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.
- */
-
-package androidx.camera.camera2.internal
-
-import android.content.Context
-import android.graphics.SurfaceTexture
-import android.hardware.camera2.CameraCaptureSession
-import android.hardware.camera2.CameraCharacteristics
-import android.hardware.camera2.CameraDevice
-import android.hardware.camera2.CameraManager
-import android.hardware.camera2.CameraMetadata
-import android.hardware.camera2.CaptureRequest
-import android.hardware.camera2.TotalCaptureResult
-import android.os.Build
-import android.os.Handler
-import android.os.HandlerThread
-import android.view.Surface
-import androidx.camera.camera2.impl.Camera2ImplConfig
-import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
-import androidx.camera.camera2.internal.compat.quirk.AutoFlashUnderExposedQuirk
-import androidx.camera.camera2.internal.compat.quirk.CameraQuirks
-import androidx.camera.camera2.internal.compat.quirk.UseTorchAsFlashQuirk
-import androidx.camera.core.ImageCapture
-import androidx.camera.core.ImageCapture.FLASH_MODE_AUTO
-import androidx.camera.core.ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH
-import androidx.camera.core.ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH
-import androidx.camera.core.impl.CameraCaptureCallback
-import androidx.camera.core.impl.CameraCaptureFailure
-import androidx.camera.core.impl.CameraCaptureResult
-import androidx.camera.core.impl.CameraControlInternal
-import androidx.camera.core.impl.CaptureConfig
-import androidx.camera.core.impl.ImmediateSurface
-import androidx.camera.core.impl.Quirks
-import androidx.camera.core.impl.SessionConfig
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.testing.HandlerUtil
-import androidx.core.os.HandlerCompat
-import androidx.test.core.app.ApplicationProvider
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mockito.`when`
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.reset
-import org.mockito.Mockito.verify
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.annotation.Config
-import org.robolectric.annotation.internal.DoNotInstrument
-import org.robolectric.shadow.api.Shadow
-import org.robolectric.shadows.ShadowCameraCharacteristics
-import org.robolectric.shadows.ShadowCameraManager
-
-private const val CAMERA_ID_0 = "0"
-
-@RunWith(RobolectricTestRunner::class)
-@DoNotInstrument
-@Config(
- minSdk = Build.VERSION_CODES.LOLLIPOP
-)
-class Camera2CameraControlImplTest {
-
- private val context = ApplicationProvider.getApplicationContext() as Context
- private val controlUpdateCallback =
- mock(CameraControlInternal.ControlUpdateCallback::class.java)
- private lateinit var cameraControl: Camera2CameraControlImpl
- private lateinit var handlerThread: HandlerThread
- private lateinit var handler: Handler
-
- @Before
- fun setUp() {
- initCameras()
-
- handlerThread = HandlerThread("ControlThread").apply { start() }
- handler = HandlerCompat.createAsync(handlerThread.looper)
-
- createCameraControl()
- }
-
- @After
- fun tearDown() {
- if (::handlerThread.isInitialized) {
- handlerThread.quitSafely()
- }
- }
-
- @Test
- fun triggerAf_captureRequestSent() {
- // Act.
- cameraControl.triggerAf()
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- assertAfTrigger()
- }
-
- @Test
- fun cancelAf_captureRequestSent() {
- // Act.
- cameraControl.triggerAf()
- HandlerUtil.waitForLooperToIdle(handler)
-
- reset(controlUpdateCallback)
-
- cameraControl.cancelAfAndFinishFlashSequence(true, false)
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- assertCancelAfTrigger()
- }
-
- @Test
- fun startFlashSequence_aePrecaptureSent() {
- // Act.
- cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- assertAePrecaptureTrigger()
- }
-
- @Test
- fun startFlashSequence_flashModeWasSet() {
- // Act 1
- cameraControl.flashMode = ImageCapture.FLASH_MODE_ON
- cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert 1: ensures AePrecapture is not invoked.
- verify(controlUpdateCallback, never()).onCameraControlCaptureRequests(any())
-
- // Act 2: Send the CaptureResult
- triggerRepeatRequestResult()
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert 2: AePrecapture is triggered.
- assertAePrecaptureTrigger()
- }
-
- private fun triggerRepeatRequestResult() {
- val tagBundle = cameraControl.sessionConfig.repeatingCaptureConfig.tagBundle
- val mockCaptureRequest = mock(CaptureRequest::class.java)
- `when`(mockCaptureRequest.tag).thenReturn(tagBundle)
- val mockCaptureResult = mock(TotalCaptureResult::class.java)
- `when`(mockCaptureResult.request).thenReturn(mockCaptureRequest)
- for (cameraCaptureCallback in cameraControl.sessionConfig.repeatingCameraCaptureCallbacks) {
- val callback = CaptureCallbackConverter.toCaptureCallback(cameraCaptureCallback)
- callback.onCaptureCompleted(
- mock(CameraCaptureSession::class.java),
- mockCaptureRequest, mockCaptureResult
- )
- }
- }
-
- @Config(minSdk = 23)
- @Test
- fun finishFlashSequence_cancelAePrecaptureSent() {
- // Act.
- cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
- HandlerUtil.waitForLooperToIdle(handler)
-
- reset(controlUpdateCallback)
-
- cameraControl.cancelAfAndFinishFlashSequence(false, true)
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- assertCancelAePrecaptureTrigger()
- }
-
- @Test
- fun cancelAfAndFinishFlashSequence_cancelAfAndAePrecaptureSent() {
- // Act.
- cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
- HandlerUtil.waitForLooperToIdle(handler)
-
- reset(controlUpdateCallback)
-
- cameraControl.cancelAfAndFinishFlashSequence(true, true)
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- assertCancelAfTrigger()
- assertCancelAePrecaptureTrigger()
- }
-
- @Test
- fun startFlashSequence_withTorchAsFlashQuirk_enableTorchSent() {
- // Arrange.
- createCameraControl(quirks = Quirks(listOf(object : UseTorchAsFlashQuirk {})))
-
- // Act.
- cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- assertTorchEnable()
- }
-
- @Test
- fun startFlashSequence_withFlashTypeTorch_enableTorchSent() {
- // Arrange.
- createCameraControl()
-
- // Act.
- cameraControl.startFlashSequence(FLASH_TYPE_USE_TORCH_AS_FLASH)
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- assertTorchEnable()
- }
-
- @Test
- fun startFlashSequence_withTemplateRecord_enableTorchSent() {
- // Arrange.
- cameraControl.setTemplate(CameraDevice.TEMPLATE_RECORD)
-
- // Act.
- cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- assertTorchEnable()
- }
-
- @Test
- fun finishFlashSequence_withUseTorchAsFlashQuirk_disableTorch() {
- // Arrange.
- createCameraControl(quirks = Quirks(listOf(object : UseTorchAsFlashQuirk {})))
-
- // Act.
- cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
- HandlerUtil.waitForLooperToIdle(handler)
-
- reset(controlUpdateCallback)
-
- cameraControl.cancelAfAndFinishFlashSequence(false, true)
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- assertTorchDisable()
- }
-
- @Test
- fun finishFlashSequence_withFlashTypeTorch_disableTorch() {
- // Arrange.
- createCameraControl()
-
- // Act.
- cameraControl.startFlashSequence(FLASH_TYPE_USE_TORCH_AS_FLASH)
- HandlerUtil.waitForLooperToIdle(handler)
-
- reset(controlUpdateCallback)
-
- cameraControl.cancelAfAndFinishFlashSequence(false, true)
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- assertTorchDisable()
- }
-
- @Test
- fun startFlashSequence_withUseTorchAsFlashQuirk_torchIsAlreadyOn() {
- // Arrange.
- createCameraControl(quirks = Quirks(listOf(object : UseTorchAsFlashQuirk {})))
- cameraControl.enableTorchInternal(true)
- HandlerUtil.waitForLooperToIdle(handler)
- reset(controlUpdateCallback)
-
- // Act.
- cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- verify(controlUpdateCallback, never()).onCameraControlCaptureRequests(any())
- verify(controlUpdateCallback, never()).onCameraControlUpdateSessionConfig()
-
- // Arrange.
- reset(controlUpdateCallback)
-
- // Act.
- cameraControl.cancelAfAndFinishFlashSequence(false, true)
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- verify(controlUpdateCallback, never()).onCameraControlCaptureRequests(any())
- verify(controlUpdateCallback, never()).onCameraControlUpdateSessionConfig()
- }
-
- @Test
- fun startFlashSequence_withFlashTypeTorch_torchIsAlreadyOn() {
- // Arrange.
- createCameraControl()
- cameraControl.enableTorchInternal(true)
- HandlerUtil.waitForLooperToIdle(handler)
- reset(controlUpdateCallback)
-
- // Act.
- cameraControl.startFlashSequence(FLASH_TYPE_USE_TORCH_AS_FLASH)
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- verify(controlUpdateCallback, never()).onCameraControlCaptureRequests(any())
- verify(controlUpdateCallback, never()).onCameraControlUpdateSessionConfig()
-
- // Arrange.
- reset(controlUpdateCallback)
-
- // Act.
- cameraControl.cancelAfAndFinishFlashSequence(false, true)
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- verify(controlUpdateCallback, never()).onCameraControlCaptureRequests(any())
- verify(controlUpdateCallback, never()).onCameraControlUpdateSessionConfig()
- }
-
- @Test
- fun submitStillCaptureRequests_withTemplate_templateSent() {
- // Arrange.
- val imageCaptureConfig = CaptureConfig.Builder().let {
- it.templateType = CameraDevice.TEMPLATE_MANUAL
- it.build()
- }
-
- // Act.
- cameraControl.submitStillCaptureRequests(listOf(imageCaptureConfig))
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- val captureConfig = getIssuedCaptureConfig()
- assertThat(captureConfig.templateType).isEqualTo(CameraDevice.TEMPLATE_MANUAL)
- }
-
- @Test
- fun submitStillCaptureRequests_withNoTemplate_templateStillCaptureSent() {
- // Arrange.
- val imageCaptureConfig = CaptureConfig.Builder().build()
-
- // Act.
- cameraControl.submitStillCaptureRequests(listOf(imageCaptureConfig))
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- val captureConfig = getIssuedCaptureConfig()
- assertThat(captureConfig.templateType).isEqualTo(CameraDevice.TEMPLATE_STILL_CAPTURE)
- }
-
- @Test
- fun submitStillCaptureRequests_withTemplateRecord_templateVideoSnapshotSent() {
- // Arrange.
- cameraControl.setTemplate(CameraDevice.TEMPLATE_RECORD)
- val imageCaptureConfig = CaptureConfig.Builder().build()
-
- // Act.
- cameraControl.submitStillCaptureRequests(listOf(imageCaptureConfig))
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- val captureConfig = getIssuedCaptureConfig()
- assertThat(captureConfig.templateType).isEqualTo(CameraDevice.TEMPLATE_VIDEO_SNAPSHOT)
- }
-
- @Test
- fun overrideAeModeForStillCapture_quirkAbsent_notOverride() {
- // Arrange.
- createCameraControl(quirks = Quirks(emptyList())) // Not have the quirk.
- cameraControl.flashMode = FLASH_MODE_AUTO
- triggerRepeatRequestResult() // make sures flashMode is updated.
- val imageCaptureConfig = CaptureConfig.Builder().build()
-
- // Act.
- cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
- HandlerUtil.waitForLooperToIdle(handler)
- reset(controlUpdateCallback)
- cameraControl.submitStillCaptureRequests(listOf(imageCaptureConfig))
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- // AE mode should not be overridden
- val captureConfig = getIssuedCaptureConfig()
- assertThat(
- captureConfig.toCamera2Config()
- .getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE)
- ).isNull()
- }
-
- @Test
- fun overrideAeModeForStillCapture_aePrecaptureStarted_override() {
- // Arrange.
- createCameraControl(quirks = Quirks(listOf(AutoFlashUnderExposedQuirk())))
- cameraControl.flashMode = FLASH_MODE_AUTO
- triggerRepeatRequestResult() // make sures flashMode is updated.
- val imageCaptureConfig = getCaptureConfigWithParameters()
-
- // Act.
- cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
- HandlerUtil.waitForLooperToIdle(handler)
- reset(controlUpdateCallback)
- cameraControl.submitStillCaptureRequests(listOf(imageCaptureConfig))
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- // AE mode should be overridden to CONTROL_AE_MODE_ON_ALWAYS_FLASH
- val issuedCaptureConfig = getIssuedCaptureConfig()
- assertThat(
- issuedCaptureConfig.toCamera2Config()
- .getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE)
- ).isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH)
- issuedCaptureConfig.assertContainAllParametersFrom(imageCaptureConfig)
- }
-
- private fun getCaptureConfigWithParameters(): CaptureConfig {
- val builder = CaptureConfig.Builder()
- builder.addCameraCaptureCallback(object : CameraCaptureCallback() {
- override fun onCaptureCompleted(cameraCaptureResult: CameraCaptureResult) {
- super.onCaptureCompleted(cameraCaptureResult)
- }
-
- override fun onCaptureFailed(failure: CameraCaptureFailure) {
- super.onCaptureFailed(failure)
- }
-
- override fun onCaptureCancelled() {
- super.onCaptureCancelled()
- }
- })
- builder.addSurface(ImmediateSurface(Surface(SurfaceTexture(0))))
- builder.templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
- builder.addTag("test", "testValue")
- return builder.build()
- }
- private fun CaptureConfig.assertContainAllParametersFrom(captureConfig: CaptureConfig) {
- assertThat(captureConfig.surfaces).isEqualTo(surfaces)
- for (listOption in captureConfig.implementationOptions.listOptions()) {
- assertThat(captureConfig.implementationOptions.retrieveOption(listOption))
- .isEqualTo(implementationOptions.retrieveOption(listOption))
- }
- assertThat(captureConfig.cameraCaptureCallbacks)
- .isEqualTo(cameraCaptureCallbacks)
- assertThat(captureConfig.isUseRepeatingSurface)
- .isEqualTo(isUseRepeatingSurface)
- for (key in captureConfig.tagBundle.listKeys()) {
- assertThat(captureConfig.tagBundle.getTag(key)).isEqualTo(tagBundle.getTag(key))
- }
- assertThat(captureConfig.templateType).isEqualTo(templateType)
- }
-
- @Test
- fun overrideAeModeForStillCapture_aePrecaptureFinish_notOverride() {
- // Arrange.
- createCameraControl(quirks = Quirks(listOf(AutoFlashUnderExposedQuirk())))
- cameraControl.flashMode = FLASH_MODE_AUTO
- triggerRepeatRequestResult() // make sures flashMode is updated.
- val imageCaptureConfig = CaptureConfig.Builder().build()
-
- // Act.
- cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
- HandlerUtil.waitForLooperToIdle(handler) // required to make sure states are changed
- cameraControl.cancelAfAndFinishFlashSequence(false, true)
- HandlerUtil.waitForLooperToIdle(handler)
- reset(controlUpdateCallback)
- cameraControl.submitStillCaptureRequests(listOf(imageCaptureConfig))
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- // AE mode should not be overridden
- val issuedCaptureConfig = getIssuedCaptureConfig()
- assertThat(
- issuedCaptureConfig.toCamera2Config()
- .getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE)
- ).isNull()
- }
-
- @Test
- fun overrideAeModeForStillCapture_noAePrecaptureTriggered_notOverride() {
- // Arrange.
- createCameraControl(quirks = Quirks(listOf(AutoFlashUnderExposedQuirk())))
- cameraControl.flashMode = FLASH_MODE_AUTO
- triggerRepeatRequestResult() // make sures flashMode is updated.
- val imageCaptureConfig = CaptureConfig.Builder().build()
-
- // Act.
- cameraControl.submitStillCaptureRequests(listOf(imageCaptureConfig))
- HandlerUtil.waitForLooperToIdle(handler)
-
- // Assert.
- // AE mode should not be overridden
- val issuedCaptureConfig = getIssuedCaptureConfig()
- assertThat(
- issuedCaptureConfig.toCamera2Config()
- .getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE)
- ).isNull()
- }
-
- private fun assertAfTrigger() {
- assertCamera2ConfigValue(
- getIssuedCaptureConfig().toCamera2Config(),
- CaptureRequest.CONTROL_AF_TRIGGER,
- CaptureRequest.CONTROL_AF_TRIGGER_START
- )
- }
-
- private fun assertCancelAfTrigger() {
- assertCamera2ConfigValue(
- getIssuedCaptureConfig().toCamera2Config(),
- CaptureRequest.CONTROL_AF_TRIGGER,
- CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
- )
- }
-
- private fun assertAePrecaptureTrigger() {
- assertCamera2ConfigValue(
- getIssuedCaptureConfig().toCamera2Config(),
- CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
- CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
- )
- }
-
- private fun assertCancelAePrecaptureTrigger() {
- if (Build.VERSION.SDK_INT < 23) {
- return
- }
-
- assertCamera2ConfigValue(
- getIssuedCaptureConfig().toCamera2Config(),
- CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
- CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
- )
- }
-
- private fun assertTorchEnable() {
- val camera2Config = getIssuedSessionConfig().toCamera2Config()
-
- assertCamera2ConfigValue(
- camera2Config,
- CaptureRequest.CONTROL_AE_MODE,
- CaptureRequest.CONTROL_AE_MODE_ON,
- )
-
- assertCamera2ConfigValue(
- camera2Config,
- CaptureRequest.FLASH_MODE,
- CameraMetadata.FLASH_MODE_TORCH,
- )
- }
-
- private fun assertTorchDisable() {
- val camera2Config = getIssuedCaptureConfig().toCamera2Config()
-
- assertCamera2ConfigValue(
- camera2Config,
- CaptureRequest.CONTROL_AE_MODE,
- CaptureRequest.CONTROL_AE_MODE_ON
- )
-
- assertCamera2ConfigValue(
- camera2Config,
- CaptureRequest.FLASH_MODE,
- CaptureRequest.FLASH_MODE_OFF
- )
- }
-
- private fun getIssuedCaptureConfig(): CaptureConfig {
- @Suppress("UNCHECKED_CAST")
- val captor =
- ArgumentCaptor.forClass(List::class.java) as ArgumentCaptor<List<CaptureConfig>>
- verify(controlUpdateCallback).onCameraControlCaptureRequests(captor.capture())
- return captor.value[0]
- }
-
- private fun getIssuedSessionConfig(): SessionConfig {
- verify(controlUpdateCallback).onCameraControlUpdateSessionConfig()
- return cameraControl.sessionConfig
- }
-
- private fun <T> assertCamera2ConfigValue(
- camera2Config: Camera2ImplConfig,
- key: CaptureRequest.Key<T>,
- expectedValue: T,
- assertMsg: String = ""
- ) {
- assertWithMessage(assertMsg).that(camera2Config.getCaptureRequestOption(key, null))
- .isEqualTo(expectedValue)
- }
-
- private fun createCameraControl(
- cameraId: String = CAMERA_ID_0,
- quirks: Quirks? = null
- ) {
- val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
- val characteristics = cameraManager.getCameraCharacteristics(cameraId)
- val characteristicsCompat = CameraCharacteristicsCompat
- .toCameraCharacteristicsCompat(characteristics)
- val cameraQuirk = quirks ?: CameraQuirks.get(cameraId, characteristicsCompat)
- val executorService = CameraXExecutors.newHandlerExecutor(handler)
-
- cameraControl = Camera2CameraControlImpl(
- characteristicsCompat,
- executorService,
- executorService,
- controlUpdateCallback,
- cameraQuirk
- ).apply {
- setActive(true)
- incrementUseCount()
- }
- }
-
- private fun initCameras() {
- Shadow.extract<ShadowCameraManager>(
- context.getSystemService(Context.CAMERA_SERVICE)
- ).apply {
- addCamera(CAMERA_ID_0, intiCharacteristic0())
- }
- }
-
- private fun intiCharacteristic0(): CameraCharacteristics {
- return ShadowCameraCharacteristics.newCameraCharacteristics().also {
- Shadow.extract<ShadowCameraCharacteristics>(it).apply {
- set(CameraCharacteristics.FLASH_INFO_AVAILABLE, true)
- set(
- CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES,
- intArrayOf(
- CaptureRequest.CONTROL_AE_MODE_OFF,
- CaptureRequest.CONTROL_AE_MODE_ON,
- CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH,
- CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH,
- CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE,
- CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH
- )
- )
- set(
- CameraCharacteristics.LENS_FACING,
- CameraMetadata.LENS_FACING_BACK
- )
- }
- }
- }
-}
-
-private fun CaptureConfig.toCamera2Config() = Camera2ImplConfig(implementationOptions)
-
-private fun SessionConfig.toCamera2Config() = Camera2ImplConfig(implementationOptions)
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
new file mode 100644
index 0000000..c9bac65
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
@@ -0,0 +1,1016 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal
+
+import android.content.Context
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
+import android.os.Build
+import android.view.Surface
+import androidx.camera.camera2.impl.Camera2ImplConfig
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
+import androidx.camera.camera2.internal.compat.quirk.AutoFlashUnderExposedQuirk
+import androidx.camera.camera2.internal.compat.quirk.CameraQuirks
+import androidx.camera.camera2.internal.compat.quirk.UseTorchAsFlashQuirk
+import androidx.camera.camera2.internal.compat.workaround.OverrideAeModeForStillCapture
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCapture.FLASH_MODE_AUTO
+import androidx.camera.core.ImageCapture.FLASH_MODE_OFF
+import androidx.camera.core.ImageCapture.FLASH_MODE_ON
+import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.impl.CameraCaptureFailure
+import androidx.camera.core.impl.CameraCaptureResult
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.impl.DeferrableSurface
+import androidx.camera.core.impl.ImmediateSurface
+import androidx.camera.core.impl.Quirks
+import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.utils.futures.Futures
+import androidx.concurrent.futures.await
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert.assertThrows
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowCameraCharacteristics
+import org.robolectric.shadows.ShadowCameraManager
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledFuture
+import java.util.concurrent.TimeUnit
+
+private const val CAMERA_ID_0 = "0"
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(
+ minSdk = Build.VERSION_CODES.LOLLIPOP,
+)
+class Camera2CapturePipelineTest {
+
+ private val context = ApplicationProvider.getApplicationContext() as Context
+ private val executorService = Executors.newSingleThreadScheduledExecutor()
+
+ private val baseRepeatingResult: Map<CaptureResult.Key<*>, Any> = mapOf(
+ CaptureResult.CONTROL_MODE to CaptureResult.CONTROL_MODE_AUTO,
+ CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_AUTO,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED,
+ CaptureResult.CONTROL_AWB_MODE to CaptureResult.CONTROL_AWB_MODE_AUTO,
+ )
+
+ private val resultConverged: Map<CaptureResult.Key<*>, Any> = mapOf(
+ CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_AUTO,
+ CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED,
+ CaptureResult.CONTROL_AWB_STATE to CaptureResult.CONTROL_AWB_STATE_CONVERGED,
+ )
+
+ private val fakeStillCaptureSurface = ImmediateSurface(Surface(SurfaceTexture(0)))
+
+ private val singleRequest = CaptureConfig.Builder().apply {
+ templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
+ addSurface(fakeStillCaptureSurface)
+ }.build()
+
+ private var runningRepeatingStream: ScheduledFuture<*>? = null
+ set(value) {
+ runningRepeatingStream?.cancel(false)
+ field = value
+ }
+
+ @Before
+ fun setUp() {
+ initCameras()
+ }
+
+ @After
+ fun tearDown() {
+ runningRepeatingStream = null
+ executorService.shutdownNow()
+ }
+
+ @Test
+ fun pipelineTest_preCapturePostCaptureShouldCalled() {
+ // Arrange.
+ val fakeTask = object : Camera2CapturePipeline.PipelineTask {
+ val preCaptureCountDown = CountDownLatch(1)
+ val postCaptureCountDown = CountDownLatch(1)
+
+ override fun preCapture(captureResult: TotalCaptureResult?): ListenableFuture<Boolean> {
+ preCaptureCountDown.countDown()
+ return Futures.immediateFuture(true)
+ }
+
+ override fun isCaptureResultNeeded(): Boolean {
+ return true
+ }
+
+ override fun postCapture() {
+ postCaptureCountDown.countDown()
+ }
+ }
+
+ val cameraControl = createCameraControl().apply {
+ simulateRepeatingResult(resultParameters = resultConverged)
+ }
+
+ val pipeline = Camera2CapturePipeline.Pipeline(
+ CameraDevice.TEMPLATE_PREVIEW,
+ Dispatchers.Default.asExecutor(),
+ cameraControl,
+ false,
+ OverrideAeModeForStillCapture(Quirks(emptyList())),
+ ).apply {
+ addTask(fakeTask)
+ }
+
+ // Act.
+ pipeline.executeCapture(
+ listOf(singleRequest),
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ )
+
+ // Assert.
+ assertTrue(fakeTask.preCaptureCountDown.await(3, TimeUnit.SECONDS))
+ assertTrue(fakeTask.postCaptureCountDown.await(3, TimeUnit.SECONDS))
+ }
+
+ @Test
+ fun maxQuality_afInactive_shouldTriggerAf(): Unit = runBlocking {
+ val cameraControl = createCameraControl().apply {
+
+ // Arrange. Simulate the scenario that we need to triggerAF.
+ simulateRepeatingResult(
+ initialDelay = 100,
+ resultParameters = mapOf(
+ CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_AUTO,
+ CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_INACTIVE,
+ )
+ )
+
+ // Act.
+ submitStillCaptureRequests(
+ listOf(singleRequest),
+ ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ )
+ }
+
+ // Assert 1, verify the CONTROL_AF_TRIGGER is triggered
+ immediateCompleteCapture.verifyRequestResult {
+ it.requestContains(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
+ }
+
+ // Switch the repeating result to 3A converged state.
+ cameraControl.simulateRepeatingResult(
+ initialDelay = 500,
+ resultParameters = resultConverged
+ )
+
+ // Assert 2, that CONTROL_AF_TRIGGER should be cancelled finally.
+ immediateCompleteCapture.verifyRequestResult {
+ it.requestContains(
+ CaptureRequest.CONTROL_AF_TRIGGER,
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
+ }
+ }
+
+ @Test
+ fun miniLatency_flashOn_shouldTriggerAe() {
+ flashOn_shouldTriggerAe(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+ }
+
+ @Test
+ fun maxQuality_flashOn_shouldTriggerAe() {
+ flashOn_shouldTriggerAe(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+ }
+
+ private fun flashOn_shouldTriggerAe(imageCaptureMode: Int) {
+ val cameraControl = createCameraControl().apply {
+ // Arrange.
+ flashMode = FLASH_MODE_ON
+
+ // Act.
+ submitStillCaptureRequests(
+ listOf(singleRequest),
+ imageCaptureMode,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ )
+ simulateRepeatingResult(initialDelay = 100)
+ }
+
+ // Assert 1, verify the CONTROL_AE_PRECAPTURE_TRIGGER is triggered
+ immediateCompleteCapture.verifyRequestResult {
+ it.requestContains(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
+ )
+ }
+
+ // Switch the repeating result to 3A converged state.
+ cameraControl.simulateRepeatingResult(
+ initialDelay = 500,
+ resultParameters = resultConverged
+ )
+
+ // Assert 2 that CONTROL_AE_PRECAPTURE_TRIGGER should be cancelled finally.
+ if (Build.VERSION.SDK_INT >= 23) {
+ immediateCompleteCapture.verifyRequestResult {
+ it.requestContains(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
+ )
+ }
+ }
+ }
+
+ @Test
+ fun miniLatency_flashAutoFlashRequired_shouldTriggerAe() {
+ flashAutoFlashRequired_shouldTriggerAe(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+ }
+
+ @Test
+ fun maxQuality_flashAutoFlashRequired_shouldTriggerAe(): Unit = runBlocking {
+ flashAutoFlashRequired_shouldTriggerAe(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+ }
+
+ private fun flashAutoFlashRequired_shouldTriggerAe(imageCaptureMode: Int) {
+ val cameraControl = createCameraControl().apply {
+ // Arrange.
+ flashMode = FLASH_MODE_AUTO
+ simulateRepeatingResult(
+ initialDelay = 100,
+ resultParameters = mapOf(
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
+ )
+ )
+
+ // Act.
+ submitStillCaptureRequests(
+ listOf(singleRequest),
+ imageCaptureMode,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ )
+ }
+
+ // Assert 1, verify the CONTROL_AE_PRECAPTURE_TRIGGER is triggered
+ immediateCompleteCapture.verifyRequestResult {
+ it.requestContains(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
+ )
+ }
+
+ // Switch the repeating result to 3A converged state.
+ cameraControl.simulateRepeatingResult(
+ initialDelay = 500,
+ resultParameters = resultConverged
+ )
+
+ // Assert 2 that CONTROL_AE_PRECAPTURE_TRIGGER should be cancelled finally.
+ if (Build.VERSION.SDK_INT >= 23) {
+ immediateCompleteCapture.verifyRequestResult {
+ it.requestContains(
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
+ )
+ }
+ }
+ }
+
+ @Test
+ fun miniLatency_withTorchAsFlashQuirk_shouldOpenTorch() {
+ withTorchAsFlashQuirk_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+ }
+
+ @Test
+ fun maxQuality_withTorchAsFlashQuirk_shouldOpenTorch() {
+ withTorchAsFlashQuirk_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+ }
+
+ private fun withTorchAsFlashQuirk_shouldOpenTorch(imageCaptureMode: Int) {
+ val cameraControl = createCameraControl(
+ // Arrange.
+ quirks = Quirks(listOf(object : UseTorchAsFlashQuirk {}))
+ ).apply {
+ flashMode = FLASH_MODE_ON
+ simulateRepeatingResult(initialDelay = 100)
+
+ // Act.
+ submitStillCaptureRequests(
+ listOf(singleRequest),
+ imageCaptureMode,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ )
+ }
+
+ // Assert 1 torch should be turned on
+ cameraControl.waitForSessionConfig {
+ it.isTorchParameterEnabled()
+ }
+
+ // Switch the repeating result to 3A converged state.
+ cameraControl.simulateRepeatingResult(
+ initialDelay = 500,
+ resultParameters = resultConverged
+ )
+
+ // Assert 2 torch should be turned off
+ immediateCompleteCapture.verifyRequestResult {
+ it.isTorchParameterDisabled()
+ }
+ }
+
+ @Test
+ fun miniLatency_withTemplateRecord_shouldOpenTorch() {
+ withTemplateRecord_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+ }
+
+ @Test
+ fun maxQuality_withTemplateRecord_shouldOpenTorch() {
+ withTemplateRecord_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+ }
+
+ private fun withTemplateRecord_shouldOpenTorch(imageCaptureMode: Int) {
+
+ val cameraControl = createCameraControl().apply {
+ // Arrange.
+ setTemplate(CameraDevice.TEMPLATE_RECORD)
+ flashMode = FLASH_MODE_ON
+ simulateRepeatingResult(initialDelay = 100)
+ submitStillCaptureRequests(
+ listOf(singleRequest),
+ imageCaptureMode,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ )
+ }
+
+ // Assert 1 torch should be turned on
+ cameraControl.waitForSessionConfig {
+ it.isTorchParameterEnabled()
+ }
+
+ // Switch the repeating result to 3A converged state.
+ cameraControl.simulateRepeatingResult(
+ initialDelay = 500,
+ resultParameters = resultConverged
+ )
+
+ // Assert 2 torch should be turned off
+ immediateCompleteCapture.verifyRequestResult {
+ it.isTorchParameterDisabled()
+ }
+ }
+
+ @Test
+ fun miniLatency_withFlashTypeTorch_shouldOpenTorch() {
+ withFlashTypeTorch_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+ }
+
+ @Test
+ fun maxQuality_withFlashTypeTorch_shouldOpenTorch() {
+ withFlashTypeTorch_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+ }
+
+ private fun withFlashTypeTorch_shouldOpenTorch(imageCaptureMode: Int) {
+ val cameraControl = createCameraControl().apply {
+ flashMode = FLASH_MODE_ON
+ simulateRepeatingResult(initialDelay = 100)
+ submitStillCaptureRequests(
+ listOf(singleRequest),
+ imageCaptureMode,
+ ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH,
+ )
+ }
+
+ // Assert 1 torch should be turned on
+ cameraControl.waitForSessionConfig {
+ it.isTorchParameterEnabled()
+ }
+
+ // Switch the repeating result to 3A converged state.
+ cameraControl.simulateRepeatingResult(
+ initialDelay = 500,
+ resultParameters = resultConverged
+ )
+
+ // Assert 2 torch should be turned off
+ immediateCompleteCapture.verifyRequestResult {
+ it.isTorchParameterDisabled()
+ }
+ }
+
+ @Test
+ fun miniLatency_shouldNoPreCapture(): Unit = runBlocking {
+ // Arrange.
+ val cameraControl = createCameraControl().apply {
+ simulateRepeatingResult(initialDelay = 100)
+ }
+
+ // Act.
+ cameraControl.submitStillCaptureRequests(
+ listOf(singleRequest),
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ ).await()
+
+ // Assert, there is only 1 single capture request.
+ assertThat(immediateCompleteCapture.allResults.size).isEqualTo(1)
+ }
+
+ @Test
+ fun submitStillCaptureRequests_withTemplate_templateSent(): Unit = runBlocking {
+ // Arrange.
+ val imageCaptureConfig = CaptureConfig.Builder().let {
+ it.addSurface(fakeStillCaptureSurface)
+ it.templateType = CameraDevice.TEMPLATE_MANUAL
+ it.build()
+ }
+ val cameraControl = createCameraControl().apply {
+ simulateRepeatingResult(initialDelay = 100)
+ }
+
+ // Act.
+ cameraControl.submitStillCaptureRequests(
+ listOf(imageCaptureConfig),
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ ).await()
+
+ // Assert.
+ immediateCompleteCapture.verifyRequestResult { captureConfigList ->
+ captureConfigList.filter {
+ it.surfaces.contains(fakeStillCaptureSurface)
+ }.map { captureConfig ->
+ captureConfig.templateType
+ }.contains(CameraDevice.TEMPLATE_MANUAL)
+ }
+ }
+
+ @Test
+ fun submitStillCaptureRequests_withNoTemplate_templateStillCaptureSent(): Unit = runBlocking {
+ // Arrange.
+ val imageCaptureConfig = CaptureConfig.Builder().apply {
+ addSurface(fakeStillCaptureSurface)
+ }.build()
+ val cameraControl = createCameraControl().apply {
+ simulateRepeatingResult(initialDelay = 100)
+ }
+
+ // Act.
+ cameraControl.submitStillCaptureRequests(
+ listOf(imageCaptureConfig),
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ ).await()
+
+ // Assert.
+ immediateCompleteCapture.verifyRequestResult { captureConfigList ->
+ captureConfigList.filter {
+ it.surfaces.contains(fakeStillCaptureSurface)
+ }.map { captureConfig ->
+ captureConfig.templateType
+ }.contains(CameraDevice.TEMPLATE_STILL_CAPTURE)
+ }
+ }
+
+ @Test
+ fun submitStillCaptureRequests_withTemplateRecord_templateVideoSnapshotSent(): Unit =
+ runBlocking {
+ createCameraControl().apply {
+ // Arrange.
+ setTemplate(CameraDevice.TEMPLATE_RECORD)
+ simulateRepeatingResult(initialDelay = 100)
+
+ // Act.
+ submitStillCaptureRequests(
+ listOf(singleRequest),
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ ).await()
+ }
+
+ // Assert.
+ immediateCompleteCapture.verifyRequestResult { captureConfigList ->
+ captureConfigList.filter {
+ it.surfaces.contains(fakeStillCaptureSurface)
+ }.map { captureConfig ->
+ captureConfig.templateType
+ }.contains(CameraDevice.TEMPLATE_VIDEO_SNAPSHOT)
+ }
+ }
+
+ @Test
+ fun captureFailure_taskShouldFailure() {
+ // Arrange.
+ val immediateFailureCapture =
+ object : CameraControlInternal.ControlUpdateCallback {
+
+ override fun onCameraControlUpdateSessionConfig() {
+ }
+
+ override fun onCameraControlCaptureRequests(
+ captureConfigs: MutableList<CaptureConfig>
+ ) {
+ captureConfigs.forEach { captureConfig ->
+ captureConfig.cameraCaptureCallbacks.forEach {
+ it.onCaptureFailed(
+ CameraCaptureFailure(
+ CameraCaptureFailure.Reason.ERROR
+ )
+ )
+ }
+ }
+ }
+ }
+ val cameraControl = createCameraControl(updateCallback = immediateFailureCapture)
+
+ // Act.
+ val future = cameraControl.submitStillCaptureRequests(
+ listOf(CaptureConfig.Builder().build()),
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ )
+
+ // Assert.
+ val exception = assertThrows(ExecutionException::class.java) {
+ future.get(1, TimeUnit.SECONDS)
+ }
+ assertTrue(exception.cause is ImageCaptureException)
+ assertThat((exception.cause as ImageCaptureException).imageCaptureError).isEqualTo(
+ ImageCapture.ERROR_CAPTURE_FAILED
+ )
+ }
+
+ @Test
+ fun captureCancel_taskShouldFailureWithCAMERA_CLOSED() {
+ // Arrange.
+ val immediateCancelCapture =
+ object : CameraControlInternal.ControlUpdateCallback {
+
+ override fun onCameraControlUpdateSessionConfig() {
+ }
+
+ override fun onCameraControlCaptureRequests(
+ captureConfigs: MutableList<CaptureConfig>
+ ) {
+ captureConfigs.forEach { captureConfig ->
+ captureConfig.cameraCaptureCallbacks.forEach {
+ it.onCaptureCancelled()
+ }
+ }
+ }
+ }
+ val cameraControl = createCameraControl(updateCallback = immediateCancelCapture)
+
+ // Act.
+ val future = cameraControl.submitStillCaptureRequests(
+ listOf(CaptureConfig.Builder().build()),
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ )
+
+ // Assert.
+ val exception = assertThrows(ExecutionException::class.java) {
+ future.get(1, TimeUnit.SECONDS)
+ }
+ assertTrue(exception.cause is ImageCaptureException)
+ assertThat((exception.cause as ImageCaptureException).imageCaptureError).isEqualTo(
+ ImageCapture.ERROR_CAMERA_CLOSED
+ )
+ }
+
+ @Test
+ fun overrideAeModeForStillCapture_quirkAbsent_notOverride(): Unit = runBlocking {
+ // Arrange. Not have the quirk.
+ val cameraControl = createCameraControl(quirks = Quirks(emptyList())).apply {
+ flashMode = FLASH_MODE_ON // Set flash ON to enable aePreCapture
+ simulateRepeatingResult(initialDelay = 100) // Make sures flashMode is updated.
+ }
+
+ // Act.
+ val deferred = cameraControl.submitStillCaptureRequests(
+ listOf(singleRequest),
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ )
+ // Switch the repeating result to 3A converged state.
+ cameraControl.simulateRepeatingResult(
+ initialDelay = 500,
+ resultParameters = resultConverged
+ )
+
+ deferred.await()
+
+ // Assert.
+ // AE mode should not be overridden
+ immediateCompleteCapture.allResults.toList().flatten().filter {
+ it.surfaces.contains(fakeStillCaptureSurface)
+ }.let { stillCaptureRequests ->
+ assertThat(stillCaptureRequests).isNotEmpty()
+ stillCaptureRequests.forEach { config ->
+ assertThat(
+ config.toCamera2Config()
+ .getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE)
+ ).isNull()
+ }
+ }
+ }
+
+ @Test
+ fun overrideAeModeForStillCapture_aePrecaptureStarted_override(): Unit = runBlocking {
+ // Arrange.
+ val cameraControl = createCameraControl(
+ quirks = Quirks(listOf(AutoFlashUnderExposedQuirk()))
+ ).apply {
+ flashMode = FLASH_MODE_AUTO // Set flash auto to enable aePreCapture
+ simulateRepeatingResult(
+ initialDelay = 100,
+ resultParameters = mapOf(
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
+ )
+ ) // Make sures flashMode is updated and the flash is required.
+ }
+
+ // Act.
+ cameraControl.submitStillCaptureRequests(
+ listOf(singleRequest),
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ )
+
+ // Switch the repeating result to 3A converged state.
+ cameraControl.simulateRepeatingResult(
+ initialDelay = 500,
+ resultParameters = resultConverged
+ )
+
+ // Assert.
+ // AE mode should be overridden to CONTROL_AE_MODE_ON_ALWAYS_FLASH
+ immediateCompleteCapture.verifyRequestResult { configList ->
+ configList.requestContains(
+ CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH
+ ) && configList.surfaceContains(fakeStillCaptureSurface)
+ }
+ }
+
+ @Test
+ fun overrideAeModeForStillCapture_aePrecaptureFinish_notOverride(): Unit = runBlocking {
+ // Arrange.
+ val cameraControl = createCameraControl(
+ quirks = Quirks(listOf(AutoFlashUnderExposedQuirk()))
+ ).apply {
+ flashMode = FLASH_MODE_AUTO // Set flash auto to enable aePreCapture
+ simulateRepeatingResult(
+ initialDelay = 100,
+ resultParameters = mapOf(
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
+ )
+ ) // Make sures flashMode is updated and the flash is required.
+ }
+ val firstCapture = cameraControl.submitStillCaptureRequests(
+ listOf(singleRequest),
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ )
+
+ // Switch the repeating result to 3A converged state.
+ cameraControl.simulateRepeatingResult(
+ initialDelay = 500,
+ resultParameters = resultConverged
+ )
+ firstCapture.await()
+ immediateCompleteCapture.allResults.clear() // Clear the result of the firstCapture
+
+ // Act.
+ // Set flash OFF to disable aePreCapture for testing
+ cameraControl.flashMode = FLASH_MODE_OFF
+ cameraControl.submitStillCaptureRequests(
+ listOf(singleRequest),
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ ).await()
+
+ // Assert. The second capturing should not override the AE mode.
+ immediateCompleteCapture.allResults.toList().flatten().filter {
+ it.surfaces.contains(fakeStillCaptureSurface)
+ }.let { stillCaptureRequests ->
+ assertThat(stillCaptureRequests).isNotEmpty()
+ stillCaptureRequests.forEach { config ->
+ assertThat(
+ config.toCamera2Config()
+ .getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE)
+ ).isNull()
+ }
+ }
+ }
+
+ @Test
+ fun overrideAeModeForStillCapture_noAePrecaptureTriggered_notOverride(): Unit = runBlocking {
+ // Arrange.
+ val cameraControl =
+ createCameraControl(quirks = Quirks(listOf(AutoFlashUnderExposedQuirk()))).apply {
+ flashMode = FLASH_MODE_AUTO // Set flash auto to enable aePreCapture
+
+ // Make sures flashMode is updated but the flash is not required.
+ simulateRepeatingResult(initialDelay = 100)
+ }
+
+ // Act.
+ val deferred = cameraControl.submitStillCaptureRequests(
+ listOf(singleRequest),
+ ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+ ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+ )
+
+ // Switch the repeating result to 3A converged state.
+ cameraControl.simulateRepeatingResult(
+ initialDelay = 500,
+ resultParameters = resultConverged
+ )
+
+ deferred.await()
+
+ // Assert.
+ // AE mode should not be overridden
+ immediateCompleteCapture.allResults.toList().flatten().filter {
+ it.surfaces.contains(fakeStillCaptureSurface)
+ }.let { stillCaptureRequests ->
+ assertThat(stillCaptureRequests).isNotEmpty()
+ stillCaptureRequests.forEach { config ->
+ assertThat(
+ config.toCamera2Config()
+ .getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE)
+ ).isNull()
+ }
+ }
+ }
+
+ private fun Camera2CameraControlImpl.waitForSessionConfig(
+ checkResult: (sessionConfig: SessionConfig) -> Boolean = { true }
+ ) {
+ while (true) {
+ immediateCompleteCapture.waitForSessionConfigUpdate()
+ if (checkResult(sessionConfig)) {
+ return
+ }
+ }
+ }
+
+ private fun SessionConfig.isTorchParameterEnabled(): Boolean {
+ val config = toCamera2Config()
+
+ return config.getCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_MODE,
+ null
+ ) == CaptureRequest.CONTROL_AE_MODE_ON && config.getCaptureRequestOption(
+ CaptureRequest.FLASH_MODE,
+ null
+ ) == CameraMetadata.FLASH_MODE_TORCH
+ }
+
+ private fun List<CaptureConfig>.isTorchParameterDisabled() =
+ requestContains(
+ CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON,
+ ) && requestContains(
+ CaptureRequest.FLASH_MODE,
+ CaptureRequest.FLASH_MODE_OFF,
+ )
+
+ private fun List<CaptureConfig>.requestContains(
+ key: CaptureRequest.Key<*>,
+ value: Any?
+ ): Boolean {
+ forEach { config ->
+ if (value == config.toCamera2Config().getCaptureRequestOption(key, null)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun List<CaptureConfig>.surfaceContains(
+ surface: DeferrableSurface
+ ): Boolean {
+ forEach { config ->
+ if (config.surfaces.contains(surface)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun Camera2CameraControlImpl.simulateRepeatingResult(
+ initialDelay: Long = 100,
+ period: Long = 100, // in milliseconds
+ resultParameters: Map<CaptureResult.Key<*>, Any> = mutableMapOf(),
+ ) {
+ executorService.schedule({
+ runningRepeatingStream = executorService.scheduleAtFixedRate({
+ val tagBundle = sessionConfig.repeatingCaptureConfig.tagBundle
+ val requestOptions = sessionConfig.repeatingCaptureConfig.implementationOptions
+ val resultOptions = baseRepeatingResult.toMutableMap().apply {
+ putAll(resultParameters)
+ }
+ sendRepeatingResult(tagBundle, requestOptions.toParameters(), resultOptions)
+ }, 0, period, TimeUnit.MILLISECONDS)
+ }, initialDelay, TimeUnit.MILLISECONDS)
+ }
+
+ private fun Camera2CameraControlImpl.sendRepeatingResult(
+ requestTag: Any? = null,
+ requestParameters: Map<CaptureRequest.Key<*>, Any>,
+ resultParameters: Map<CaptureResult.Key<*>, Any>,
+ ) {
+ val request = mock(CaptureRequest::class.java)
+ Mockito.`when`(request.tag).thenReturn(requestTag)
+ requestParameters.forEach { (key, any) ->
+ Mockito.`when`(request.get(key)).thenReturn(any)
+ }
+
+ val result = mock(TotalCaptureResult::class.java)
+ Mockito.`when`(result.request).thenReturn(request)
+ resultParameters.forEach { (key, any) ->
+ Mockito.`when`(result.get(key)).thenReturn(any)
+ }
+
+ sessionConfig.repeatingCameraCaptureCallbacks.toList().forEach {
+ CaptureCallbackConverter.toCaptureCallback(it).onCaptureCompleted(
+ mock(CameraCaptureSession::class.java), request, result
+ )
+ }
+ }
+
+ private fun CaptureConfig.toCamera2Config() = Camera2ImplConfig(implementationOptions)
+
+ private fun SessionConfig.toCamera2Config() = Camera2ImplConfig(implementationOptions)
+
+ private fun createCameraControl(
+ cameraId: String = CAMERA_ID_0,
+ quirks: Quirks? = null,
+ updateCallback: CameraControlInternal.ControlUpdateCallback = immediateCompleteCapture,
+ ): Camera2CameraControlImpl {
+ val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ val characteristics = cameraManager.getCameraCharacteristics(cameraId)
+ val characteristicsCompat = CameraCharacteristicsCompat
+ .toCameraCharacteristicsCompat(characteristics)
+ val cameraQuirk = quirks ?: CameraQuirks.get(cameraId, characteristicsCompat)
+ val executorService = Executors.newSingleThreadScheduledExecutor()
+
+ return Camera2CameraControlImpl(
+ characteristicsCompat,
+ executorService,
+ executorService,
+ updateCallback,
+ cameraQuirk
+ ).apply {
+ setActive(true)
+ incrementUseCount()
+ }
+ }
+
+ private fun initCameras() {
+ Shadow.extract<ShadowCameraManager>(
+ context.getSystemService(Context.CAMERA_SERVICE)
+ ).apply {
+ addCamera(CAMERA_ID_0, intiCharacteristic0())
+ }
+ }
+
+ private fun intiCharacteristic0(): CameraCharacteristics {
+ return ShadowCameraCharacteristics.newCameraCharacteristics().also {
+ Shadow.extract<ShadowCameraCharacteristics>(it).apply {
+ set(CameraCharacteristics.FLASH_INFO_AVAILABLE, true)
+ set(
+ CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES,
+ intArrayOf(
+ CaptureRequest.CONTROL_AE_MODE_OFF,
+ CaptureRequest.CONTROL_AE_MODE_ON,
+ CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH,
+ CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH,
+ CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE,
+ CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH
+ )
+ )
+ set(
+ CameraCharacteristics.LENS_FACING,
+ CameraMetadata.LENS_FACING_BACK
+ )
+ }
+ }
+ }
+
+ private val immediateCompleteCapture =
+ object : CameraControlInternal.ControlUpdateCallback {
+ private val lock = Any()
+ val allResults: MutableList<List<CaptureConfig>> = mutableListOf()
+ val waitingList = mutableListOf<Pair<CountDownLatch,
+ (captureRequests: List<CaptureConfig>) -> Boolean>>()
+ var updateSessionCountDown: CountDownLatch? = null
+
+ fun verifyRequestResult(
+ timeout: Long = TimeUnit.SECONDS.toMillis(5),
+ verifyResults: (captureRequests: List<CaptureConfig>) -> Boolean = { true }
+ ) {
+ synchronized(lock) {
+ allResults.forEach {
+ if (verifyResults(it)) {
+ return
+ }
+ }
+ }
+ val resultPair = Pair(CountDownLatch(1), verifyResults)
+ waitingList.add(resultPair)
+ assertTrue(resultPair.first.await(timeout, TimeUnit.MILLISECONDS))
+ waitingList.remove(resultPair)
+ }
+
+ fun waitForSessionConfigUpdate(timeout: Long = TimeUnit.SECONDS.toMillis(5)) {
+ if (updateSessionCountDown == null) {
+ updateSessionCountDown = CountDownLatch(1)
+ }
+ assertTrue(updateSessionCountDown!!.await(timeout, TimeUnit.MILLISECONDS))
+ }
+
+ override fun onCameraControlUpdateSessionConfig() {
+ updateSessionCountDown?.countDown()
+ updateSessionCountDown = null
+ }
+
+ override fun onCameraControlCaptureRequests(
+ captureConfigs: MutableList<CaptureConfig>
+ ) {
+ synchronized(lock) {
+ allResults.add(captureConfigs)
+ }
+ waitingList.toList().forEach {
+ if (it.second(captureConfigs)) {
+ it.first.countDown()
+ }
+ }
+
+ // Complete the single capture with an empty result.
+ captureConfigs.forEach { captureConfig ->
+ captureConfig.cameraCaptureCallbacks.forEach {
+ it.onCaptureCompleted(CameraCaptureResult.EmptyCameraCaptureResult())
+ }
+ }
+ }
+ }
+
+ /**
+ * Convert the Config to the CaptureRequest key-value map.
+ */
+ private fun androidx.camera.core.impl.Config.toParameters(): Map<CaptureRequest.Key<*>, Any> {
+ val parameters = mutableMapOf<CaptureRequest.Key<*>, Any>()
+ for (configOption in listOptions()) {
+ val requestKey = configOption.token as? CaptureRequest.Key<*> ?: continue
+ val value = retrieveOption(configOption) ?: continue
+ parameters[requestKey] = value
+ }
+
+ return parameters
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompatTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompatTest.java
index 4564f50..0738fcd 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompatTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompatTest.java
@@ -20,6 +20,7 @@
import android.graphics.ImageFormat;
import android.hardware.camera2.params.InputConfiguration;
+import android.hardware.camera2.params.MultiResolutionStreamInfo;
import android.os.Build;
import org.junit.Test;
@@ -28,6 +29,9 @@
import org.robolectric.annotation.Config;
import org.robolectric.annotation.internal.DoNotInstrument;
+import java.util.ArrayList;
+import java.util.List;
+
@RunWith(RobolectricTestRunner.class)
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
@@ -36,6 +40,7 @@
private static final int WIDTH = 1024;
private static final int HEIGHT = 768;
private static final int FORMAT = ImageFormat.YUV_420_888;
+ private static final String CAMERA_ID = "0";
@Test
public void canCreateInputConfigurationCompat() {
@@ -75,7 +80,7 @@
}
@Test
- @Config(minSdk = Build.VERSION_CODES.M)
+ @Config(minSdk = Build.VERSION_CODES.M, maxSdk = Build.VERSION_CODES.R)
public void baseImplHashCodeMatchesFramework() {
InputConfigurationCompat.InputConfigurationCompatBaseImpl baseImpl =
new InputConfigurationCompat.InputConfigurationCompatBaseImpl(WIDTH, HEIGHT,
@@ -87,7 +92,7 @@
}
@Test
- @Config(minSdk = Build.VERSION_CODES.M)
+ @Config(minSdk = Build.VERSION_CODES.M, maxSdk = Build.VERSION_CODES.R)
public void baseImplToStringMatchesFramework() {
InputConfigurationCompat.InputConfigurationCompatBaseImpl baseImpl =
new InputConfigurationCompat.InputConfigurationCompatBaseImpl(WIDTH, HEIGHT,
@@ -97,4 +102,17 @@
assertThat(baseImpl.toString()).isEqualTo(config.toString());
}
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.S)
+ public void isMultiResolutionMatchesFramework() {
+ List<MultiResolutionStreamInfo> multiResolutionInputs = new ArrayList<>();
+ multiResolutionInputs.add(new MultiResolutionStreamInfo(WIDTH, HEIGHT, CAMERA_ID));
+
+ InputConfiguration inputConfig = new InputConfiguration(multiResolutionInputs, FORMAT);
+ InputConfigurationCompat compat = InputConfigurationCompat.wrap(inputConfig);
+
+ assertThat(compat).isNotNull();
+ assertThat(compat.isMultiResolution()).isEqualTo(inputConfig.isMultiResolution());
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 7c78c0d..65f8b14 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -54,7 +54,6 @@
import android.net.Uri;
import android.os.Build;
import android.os.Looper;
-import android.os.SystemClock;
import android.provider.MediaStore;
import android.util.Log;
import android.util.Pair;
@@ -75,13 +74,6 @@
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
import androidx.camera.core.impl.CameraCaptureCallback;
-import androidx.camera.core.impl.CameraCaptureFailure;
-import androidx.camera.core.impl.CameraCaptureMetaData.AeState;
-import androidx.camera.core.impl.CameraCaptureMetaData.AfMode;
-import androidx.camera.core.impl.CameraCaptureMetaData.AfState;
-import androidx.camera.core.impl.CameraCaptureMetaData.AwbState;
-import androidx.camera.core.impl.CameraCaptureResult;
-import androidx.camera.core.impl.CameraCaptureResult.EmptyCameraCaptureResult;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.CaptureBundle;
@@ -107,7 +99,6 @@
import androidx.camera.core.impl.utils.Threads;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
-import androidx.camera.core.impl.utils.futures.FutureChain;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.camera.core.internal.IoConfig;
import androidx.camera.core.internal.TargetConfig;
@@ -131,10 +122,8 @@
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
-import java.util.HashSet;
import java.util.List;
import java.util.Locale;
-import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
@@ -245,8 +234,6 @@
public static final Defaults DEFAULT_CONFIG = new Defaults();
private static final String TAG = "ImageCapture";
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
- private static final long CHECK_3A_TIMEOUT_IN_MS = 1000L;
- private static final long CHECK_3A_WITH_FLASH_TIMEOUT_IN_MS = 5000L;
private static final int MAX_IMAGES = 2;
// TODO(b/149336664) Move the quality to a compatibility class when there is a per device case.
private static final byte JPEG_QUALITY_MAXIMIZE_QUALITY_MODE = 100;
@@ -256,8 +243,6 @@
@FlashMode
private static final int DEFAULT_FLASH_MODE = FLASH_MODE_OFF;
- private final CaptureCallbackChecker mSessionCallbackChecker = new CaptureCallbackChecker();
-
private final ImageReaderProxy.OnImageAvailableListener mClosingListener = (imageReader -> {
try (ImageProxy image = imageReader.acquireLatestImage()) {
Log.d(TAG, "Discarding ImageProxy which was inadvertently acquired: " + image);
@@ -272,15 +257,6 @@
@CaptureMode
private final int mCaptureMode;
- /**
- * A flag to check 3A converged or not.
- *
- * <p>In order to speed up the taking picture process, trigger AF / AE should be skipped when
- * the flag is disabled. Set it to be enabled in the maximum quality mode and disabled in the
- * minimum latency mode.
- */
- private final boolean mEnableCheck3AConverged;
-
@GuardedBy("mLockedFlashMode")
private final AtomicReference<Integer> mLockedFlashMode = new AtomicReference<>(null);
@@ -368,11 +344,6 @@
useCaseConfig.getIoExecutor(CameraXExecutors.ioExecutor()));
mSequentialIoExecutor = CameraXExecutors.newSequentialExecutor(mIoExecutor);
- if (mCaptureMode == CAPTURE_MODE_MAXIMIZE_QUALITY) {
- mEnableCheck3AConverged = true; // check 3A convergence in MAX_QUALITY mode
- } else {
- mEnableCheck3AConverged = false; // skip 3A convergence in MIN_LATENCY mode
- }
}
@UiThread
@@ -381,7 +352,6 @@
@NonNull ImageCaptureConfig config, @NonNull Size resolution) {
Threads.checkMainThread();
SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
- sessionConfigBuilder.addRepeatingCameraCaptureCallback(mSessionCallbackChecker);
YuvToJpegProcessor softwareJpegProcessor = null;
// Setup the ImageReader to do processing
@@ -1123,7 +1093,7 @@
}
}
- private void unlockFlashMode() {
+ void unlockFlashMode() {
synchronized (mLockedFlashMode) {
Integer lockedFlashMode = mLockedFlashMode.getAndSet(null);
if (lockedFlashMode == null) {
@@ -1208,20 +1178,19 @@
},
CameraXExecutors.mainThreadExecutor());
- TakePictureState state = new TakePictureState();
- ListenableFuture<Void> future = FutureChain.from(preTakePicture(state))
- .transformAsync(v -> issueTakePicture(imageCaptureRequest), mExecutor);
+ lockFlashMode();
+ ListenableFuture<Void> future = issueTakePicture(imageCaptureRequest);
Futures.addCallback(future,
new FutureCallback<Void>() {
@Override
public void onSuccess(Void result) {
- postTakePicture(state);
+ unlockFlashMode();
}
@Override
public void onFailure(Throwable throwable) {
- postTakePicture(state);
+ unlockFlashMode();
completer.setException(throwable);
}
@@ -1425,8 +1394,8 @@
static int getError(Throwable throwable) {
if (throwable instanceof CameraClosedException) {
return ERROR_CAMERA_CLOSED;
- } else if (throwable instanceof CaptureFailedException) {
- return ERROR_CAPTURE_FAILED;
+ } else if (throwable instanceof ImageCaptureException) {
+ return ((ImageCaptureException) throwable).getImageCaptureError();
} else {
return ERROR_UNKNOWN;
}
@@ -1541,183 +1510,6 @@
}
/**
- * Routine before taking picture.
- *
- * <p>For example, trigger 3A scan, open torch and check 3A converged if necessary.
- */
- private ListenableFuture<Void> preTakePicture(final TakePictureState state) {
- lockFlashMode();
- return FutureChain.from(getPreCaptureStateIfNeeded())
- .transformAsync(captureResult -> {
- state.mPreCaptureState = captureResult;
- triggerAfIfNeeded(state);
- if (isFlashRequired(state)) {
- return startFlashSequence(state);
- }
- return Futures.immediateFuture(null);
- }, mExecutor)
- .transformAsync(v -> check3AConverged(state), mExecutor)
- // Ignore the 3A convergence result.
- .transform(is3AConverged -> null, mExecutor);
- }
-
- /**
- * Routine after picture was taken.
- *
- * <p>For example, cancel 3A scan, close torch if necessary.
- */
- void postTakePicture(final TakePictureState state) {
- cancelAfAndFinishFlashSequence(state);
- unlockFlashMode();
- }
-
- /**
- * Gets a capture result or not according to current configuration.
- *
- * <p>Conditions to get a capture result.
- *
- * <p>(1) The enableCheck3AConverged is enabled because it needs to know current AF mode and
- * state.
- *
- * <p>(2) The flashMode is AUTO because it needs to know the current AE state.
- */
- // Currently this method is used to prevent there is no repeating surface to get capture result.
- // If app is in min-latency mode and flash ALWAYS/OFF mode, it can still take picture without
- // checking the capture result. Remove this check once no repeating surface issue is fixed.
- private ListenableFuture<CameraCaptureResult> getPreCaptureStateIfNeeded() {
- if (mEnableCheck3AConverged || getFlashMode() == FLASH_MODE_AUTO) {
- return mSessionCallbackChecker.checkCaptureResult(
- new CaptureCallbackChecker.CaptureResultChecker<CameraCaptureResult>() {
- @Override
- public CameraCaptureResult check(
- @NonNull CameraCaptureResult captureResult) {
- if (Logger.isDebugEnabled(TAG)) {
- Logger.d(TAG, "preCaptureState, AE=" + captureResult.getAeState()
- + " AF =" + captureResult.getAfState()
- + " AWB=" + captureResult.getAwbState());
- }
- return captureResult;
- }
- });
- }
- return Futures.immediateFuture(null);
- }
-
- boolean isFlashRequired(@NonNull TakePictureState state) {
- switch (getFlashMode()) {
- case FLASH_MODE_ON:
- return true;
- case FLASH_MODE_AUTO:
- return state.mPreCaptureState.getAeState() == AeState.FLASH_REQUIRED;
- case FLASH_MODE_OFF:
- return false;
- }
- throw new AssertionError(getFlashMode());
- }
-
- ListenableFuture<Boolean> check3AConverged(TakePictureState state) {
- if (!mEnableCheck3AConverged && !state.mIsFlashSequenceStarted) {
- return Futures.immediateFuture(false);
- }
-
- long waitTimeout = CHECK_3A_TIMEOUT_IN_MS;
- if (state.mIsFlashSequenceStarted) {
- waitTimeout = CHECK_3A_WITH_FLASH_TIMEOUT_IN_MS;
- }
-
- return mSessionCallbackChecker.checkCaptureResult(
- new CaptureCallbackChecker.CaptureResultChecker<Boolean>() {
- @Override
- public Boolean check(@NonNull CameraCaptureResult captureResult) {
- if (Logger.isDebugEnabled(TAG)) {
- Logger.d(TAG, "checkCaptureResult, AE=" + captureResult.getAeState()
- + " AF =" + captureResult.getAfState()
- + " AWB=" + captureResult.getAwbState());
- }
-
- if (is3AConverged(captureResult)) {
- return true;
- }
- // Return null to continue check.
- return null;
- }
- },
- waitTimeout,
- false);
- }
-
- boolean is3AConverged(CameraCaptureResult captureResult) {
- if (captureResult == null) {
- return false;
- }
-
- // If afMode is OFF or UNKNOWN , no need for waiting.
- // otherwise wait until af is locked or focused.
- boolean isAfReady = captureResult.getAfMode() == AfMode.OFF
- || captureResult.getAfMode() == AfMode.UNKNOWN
- || captureResult.getAfState() == AfState.PASSIVE_FOCUSED
- || captureResult.getAfState() == AfState.PASSIVE_NOT_FOCUSED
- || captureResult.getAfState() == AfState.LOCKED_FOCUSED
- || captureResult.getAfState() == AfState.LOCKED_NOT_FOCUSED;
-
- // Unknown means cannot get valid state from CaptureResult
- boolean isAeReady = captureResult.getAeState() == AeState.CONVERGED
- || captureResult.getAeState() == AeState.FLASH_REQUIRED
- || captureResult.getAeState() == AeState.UNKNOWN;
-
- // Unknown means cannot get valid state from CaptureResult
- boolean isAwbReady = captureResult.getAwbState() == AwbState.CONVERGED
- || captureResult.getAwbState() == AwbState.UNKNOWN;
-
- return (isAfReady && isAeReady && isAwbReady);
- }
-
- /**
- * Issues the AF scan if needed.
- *
- * <p>If enableCheck3AConverged is disabled or it is in CAF mode, AF scan should not be
- * triggered. Trigger AF scan only in {@link AfMode#ON_MANUAL_AUTO} and current AF state is
- * {@link AfState#INACTIVE}. If the AF mode is {@link AfMode#ON_MANUAL_AUTO} and AF state is not
- * inactive, it means that a manual or auto focus request may be in progress or completed.
- */
- void triggerAfIfNeeded(TakePictureState state) {
- if (mEnableCheck3AConverged
- && state.mPreCaptureState.getAfMode() == AfMode.ON_MANUAL_AUTO
- && state.mPreCaptureState.getAfState() == AfState.INACTIVE) {
- triggerAf(state);
- }
- }
-
- /** Issues a request to start auto focus scan. */
- private void triggerAf(TakePictureState state) {
- Logger.d(TAG, "triggerAf");
- state.mIsAfTriggered = true;
- ListenableFuture<CameraCaptureResult> future = getCameraControl().triggerAf();
- // Add listener to avoid FutureReturnValueIgnored error.
- future.addListener(() -> {
- }, CameraXExecutors.directExecutor());
- }
-
- /** Issues a request to start auto exposure scan. */
- @NonNull
- ListenableFuture<Void> startFlashSequence(@NonNull TakePictureState state) {
- Logger.d(TAG, "startFlashSequence");
- state.mIsFlashSequenceStarted = true;
- return getCameraControl().startFlashSequence(mFlashType);
- }
-
- /** Issues a request to cancel auto focus and/or auto exposure scan. */
- void cancelAfAndFinishFlashSequence(@NonNull TakePictureState state) {
- if (!state.mIsAfTriggered && !state.mIsFlashSequenceStarted) {
- return;
- }
- getCameraControl().cancelAfAndFinishFlashSequence(state.mIsAfTriggered,
- state.mIsFlashSequenceStarted);
- state.mIsAfTriggered = false;
- state.mIsFlashSequenceStarted = false;
- }
-
- /**
* Initiates a set of captures that will be used to create the output of
* {@link #takePicture(OutputFileOptions, Executor, OnImageSavedCallback)} and its variants.
*
@@ -1729,7 +1521,6 @@
ListenableFuture<Void> issueTakePicture(@NonNull ImageCaptureRequest imageCaptureRequest) {
Logger.d(TAG, "issueTakePicture");
- final List<ListenableFuture<Void>> futureList = new ArrayList<>();
final List<CaptureConfig> captureConfigs = new ArrayList<>();
String tagBundleKey = null;
@@ -1793,61 +1584,14 @@
builder.addTag(tagBundleKey, captureStage.getId());
}
builder.addCameraCaptureCallback(mMetadataMatchingCaptureCallback);
-
- ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(
- completer -> {
- CameraCaptureCallback completerCallback = new CameraCaptureCallback() {
- @Override
- public void onCaptureCompleted(
- @NonNull CameraCaptureResult result) {
- completer.set(null);
- }
-
- @Override
- public void onCaptureFailed(
- @NonNull CameraCaptureFailure failure) {
- String msg = "Capture request failed with reason "
- + failure.getReason();
- completer.setException(new CaptureFailedException(msg));
- }
-
- @Override
- public void onCaptureCancelled() {
- String msg = "Capture request is cancelled because "
- + "camera is closed";
- completer.setException(new CameraClosedException(msg));
- }
- };
- builder.addCameraCaptureCallback(completerCallback);
-
- captureConfigs.add(builder.build());
- return "issueTakePicture[stage=" + captureStage.getId() + "]";
- });
- futureList.add(future);
-
+ captureConfigs.add(builder.build());
}
- getCameraControl().submitStillCaptureRequests(captureConfigs);
- return Futures.transform(Futures.allAsList(futureList),
+ return Futures.transform(getCameraControl().submitStillCaptureRequests(
+ captureConfigs, mCaptureMode, mFlashType),
input -> null, CameraXExecutors.directExecutor());
}
- /** This exception is thrown when request is failed (reported by framework) */
- static final class CaptureFailedException extends RuntimeException {
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- CaptureFailedException(String s, Throwable e) {
- super(s, e);
- }
-
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- CaptureFailedException(String s) {
- super(s);
- }
- }
-
-
private CaptureBundle getCaptureBundle(CaptureBundle defaultCaptureBundle) {
List<CaptureStage> captureStages = mCaptureBundle.getCaptureStages();
if (captureStages == null || captureStages.isEmpty()) {
@@ -2295,148 +2039,6 @@
}
}
- /**
- * An intermediate action recorder while taking picture. It is used to restore certain states.
- * For example, cancel AF/AE scan, and close flash light.
- */
- static final class TakePictureState {
- CameraCaptureResult mPreCaptureState = EmptyCameraCaptureResult.create();
- boolean mIsAfTriggered = false;
- boolean mIsFlashSequenceStarted = false;
- }
-
- /**
- * A helper class to check camera capture result.
- *
- * <p>CaptureCallbackChecker is an implementation of {@link CameraCaptureCallback} that checks a
- * specified list of condition and sets a ListenableFuture when the conditions have been met. It
- * is mainly used to continuously capture callbacks to detect specific conditions. It also
- * handles the timeout condition if the check condition does not satisfy the given timeout, and
- * returns the given default value if the timeout is met.
- */
- static final class CaptureCallbackChecker extends CameraCaptureCallback {
- private static final long NO_TIMEOUT = 0L;
-
- /** Capture listeners. */
- private final Set<CaptureResultListener> mCaptureResultListeners = new HashSet<>();
-
- @Override
- public void onCaptureCompleted(@NonNull CameraCaptureResult cameraCaptureResult) {
- deliverCaptureResultToListeners(cameraCaptureResult);
- }
-
- /**
- * Check the capture results of current session capture callback by giving a {@link
- * CaptureResultChecker}.
- *
- * @param checker a CaptureResult checker that returns an object with type T if the check is
- * complete, returning null to continue the check process.
- * @param <T> the type parameter for CaptureResult checker.
- * @return a listenable future for capture result check process.
- */
- <T> ListenableFuture<T> checkCaptureResult(CaptureResultChecker<T> checker) {
- return checkCaptureResult(checker, NO_TIMEOUT, null);
- }
-
- /**
- * Check the capture results of current session capture callback with timeout limit by
- * giving a {@link CaptureResultChecker}.
- *
- * @param checker a CaptureResult checker that returns an object with type T if the
- * check is
- * complete, returning null to continue the check process.
- * @param timeoutInMs used to force stop checking.
- * @param defValue the default return value if timeout occur.
- * @param <T> the type parameter for CaptureResult checker.
- * @return a listenable future for capture result check process.
- */
- <T> ListenableFuture<T> checkCaptureResult(
- final CaptureResultChecker<T> checker, final long timeoutInMs, final T defValue) {
- if (timeoutInMs < NO_TIMEOUT) {
- throw new IllegalArgumentException("Invalid timeout value: " + timeoutInMs);
- }
- final long startTimeInMs =
- (timeoutInMs != NO_TIMEOUT) ? SystemClock.elapsedRealtime() : 0L;
-
- return CallbackToFutureAdapter.getFuture(
- completer -> {
- addListener(
- new CaptureResultListener() {
- @Override
- public boolean onCaptureResult(
- @NonNull CameraCaptureResult captureResult) {
- T result = checker.check(captureResult);
- if (result != null) {
- completer.set(result);
- return true;
- } else if (startTimeInMs > 0
- && SystemClock.elapsedRealtime() - startTimeInMs
- > timeoutInMs) {
- completer.set(defValue);
- return true;
- }
- // Return false to continue check.
- return false;
- }
- });
- return "checkCaptureResult";
- });
- }
-
- /**
- * Delivers camera capture result to {@link CaptureCallbackChecker#mCaptureResultListeners}.
- */
- private void deliverCaptureResultToListeners(@NonNull CameraCaptureResult captureResult) {
- synchronized (mCaptureResultListeners) {
- Set<CaptureResultListener> removeSet = null;
- for (CaptureResultListener listener : new HashSet<>(mCaptureResultListeners)) {
- // Remove listener if the callback return true
- if (listener.onCaptureResult(captureResult)) {
- if (removeSet == null) {
- removeSet = new HashSet<>();
- }
- removeSet.add(listener);
- }
- }
- if (removeSet != null) {
- mCaptureResultListeners.removeAll(removeSet);
- }
- }
- }
-
- /** Add capture result listener. */
- void addListener(CaptureResultListener listener) {
- synchronized (mCaptureResultListeners) {
- mCaptureResultListeners.add(listener);
- }
- }
-
- /** An interface to check camera capture result. */
- public interface CaptureResultChecker<T> {
-
- /**
- * The callback to check camera capture result.
- *
- * @param captureResult the camera capture result.
- * @return the check result, return null to continue checking.
- */
- @Nullable
- T check(@NonNull CameraCaptureResult captureResult);
- }
-
- /** An interface to listen to camera capture results. */
- private interface CaptureResultListener {
-
- /**
- * Callback to handle camera capture results.
- *
- * @param captureResult camera capture result.
- * @return true to finish listening, false to continue listening.
- */
- boolean onCaptureResult(@NonNull CameraCaptureResult captureResult);
- }
- }
-
@VisibleForTesting
static class ImageCaptureRequest {
@RotationValue
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java
index d770f51..d54398b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java
@@ -26,11 +26,14 @@
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.FocusMeteringResult;
import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCapture.CaptureMode;
import androidx.camera.core.ImageCapture.FlashMode;
+import androidx.camera.core.ImageCapture.FlashType;
import androidx.camera.core.impl.utils.futures.Futures;
import com.google.common.util.concurrent.ListenableFuture;
+import java.util.Collections;
import java.util.List;
/**
@@ -55,42 +58,23 @@
void setFlashMode(@FlashMode int flashMode);
/**
- * Performs a AF trigger.
+ * Performs still capture requests with the desired capture mode.
*
- * @return a {@link ListenableFuture} which completes when the request is completed.
- * Cancelling the ListenableFuture is a no-op.
+ * @param captureConfigs capture configuration used for creating CaptureRequest
+ * @param captureMode the mode to capture the image, possible value is
+ * {@link ImageCapture#CAPTURE_MODE_MINIMIZE_LATENCY} or
+ * {@link ImageCapture#CAPTURE_MODE_MAXIMIZE_QUALITY}
+ * @param flashType the options when flash is required for taking a picture.
+ * @return ListenableFuture that would be completed while all the captures are completed. It
+ * would fail with a {@link androidx.camera.core.ImageCapture#ERROR_CAMERA_CLOSED} when the
+ * capture was canceled, or a {@link androidx.camera.core.ImageCapture#ERROR_CAPTURE_FAILED}
+ * when the capture was failed.
*/
@NonNull
- ListenableFuture<CameraCaptureResult> triggerAf();
-
- /**
- * Starts a flash sequence.
- *
- * @param flashType Uses one shot flash or use torch as flash when taking a picture.
- * @return a {@link ListenableFuture} which completes when the request is completed.
- * Cancelling the ListenableFuture is a no-op.
- */
- @NonNull
- ListenableFuture<Void> startFlashSequence(@ImageCapture.FlashType int flashType);
-
- /** Cancels AF trigger AND/OR finishes flash sequence.* */
- void cancelAfAndFinishFlashSequence(boolean cancelAfTrigger, boolean finishFlashSequence);
-
- /**
- * Set a exposure compensation to the camera
- *
- * @param exposure the exposure compensation value to set
- * @return a ListenableFuture which is completed when the new exposure compensation reach the
- * target.
- */
- @NonNull
- @Override
- ListenableFuture<Integer> setExposureCompensationIndex(int exposure);
-
- /**
- * Performs still capture requests.
- */
- void submitStillCaptureRequests(@NonNull List<CaptureConfig> captureConfigs);
+ ListenableFuture<List<Void>> submitStillCaptureRequests(
+ @NonNull List<CaptureConfig> captureConfigs,
+ @CaptureMode int captureMode,
+ @FlashType int flashType);
/**
* Gets the current SessionConfig.
@@ -141,31 +125,19 @@
return Futures.immediateFuture(null);
}
- @Override
- @NonNull
- public ListenableFuture<CameraCaptureResult> triggerAf() {
- return Futures.immediateFuture(CameraCaptureResult.EmptyCameraCaptureResult.create());
- }
-
- @Override
- @NonNull
- public ListenableFuture<Void> startFlashSequence(@ImageCapture.FlashType int flashType) {
- return Futures.immediateFuture(null);
- }
-
- @Override
- public void cancelAfAndFinishFlashSequence(boolean cancelAfTrigger,
- boolean finishFlashSequence) {
- }
-
@NonNull
@Override
public ListenableFuture<Integer> setExposureCompensationIndex(int exposure) {
return Futures.immediateFuture(0);
}
+ @NonNull
@Override
- public void submitStillCaptureRequests(@NonNull List<CaptureConfig> captureConfigs) {
+ public ListenableFuture<List<Void>> submitStillCaptureRequests(
+ @NonNull List<CaptureConfig> captureConfigs,
+ @CaptureMode int captureMode,
+ @FlashType int flashType) {
+ return Futures.immediateFuture(Collections.emptyList());
}
@NonNull
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
index 875f924..2ee6507 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
@@ -26,6 +26,7 @@
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.FocusMeteringResult;
import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.Logger;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraCaptureFailure;
@@ -36,6 +37,7 @@
import androidx.camera.core.impl.MutableOptionsBundle;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
import com.google.common.util.concurrent.ListenableFuture;
@@ -56,6 +58,8 @@
private ArrayList<CaptureConfig> mSubmittedCaptureRequests = new ArrayList<>();
private OnNewCaptureRequestListener mOnNewCaptureRequestListener;
private MutableOptionsBundle mInteropConfig = MutableOptionsBundle.create();
+ private final ArrayList<CallbackToFutureAdapter.Completer<Void>> mSubmittedCompleterList =
+ new ArrayList<>();
public FakeCameraControl(@NonNull ControlUpdateCallback controlUpdateCallback) {
mControlUpdateCallback = controlUpdateCallback;
@@ -69,6 +73,12 @@
cameraCaptureCallback.onCaptureCancelled();
}
}
+ for (CallbackToFutureAdapter.Completer<Void> completer : mSubmittedCompleterList) {
+ completer.setException(
+ new ImageCaptureException(ImageCapture.ERROR_CAMERA_CLOSED, "Simulate "
+ + "capture cancelled", null));
+ }
+ mSubmittedCompleterList.clear();
mSubmittedCaptureRequests.clear();
}
@@ -81,17 +91,26 @@
CameraCaptureFailure.Reason.ERROR));
}
}
+ for (CallbackToFutureAdapter.Completer<Void> completer : mSubmittedCompleterList) {
+ completer.setException(new ImageCaptureException(ImageCapture.ERROR_CAPTURE_FAILED,
+ "Simulate capture fail", null));
+ }
+ mSubmittedCompleterList.clear();
mSubmittedCaptureRequests.clear();
}
/** Notifies all submitted requests onCaptureCompleted */
- public void notifyAllRequestsOnCaptureCompleted(CameraCaptureResult result) {
+ public void notifyAllRequestsOnCaptureCompleted(@NonNull CameraCaptureResult result) {
for (CaptureConfig captureConfig : mSubmittedCaptureRequests) {
for (CameraCaptureCallback cameraCaptureCallback :
captureConfig.getCameraCaptureCallbacks()) {
cameraCaptureCallback.onCaptureCompleted(result);
}
}
+ for (CallbackToFutureAdapter.Completer<Void> completer : mSubmittedCompleterList) {
+ completer.set(null);
+ }
+ mSubmittedCompleterList.clear();
mSubmittedCaptureRequests.clear();
}
@@ -114,40 +133,31 @@
return Futures.immediateFuture(null);
}
- @Override
- @NonNull
- public ListenableFuture<CameraCaptureResult> triggerAf() {
- Logger.d(TAG, "triggerAf()");
- return Futures.immediateFuture(CameraCaptureResult.EmptyCameraCaptureResult.create());
- }
-
- @Override
- @NonNull
- public ListenableFuture<Void> startFlashSequence(@ImageCapture.FlashType int flashType) {
- Logger.d(TAG, "startFlashSequence()");
- return Futures.immediateFuture(null);
- }
-
- @Override
- public void cancelAfAndFinishFlashSequence(final boolean cancelAfTrigger,
- final boolean finishFlashSequence) {
- Logger.d(TAG, "cancelAfAndFinishFlashSequence(" + cancelAfTrigger + ", "
- + finishFlashSequence + ")");
- }
-
@NonNull
@Override
public ListenableFuture<Integer> setExposureCompensationIndex(int exposure) {
return Futures.immediateFuture(null);
}
+ @NonNull
@Override
- public void submitStillCaptureRequests(@NonNull List<CaptureConfig> captureConfigs) {
+ public ListenableFuture<List<Void>> submitStillCaptureRequests(
+ @NonNull List<CaptureConfig> captureConfigs,
+ int captureMode, int flashType) {
mSubmittedCaptureRequests.addAll(captureConfigs);
mControlUpdateCallback.onCameraControlCaptureRequests(captureConfigs);
+ List<ListenableFuture<Void>> fakeFutures = new ArrayList<>();
+ for (int i = 0; i < captureConfigs.size(); i++) {
+ fakeFutures.add(CallbackToFutureAdapter.getFuture(completer -> {
+ mSubmittedCompleterList.add(completer);
+ return "fakeFuture";
+ }));
+ }
+
if (mOnNewCaptureRequestListener != null) {
mOnNewCaptureRequestListener.onNewCaptureRequests(captureConfigs);
}
+ return Futures.allAsList(fakeFutures);
}
@NonNull
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/AutomotiveCarHardwareManager.java b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/AutomotiveCarHardwareManager.java
index 4ce76a7..23a3b70 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/AutomotiveCarHardwareManager.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/AutomotiveCarHardwareManager.java
@@ -43,8 +43,8 @@
private final AutomotiveCarSensors mCarSensors;
public AutomotiveCarHardwareManager(@NonNull Context context) {
- requireNonNull(context);
- mCarInfo = new AutomotiveCarInfo(new PropertyManager(context));
+ Context appContext = requireNonNull(context.getApplicationContext());
+ mCarInfo = new AutomotiveCarInfo(new PropertyManager(appContext));
mCarSensors = new AutomotiveCarSensors();
}
diff --git a/car/app/app-samples/showcase/automotive/build.gradle b/car/app/app-samples/showcase/automotive/build.gradle
index a3c02a9..5dd1a78 100644
--- a/car/app/app-samples/showcase/automotive/build.gradle
+++ b/car/app/app-samples/showcase/automotive/build.gradle
@@ -24,8 +24,10 @@
applicationId "androidx.car.app.sample.showcase"
minSdkVersion 29
targetSdkVersion 31
- versionCode 106 // Increment this to generate signed builds for uploading to Playstore
- versionName "106"
+ // Increment this to generate signed builds for uploading to Playstore
+ // Make sure this is different from the showcase-mobile version
+ versionCode 107
+ versionName "107"
}
buildTypes {
diff --git a/car/app/app-samples/showcase/automotive/github_build.gradle b/car/app/app-samples/showcase/automotive/github_build.gradle
index 289cbdb..d41d257 100644
--- a/car/app/app-samples/showcase/automotive/github_build.gradle
+++ b/car/app/app-samples/showcase/automotive/github_build.gradle
@@ -23,8 +23,10 @@
applicationId "androidx.car.app.sample.showcase"
minSdkVersion 29
targetSdkVersion 31
- versionCode 106 // Increment this to generate signed builds for uploading to Playstore
- versionName "106"
+ // Increment this to generate signed builds for uploading to Playstore
+ // Make sure this is different from the showcase-mobile version
+ versionCode 107
+ versionName "107"
}
buildTypes {
diff --git a/car/app/app-samples/showcase/mobile/build.gradle b/car/app/app-samples/showcase/mobile/build.gradle
index 27d12da..74bf224 100644
--- a/car/app/app-samples/showcase/mobile/build.gradle
+++ b/car/app/app-samples/showcase/mobile/build.gradle
@@ -24,7 +24,9 @@
applicationId "androidx.car.app.sample.showcase"
minSdkVersion 23
targetSdkVersion 31
- versionCode 106 // Increment this to generate signed builds for uploading to Playstore
+ // Increment this to generate signed builds for uploading to Playstore
+ // Make sure this is different from the showcase-automotive version
+ versionCode 106
versionName "106"
}
diff --git a/car/app/app-samples/showcase/mobile/github_build.gradle b/car/app/app-samples/showcase/mobile/github_build.gradle
index 4d8d3a7..83498fb 100644
--- a/car/app/app-samples/showcase/mobile/github_build.gradle
+++ b/car/app/app-samples/showcase/mobile/github_build.gradle
@@ -23,7 +23,9 @@
applicationId "androidx.car.app.sample.showcase"
minSdkVersion 23
targetSdkVersion 31
- versionCode 106 // Increment this to generate signed builds for uploading to Playstore
+ // Increment this to generate signed builds for uploading to Playstore
+ // Make sure this is different from the showcase-automotive version
+ versionCode 106
versionName "106"
}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/PanModeDelegate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/PanModeDelegate.java
index 6c51b0a..3a11339 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/PanModeDelegate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/PanModeDelegate.java
@@ -32,7 +32,7 @@
/**
* Notifies that the user has entered or exited pan mode.
*
- * @param isInPanMode the updated checked state
+ * @param isInPanMode the latest pan mode state.
* @param callback the {@link OnDoneCallback} to trigger when the client finishes handling
* the event
*/
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt
index bd258f6..4a4ede6 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt
@@ -458,4 +458,6 @@
* [BoundReached] being the end reason.
*/
val endReason: AnimationEndReason
-)
\ No newline at end of file
+) {
+ override fun toString(): String = "AnimationResult(endReason=$endReason, endState=$endState)"
+}
\ No newline at end of file
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt
index ab8f9c4..49ddd38 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt
@@ -95,6 +95,16 @@
*/
val velocity: T
get() = typeConverter.convertFromVector(velocityVector)
+
+ override fun toString(): String {
+ return "AnimationState(" +
+ "value=$value, " +
+ "velocity=$velocity, " +
+ "isRunning=$isRunning, " +
+ "lastFrameTimeNanos=$lastFrameTimeNanos, " +
+ "finishedTimeNanos=$finishedTimeNanos" +
+ ")"
+ }
}
/**
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
index bc0c712..4d6a0d6 100644
--- a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
+++ b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
+import com.google.common.truth.Truth.assertThat
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
@@ -292,4 +293,34 @@
}
assertEquals(animatable.lowerBound!!, animatable.value)
}
+
+ @Test
+ fun animationResult_toString() {
+ val animatable = AnimationResult(
+ endReason = AnimationEndReason.Finished,
+ endState = AnimationState(42f)
+ )
+ val string = animatable.toString()
+ assertThat(string).contains(AnimationResult::class.java.simpleName)
+ assertThat(string).contains("endReason=Finished")
+ assertThat(string).contains("endState=")
+ }
+
+ @Test
+ fun animationState_toString() {
+ val state = AnimationState(
+ initialValue = 42f,
+ initialVelocity = 2f,
+ lastFrameTimeNanos = 4000L,
+ finishedTimeNanos = 3000L,
+ isRunning = true
+ )
+ val string = state.toString()
+ assertThat(string).contains(AnimationState::class.java.simpleName)
+ assertThat(string).contains("value=42.0")
+ assertThat(string).contains("velocity=2.0")
+ assertThat(string).contains("lastFrameTimeNanos=4000")
+ assertThat(string).contains("finishedTimeNanos=3000")
+ assertThat(string).contains("isRunning=true")
+ }
}
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
index 91d535a..3bed06c 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
@@ -782,8 +782,8 @@
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? LayoutWeightImpl ?: return false
- return weight != otherModifier.weight &&
- fill != otherModifier.fill
+ return weight == otherModifier.weight &&
+ fill == otherModifier.fill
}
override fun hashCode(): Int {
diff --git a/emoji2/integration-tests/init-disabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/disabled/EmojiStartupBenchmark.kt b/emoji2/integration-tests/init-disabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/disabled/EmojiStartupBenchmark.kt
index 6b4847f..1cfc22d 100644
--- a/emoji2/integration-tests/init-disabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/disabled/EmojiStartupBenchmark.kt
+++ b/emoji2/integration-tests/init-disabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/disabled/EmojiStartupBenchmark.kt
@@ -16,6 +16,7 @@
package androidx.emoji2.integration.macrobenchmark.disabled
+import android.os.Build
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
@@ -35,7 +36,11 @@
@Test
fun emojiCompatInitDisabledStartup() {
benchmarkRule.measureStartup(
- compilationMode = CompilationMode.None,
+ compilationMode = if (Build.VERSION.SDK_INT >= 24) {
+ CompilationMode.None()
+ } else {
+ CompilationMode.Full()
+ },
startupMode = StartupMode.COLD,
packageName = "androidx.emoji2.integration.macrobenchmark.disabled.target"
) {
diff --git a/emoji2/integration-tests/init-enabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/enabled/EmojiStartupBenchmark.kt b/emoji2/integration-tests/init-enabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/enabled/EmojiStartupBenchmark.kt
index 0580f35..aa0807e 100644
--- a/emoji2/integration-tests/init-enabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/enabled/EmojiStartupBenchmark.kt
+++ b/emoji2/integration-tests/init-enabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/enabled/EmojiStartupBenchmark.kt
@@ -16,6 +16,7 @@
package androidx.emoji2.integration.macrobenchmark.enabled
+import android.os.Build
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
@@ -40,7 +41,11 @@
// only run this test if the device can configure emoji2
assumeTrue(hasDiscoverableFontProviderOnDevice())
benchmarkRule.measureStartup(
- compilationMode = CompilationMode.None,
+ compilationMode = if (Build.VERSION.SDK_INT >= 24) {
+ CompilationMode.None()
+ } else {
+ CompilationMode.Full()
+ },
startupMode = StartupMode.COLD,
packageName = "androidx.emoji2.integration.macrobenchmark.enabled.target"
) {
@@ -52,4 +57,4 @@
val context = InstrumentationRegistry.getInstrumentation().targetContext
return DefaultEmojiCompatConfig.create(context) != null
}
-}
\ No newline at end of file
+}
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
index 8e99bf8..4c1d46011 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
@@ -310,12 +310,14 @@
val originalWho = fragment1.mWho
- fm.beginTransaction()
- .add(fragment1, "fragment1")
- .setReorderingAllowed(true)
- .addToBackStack("stack1")
- .commit()
- fm.popBackStack()
+ withActivity {
+ fm.beginTransaction()
+ .add(fragment1, "fragment1")
+ .setReorderingAllowed(true)
+ .addToBackStack("stack1")
+ .commit()
+ fm.popBackStack()
+ }
executePendingTransactions()
assertThat(fragment1.mWho).isNotEqualTo(originalWho)
@@ -326,6 +328,43 @@
}
@Test
+ fun reAddRemovedBeforeAttached() {
+ with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+ val fm = withActivity {
+ supportFragmentManager
+ }
+ val fragment1 = StrictFragment()
+
+ val originalWho = fragment1.mWho
+
+ withActivity {
+ fm.beginTransaction()
+ .add(fragment1, "fragment1")
+ .setReorderingAllowed(true)
+ .addToBackStack("stack1")
+ .commit()
+ fm.popBackStack()
+ }
+ executePendingTransactions()
+
+ assertThat(fragment1.mWho).isNotEqualTo(originalWho)
+ assertThat(fragment1.mFragmentManager).isNull()
+ assertThat(fm.findFragmentByWho(originalWho)).isNull()
+ assertThat(fm.findFragmentByWho(fragment1.mWho)).isNull()
+
+ val afterRemovalWho = fragment1.mWho
+
+ fm.beginTransaction()
+ .add(fragment1, "fragment1")
+ .setReorderingAllowed(true)
+ .commit()
+ executePendingTransactions()
+
+ assertThat(fragment1.mWho).isEqualTo(afterRemovalWho)
+ }
+ }
+
+ @Test
fun popBackStackImmediate() {
with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
val fm = withActivity {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
index 8fcec63..2cd364b 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -309,6 +309,13 @@
abstract void onPreAttached();
}
+ private final OnPreAttachedListener mSavedStateAttachListener = new OnPreAttachedListener() {
+ @Override
+ void onPreAttached() {
+ mSavedStateRegistryController.performAttach();
+ }
+ };
+
/**
* {@inheritDoc}
* <p>
@@ -572,12 +579,9 @@
// The default factory depends on the SavedStateRegistry so it
// needs to be reset when the SavedStateRegistry is reset
mDefaultFactory = null;
- registerOnPreAttachListener(new OnPreAttachedListener() {
- @Override
- void onPreAttached() {
- mSavedStateRegistryController.performAttach();
- }
- });
+ if (!mOnPreAttachedListeners.contains(mSavedStateAttachListener)) {
+ registerOnPreAttachListener(mSavedStateAttachListener);
+ }
}
/**
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/MediaLibraryServiceLegacyStub.java b/media2/media2-session/src/main/java/androidx/media2/session/MediaLibraryServiceLegacyStub.java
index 41be4d5..14b0d3c 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/MediaLibraryServiceLegacyStub.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/MediaLibraryServiceLegacyStub.java
@@ -144,8 +144,7 @@
}
LibraryParams params = MediaUtils.convertToLibraryParams(
mLibrarySessionImpl.getContext(), option);
- mLibrarySessionImpl.getCallback().onSubscribe(mLibrarySessionImpl.getInstance(),
- controller, id, params);
+ mLibrarySessionImpl.onSubscribeOnExecutor(controller, id, params);
}
});
}
@@ -168,8 +167,7 @@
}
return;
}
- mLibrarySessionImpl.getCallback().onUnsubscribe(mLibrarySessionImpl.getInstance(),
- controller, id);
+ mLibrarySessionImpl.onUnsubscribeOnExecutor(controller, id);
}
});
}
diff --git a/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt b/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt
index 42a98af..d287c1e 100644
--- a/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt
+++ b/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt
@@ -19,6 +19,7 @@
import android.content.Intent
import android.os.Build
import androidx.annotation.RequiresApi
+import androidx.benchmark.macro.BaselineProfileMode
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingLegacyMetric
@@ -34,12 +35,16 @@
*/
val BASIC_COMPILATION_MODES = if (Build.VERSION.SDK_INT < 24) {
// other modes aren't supported
- listOf(CompilationMode.None)
+ listOf(CompilationMode.Full())
} else {
listOf(
- CompilationMode.None,
+ CompilationMode.None(),
CompilationMode.Interpreted,
- CompilationMode.SpeedProfile()
+ CompilationMode.Partial(
+ baselineProfileMode = BaselineProfileMode.Disable,
+ warmupIterations = 3
+ ),
+ CompilationMode.Full()
)
}
@@ -49,8 +54,9 @@
* Baseline profiles are only supported from Nougat (API 24),
* currently through Android 11 (API 30)
*/
-val COMPILATION_MODES = if (Build.VERSION.SDK_INT in 24..30) {
- listOf(CompilationMode.BaselineProfile)
+@Suppress("ConvertTwoComparisonsToRangeCheck") // lint doesn't understand range checks
+val COMPILATION_MODES = if (Build.VERSION.SDK_INT >= 24 && Build.VERSION.SDK_INT <= 30) {
+ listOf(CompilationMode.Partial())
} else {
emptyList()
} + BASIC_COMPILATION_MODES
diff --git a/tracing/tracing-ktx/api/1.1.0-beta03.txt b/tracing/tracing-ktx/api/1.1.0-beta03.txt
new file mode 100644
index 0000000..e77d248
--- /dev/null
+++ b/tracing/tracing-ktx/api/1.1.0-beta03.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.tracing {
+
+ public final class TraceKt {
+ method public static inline <T> T! trace(String label, kotlin.jvm.functions.Function0<? extends T> block);
+ method public static suspend inline <T> Object? traceAsync(String methodName, int cookie, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T> p);
+ }
+
+}
+
diff --git a/tracing/tracing-ktx/api/public_plus_experimental_1.1.0-beta03.txt b/tracing/tracing-ktx/api/public_plus_experimental_1.1.0-beta03.txt
new file mode 100644
index 0000000..e77d248
--- /dev/null
+++ b/tracing/tracing-ktx/api/public_plus_experimental_1.1.0-beta03.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.tracing {
+
+ public final class TraceKt {
+ method public static inline <T> T! trace(String label, kotlin.jvm.functions.Function0<? extends T> block);
+ method public static suspend inline <T> Object? traceAsync(String methodName, int cookie, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T> p);
+ }
+
+}
+
diff --git a/tracing/tracing-ktx/api/res-1.1.0-beta03.txt b/tracing/tracing-ktx/api/res-1.1.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tracing/tracing-ktx/api/res-1.1.0-beta03.txt
diff --git a/tracing/tracing-ktx/api/restricted_1.1.0-beta03.txt b/tracing/tracing-ktx/api/restricted_1.1.0-beta03.txt
new file mode 100644
index 0000000..e77d248
--- /dev/null
+++ b/tracing/tracing-ktx/api/restricted_1.1.0-beta03.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.tracing {
+
+ public final class TraceKt {
+ method public static inline <T> T! trace(String label, kotlin.jvm.functions.Function0<? extends T> block);
+ method public static suspend inline <T> Object? traceAsync(String methodName, int cookie, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T> p);
+ }
+
+}
+
diff --git a/tracing/tracing/api/1.1.0-beta03.txt b/tracing/tracing/api/1.1.0-beta03.txt
new file mode 100644
index 0000000..c883da2
--- /dev/null
+++ b/tracing/tracing/api/1.1.0-beta03.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.tracing {
+
+ public final class Trace {
+ method public static void beginAsyncSection(String, int);
+ method public static void beginSection(String);
+ method public static void endAsyncSection(String, int);
+ method public static void endSection();
+ method public static void forceEnableAppTracing();
+ method public static boolean isEnabled();
+ method public static void setCounter(String, int);
+ }
+
+}
+
diff --git a/tracing/tracing/api/public_plus_experimental_1.1.0-beta03.txt b/tracing/tracing/api/public_plus_experimental_1.1.0-beta03.txt
new file mode 100644
index 0000000..c883da2
--- /dev/null
+++ b/tracing/tracing/api/public_plus_experimental_1.1.0-beta03.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.tracing {
+
+ public final class Trace {
+ method public static void beginAsyncSection(String, int);
+ method public static void beginSection(String);
+ method public static void endAsyncSection(String, int);
+ method public static void endSection();
+ method public static void forceEnableAppTracing();
+ method public static boolean isEnabled();
+ method public static void setCounter(String, int);
+ }
+
+}
+
diff --git a/tracing/tracing/api/res-1.1.0-beta03.txt b/tracing/tracing/api/res-1.1.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tracing/tracing/api/res-1.1.0-beta03.txt
diff --git a/tracing/tracing/api/restricted_1.1.0-beta03.txt b/tracing/tracing/api/restricted_1.1.0-beta03.txt
new file mode 100644
index 0000000..c883da2
--- /dev/null
+++ b/tracing/tracing/api/restricted_1.1.0-beta03.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.tracing {
+
+ public final class Trace {
+ method public static void beginAsyncSection(String, int);
+ method public static void beginSection(String);
+ method public static void endAsyncSection(String, int);
+ method public static void endSection();
+ method public static void forceEnableAppTracing();
+ method public static boolean isEnabled();
+ method public static void setCounter(String, int);
+ }
+
+}
+
diff --git a/wear/watchface/watchface-client/api/current.txt b/wear/watchface/watchface-client/api/current.txt
index 210b4a0..9afd83d 100644
--- a/wear/watchface/watchface-client/api/current.txt
+++ b/wear/watchface/watchface-client/api/current.txt
@@ -196,8 +196,8 @@
}
public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
- method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
- method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+ method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws android.os.RemoteException;
+ method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws android.os.RemoteException;
field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
}
diff --git a/wear/watchface/watchface-client/api/public_plus_experimental_current.txt b/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
index a4e489e..b947824 100644
--- a/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
@@ -199,8 +199,8 @@
}
public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
- method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
- method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+ method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws android.os.RemoteException;
+ method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws android.os.RemoteException;
field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
}
diff --git a/wear/watchface/watchface-client/api/restricted_current.txt b/wear/watchface/watchface-client/api/restricted_current.txt
index 8160792..bf20972 100644
--- a/wear/watchface/watchface-client/api/restricted_current.txt
+++ b/wear/watchface/watchface-client/api/restricted_current.txt
@@ -196,8 +196,8 @@
}
public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
- method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
- method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+ method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws android.os.RemoteException;
+ method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws android.os.RemoteException;
field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
}
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt
index 5bf74e2..7252e3f 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt
@@ -163,12 +163,14 @@
/**
* Returns the watch face's [UserStyleSchema].
*/
+ @Throws(RemoteException::class)
public fun getUserStyleSchema(): UserStyleSchema
/**
* Returns a map of [androidx.wear.watchface.ComplicationSlot] ID to [ComplicationSlotMetadata]
* for each slot in the watch face's [androidx.wear.watchface.ComplicationSlotsManager].
*/
+ @Throws(RemoteException::class)
public fun getComplicationSlotMetadataMap(): Map<Int, ComplicationSlotMetadata>
}
@@ -246,70 +248,61 @@
}
override fun getUserStyleSchema(): UserStyleSchema {
- requireNotClosed()
- try {
- return if (service.apiVersion >= 3) {
- UserStyleSchema(service.getUserStyleSchema(GetUserStyleSchemaParams(watchFaceName)))
- } else {
- headlessClient.userStyleSchema
- }
- } catch (e: RemoteException) {
- throw RuntimeException(e)
+ return if (service.apiVersion >= 3) {
+ UserStyleSchema(service.getUserStyleSchema(GetUserStyleSchemaParams(watchFaceName)))
+ } else {
+ headlessClient.userStyleSchema
}
}
override fun getComplicationSlotMetadataMap(): Map<Int, ComplicationSlotMetadata> {
requireNotClosed()
- try {
- return if (service.apiVersion >= 3) {
- val wireFormat = service.getComplicationSlotMetadata(
- GetComplicationSlotMetadataParams(watchFaceName)
- )
- wireFormat.associateBy(
- { it.id },
- {
- val perSlotBounds = HashMap<ComplicationType, RectF>()
- for (i in it.complicationBoundsType.indices) {
- perSlotBounds[
- ComplicationType.fromWireType(it.complicationBoundsType[i])
- ] = it.complicationBounds[i]
- }
- ComplicationSlotMetadata(
- ComplicationSlotBounds(perSlotBounds),
- it.boundsType,
- it.supportedTypes.map { ComplicationType.fromWireType(it) },
- DefaultComplicationDataSourcePolicy(
- it.defaultDataSourcesToTry ?: emptyList(),
- it.fallbackSystemDataSource,
- ComplicationType.fromWireType(
- it.primaryDataSourceDefaultType
- ),
- ComplicationType.fromWireType(
- it.secondaryDataSourceDefaultType
- ),
- ComplicationType.fromWireType(it.defaultDataSourceType)
- ),
- it.isInitiallyEnabled,
- it.isFixedComplicationDataSource,
- it.complicationConfigExtras
- )
+ return if (service.apiVersion >= 3) {
+ val wireFormat = service.getComplicationSlotMetadata(
+ GetComplicationSlotMetadataParams(watchFaceName)
+ )
+ wireFormat.associateBy(
+ { it.id },
+ {
+ val perSlotBounds = HashMap<ComplicationType, RectF>()
+ for (i in it.complicationBoundsType.indices) {
+ perSlotBounds[
+ ComplicationType.fromWireType(it.complicationBoundsType[i])
+ ] = it.complicationBounds[i]
}
- )
- } else {
- headlessClient.complicationSlotsState.mapValues {
ComplicationSlotMetadata(
- null,
- it.value.boundsType,
- it.value.supportedTypes,
- it.value.defaultDataSourcePolicy,
- it.value.isInitiallyEnabled,
- it.value.fixedComplicationDataSource,
- it.value.complicationConfigExtras
+ ComplicationSlotBounds(perSlotBounds),
+ it.boundsType,
+ it.supportedTypes.map { ComplicationType.fromWireType(it) },
+ DefaultComplicationDataSourcePolicy(
+ it.defaultDataSourcesToTry ?: emptyList(),
+ it.fallbackSystemDataSource,
+ ComplicationType.fromWireType(
+ it.primaryDataSourceDefaultType
+ ),
+ ComplicationType.fromWireType(
+ it.secondaryDataSourceDefaultType
+ ),
+ ComplicationType.fromWireType(it.defaultDataSourceType)
+ ),
+ it.isInitiallyEnabled,
+ it.isFixedComplicationDataSource,
+ it.complicationConfigExtras
)
}
+ )
+ } else {
+ headlessClient.complicationSlotsState.mapValues {
+ ComplicationSlotMetadata(
+ null,
+ it.value.boundsType,
+ it.value.supportedTypes,
+ it.value.defaultDataSourcePolicy,
+ it.value.isInitiallyEnabled,
+ it.value.fixedComplicationDataSource,
+ it.value.complicationConfigExtras
+ )
}
- } catch (e: RemoteException) {
- throw RuntimeException(e)
}
}
diff --git a/work/work-runtime/build.gradle b/work/work-runtime/build.gradle
index a6fe6da..6db8f53 100644
--- a/work/work-runtime/build.gradle
+++ b/work/work-runtime/build.gradle
@@ -23,6 +23,7 @@
id("AndroidXPlugin")
id("com.android.library")
id("kotlin-android")
+ id("com.google.devtools.ksp")
}
android {
@@ -51,7 +52,7 @@
dependencies {
implementation("androidx.core:core:1.6.0")
- annotationProcessor("androidx.room:room-compiler:2.4.0-rc01")
+ ksp("androidx.room:room-compiler:2.4.0-rc01")
implementation("androidx.room:room-runtime:2.4.0-rc01")
androidTestImplementation("androidx.room:room-testing:2.4.0-rc01")
implementation("androidx.sqlite:sqlite:2.1.0")
@@ -80,6 +81,11 @@
packageInspector(project, project(":work:work-inspection"))
+// KSP does not support argument per flavor so we set it to global instead.
+ksp {
+ arg("room.schemaLocation","$projectDir/src/schemas".toString())
+}
+
androidx {
name = "Android WorkManager Runtime"
publish = Publish.SNAPSHOT_AND_RELEASE
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java b/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
index a593404..39c636d 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
@@ -19,24 +19,18 @@
import static android.content.Context.MODE_PRIVATE;
import static android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_11_12;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_3_4;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_4_5;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_6_7;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_7_8;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_8_9;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_1;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_10;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_11;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_12;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_2;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_3;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_4;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_5;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_6;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_7;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_8;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_9;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_1;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_10;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_11;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_12;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_2;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_3;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_4;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_5;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_6;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_7;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_8;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_9;
import static androidx.work.impl.utils.IdGenerator.NEXT_ALARM_MANAGER_ID_KEY;
import static androidx.work.impl.utils.IdGenerator.NEXT_JOB_SCHEDULER_ID_KEY;
import static androidx.work.impl.utils.IdGenerator.PREFERENCE_FILE_KEY;
@@ -62,9 +56,17 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.work.impl.Migration_11_12;
+import androidx.work.impl.Migration_1_2;
+import androidx.work.impl.Migration_3_4;
+import androidx.work.impl.Migration_4_5;
+import androidx.work.impl.Migration_6_7;
+import androidx.work.impl.Migration_7_8;
+import androidx.work.impl.Migration_8_9;
+import androidx.work.impl.RescheduleMigration;
import androidx.work.impl.WorkDatabase;
-import androidx.work.impl.WorkDatabaseMigrations;
import androidx.work.impl.WorkManagerImpl;
+import androidx.work.impl.WorkMigration9To10;
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.model.WorkTypeConverters;
import androidx.work.worker.TestWorker;
@@ -76,6 +78,7 @@
import java.io.File;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.UUID;
@RunWith(AndroidJUnit4.class)
@@ -117,7 +120,8 @@
@Rule
public MigrationTestHelper mMigrationTestHelper = new MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
- WorkDatabase.class.getCanonicalName(),
+ WorkDatabase.class,
+ new ArrayList<>(),
new FrameworkSQLiteOpenHelperFactory());
@Before
@@ -165,7 +169,7 @@
TEST_DATABASE,
VERSION_2,
VALIDATE_DROPPED_TABLES,
- WorkDatabaseMigrations.MIGRATION_1_2);
+ Migration_1_2.INSTANCE);
Cursor tagCursor = database.query("SELECT * FROM worktag");
assertThat(tagCursor.getCount(), is(prepopulatedWorkSpecIds.length));
@@ -209,8 +213,8 @@
public void testMigrationVersion2To3() throws IOException {
SupportSQLiteDatabase database =
mMigrationTestHelper.createDatabase(TEST_DATABASE, VERSION_2);
- WorkDatabaseMigrations.RescheduleMigration migration2To3 =
- new WorkDatabaseMigrations.RescheduleMigration(mContext, VERSION_2, VERSION_3);
+ RescheduleMigration migration2To3 =
+ new RescheduleMigration(mContext, VERSION_2, VERSION_3);
database = mMigrationTestHelper.runMigrationsAndValidate(
TEST_DATABASE,
@@ -246,7 +250,7 @@
TEST_DATABASE,
VERSION_4,
VALIDATE_DROPPED_TABLES,
- MIGRATION_3_4);
+ Migration_3_4.INSTANCE);
Cursor cursor = database.query("SELECT * from workspec");
assertThat(cursor.getCount(), is(2));
@@ -277,7 +281,7 @@
TEST_DATABASE,
VERSION_5,
VALIDATE_DROPPED_TABLES,
- MIGRATION_4_5);
+ Migration_4_5.INSTANCE);
assertThat(checkExists(database, TABLE_WORKSPEC), is(true));
assertThat(
checkColumnExists(database, TABLE_WORKSPEC, TRIGGER_CONTENT_UPDATE_DELAY),
@@ -293,8 +297,7 @@
public void testMigrationVersion5To6() throws IOException {
SupportSQLiteDatabase database =
mMigrationTestHelper.createDatabase(TEST_DATABASE, VERSION_5);
- WorkDatabaseMigrations.RescheduleMigration migration5To6 =
- new WorkDatabaseMigrations.RescheduleMigration(mContext, VERSION_5, VERSION_6);
+ RescheduleMigration migration5To6 = new RescheduleMigration(mContext, VERSION_5, VERSION_6);
database = mMigrationTestHelper.runMigrationsAndValidate(
TEST_DATABASE,
@@ -317,7 +320,7 @@
TEST_DATABASE,
VERSION_7,
VALIDATE_DROPPED_TABLES,
- MIGRATION_6_7);
+ Migration_6_7.INSTANCE);
assertThat(checkExists(database, TABLE_WORKPROGRESS), is(true));
database.close();
}
@@ -331,7 +334,7 @@
TEST_DATABASE,
VERSION_8,
VALIDATE_DROPPED_TABLES,
- MIGRATION_7_8);
+ Migration_7_8.INSTANCE);
assertThat(checkIndexExists(database, INDEX_PERIOD_START_TIME, TABLE_WORKSPEC), is(true));
database.close();
@@ -346,7 +349,7 @@
TEST_DATABASE,
VERSION_9,
VALIDATE_DROPPED_TABLES,
- MIGRATION_8_9);
+ Migration_8_9.INSTANCE);
assertThat(checkColumnExists(database, TABLE_WORKSPEC, COLUMN_RUN_IN_FOREGROUND),
is(true));
@@ -378,7 +381,7 @@
TEST_DATABASE,
VERSION_10,
VALIDATE_DROPPED_TABLES,
- new WorkDatabaseMigrations.WorkMigration9To10(mContext));
+ new WorkMigration9To10(mContext));
assertThat(checkExists(database, TABLE_PREFERENCE), is(true));
String query = "SELECT * FROM `Preference` where `key`=@key";
@@ -411,8 +414,8 @@
public void testMigrationVersion10To11() throws IOException {
SupportSQLiteDatabase database =
mMigrationTestHelper.createDatabase(TEST_DATABASE, VERSION_10);
- WorkDatabaseMigrations.RescheduleMigration migration10To11 =
- new WorkDatabaseMigrations.RescheduleMigration(mContext, VERSION_10, VERSION_11);
+ RescheduleMigration migration10To11 =
+ new RescheduleMigration(mContext, VERSION_10, VERSION_11);
database = mMigrationTestHelper.runMigrationsAndValidate(
TEST_DATABASE,
VERSION_11,
@@ -448,7 +451,7 @@
TEST_DATABASE,
VERSION_12,
VALIDATE_DROPPED_TABLES,
- MIGRATION_11_12);
+ Migration_11_12.INSTANCE);
assertThat(checkColumnExists(database, TABLE_WORKSPEC, COLUMN_OUT_OF_QUOTA_POLICY),
is(true));
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabasePathHelperTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabasePathHelperTest.kt
index cd70bd7..b02375a 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabasePathHelperTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabasePathHelperTest.kt
@@ -24,9 +24,9 @@
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
-import androidx.work.impl.WorkDatabasePathHelper
import androidx.work.impl.WorkDatabase
-import androidx.work.impl.WorkDatabaseMigrations.VERSION_9
+import androidx.work.impl.WorkDatabasePathHelper
+import androidx.work.impl.WorkDatabaseVersions.VERSION_9
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
@@ -37,13 +37,12 @@
@RunWith(AndroidJUnit4::class)
@LargeTest
-@Suppress("DEPRECATION")
-// TODO: (b/189268580) Update this test to use the new constructors in MigrationTestHelper.
class WorkDatabasePathHelperTest {
@get:Rule
val migrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
- WorkDatabase::class.java.canonicalName,
+ WorkDatabase::class.java,
+ emptyList(),
FrameworkSQLiteOpenHelperFactory()
)
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
index 5f3abfc..69fab95 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
@@ -1614,7 +1614,8 @@
public void testGenerateCleanupCallback_deletesOldFinishedWork() {
OneTimeWorkRequest work1 = new OneTimeWorkRequest.Builder(TestWorker.class)
.setInitialState(SUCCEEDED)
- .setPeriodStartTime(WorkDatabase.getPruneDate() - 1L, TimeUnit.MILLISECONDS)
+ .setPeriodStartTime(CleanupCallback.INSTANCE.getPruneDate() - 1L,
+ TimeUnit.MILLISECONDS)
.build();
OneTimeWorkRequest work2 = new OneTimeWorkRequest.Builder(TestWorker.class)
.setPeriodStartTime(Long.MAX_VALUE, TimeUnit.MILLISECONDS)
@@ -1625,7 +1626,7 @@
SupportSQLiteOpenHelper openHelper = mDatabase.getOpenHelper();
SupportSQLiteDatabase db = openHelper.getWritableDatabase();
- WorkDatabase.generateCleanupCallback().onOpen(db);
+ CleanupCallback.INSTANCE.onOpen(db);
WorkSpecDao workSpecDao = mDatabase.workSpecDao();
assertThat(workSpecDao.getWorkSpec(work1.getStringId()), is(nullValue()));
@@ -1637,15 +1638,18 @@
public void testGenerateCleanupCallback_doesNotDeleteOldFinishedWorkWithActiveDependents() {
OneTimeWorkRequest work0 = new OneTimeWorkRequest.Builder(TestWorker.class)
.setInitialState(SUCCEEDED)
- .setPeriodStartTime(WorkDatabase.getPruneDate() - 1L, TimeUnit.MILLISECONDS)
+ .setPeriodStartTime(CleanupCallback.INSTANCE.getPruneDate() - 1L,
+ TimeUnit.MILLISECONDS)
.build();
OneTimeWorkRequest work1 = new OneTimeWorkRequest.Builder(TestWorker.class)
.setInitialState(SUCCEEDED)
- .setPeriodStartTime(WorkDatabase.getPruneDate() - 1L, TimeUnit.MILLISECONDS)
+ .setPeriodStartTime(CleanupCallback.INSTANCE.getPruneDate() - 1L,
+ TimeUnit.MILLISECONDS)
.build();
OneTimeWorkRequest work2 = new OneTimeWorkRequest.Builder(TestWorker.class)
.setInitialState(ENQUEUED)
- .setPeriodStartTime(WorkDatabase.getPruneDate() - 1L, TimeUnit.MILLISECONDS)
+ .setPeriodStartTime(CleanupCallback.INSTANCE.getPruneDate() - 1L,
+ TimeUnit.MILLISECONDS)
.build();
insertWorkSpecAndTags(work0);
@@ -1658,7 +1662,7 @@
SupportSQLiteOpenHelper openHelper = mDatabase.getOpenHelper();
SupportSQLiteDatabase db = openHelper.getWritableDatabase();
- WorkDatabase.generateCleanupCallback().onOpen(db);
+ CleanupCallback.INSTANCE.onOpen(db);
WorkSpecDao workSpecDao = mDatabase.workSpecDao();
assertThat(workSpecDao.getWorkSpec(work0.getStringId()), is(nullValue()));
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerInitializationTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerInitializationTest.kt
index e1cdbc1..fc84c9b 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerInitializationTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerInitializationTest.kt
@@ -16,53 +16,49 @@
package androidx.work.impl
-import android.content.Context
+import android.content.ContextWrapper
+import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import androidx.work.Configuration
-import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import androidx.work.impl.utils.SynchronousExecutor
+import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor
import org.junit.Assert.assertNotNull
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mockito.`when`
-import org.mockito.Mockito.mock
-import java.util.concurrent.Executor
@RunWith(AndroidJUnit4::class)
class WorkManagerInitializationTest {
- private lateinit var mContext: Context
- private lateinit var mConfiguration: Configuration
- private lateinit var mExecutor: Executor
- private lateinit var mTaskExecutor: TaskExecutor
-
- @Before
- fun setUp() {
- mContext = mock(Context::class.java)
- `when`(mContext.applicationContext).thenReturn(mContext)
- mExecutor = mock(Executor::class.java)
- mConfiguration = Configuration.Builder()
- .setExecutor(mExecutor)
- .setTaskExecutor(mExecutor)
- .build()
- mTaskExecutor = mock(TaskExecutor::class.java)
- }
+ private val executor = SynchronousExecutor()
+ private val configuration = Configuration.Builder()
+ .setExecutor(executor)
+ .setTaskExecutor(executor)
+ .build()
+ private val taskExecutor = InstantWorkTaskExecutor()
@Test(expected = IllegalStateException::class)
@SmallTest
@SdkSuppress(minSdkVersion = 24)
fun directBootTest() {
- `when`(mContext.isDeviceProtectedStorage).thenReturn(true)
- WorkManagerImpl(mContext, mConfiguration, mTaskExecutor, true)
+ val context = DeviceProtectedStoreContext(true)
+ WorkManagerImpl(context, configuration, taskExecutor, true)
}
@Test
@SmallTest
@SdkSuppress(minSdkVersion = 24)
fun credentialBackedStorageTest() {
- `when`(mContext.isDeviceProtectedStorage).thenReturn(false)
- val workManager = WorkManagerImpl(mContext, mConfiguration, mTaskExecutor, true)
+ val context = DeviceProtectedStoreContext(false)
+ val workManager = WorkManagerImpl(context, configuration, taskExecutor, true)
assertNotNull(workManager)
}
}
+
+private class DeviceProtectedStoreContext(
+ val deviceProtectedStorage: Boolean
+) : ContextWrapper(ApplicationProvider.getApplicationContext()) {
+ override fun isDeviceProtectedStorage() = deviceProtectedStorage
+
+ override fun getApplicationContext() = this
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.java
deleted file mode 100644
index ed651cd..0000000
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2017 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.work.impl.constraints;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.hamcrest.Matchers.empty;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-import androidx.work.impl.constraints.controllers.ConstraintController;
-import androidx.work.impl.model.WorkSpec;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class WorkConstraintsTrackerTest {
- private static final List<String> TEST_WORKSPEC_IDS = new ArrayList<>();
- static {
- TEST_WORKSPEC_IDS.add("A");
- TEST_WORKSPEC_IDS.add("B");
- TEST_WORKSPEC_IDS.add("C");
- }
-
- private WorkConstraintsCallback mCallback = new WorkConstraintsCallback() {
- @Override
- public void onAllConstraintsMet(@NonNull List<String> workSpecIds) {
- mUnconstrainedWorkSpecIds = workSpecIds;
- }
-
- @Override
- public void onAllConstraintsNotMet(@NonNull List<String> workSpecIds) {
- mConstrainedWorkSpecIds = workSpecIds;
- }
- };
-
- private ConstraintController mMockController = mock(ConstraintController.class);
- private List<String> mUnconstrainedWorkSpecIds;
- private List<String> mConstrainedWorkSpecIds;
- private WorkConstraintsTrackerImpl mWorkConstraintsTracker;
-
- @Before
- public void setUp() {
- ConstraintController[] controllers = new ConstraintController[] {mMockController};
- mWorkConstraintsTracker = new WorkConstraintsTrackerImpl(mCallback, controllers);
- }
-
- @SuppressWarnings("unchecked")
- @Test
- public void testReplace() {
- List<WorkSpec> emptyList = Collections.emptyList();
-
- ArgumentCaptor<ConstraintController.OnConstraintUpdatedCallback> captor =
- ArgumentCaptor.forClass(ConstraintController.OnConstraintUpdatedCallback.class);
-
- mWorkConstraintsTracker.replace(emptyList);
- verify(mMockController).replace(emptyList);
- verify(mMockController, times(2)).setCallback(captor.capture());
- assertThat(captor.getAllValues().size(), is(2));
- assertThat(captor.getAllValues().get(0), is(nullValue()));
- assertThat(captor.getAllValues().get(1),
- is((ConstraintController.OnConstraintUpdatedCallback) mWorkConstraintsTracker));
- }
-
- @Test
- public void testReset() {
- mWorkConstraintsTracker.reset();
- verify(mMockController).reset();
- }
-
- @Test
- public void testOnConstraintMet_controllerInvoked() {
- mWorkConstraintsTracker.onConstraintMet(TEST_WORKSPEC_IDS);
- for (String id : TEST_WORKSPEC_IDS) {
- verify(mMockController).isWorkSpecConstrained(id);
- }
- }
-
- @Test
- public void testOnConstraintMet_allConstraintsMet() {
- when(mMockController.isWorkSpecConstrained(any(String.class))).thenReturn(false);
- mWorkConstraintsTracker.onConstraintMet(TEST_WORKSPEC_IDS);
- assertThat(mUnconstrainedWorkSpecIds, is(TEST_WORKSPEC_IDS));
- }
-
- @Test
- public void testOnConstraintMet_allConstraintsMet_subList() {
- when(mMockController.isWorkSpecConstrained(TEST_WORKSPEC_IDS.get(0))).thenReturn(true);
- when(mMockController.isWorkSpecConstrained(TEST_WORKSPEC_IDS.get(1))).thenReturn(false);
- when(mMockController.isWorkSpecConstrained(TEST_WORKSPEC_IDS.get(2))).thenReturn(false);
- mWorkConstraintsTracker.onConstraintMet(TEST_WORKSPEC_IDS);
- assertThat(mUnconstrainedWorkSpecIds,
- containsInAnyOrder(TEST_WORKSPEC_IDS.get(1), TEST_WORKSPEC_IDS.get(2)));
- }
-
- @Test
- public void testOnConstraintMet_allConstraintsNotMet() {
- when(mMockController.isWorkSpecConstrained(any(String.class))).thenReturn(true);
- mWorkConstraintsTracker.onConstraintMet(TEST_WORKSPEC_IDS);
- assertThat(mUnconstrainedWorkSpecIds, is(empty()));
- }
-
- @Test
- public void testOnConstraintNotMet() {
- mWorkConstraintsTracker.onConstraintNotMet(TEST_WORKSPEC_IDS);
- assertThat(mConstrainedWorkSpecIds, is(TEST_WORKSPEC_IDS));
- }
-}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.kt
new file mode 100644
index 0000000..540925e
--- /dev/null
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2017 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.work.impl.constraints
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.work.impl.constraints.controllers.ConstraintController
+import androidx.work.impl.constraints.trackers.ConstraintTracker
+import androidx.work.impl.model.WorkSpec
+import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WorkConstraintsTrackerTest {
+
+ private val capturingCallback = CapturingWorkConstraintsCallback()
+
+ @Test
+ fun testReplace() {
+ val tracker = TestConstraintTracker(true)
+ val workConstraintsTracker = WorkConstraintsTracker(capturingCallback, tracker)
+ workConstraintsTracker.replace(TEST_WORKSPECS.subList(0, 2))
+ val (unconstrained1, _) = capturingCallback.consumeCurrent()
+ assertThat(unconstrained1).containsExactly(TEST_WORKSPEC_IDS[0], TEST_WORKSPEC_IDS[1])
+ workConstraintsTracker.replace(TEST_WORKSPECS.subList(1, 3))
+ val (unconstrained2, _) = capturingCallback.consumeCurrent()
+ assertThat(unconstrained2).containsExactly(TEST_WORKSPEC_IDS[1], TEST_WORKSPEC_IDS[2])
+ }
+
+ @Test
+ fun testReset() {
+ val tracker = TestConstraintTracker(true)
+ val workConstraintsTracker = WorkConstraintsTracker(capturingCallback, tracker)
+ workConstraintsTracker.replace(TEST_WORKSPECS)
+ assertThat(tracker.isTracking).isTrue()
+ workConstraintsTracker.reset()
+ assertThat(tracker.isTracking).isFalse()
+ }
+
+ @Test
+ fun testOnConstraintMet_allConstraintsMet() {
+ val tracker = TestConstraintTracker()
+ val workConstraintsTracker = WorkConstraintsTracker(capturingCallback, tracker)
+ workConstraintsTracker.replace(TEST_WORKSPECS)
+ val (_, constrained) = capturingCallback.consumeCurrent()
+ assertThat(constrained).isEqualTo(TEST_WORKSPEC_IDS)
+ tracker.setState(true)
+ val (unconstrained, _) = capturingCallback.consumeCurrent()
+ assertThat(unconstrained).isEqualTo(TEST_WORKSPEC_IDS)
+ }
+
+ @Test
+ fun testOnConstraintMet_allConstraintsMet_subList() {
+ val tracker1 = TestConstraintTracker()
+ val tracker2 = TestConstraintTracker()
+ val controller1 = TestConstraintController(tracker1, TEST_WORKSPEC_IDS.subList(0, 2))
+ val controller2 = TestConstraintController(tracker2, TEST_WORKSPEC_IDS.subList(2, 3))
+ val workConstraintsTracker = WorkConstraintsTrackerImpl(
+ capturingCallback,
+ arrayOf(controller1, controller2)
+ )
+ workConstraintsTracker.replace(TEST_WORKSPECS)
+ capturingCallback.consumeCurrent()
+ tracker1.setState(true)
+ val (unconstrained, _) = capturingCallback.consumeCurrent()
+ assertThat(unconstrained).containsExactly(TEST_WORKSPEC_IDS[0], TEST_WORKSPEC_IDS[1])
+ }
+
+ @Test
+ fun testOnConstraintMet_allConstraintsNotMet() {
+ val tracker1 = TestConstraintTracker()
+ val tracker2 = TestConstraintTracker()
+ val workConstraintsTracker = WorkConstraintsTracker(capturingCallback, tracker1, tracker2)
+ workConstraintsTracker.replace(TEST_WORKSPECS)
+ capturingCallback.consumeCurrent()
+ tracker1.setState(true)
+ val (unconstrained, _) = capturingCallback.consumeCurrent()
+ // only one constraint is resolved, so unconstrained is empty list
+ assertThat(unconstrained).isEqualTo(emptyList<String>())
+ }
+
+ @Test
+ fun testOnConstraintNotMet() {
+ val tracker1 = TestConstraintTracker(true)
+ val tracker2 = TestConstraintTracker(true)
+ val workConstraintsTracker = WorkConstraintsTracker(capturingCallback, tracker1, tracker2)
+ workConstraintsTracker.replace(TEST_WORKSPECS)
+ val (unconstrained, _) = capturingCallback.consumeCurrent()
+ assertThat(unconstrained).isEqualTo(TEST_WORKSPEC_IDS)
+ tracker1.setState(false)
+ val (_, constrained) = capturingCallback.consumeCurrent()
+ assertThat(constrained).isEqualTo(TEST_WORKSPEC_IDS)
+ }
+}
+
+private val TEST_WORKSPECS = listOf(
+ WorkSpec("A", "Worker1"),
+ WorkSpec("B", "Worker2"),
+ WorkSpec("C", "Worker3"),
+)
+private val TEST_WORKSPEC_IDS = TEST_WORKSPECS.map { it.id }
+
+private fun WorkConstraintsTracker(
+ callback: WorkConstraintsCallback,
+ vararg trackers: ConstraintTracker<Boolean>
+): WorkConstraintsTrackerImpl {
+ val controllers = trackers.map { TestConstraintController(it) }
+ return WorkConstraintsTrackerImpl(callback, controllers.toTypedArray())
+}
+
+private class TestConstraintTracker(
+ val initialState: Boolean = false,
+ context: Context = ApplicationProvider.getApplicationContext(),
+ taskExecutor: TaskExecutor = InstantWorkTaskExecutor(),
+) : ConstraintTracker<Boolean>(context, taskExecutor) {
+ var isTracking = false
+ override fun getInitialState() = initialState
+
+ override fun startTracking() {
+ isTracking = true
+ }
+
+ override fun stopTracking() {
+ isTracking = false
+ }
+}
+
+private class TestConstraintController(
+ tracker: ConstraintTracker<Boolean>,
+ private val constrainedIds: List<String> = TEST_WORKSPEC_IDS
+) : ConstraintController<Boolean>(tracker) {
+ override fun hasConstraint(workSpec: WorkSpec) = workSpec.id in constrainedIds
+ override fun isConstrained(value: Boolean) = !value
+}
+
+private class CapturingWorkConstraintsCallback(
+ var unconstrainedWorkSpecIds: List<String>? = null,
+ var constrainedWorkSpecIds: List<String>? = null,
+) : WorkConstraintsCallback {
+ override fun onAllConstraintsMet(workSpecIds: List<String>) {
+ unconstrainedWorkSpecIds = workSpecIds
+ }
+
+ override fun onAllConstraintsNotMet(workSpecIds: List<String>) {
+ constrainedWorkSpecIds = workSpecIds
+ }
+
+ fun consumeCurrent(): Pair<List<String>?, List<String>?> {
+ val result = unconstrainedWorkSpecIds to constrainedWorkSpecIds
+ unconstrainedWorkSpecIds = null
+ constrainedWorkSpecIds = null
+ return result
+ }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java
index 82367fe..ffaa451 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java
@@ -250,12 +250,12 @@
}
@Override
- boolean hasConstraint(@NonNull WorkSpec workSpec) {
+ public boolean hasConstraint(@NonNull WorkSpec workSpec) {
return workSpec.constraints.requiresDeviceIdle();
}
@Override
- boolean isConstrained(@NonNull Boolean isDeviceIdle) {
+ public boolean isConstrained(@NonNull Boolean isDeviceIdle) {
return !isDeviceIdle;
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.java
deleted file mode 100644
index 5073e66..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.java
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- * Copyright 2017 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.work.impl;
-
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_3_4;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_4_5;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_6_7;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_7_8;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_8_9;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_10;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_11;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_2;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_3;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_5;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_6;
-import static androidx.work.impl.model.WorkTypeConverters.StateIds.COMPLETED_STATES;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.room.Database;
-import androidx.room.Room;
-import androidx.room.RoomDatabase;
-import androidx.room.TypeConverters;
-import androidx.sqlite.db.SupportSQLiteDatabase;
-import androidx.sqlite.db.SupportSQLiteOpenHelper;
-import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
-import androidx.work.Data;
-import androidx.work.impl.model.Dependency;
-import androidx.work.impl.model.DependencyDao;
-import androidx.work.impl.model.Preference;
-import androidx.work.impl.model.PreferenceDao;
-import androidx.work.impl.model.RawWorkInfoDao;
-import androidx.work.impl.model.SystemIdInfo;
-import androidx.work.impl.model.SystemIdInfoDao;
-import androidx.work.impl.model.WorkName;
-import androidx.work.impl.model.WorkNameDao;
-import androidx.work.impl.model.WorkProgress;
-import androidx.work.impl.model.WorkProgressDao;
-import androidx.work.impl.model.WorkSpec;
-import androidx.work.impl.model.WorkSpecDao;
-import androidx.work.impl.model.WorkTag;
-import androidx.work.impl.model.WorkTagDao;
-import androidx.work.impl.model.WorkTypeConverters;
-
-import java.util.concurrent.Executor;
-import java.util.concurrent.TimeUnit;
-
-/**
- * A Room database for keeping track of work states.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@Database(entities = {
- Dependency.class,
- WorkSpec.class,
- WorkTag.class,
- SystemIdInfo.class,
- WorkName.class,
- WorkProgress.class,
- Preference.class},
- version = 12)
-@TypeConverters(value = {Data.class, WorkTypeConverters.class})
-public abstract class WorkDatabase extends RoomDatabase {
- // Delete rows in the workspec table that...
- private static final String PRUNE_SQL_FORMAT_PREFIX = "DELETE FROM workspec WHERE "
- // are completed...
- + "state IN " + COMPLETED_STATES + " AND "
- // and the minimum retention time has expired...
- + "(period_start_time + minimum_retention_duration) < ";
- // and all dependents are completed.
- private static final String PRUNE_SQL_FORMAT_SUFFIX = " AND "
- + "(SELECT COUNT(*)=0 FROM dependency WHERE "
- + " prerequisite_id=id AND "
- + " work_spec_id NOT IN "
- + " (SELECT id FROM workspec WHERE state IN " + COMPLETED_STATES + "))";
-
- private static final long PRUNE_THRESHOLD_MILLIS = TimeUnit.DAYS.toMillis(1);
-
- /**
- * Creates an instance of the WorkDatabase.
- *
- * @param context A context (this method will use the application context from it)
- * @param queryExecutor An {@link Executor} that will be used to execute all async Room
- * queries.
- * @param useTestDatabase {@code true} to generate an in-memory database that allows main thread
- * access
- * @return The created WorkDatabase
- */
- @NonNull
- public static WorkDatabase create(
- @NonNull final Context context,
- @NonNull Executor queryExecutor,
- boolean useTestDatabase) {
- RoomDatabase.Builder<WorkDatabase> builder;
- if (useTestDatabase) {
- builder = Room.inMemoryDatabaseBuilder(context, WorkDatabase.class)
- .allowMainThreadQueries();
- } else {
- String name = WorkDatabasePathHelper.getWorkDatabaseName();
- builder = Room.databaseBuilder(context, WorkDatabase.class, name);
- builder.openHelperFactory(new SupportSQLiteOpenHelper.Factory() {
- @NonNull
- @Override
- public SupportSQLiteOpenHelper create(
- @NonNull SupportSQLiteOpenHelper.Configuration configuration) {
- SupportSQLiteOpenHelper.Configuration.Builder configBuilder =
- SupportSQLiteOpenHelper.Configuration.builder(context);
- configBuilder.name(configuration.name)
- .callback(configuration.callback)
- .noBackupDirectory(true);
- FrameworkSQLiteOpenHelperFactory factory =
- new FrameworkSQLiteOpenHelperFactory();
- return factory.create(configBuilder.build());
- }
- });
- }
-
- return builder.setQueryExecutor(queryExecutor)
- .addCallback(generateCleanupCallback())
- .addMigrations(WorkDatabaseMigrations.MIGRATION_1_2)
- .addMigrations(
- new WorkDatabaseMigrations.RescheduleMigration(context, VERSION_2,
- VERSION_3))
- .addMigrations(MIGRATION_3_4)
- .addMigrations(MIGRATION_4_5)
- .addMigrations(
- new WorkDatabaseMigrations.RescheduleMigration(context, VERSION_5,
- VERSION_6))
- .addMigrations(MIGRATION_6_7)
- .addMigrations(MIGRATION_7_8)
- .addMigrations(MIGRATION_8_9)
- .addMigrations(new WorkDatabaseMigrations.WorkMigration9To10(context))
- .addMigrations(
- new WorkDatabaseMigrations.RescheduleMigration(context, VERSION_10,
- VERSION_11))
- .addMigrations(WorkDatabaseMigrations.MIGRATION_11_12)
- .fallbackToDestructiveMigration()
- .build();
- }
-
- static Callback generateCleanupCallback() {
- return new Callback() {
- @Override
- public void onOpen(@NonNull SupportSQLiteDatabase db) {
- super.onOpen(db);
- db.beginTransaction();
- try {
- // Prune everything that is completed, has an expired retention time, and has no
- // active dependents:
- db.execSQL(getPruneSQL());
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
- };
- }
-
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- @NonNull
- static String getPruneSQL() {
- return PRUNE_SQL_FORMAT_PREFIX + getPruneDate() + PRUNE_SQL_FORMAT_SUFFIX;
- }
-
- static long getPruneDate() {
- return System.currentTimeMillis() - PRUNE_THRESHOLD_MILLIS;
- }
-
- /**
- * @return The Data Access Object for {@link WorkSpec}s.
- */
- @NonNull
- public abstract WorkSpecDao workSpecDao();
-
- /**
- * @return The Data Access Object for {@link Dependency}s.
- */
- @NonNull
- public abstract DependencyDao dependencyDao();
-
- /**
- * @return The Data Access Object for {@link WorkTag}s.
- */
- @NonNull
- public abstract WorkTagDao workTagDao();
-
- /**
- * @return The Data Access Object for {@link SystemIdInfo}s.
- */
- @NonNull
- public abstract SystemIdInfoDao systemIdInfoDao();
-
- /**
- * @return The Data Access Object for {@link WorkName}s.
- */
- @NonNull
- public abstract WorkNameDao workNameDao();
-
- /**
- * @return The Data Access Object for {@link WorkProgress}.
- */
- @NonNull
- public abstract WorkProgressDao workProgressDao();
-
- /**
- * @return The Data Access Object for {@link Preference}.
- */
- @NonNull
- public abstract PreferenceDao preferenceDao();
-
- /**
- * @return The Data Access Object which can be used to execute raw queries.
- */
- @NonNull
- public abstract RawWorkInfoDao rawWorkInfoDao();
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.kt
new file mode 100644
index 0000000..e2e3c89
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2017 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.work.impl
+
+import android.content.Context
+import androidx.annotation.RestrictTo
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
+import androidx.work.Data
+import androidx.work.impl.WorkDatabaseVersions.VERSION_10
+import androidx.work.impl.WorkDatabaseVersions.VERSION_11
+import androidx.work.impl.WorkDatabaseVersions.VERSION_2
+import androidx.work.impl.WorkDatabaseVersions.VERSION_3
+import androidx.work.impl.WorkDatabaseVersions.VERSION_5
+import androidx.work.impl.WorkDatabaseVersions.VERSION_6
+import androidx.work.impl.model.Dependency
+import androidx.work.impl.model.DependencyDao
+import androidx.work.impl.model.Preference
+import androidx.work.impl.model.PreferenceDao
+import androidx.work.impl.model.RawWorkInfoDao
+import androidx.work.impl.model.SystemIdInfo
+import androidx.work.impl.model.SystemIdInfoDao
+import androidx.work.impl.model.WorkName
+import androidx.work.impl.model.WorkNameDao
+import androidx.work.impl.model.WorkProgress
+import androidx.work.impl.model.WorkProgressDao
+import androidx.work.impl.model.WorkSpec
+import androidx.work.impl.model.WorkSpecDao
+import androidx.work.impl.model.WorkTag
+import androidx.work.impl.model.WorkTagDao
+import androidx.work.impl.model.WorkTypeConverters
+import androidx.work.impl.model.WorkTypeConverters.StateIds.COMPLETED_STATES
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
+
+/**
+ * A Room database for keeping track of work states.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Database(
+ entities = [Dependency::class, WorkSpec::class, WorkTag::class, SystemIdInfo::class,
+ WorkName::class, WorkProgress::class, Preference::class],
+ version = 12
+)
+@TypeConverters(value = [Data::class, WorkTypeConverters::class])
+abstract class WorkDatabase : RoomDatabase() {
+ /**
+ * @return The Data Access Object for [WorkSpec]s.
+ */
+ abstract fun workSpecDao(): WorkSpecDao
+
+ /**
+ * @return The Data Access Object for [Dependency]s.
+ */
+ abstract fun dependencyDao(): DependencyDao
+
+ /**
+ * @return The Data Access Object for [WorkTag]s.
+ */
+ abstract fun workTagDao(): WorkTagDao
+
+ /**
+ * @return The Data Access Object for [SystemIdInfo]s.
+ */
+ abstract fun systemIdInfoDao(): SystemIdInfoDao
+
+ /**
+ * @return The Data Access Object for [WorkName]s.
+ */
+ abstract fun workNameDao(): WorkNameDao
+
+ /**
+ * @return The Data Access Object for [WorkProgress].
+ */
+ abstract fun workProgressDao(): WorkProgressDao
+
+ /**
+ * @return The Data Access Object for [Preference].
+ */
+ abstract fun preferenceDao(): PreferenceDao
+
+ /**
+ * @return The Data Access Object which can be used to execute raw queries.
+ */
+ abstract fun rawWorkInfoDao(): RawWorkInfoDao
+
+ companion object {
+ /**
+ * Creates an instance of the WorkDatabase.
+ *
+ * @param context A context (this method will use the application context from it)
+ * @param queryExecutor An [Executor] that will be used to execute all async Room
+ * queries.
+ * @param useTestDatabase `true` to generate an in-memory database that allows main thread
+ * access
+ * @return The created WorkDatabase
+ */
+ @JvmStatic
+ fun create(
+ context: Context,
+ queryExecutor: Executor,
+ useTestDatabase: Boolean
+ ): WorkDatabase {
+ val builder = if (useTestDatabase) {
+ Room.inMemoryDatabaseBuilder(context, WorkDatabase::class.java)
+ .allowMainThreadQueries()
+ } else {
+ Room.databaseBuilder(context, WorkDatabase::class.java, WORK_DATABASE_NAME)
+ .openHelperFactory { configuration ->
+ val configBuilder = SupportSQLiteOpenHelper.Configuration.builder(context)
+ configBuilder.name(configuration.name)
+ .callback(configuration.callback)
+ .noBackupDirectory(true)
+ FrameworkSQLiteOpenHelperFactory().create(configBuilder.build())
+ }
+ }
+ return builder.setQueryExecutor(queryExecutor)
+ .addCallback(CleanupCallback)
+ .addMigrations(Migration_1_2)
+ .addMigrations(RescheduleMigration(context, VERSION_2, VERSION_3))
+ .addMigrations(Migration_3_4)
+ .addMigrations(Migration_4_5)
+ .addMigrations(RescheduleMigration(context, VERSION_5, VERSION_6))
+ .addMigrations(Migration_6_7)
+ .addMigrations(Migration_7_8)
+ .addMigrations(Migration_8_9)
+ .addMigrations(WorkMigration9To10(context))
+ .addMigrations(RescheduleMigration(context, VERSION_10, VERSION_11))
+ .addMigrations(Migration_11_12)
+ .fallbackToDestructiveMigration()
+ .build()
+ }
+ }
+}
+
+// Delete rows in the workspec table that...
+private const val PRUNE_SQL_FORMAT_PREFIX =
+ // are completed...
+ "DELETE FROM workspec WHERE state IN $COMPLETED_STATES AND " +
+ // and the minimum retention time has expired...
+ "(period_start_time + minimum_retention_duration) < "
+
+// and all dependents are completed.
+private const val PRUNE_SQL_FORMAT_SUFFIX = " AND " +
+ "(SELECT COUNT(*)=0 FROM dependency WHERE " +
+ " prerequisite_id=id AND " +
+ " work_spec_id NOT IN " +
+ " (SELECT id FROM workspec WHERE state IN $COMPLETED_STATES))"
+private val PRUNE_THRESHOLD_MILLIS = TimeUnit.DAYS.toMillis(1)
+
+internal object CleanupCallback : RoomDatabase.Callback() {
+ private val pruneSQL: String
+ get() = "$PRUNE_SQL_FORMAT_PREFIX$pruneDate$PRUNE_SQL_FORMAT_SUFFIX"
+
+ val pruneDate: Long
+ get() = System.currentTimeMillis() - PRUNE_THRESHOLD_MILLIS
+
+ override fun onOpen(db: SupportSQLiteDatabase) {
+ super.onOpen(db)
+ db.beginTransaction()
+ try {
+ // Prune everything that is completed, has an expired retention time, and has no
+ // active dependents:
+ db.execSQL(pruneSQL)
+ db.setTransactionSuccessful()
+ } finally {
+ db.endTransaction()
+ }
+ }
+}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java
deleted file mode 100644
index 7a1d045..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * Copyright 2018 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.work.impl;
-
-import static android.content.Context.MODE_PRIVATE;
-
-import static androidx.work.impl.utils.PreferenceUtils.KEY_RESCHEDULE_NEEDED;
-import static androidx.work.impl.utils.PreferenceUtils.PREFERENCES_FILE_NAME;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.room.migration.Migration;
-import androidx.sqlite.db.SupportSQLiteDatabase;
-import androidx.work.impl.model.Preference;
-import androidx.work.impl.model.WorkSpec;
-import androidx.work.impl.model.WorkTypeConverters;
-import androidx.work.impl.utils.IdGenerator;
-import androidx.work.impl.utils.PreferenceUtils;
-
-/**
- * Migration helpers for {@link androidx.work.impl.WorkDatabase}.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class WorkDatabaseMigrations {
-
- private WorkDatabaseMigrations() {
- // does nothing
- }
-
- // Known WorkDatabase versions
- public static final int VERSION_1 = 1;
- public static final int VERSION_2 = 2;
- public static final int VERSION_3 = 3;
- public static final int VERSION_4 = 4;
- public static final int VERSION_5 = 5;
- public static final int VERSION_6 = 6;
- public static final int VERSION_7 = 7;
- public static final int VERSION_8 = 8;
- public static final int VERSION_9 = 9;
- public static final int VERSION_10 = 10;
- public static final int VERSION_11 = 11;
- public static final int VERSION_12 = 12;
-
- private static final String CREATE_SYSTEM_ID_INFO =
- "CREATE TABLE IF NOT EXISTS `SystemIdInfo` (`work_spec_id` TEXT NOT NULL, `system_id`"
- + " INTEGER NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`)"
- + " REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )";
-
- private static final String MIGRATE_ALARM_INFO_TO_SYSTEM_ID_INFO =
- "INSERT INTO SystemIdInfo(work_spec_id, system_id) "
- + "SELECT work_spec_id, alarm_id AS system_id FROM alarmInfo";
-
- private static final String PERIODIC_WORK_SET_SCHEDULE_REQUESTED_AT =
- "UPDATE workspec SET schedule_requested_at=0"
- + " WHERE state NOT IN " + WorkTypeConverters.StateIds.COMPLETED_STATES
- + " AND schedule_requested_at=" + WorkSpec.SCHEDULE_NOT_REQUESTED_YET
- + " AND interval_duration<>0";
-
- private static final String REMOVE_ALARM_INFO = "DROP TABLE IF EXISTS alarmInfo";
-
- private static final String WORKSPEC_ADD_TRIGGER_UPDATE_DELAY =
- "ALTER TABLE workspec ADD COLUMN `trigger_content_update_delay` INTEGER NOT NULL "
- + "DEFAULT -1";
-
- private static final String WORKSPEC_ADD_TRIGGER_MAX_CONTENT_DELAY =
- "ALTER TABLE workspec ADD COLUMN `trigger_max_content_delay` INTEGER NOT NULL DEFAULT"
- + " -1";
-
- private static final String CREATE_WORK_PROGRESS =
- "CREATE TABLE IF NOT EXISTS `WorkProgress` (`work_spec_id` TEXT NOT NULL, `progress`"
- + " BLOB NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`) "
- + "REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )";
-
- private static final String CREATE_INDEX_PERIOD_START_TIME =
- "CREATE INDEX IF NOT EXISTS `index_WorkSpec_period_start_time` ON `workspec` "
- + "(`period_start_time`)";
-
- private static final String CREATE_RUN_IN_FOREGROUND =
- "ALTER TABLE workspec ADD COLUMN `run_in_foreground` INTEGER NOT NULL DEFAULT 0";
-
- public static final String INSERT_PREFERENCE =
- "INSERT OR REPLACE INTO `Preference`"
- + " (`key`, `long_value`) VALUES"
- + " (@key, @long_value)";
-
- private static final String CREATE_PREFERENCE =
- "CREATE TABLE IF NOT EXISTS `Preference` (`key` TEXT NOT NULL, `long_value` INTEGER, "
- + "PRIMARY KEY(`key`))";
-
- private static final String CREATE_OUT_OF_QUOTA_POLICY =
- "ALTER TABLE workspec ADD COLUMN `out_of_quota_policy` INTEGER NOT NULL DEFAULT 0";
-
- /**
- * Removes the {@code alarmInfo} table and substitutes it for a more general
- * {@code SystemIdInfo} table.
- * Adds implicit work tags for all work (a tag with the worker class name).
- */
- @NonNull
- public static Migration MIGRATION_1_2 = new Migration(VERSION_1, VERSION_2) {
- @Override
- public void migrate(@NonNull SupportSQLiteDatabase database) {
- database.execSQL(CREATE_SYSTEM_ID_INFO);
- database.execSQL(MIGRATE_ALARM_INFO_TO_SYSTEM_ID_INFO);
- database.execSQL(REMOVE_ALARM_INFO);
- database.execSQL("INSERT OR IGNORE INTO worktag(tag, work_spec_id) "
- + "SELECT worker_class_name AS tag, id AS work_spec_id FROM workspec");
- }
- };
-
- /**
- * A {@link WorkDatabase} migration that reschedules all eligible Workers.
- */
- public static class RescheduleMigration extends Migration {
- final Context mContext;
-
- public RescheduleMigration(@NonNull Context context, int startVersion, int endVersion) {
- super(startVersion, endVersion);
- mContext = context;
- }
-
- @Override
- public void migrate(@NonNull SupportSQLiteDatabase database) {
- if (endVersion >= VERSION_10) {
- database.execSQL(INSERT_PREFERENCE, new Object[]{KEY_RESCHEDULE_NEEDED, 1});
- } else {
- SharedPreferences preferences =
- mContext.getSharedPreferences(PREFERENCES_FILE_NAME, MODE_PRIVATE);
-
- // Mutate the shared preferences directly, and eventually they will get
- // migrated to the data store post v10.
- preferences.edit()
- .putBoolean(KEY_RESCHEDULE_NEEDED, true)
- .apply();
- }
- }
- }
-
- /**
- * Marks {@code SCHEDULE_REQUESTED_AT} to something other than
- * {@code SCHEDULE_NOT_REQUESTED_AT}.
- */
- @NonNull
- public static Migration MIGRATION_3_4 = new Migration(VERSION_3, VERSION_4) {
- @Override
- public void migrate(@NonNull SupportSQLiteDatabase database) {
- if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
- database.execSQL(PERIODIC_WORK_SET_SCHEDULE_REQUESTED_AT);
- }
- }
- };
-
- /**
- * Adds the {@code ContentUri} delays to the WorkSpec table.
- */
- @NonNull
- public static Migration MIGRATION_4_5 = new Migration(VERSION_4, VERSION_5) {
- @Override
- public void migrate(@NonNull SupportSQLiteDatabase database) {
- database.execSQL(WORKSPEC_ADD_TRIGGER_UPDATE_DELAY);
- database.execSQL(WORKSPEC_ADD_TRIGGER_MAX_CONTENT_DELAY);
- }
- };
-
- /**
- * Adds {@link androidx.work.impl.model.WorkProgress}.
- */
- @NonNull
- public static Migration MIGRATION_6_7 = new Migration(VERSION_6, VERSION_7) {
- @Override
- public void migrate(@NonNull SupportSQLiteDatabase database) {
- database.execSQL(CREATE_WORK_PROGRESS);
- }
- };
-
- /**
- * Adds an index on period_start_time in {@link WorkSpec}.
- */
- @NonNull
- public static Migration MIGRATION_7_8 = new Migration(VERSION_7, VERSION_8) {
- @Override
- public void migrate(@NonNull SupportSQLiteDatabase database) {
- database.execSQL(CREATE_INDEX_PERIOD_START_TIME);
- }
- };
-
- /**
- * Adds a notification_provider to the {@link WorkSpec}.
- */
- @NonNull
- public static Migration MIGRATION_8_9 = new Migration(VERSION_8, VERSION_9) {
- @Override
- public void migrate(@NonNull SupportSQLiteDatabase database) {
- database.execSQL(CREATE_RUN_IN_FOREGROUND);
- }
- };
-
- /**
- * Adds the {@link Preference} table.
- */
- public static class WorkMigration9To10 extends Migration {
- final Context mContext;
-
- public WorkMigration9To10(@NonNull Context context) {
- super(VERSION_9, VERSION_10);
- mContext = context;
- }
-
- @Override
- public void migrate(@NonNull SupportSQLiteDatabase database) {
- database.execSQL(CREATE_PREFERENCE);
- PreferenceUtils.migrateLegacyPreferences(mContext, database);
- IdGenerator.migrateLegacyIdGenerator(mContext, database);
- }
- }
-
- /**
- * Adds a notification_provider to the {@link WorkSpec}.
- */
- @NonNull
- public static Migration MIGRATION_11_12 = new Migration(VERSION_11, VERSION_12) {
- @Override
- public void migrate(@NonNull SupportSQLiteDatabase database) {
- database.execSQL(CREATE_OUT_OF_QUOTA_POLICY);
- }
- };
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt
new file mode 100644
index 0000000..17fbb18
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2018 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.work.impl
+
+import android.content.Context
+import android.os.Build
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.work.impl.WorkDatabaseVersions.VERSION_1
+import androidx.work.impl.WorkDatabaseVersions.VERSION_10
+import androidx.work.impl.WorkDatabaseVersions.VERSION_11
+import androidx.work.impl.WorkDatabaseVersions.VERSION_12
+import androidx.work.impl.WorkDatabaseVersions.VERSION_2
+import androidx.work.impl.WorkDatabaseVersions.VERSION_3
+import androidx.work.impl.WorkDatabaseVersions.VERSION_4
+import androidx.work.impl.WorkDatabaseVersions.VERSION_5
+import androidx.work.impl.WorkDatabaseVersions.VERSION_6
+import androidx.work.impl.WorkDatabaseVersions.VERSION_7
+import androidx.work.impl.WorkDatabaseVersions.VERSION_8
+import androidx.work.impl.WorkDatabaseVersions.VERSION_9
+import androidx.work.impl.model.WorkSpec
+import androidx.work.impl.model.WorkTypeConverters.StateIds.COMPLETED_STATES
+import androidx.work.impl.utils.IdGenerator
+import androidx.work.impl.utils.PreferenceUtils
+
+/**
+ * Migration helpers for [androidx.work.impl.WorkDatabase].
+ */
+internal object WorkDatabaseVersions {
+ // Known WorkDatabase versions
+ const val VERSION_1 = 1
+ const val VERSION_2 = 2
+ const val VERSION_3 = 3
+ const val VERSION_4 = 4
+ const val VERSION_5 = 5
+ const val VERSION_6 = 6
+ const val VERSION_7 = 7
+ const val VERSION_8 = 8
+ const val VERSION_9 = 9
+ const val VERSION_10 = 10
+ const val VERSION_11 = 11
+ const val VERSION_12 = 12
+}
+
+private const val CREATE_SYSTEM_ID_INFO =
+ """
+ CREATE TABLE IF NOT EXISTS `SystemIdInfo` (`work_spec_id` TEXT NOT NULL, `system_id`
+ INTEGER NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`)
+ REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )
+ """
+private const val MIGRATE_ALARM_INFO_TO_SYSTEM_ID_INFO =
+ """
+ INSERT INTO SystemIdInfo(work_spec_id, system_id)
+ SELECT work_spec_id, alarm_id AS system_id FROM alarmInfo
+ """
+private const val PERIODIC_WORK_SET_SCHEDULE_REQUESTED_AT =
+ """
+ UPDATE workspec SET schedule_requested_at=0
+ WHERE state NOT IN $COMPLETED_STATES
+ AND schedule_requested_at=${WorkSpec.SCHEDULE_NOT_REQUESTED_YET}
+ AND interval_duration<>0
+ """
+private const val REMOVE_ALARM_INFO = "DROP TABLE IF EXISTS alarmInfo"
+private const val WORKSPEC_ADD_TRIGGER_UPDATE_DELAY =
+ "ALTER TABLE workspec ADD COLUMN `trigger_content_update_delay` INTEGER NOT NULL DEFAULT -1"
+private const val WORKSPEC_ADD_TRIGGER_MAX_CONTENT_DELAY =
+ "ALTER TABLE workspec ADD COLUMN `trigger_max_content_delay` INTEGER NOT NULL DEFAULT -1"
+private const val CREATE_WORK_PROGRESS =
+ """
+ CREATE TABLE IF NOT EXISTS `WorkProgress` (`work_spec_id` TEXT NOT NULL, `progress`
+ BLOB NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`)
+ REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )
+ """
+private const val CREATE_INDEX_PERIOD_START_TIME =
+ """
+ CREATE INDEX IF NOT EXISTS `index_WorkSpec_period_start_time` ON `workspec`(`period_start_time`)
+ """
+private const val CREATE_RUN_IN_FOREGROUND =
+ "ALTER TABLE workspec ADD COLUMN `run_in_foreground` INTEGER NOT NULL DEFAULT 0"
+private const val CREATE_OUT_OF_QUOTA_POLICY =
+ "ALTER TABLE workspec ADD COLUMN `out_of_quota_policy` INTEGER NOT NULL DEFAULT 0"
+
+/**
+ * Removes the `alarmInfo` table and substitutes it for a more general
+ * `SystemIdInfo` table.
+ * Adds implicit work tags for all work (a tag with the worker class name).
+ */
+object Migration_1_2 : Migration(VERSION_1, VERSION_2) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(CREATE_SYSTEM_ID_INFO)
+ database.execSQL(MIGRATE_ALARM_INFO_TO_SYSTEM_ID_INFO)
+ database.execSQL(REMOVE_ALARM_INFO)
+ database.execSQL(
+ """
+ INSERT OR IGNORE INTO worktag(tag, work_spec_id)
+ SELECT worker_class_name AS tag, id AS work_spec_id FROM workspec
+ """
+ )
+ }
+}
+
+/**
+ * Marks `SCHEDULE_REQUESTED_AT` to something other than
+ * `SCHEDULE_NOT_REQUESTED_AT`.
+ */
+object Migration_3_4 : Migration(VERSION_3, VERSION_4) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
+ database.execSQL(PERIODIC_WORK_SET_SCHEDULE_REQUESTED_AT)
+ }
+ }
+}
+
+/**
+ * Adds the `ContentUri` delays to the WorkSpec table.
+ */
+object Migration_4_5 : Migration(VERSION_4, VERSION_5) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(WORKSPEC_ADD_TRIGGER_UPDATE_DELAY)
+ database.execSQL(WORKSPEC_ADD_TRIGGER_MAX_CONTENT_DELAY)
+ }
+}
+
+/**
+ * Adds [androidx.work.impl.model.WorkProgress].
+ */
+object Migration_6_7 : Migration(VERSION_6, VERSION_7) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(CREATE_WORK_PROGRESS)
+ }
+}
+
+/**
+ * Adds an index on period_start_time in [WorkSpec].
+ */
+object Migration_7_8 : Migration(VERSION_7, VERSION_8) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(CREATE_INDEX_PERIOD_START_TIME)
+ }
+}
+
+/**
+ * Adds a notification_provider to the [WorkSpec].
+ */
+object Migration_8_9 : Migration(VERSION_8, VERSION_9) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(CREATE_RUN_IN_FOREGROUND)
+ }
+}
+
+/**
+ * Adds a notification_provider to the [WorkSpec].
+ */
+object Migration_11_12 : Migration(VERSION_11, VERSION_12) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(CREATE_OUT_OF_QUOTA_POLICY)
+ }
+}
+
+/**
+ * A [WorkDatabase] migration that reschedules all eligible Workers.
+ */
+class RescheduleMigration(val mContext: Context, startVersion: Int, endVersion: Int) :
+ Migration(startVersion, endVersion) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ if (endVersion >= VERSION_10) {
+ database.execSQL(
+ PreferenceUtils.INSERT_PREFERENCE,
+ arrayOf<Any>(PreferenceUtils.KEY_RESCHEDULE_NEEDED, 1)
+ )
+ } else {
+ val preferences = mContext.getSharedPreferences(
+ PreferenceUtils.PREFERENCES_FILE_NAME,
+ Context.MODE_PRIVATE
+ )
+
+ // Mutate the shared preferences directly, and eventually they will get
+ // migrated to the data store post v10.
+ preferences.edit()
+ .putBoolean(PreferenceUtils.KEY_RESCHEDULE_NEEDED, true)
+ .apply()
+ }
+ }
+}
+
+/**
+ * Adds the [androidx.work.impl.model.Preference] table.
+ */
+internal class WorkMigration9To10(private val context: Context) : Migration(VERSION_9, VERSION_10) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(PreferenceUtils.CREATE_PREFERENCE)
+ PreferenceUtils.migrateLegacyPreferences(context, database)
+ IdGenerator.migrateLegacyIdGenerator(context, database)
+ }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabasePathHelper.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabasePathHelper.java
deleted file mode 100644
index 2c419c1..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabasePathHelper.java
+++ /dev/null
@@ -1,160 +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.work.impl;
-
-import android.content.Context;
-import android.os.Build;
-
-import androidx.annotation.DoNotInline;
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
-import androidx.work.Logger;
-
-import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Keeps track of {@link WorkDatabase} paths.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class WorkDatabasePathHelper {
- private WorkDatabasePathHelper() {
- }
-
- private static final String TAG = Logger.tagWithPrefix("WrkDbPathHelper");
-
- private static final String WORK_DATABASE_NAME = "androidx.work.workdb";
-
- // Supporting files for a SQLite database
- private static final String[] DATABASE_EXTRA_FILES = new String[]{"-journal", "-shm", "-wal"};
-
- /**
- * @return The name of the database.
- */
- @NonNull
- public static String getWorkDatabaseName() {
- return WORK_DATABASE_NAME;
- }
-
- /**
- * Migrates {@link WorkDatabase} to the no-backup directory.
- *
- * @param context The application context.
- */
- public static void migrateDatabase(@NonNull Context context) {
- File defaultDatabasePath = getDefaultDatabasePath(context);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && defaultDatabasePath.exists()) {
- Logger.get().debug(TAG, "Migrating WorkDatabase to the no-backup directory");
- Map<File, File> paths = migrationPaths(context);
- for (File source : paths.keySet()) {
- File destination = paths.get(source);
- if (source.exists() && destination != null) {
- if (destination.exists()) {
- String message = "Over-writing contents of " + destination;
- Logger.get().warning(TAG, message);
- }
- boolean renamed = source.renameTo(destination);
- String message;
- if (renamed) {
- message = "Migrated " + source + "to " + destination;
- } else {
- message = "Renaming " + source + " to " + destination + " failed";
- }
- Logger.get().debug(TAG, message);
- }
- }
- }
- }
-
- /**
- * Returns a {@link Map} of all paths which need to be migrated to the no-backup directory.
- *
- * @param context The application {@link Context}
- * @return a {@link Map} of paths to be migrated from source -> destination
- */
- @NonNull
- @VisibleForTesting
- public static Map<File, File> migrationPaths(@NonNull Context context) {
- Map<File, File> paths = new HashMap<>();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- File databasePath = getDefaultDatabasePath(context);
- File migratedPath = getDatabasePath(context);
- paths.put(databasePath, migratedPath);
- for (String extra : DATABASE_EXTRA_FILES) {
- File source = new File(databasePath.getPath() + extra);
- File destination = new File(migratedPath.getPath() + extra);
- paths.put(source, destination);
- }
- }
- return paths;
- }
-
- /**
- * @param context The application {@link Context}
- * @return The database path before migration to the no-backup directory.
- */
- @NonNull
- @VisibleForTesting
- public static File getDefaultDatabasePath(@NonNull Context context) {
- return context.getDatabasePath(WORK_DATABASE_NAME);
- }
-
- /**
- * @param context The application {@link Context}
- * @return The the migrated database path.
- */
- @NonNull
- @VisibleForTesting
- public static File getDatabasePath(@NonNull Context context) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- // No notion of a backup directory exists.
- return getDefaultDatabasePath(context);
- } else {
- return getNoBackupPath(context, WORK_DATABASE_NAME);
- }
- }
-
- /**
- * Return the path for a {@link File} path in the {@link Context#getNoBackupFilesDir()}
- * identified by the {@link String} fragment.
- *
- * @param context The application {@link Context}
- * @param filePath The {@link String} file path
- * @return the {@link File}
- */
- @RequiresApi(23)
- private static File getNoBackupPath(@NonNull Context context, @NonNull String filePath) {
- return new File(Api21Impl.getNoBackupFilesDir(context), filePath);
- }
-
- @RequiresApi(21)
- static class Api21Impl {
- private Api21Impl() {
- // This class is not instantiable.
- }
-
- @DoNotInline
- static File getNoBackupFilesDir(Context context) {
- return context.getNoBackupFilesDir();
- }
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabasePathHelper.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabasePathHelper.kt
new file mode 100644
index 0000000..6ab27fa
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabasePathHelper.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.work.impl
+
+import android.content.Context
+import android.os.Build
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.work.Logger
+import java.io.File
+
+private val TAG = Logger.tagWithPrefix("WrkDbPathHelper")
+
+/**
+ * @return The name of the database.
+ */
+internal const val WORK_DATABASE_NAME = "androidx.work.workdb"
+
+// Supporting files for a SQLite database
+private val DATABASE_EXTRA_FILES = arrayOf("-journal", "-shm", "-wal")
+
+/**
+ * Keeps track of {@link WorkDatabase} paths.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+object WorkDatabasePathHelper {
+ /**
+ * Migrates [WorkDatabase] to the no-backup directory.
+ *
+ * @param context The application context.
+ */
+ @JvmStatic
+ fun migrateDatabase(context: Context) {
+ val defaultDatabasePath = getDefaultDatabasePath(context)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && defaultDatabasePath.exists()) {
+ Logger.get().debug(TAG, "Migrating WorkDatabase to the no-backup directory")
+ migrationPaths(context).forEach { (source, destination) ->
+ if (source.exists()) {
+ if (destination.exists()) {
+ Logger.get().warning(TAG, "Over-writing contents of $destination")
+ }
+ val renamed = source.renameTo(destination)
+ val message = if (renamed) {
+ "Migrated ${source}to $destination"
+ } else {
+ "Renaming $source to $destination failed"
+ }
+ Logger.get().debug(TAG, message)
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a [Map] of all paths which need to be migrated to the no-backup directory.
+ *
+ * @param context The application [Context]
+ * @return a [Map] of paths to be migrated from source -> destination
+ */
+ fun migrationPaths(context: Context): Map<File, File> {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ val databasePath = getDefaultDatabasePath(context)
+ val migratedPath = getDatabasePath(context)
+ val map = DATABASE_EXTRA_FILES.associate { extra ->
+ File(databasePath.path + extra) to File(migratedPath.path + extra)
+ }
+ map + (databasePath to migratedPath)
+ } else emptyMap()
+ }
+
+ /**
+ * @param context The application [Context]
+ * @return The database path before migration to the no-backup directory.
+ */
+ fun getDefaultDatabasePath(context: Context): File {
+ return context.getDatabasePath(WORK_DATABASE_NAME)
+ }
+
+ /**
+ * @param context The application [Context]
+ * @return The the migrated database path.
+ */
+ fun getDatabasePath(context: Context): File {
+ return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ // No notion of a backup directory exists.
+ getDefaultDatabasePath(context)
+ } else {
+ getNoBackupPath(context)
+ }
+ }
+
+ /**
+ * Return the path for a [File] path in the [Context.getNoBackupFilesDir]
+ * identified by the [String] fragment.
+ *
+ * @param context The application [Context]
+ * @return the [File]
+ */
+ @RequiresApi(23)
+ private fun getNoBackupPath(context: Context): File {
+ return File(Api21Impl.getNoBackupFilesDir(context), WORK_DATABASE_NAME)
+ }
+}
+
+@RequiresApi(21)
+internal object Api21Impl {
+ @DoNotInline
+ fun getNoBackupFilesDir(context: Context): File {
+ return context.noBackupFilesDir
+ }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/WorkConstraintsTracker.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/WorkConstraintsTracker.kt
index b5b769b..3928d63 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/WorkConstraintsTracker.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/WorkConstraintsTracker.kt
@@ -86,13 +86,13 @@
override fun replace(workSpecs: Iterable<WorkSpec>) {
synchronized(lock) {
for (controller in constraintControllers) {
- controller.setCallback(null)
+ controller.callback = null
}
for (controller in constraintControllers) {
controller.replace(workSpecs)
}
for (controller in constraintControllers) {
- controller.setCallback(this)
+ controller.callback = this
}
}
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/BatteryChargingController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/BatteryChargingController.java
deleted file mode 100644
index 7ff030b..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/BatteryChargingController.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2017 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.work.impl.constraints.controllers;
-
-import androidx.annotation.NonNull;
-import androidx.work.impl.constraints.trackers.BatteryChargingTracker;
-import androidx.work.impl.model.WorkSpec;
-
-/**
- * A {@link ConstraintController} for battery charging events.
- */
-
-public class BatteryChargingController extends ConstraintController<Boolean> {
- public BatteryChargingController(@NonNull BatteryChargingTracker tracker) {
- super(tracker);
- }
-
- @Override
- boolean hasConstraint(@NonNull WorkSpec workSpec) {
- return workSpec.constraints.requiresCharging();
- }
-
- @Override
- boolean isConstrained(@NonNull Boolean isBatteryCharging) {
- return !isBatteryCharging;
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/BatteryNotLowController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/BatteryNotLowController.java
deleted file mode 100644
index ff2190e..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/BatteryNotLowController.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2017 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.work.impl.constraints.controllers;
-
-import androidx.annotation.NonNull;
-import androidx.work.impl.constraints.trackers.BatteryNotLowTracker;
-import androidx.work.impl.model.WorkSpec;
-
-/**
- * A {@link ConstraintController} for battery not low events.
- */
-
-public class BatteryNotLowController extends ConstraintController<Boolean> {
- public BatteryNotLowController(@NonNull BatteryNotLowTracker tracker) {
- super(tracker);
- }
-
- @Override
- boolean hasConstraint(@NonNull WorkSpec workSpec) {
- return workSpec.constraints.requiresBatteryNotLow();
- }
-
- @Override
- boolean isConstrained(@NonNull Boolean isBatteryNotLow) {
- return !isBatteryNotLow;
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ConstraintController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ConstraintController.java
deleted file mode 100644
index b330c34..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ConstraintController.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * Copyright (C) 2017 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.work.impl.constraints.controllers;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.work.impl.constraints.ConstraintListener;
-import androidx.work.impl.constraints.trackers.ConstraintTracker;
-import androidx.work.impl.model.WorkSpec;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A controller for a particular constraint.
- *
- * @param <T> the constraint data type managed by this controller.
- */
-
-public abstract class ConstraintController<T> implements ConstraintListener<T> {
-
- /**
- * A callback for when a constraint changes.
- */
- public interface OnConstraintUpdatedCallback {
-
- /**
- * Called when a constraint is met.
- *
- * @param workSpecIds A list of {@link WorkSpec} IDs that may have become eligible to run
- */
- void onConstraintMet(@NonNull List<String> workSpecIds);
-
- /**
- * Called when a constraint is not met.
- *
- * @param workSpecIds A list of {@link WorkSpec} IDs that have become ineligible to run
- */
- void onConstraintNotMet(@NonNull List<String> workSpecIds);
- }
-
- private final List<String> mMatchingWorkSpecIds = new ArrayList<>();
-
- private T mCurrentValue;
- private ConstraintTracker<T> mTracker;
- private OnConstraintUpdatedCallback mCallback;
-
- ConstraintController(ConstraintTracker<T> tracker) {
- mTracker = tracker;
- }
-
- /**
- * Sets the callback to inform when constraints change. This callback is also triggered the
- * first time it is set.
- *
- * @param callback The callback to inform about constraint met/unmet states
- */
- public void setCallback(@Nullable OnConstraintUpdatedCallback callback) {
- if (mCallback != callback) {
- mCallback = callback;
- updateCallback(mCallback, mCurrentValue);
- }
- }
-
- abstract boolean hasConstraint(@NonNull WorkSpec workSpec);
-
- abstract boolean isConstrained(@NonNull T currentValue);
-
- /**
- * Replaces the list of {@link WorkSpec}s to monitor constraints for.
- *
- * @param workSpecs A list of {@link WorkSpec}s to monitor constraints for
- */
- public void replace(@NonNull Iterable<WorkSpec> workSpecs) {
- mMatchingWorkSpecIds.clear();
-
- for (WorkSpec workSpec : workSpecs) {
- if (hasConstraint(workSpec)) {
- mMatchingWorkSpecIds.add(workSpec.id);
- }
- }
-
- if (mMatchingWorkSpecIds.isEmpty()) {
- mTracker.removeListener(this);
- } else {
- mTracker.addListener(this);
- }
- updateCallback(mCallback, mCurrentValue);
- }
-
- /**
- * Clears all tracked {@link WorkSpec}s.
- */
- public void reset() {
- if (!mMatchingWorkSpecIds.isEmpty()) {
- mMatchingWorkSpecIds.clear();
- mTracker.removeListener(this);
- }
- }
-
- /**
- * Determines if a particular {@link WorkSpec} is constrained. It is constrained if it is
- * tracked by this controller, and the controller constraint was set, but not satisfied.
- *
- * @param workSpecId The ID of the {@link WorkSpec} to check if it is constrained.
- * @return {@code true} if the {@link WorkSpec} is considered constrained
- */
- public boolean isWorkSpecConstrained(@NonNull String workSpecId) {
- return mCurrentValue != null && isConstrained(mCurrentValue)
- && mMatchingWorkSpecIds.contains(workSpecId);
- }
-
- private void updateCallback(
- @Nullable OnConstraintUpdatedCallback callback,
- T currentValue) {
-
- // We pass copies of references (callback, currentValue) to updateCallback because public
- // APIs on ConstraintController may be called from any thread, and onConstraintChanged() is
- // called from the main thread.
- if (mMatchingWorkSpecIds.isEmpty() || callback == null) {
- return;
- }
-
- if (currentValue == null || isConstrained(currentValue)) {
- callback.onConstraintNotMet(mMatchingWorkSpecIds);
- } else {
- callback.onConstraintMet(mMatchingWorkSpecIds);
- }
- }
-
- @Override
- public void onConstraintChanged(T newValue) {
- mCurrentValue = newValue;
- updateCallback(mCallback, mCurrentValue);
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ConstraintController.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ConstraintController.kt
new file mode 100644
index 0000000..5081fcf
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ConstraintController.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2017 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.work.impl.constraints.controllers
+
+import androidx.work.impl.constraints.ConstraintListener
+import androidx.work.impl.constraints.trackers.ConstraintTracker
+import androidx.work.impl.model.WorkSpec
+
+/**
+ * A controller for a particular constraint.
+ *
+ * @param <T> the constraint data type managed by this controller.
+ */
+abstract class ConstraintController<T> internal constructor(
+ private val tracker: ConstraintTracker<T>
+) : ConstraintListener<T> {
+ /**
+ * A callback for when a constraint changes.
+ */
+ interface OnConstraintUpdatedCallback {
+ /**
+ * Called when a constraint is met.
+ *
+ * @param workSpecIds A list of [WorkSpec] IDs that may have become eligible to run
+ */
+ fun onConstraintMet(workSpecIds: List<String>)
+
+ /**
+ * Called when a constraint is not met.
+ *
+ * @param workSpecIds A list of [WorkSpec] IDs that have become ineligible to run
+ */
+ fun onConstraintNotMet(workSpecIds: List<String>)
+ }
+
+ private val matchingWorkSpecIds = mutableListOf<String>()
+ private var currentValue: T? = null
+
+ /**
+ * Sets the callback to inform when constraints change. This callback is also triggered the
+ * first time it is set.
+ */
+ var callback: OnConstraintUpdatedCallback? = null
+ set(value) {
+ if (field !== value) {
+ field = value
+ updateCallback(value, currentValue)
+ }
+ }
+
+ abstract fun hasConstraint(workSpec: WorkSpec): Boolean
+ abstract fun isConstrained(value: T): Boolean
+
+ /**
+ * Replaces the list of [WorkSpec]s to monitor constraints for.
+ *
+ * @param workSpecs A list of [WorkSpec]s to monitor constraints for
+ */
+ fun replace(workSpecs: Iterable<WorkSpec>) {
+ matchingWorkSpecIds.clear()
+ workSpecs.mapNotNullTo(matchingWorkSpecIds) {
+ if (hasConstraint(it)) it.id else null
+ }
+
+ if (matchingWorkSpecIds.isEmpty()) {
+ tracker.removeListener(this)
+ } else {
+ tracker.addListener(this)
+ }
+ updateCallback(callback, currentValue)
+ }
+
+ /**
+ * Clears all tracked [WorkSpec]s.
+ */
+ fun reset() {
+ if (matchingWorkSpecIds.isNotEmpty()) {
+ matchingWorkSpecIds.clear()
+ tracker.removeListener(this)
+ }
+ }
+
+ /**
+ * Determines if a particular [WorkSpec] is constrained. It is constrained if it is
+ * tracked by this controller, and the controller constraint was set, but not satisfied.
+ *
+ * @param workSpecId The ID of the [WorkSpec] to check if it is constrained.
+ * @return `true` if the [WorkSpec] is considered constrained
+ */
+ fun isWorkSpecConstrained(workSpecId: String): Boolean {
+ // TODO: unify `null` treatment here and in updateCallback, because
+ // here it is considered as not constrained and but in updateCallback as constrained.
+ val value = currentValue
+ return (value != null && isConstrained(value) && workSpecId in matchingWorkSpecIds)
+ }
+
+ private fun updateCallback(callback: OnConstraintUpdatedCallback?, currentValue: T?) {
+ // We pass copies of references (callback, currentValue) to updateCallback because public
+ // APIs on ConstraintController may be called from any thread, and onConstraintChanged() is
+ // called from the main thread.
+ if (matchingWorkSpecIds.isEmpty() || callback == null) {
+ return
+ }
+ if (currentValue == null || isConstrained(currentValue)) {
+ callback.onConstraintNotMet(matchingWorkSpecIds)
+ } else {
+ callback.onConstraintMet(matchingWorkSpecIds)
+ }
+ }
+
+ override fun onConstraintChanged(newValue: T) {
+ currentValue = newValue
+ updateCallback(callback, currentValue)
+ }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ContraintControllers.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ContraintControllers.kt
new file mode 100644
index 0000000..c72820c
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ContraintControllers.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.impl.constraints.controllers
+
+import android.os.Build
+import androidx.work.Logger
+import androidx.work.NetworkType
+import androidx.work.NetworkType.TEMPORARILY_UNMETERED
+import androidx.work.NetworkType.UNMETERED
+import androidx.work.impl.constraints.NetworkState
+import androidx.work.impl.constraints.trackers.BatteryChargingTracker
+import androidx.work.impl.constraints.trackers.BatteryNotLowTracker
+import androidx.work.impl.constraints.trackers.NetworkStateTracker
+import androidx.work.impl.constraints.trackers.StorageNotLowTracker
+import androidx.work.impl.model.WorkSpec
+
+/**
+ * A [ConstraintController] for battery charging events.
+ */
+class BatteryChargingController(tracker: BatteryChargingTracker) :
+ ConstraintController<Boolean>(tracker) {
+ override fun hasConstraint(workSpec: WorkSpec) = workSpec.constraints.requiresCharging()
+
+ override fun isConstrained(value: Boolean) = !value
+}
+
+/**
+ * A [ConstraintController] for battery not low events.
+ */
+class BatteryNotLowController(tracker: BatteryNotLowTracker) :
+ ConstraintController<Boolean>(tracker) {
+ override fun hasConstraint(workSpec: WorkSpec) = workSpec.constraints.requiresBatteryNotLow()
+
+ override fun isConstrained(value: Boolean) = !value
+}
+
+/**
+ * A [ConstraintController] for monitoring that the network connection is unmetered.
+ */
+class NetworkUnmeteredController(tracker: NetworkStateTracker) :
+ ConstraintController<NetworkState>(tracker) {
+ override fun hasConstraint(workSpec: WorkSpec): Boolean {
+ val requiredNetworkType = workSpec.constraints.requiredNetworkType
+ return requiredNetworkType == UNMETERED ||
+ (Build.VERSION.SDK_INT >= 30 && requiredNetworkType == TEMPORARILY_UNMETERED)
+ }
+
+ override fun isConstrained(value: NetworkState) = !value.isConnected || value.isMetered
+}
+
+/**
+ * A [ConstraintController] for storage not low events.
+ */
+class StorageNotLowController(tracker: StorageNotLowTracker) :
+ ConstraintController<Boolean>(tracker) {
+
+ override fun hasConstraint(workSpec: WorkSpec) = workSpec.constraints.requiresStorageNotLow()
+
+ override fun isConstrained(value: Boolean) = !value
+}
+
+/**
+ * A [ConstraintController] for monitoring that the network connection is not roaming.
+ */
+class NetworkNotRoamingController(tracker: NetworkStateTracker) :
+ ConstraintController<NetworkState>(tracker) {
+ override fun hasConstraint(workSpec: WorkSpec): Boolean {
+ return workSpec.constraints.requiredNetworkType == NetworkType.NOT_ROAMING
+ }
+
+ /**
+ * Check for not-roaming constraint on API 24+, when JobInfo#NETWORK_TYPE_NOT_ROAMING was added,
+ * to be consistent with JobScheduler functionality.
+ */
+ override fun isConstrained(value: NetworkState): Boolean {
+ return if (Build.VERSION.SDK_INT < 24) {
+ Logger.get().debug(
+ TAG, "Not-roaming network constraint is not supported before API 24, " +
+ "only checking for connected state."
+ )
+ !value.isConnected
+ } else !value.isConnected || !value.isNotRoaming
+ }
+
+ companion object {
+ private val TAG = Logger.tagWithPrefix("NetworkNotRoamingCtrlr")
+ }
+}
+
+/**
+ * A [ConstraintController] for monitoring that any usable network connection is available.
+ *
+ *
+ * For API 26 and above, usable means that the [NetworkState] is validated, i.e.
+ * it has a working internet connection.
+ *
+ *
+ * For API 25 and below, usable simply means that [NetworkState] is connected.
+ */
+class NetworkConnectedController(tracker: NetworkStateTracker) :
+ ConstraintController<NetworkState>(tracker) {
+ override fun hasConstraint(workSpec: WorkSpec) =
+ workSpec.constraints.requiredNetworkType == NetworkType.CONNECTED
+
+ override fun isConstrained(value: NetworkState) =
+ if (Build.VERSION.SDK_INT >= 26) {
+ !value.isConnected || !value.isValidated
+ } else {
+ !value.isConnected
+ }
+}
+
+/**
+ * A [ConstraintController] for monitoring that the network connection is metered.
+ */
+class NetworkMeteredController(tracker: NetworkStateTracker) :
+ ConstraintController<NetworkState>(tracker) {
+ override fun hasConstraint(workSpec: WorkSpec) =
+ workSpec.constraints.requiredNetworkType == NetworkType.METERED
+
+ /**
+ * Check for metered constraint on API 26+, when JobInfo#NETWORK_METERED was added, to
+ * be consistent with JobScheduler functionality.
+ */
+ override fun isConstrained(value: NetworkState): Boolean {
+ return if (Build.VERSION.SDK_INT < 26) {
+ Logger.get().debug(
+ TAG, "Metered network constraint is not supported before API 26, " +
+ "only checking for connected state."
+ )
+ !value.isConnected
+ } else !value.isConnected || !value.isMetered
+ }
+
+ companion object {
+ private val TAG = Logger.tagWithPrefix("NetworkMeteredCtrlr")
+ }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkConnectedController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkConnectedController.java
deleted file mode 100644
index 099953b9..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkConnectedController.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 2017 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.work.impl.constraints.controllers;
-
-import static androidx.work.NetworkType.CONNECTED;
-
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.work.impl.constraints.NetworkState;
-import androidx.work.impl.constraints.trackers.NetworkStateTracker;
-import androidx.work.impl.model.WorkSpec;
-
-/**
- * A {@link ConstraintController} for monitoring that any usable network connection is available.
- * <p>
- * For API 26 and above, usable means that the {@link NetworkState} is validated, i.e.
- * it has a working internet connection.
- * <p>
- * For API 25 and below, usable simply means that {@link NetworkState} is connected.
- */
-
-public class NetworkConnectedController extends ConstraintController<NetworkState> {
- public NetworkConnectedController(@NonNull NetworkStateTracker tracker) {
- super(tracker);
- }
-
- @Override
- boolean hasConstraint(@NonNull WorkSpec workSpec) {
- return workSpec.constraints.getRequiredNetworkType() == CONNECTED;
- }
-
- @Override
- boolean isConstrained(@NonNull NetworkState state) {
- if (Build.VERSION.SDK_INT >= 26) {
- return !state.isConnected() || !state.isValidated();
- } else {
- return !state.isConnected();
- }
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkMeteredController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkMeteredController.java
deleted file mode 100644
index 99a5519..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkMeteredController.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2017 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.work.impl.constraints.controllers;
-
-import static androidx.work.NetworkType.METERED;
-
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.work.Logger;
-import androidx.work.impl.constraints.NetworkState;
-import androidx.work.impl.constraints.trackers.NetworkStateTracker;
-import androidx.work.impl.model.WorkSpec;
-
-/**
- * A {@link ConstraintController} for monitoring that the network connection is metered.
- */
-
-public class NetworkMeteredController extends ConstraintController<NetworkState> {
- private static final String TAG = Logger.tagWithPrefix("NetworkMeteredCtrlr");
-
- public NetworkMeteredController(@NonNull NetworkStateTracker tracker) {
- super(tracker);
- }
-
- @Override
- boolean hasConstraint(@NonNull WorkSpec workSpec) {
- return workSpec.constraints.getRequiredNetworkType() == METERED;
- }
-
- /**
- * Check for metered constraint on API 26+, when JobInfo#NETWORK_METERED was added, to
- * be consistent with JobScheduler functionality.
- */
- @Override
- boolean isConstrained(@NonNull NetworkState state) {
- if (Build.VERSION.SDK_INT < 26) {
- Logger.get().debug(TAG, "Metered network constraint is not supported before API 26, "
- + "only checking for connected state.");
- return !state.isConnected();
- }
- return !state.isConnected() || !state.isMetered();
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkNotRoamingController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkNotRoamingController.java
deleted file mode 100644
index fb13514..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkNotRoamingController.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2017 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.work.impl.constraints.controllers;
-
-import static androidx.work.NetworkType.NOT_ROAMING;
-
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.work.Logger;
-import androidx.work.impl.constraints.NetworkState;
-import androidx.work.impl.constraints.trackers.NetworkStateTracker;
-import androidx.work.impl.model.WorkSpec;
-
-/**
- * A {@link ConstraintController} for monitoring that the network connection is not roaming.
- */
-
-public class NetworkNotRoamingController extends ConstraintController<NetworkState> {
- private static final String TAG = Logger.tagWithPrefix("NetworkNotRoamingCtrlr");
-
- public NetworkNotRoamingController(@NonNull NetworkStateTracker tracker) {
- super(tracker);
- }
-
- @Override
- boolean hasConstraint(@NonNull WorkSpec workSpec) {
- return workSpec.constraints.getRequiredNetworkType() == NOT_ROAMING;
- }
-
- /**
- * Check for not-roaming constraint on API 24+, when JobInfo#NETWORK_TYPE_NOT_ROAMING was added,
- * to be consistent with JobScheduler functionality.
- */
- @Override
- boolean isConstrained(@NonNull NetworkState state) {
- if (Build.VERSION.SDK_INT < 24) {
- Logger.get().debug(
- TAG,
- "Not-roaming network constraint is not supported before API 24, "
- + "only checking for connected state.");
- return !state.isConnected();
- }
- return !state.isConnected() || !state.isNotRoaming();
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java
deleted file mode 100644
index 24b4573..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2017 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.work.impl.constraints.controllers;
-
-import static androidx.work.NetworkType.TEMPORARILY_UNMETERED;
-import static androidx.work.NetworkType.UNMETERED;
-
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.work.impl.constraints.NetworkState;
-import androidx.work.impl.constraints.trackers.NetworkStateTracker;
-import androidx.work.impl.model.WorkSpec;
-
-/**
- * A {@link ConstraintController} for monitoring that the network connection is unmetered.
- */
-
-public class NetworkUnmeteredController extends ConstraintController<NetworkState> {
- public NetworkUnmeteredController(@NonNull NetworkStateTracker tracker) {
- super(tracker);
- }
-
- @Override
- boolean hasConstraint(@NonNull WorkSpec workSpec) {
- return workSpec.constraints.getRequiredNetworkType() == UNMETERED
- || (Build.VERSION.SDK_INT >= 30
- && workSpec.constraints.getRequiredNetworkType() == TEMPORARILY_UNMETERED);
- }
-
- @Override
- boolean isConstrained(@NonNull NetworkState state) {
- return !state.isConnected() || state.isMetered();
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/StorageNotLowController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/StorageNotLowController.java
deleted file mode 100644
index dc8c18b..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/StorageNotLowController.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2017 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.work.impl.constraints.controllers;
-
-import androidx.annotation.NonNull;
-import androidx.work.impl.constraints.trackers.StorageNotLowTracker;
-import androidx.work.impl.model.WorkSpec;
-
-/**
- * A {@link ConstraintController} for storage not low events.
- */
-
-public class StorageNotLowController extends ConstraintController<Boolean> {
- public StorageNotLowController(@NonNull StorageNotLowTracker tracker) {
- super(tracker);
- }
-
- @Override
- boolean hasConstraint(@NonNull WorkSpec workSpec) {
- return workSpec.constraints.requiresStorageNotLow();
- }
-
- @Override
- boolean isConstrained(@NonNull Boolean isStorageNotLow) {
- return !isStorageNotLow;
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java
index 7d64637..935d3ab 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java
@@ -48,7 +48,7 @@
// Synthetic access
T mCurrentState;
- ConstraintTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
+ protected ConstraintTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
mAppContext = context.getApplicationContext();
mTaskExecutor = taskExecutor;
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTypeConverters.java b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
deleted file mode 100644
index f60ac02..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
+++ /dev/null
@@ -1,390 +0,0 @@
-/*
- * Copyright 2018 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.work.impl.model;
-
-import static androidx.work.BackoffPolicy.EXPONENTIAL;
-import static androidx.work.BackoffPolicy.LINEAR;
-import static androidx.work.WorkInfo.State.BLOCKED;
-import static androidx.work.WorkInfo.State.CANCELLED;
-import static androidx.work.WorkInfo.State.ENQUEUED;
-import static androidx.work.WorkInfo.State.FAILED;
-import static androidx.work.WorkInfo.State.RUNNING;
-import static androidx.work.WorkInfo.State.SUCCEEDED;
-
-import android.net.Uri;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.room.TypeConverter;
-import androidx.work.BackoffPolicy;
-import androidx.work.ContentUriTriggers;
-import androidx.work.NetworkType;
-import androidx.work.OutOfQuotaPolicy;
-import androidx.work.WorkInfo;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-
-/**
- * TypeConverters for WorkManager enums and classes.
- */
-
-public class WorkTypeConverters {
-
- /**
- * Integer identifiers that map to {@link WorkInfo.State}.
- */
- public interface StateIds {
- int ENQUEUED = 0;
- int RUNNING = 1;
- int SUCCEEDED = 2;
- int FAILED = 3;
- int BLOCKED = 4;
- int CANCELLED = 5;
-
- String COMPLETED_STATES = "(" + SUCCEEDED + ", " + FAILED + ", " + CANCELLED + ")";
- }
-
- /**
- * Integer identifiers that map to {@link BackoffPolicy}.
- */
- public interface BackoffPolicyIds {
- int EXPONENTIAL = 0;
- int LINEAR = 1;
- }
-
- /**
- * Integer identifiers that map to {@link NetworkType}.
- */
- public interface NetworkTypeIds {
- int NOT_REQUIRED = 0;
- int CONNECTED = 1;
- int UNMETERED = 2;
- int NOT_ROAMING = 3;
- int METERED = 4;
- int TEMPORARILY_UNMETERED = 5;
- }
-
- /**
- * Integer identifiers that map to {@link OutOfQuotaPolicy}.
- */
- public interface OutOfPolicyIds {
- int RUN_AS_NON_EXPEDITED_WORK_REQUEST = 0;
- int DROP_WORK_REQUEST = 1;
- }
-
- /**
- * TypeConverter for a State to an int.
- *
- * @param state The input State
- * @return The associated int constant
- */
- @TypeConverter
- public static int stateToInt(WorkInfo.State state) {
- switch (state) {
- case ENQUEUED:
- return StateIds.ENQUEUED;
-
- case RUNNING:
- return StateIds.RUNNING;
-
- case SUCCEEDED:
- return StateIds.SUCCEEDED;
-
- case FAILED:
- return StateIds.FAILED;
-
- case BLOCKED:
- return StateIds.BLOCKED;
-
- case CANCELLED:
- return StateIds.CANCELLED;
-
- default:
- throw new IllegalArgumentException(
- "Could not convert " + state + " to int");
- }
- }
-
- /**
- * TypeConverter for an int to a State.
- *
- * @param value The input integer
- * @return The associated State enum value
- */
- @TypeConverter
- public static WorkInfo.State intToState(int value) {
- switch (value) {
- case StateIds.ENQUEUED:
- return ENQUEUED;
-
- case StateIds.RUNNING:
- return RUNNING;
-
- case StateIds.SUCCEEDED:
- return SUCCEEDED;
-
- case StateIds.FAILED:
- return FAILED;
-
- case StateIds.BLOCKED:
- return BLOCKED;
-
- case StateIds.CANCELLED:
- return CANCELLED;
-
- default:
- throw new IllegalArgumentException(
- "Could not convert " + value + " to State");
- }
- }
-
- /**
- * TypeConverter for a BackoffPolicy to an int.
- *
- * @param backoffPolicy The input BackoffPolicy
- * @return The associated int constant
- */
- @TypeConverter
- public static int backoffPolicyToInt(BackoffPolicy backoffPolicy) {
- switch (backoffPolicy) {
- case EXPONENTIAL:
- return BackoffPolicyIds.EXPONENTIAL;
-
- case LINEAR:
- return BackoffPolicyIds.LINEAR;
-
- default:
- throw new IllegalArgumentException(
- "Could not convert " + backoffPolicy + " to int");
- }
- }
-
- /**
- * TypeConverter for an int to a BackoffPolicy.
- *
- * @param value The input integer
- * @return The associated BackoffPolicy enum value
- */
- @TypeConverter
- public static BackoffPolicy intToBackoffPolicy(int value) {
- switch (value) {
- case BackoffPolicyIds.EXPONENTIAL:
- return EXPONENTIAL;
-
- case BackoffPolicyIds.LINEAR:
- return LINEAR;
-
- default:
- throw new IllegalArgumentException(
- "Could not convert " + value + " to BackoffPolicy");
- }
- }
-
- /**
- * TypeConverter for a NetworkType to an int.
- *
- * @param networkType The input NetworkType
- * @return The associated int constant
- */
- @TypeConverter
- public static int networkTypeToInt(NetworkType networkType) {
- switch (networkType) {
- case NOT_REQUIRED:
- return NetworkTypeIds.NOT_REQUIRED;
-
- case CONNECTED:
- return NetworkTypeIds.CONNECTED;
-
- case UNMETERED:
- return NetworkTypeIds.UNMETERED;
-
- case NOT_ROAMING:
- return NetworkTypeIds.NOT_ROAMING;
-
- case METERED:
- return NetworkTypeIds.METERED;
-
- default:
- if (Build.VERSION.SDK_INT >= 30
- && networkType == NetworkType.TEMPORARILY_UNMETERED) {
- return NetworkTypeIds.TEMPORARILY_UNMETERED;
- }
- throw new IllegalArgumentException(
- "Could not convert " + networkType + " to int");
-
- }
- }
-
- /**
- * TypeConverter for an int to a NetworkType.
- *
- * @param value The input integer
- * @return The associated NetworkType enum value
- */
- @TypeConverter
- public static NetworkType intToNetworkType(int value) {
- switch (value) {
- case NetworkTypeIds.NOT_REQUIRED:
- return NetworkType.NOT_REQUIRED;
-
- case NetworkTypeIds.CONNECTED:
- return NetworkType.CONNECTED;
-
- case NetworkTypeIds.UNMETERED:
- return NetworkType.UNMETERED;
-
- case NetworkTypeIds.NOT_ROAMING:
- return NetworkType.NOT_ROAMING;
-
- case NetworkTypeIds.METERED:
- return NetworkType.METERED;
-
- default:
- if (Build.VERSION.SDK_INT >= 30 && value == NetworkTypeIds.TEMPORARILY_UNMETERED) {
- return NetworkType.TEMPORARILY_UNMETERED;
- }
- throw new IllegalArgumentException(
- "Could not convert " + value + " to NetworkType");
- }
- }
-
- /**
- * Converts a {@link OutOfQuotaPolicy} to an int.
- *
- * @param policy The {@link OutOfQuotaPolicy} policy being used
- * @return the corresponding int representation.
- */
- @TypeConverter
- public static int outOfQuotaPolicyToInt(@NonNull OutOfQuotaPolicy policy) {
- switch (policy) {
- case RUN_AS_NON_EXPEDITED_WORK_REQUEST:
- return OutOfPolicyIds.RUN_AS_NON_EXPEDITED_WORK_REQUEST;
- case DROP_WORK_REQUEST:
- return OutOfPolicyIds.DROP_WORK_REQUEST;
- default:
- throw new IllegalArgumentException(
- "Could not convert " + policy + " to int");
- }
- }
-
- /**
- * Converter from an int to a {@link OutOfQuotaPolicy}.
- *
- * @param value The input integer
- * @return An {@link OutOfQuotaPolicy}
- */
- @TypeConverter
- @NonNull
- public static OutOfQuotaPolicy intToOutOfQuotaPolicy(int value) {
- switch (value) {
- case OutOfPolicyIds.RUN_AS_NON_EXPEDITED_WORK_REQUEST:
- return OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST;
- case OutOfPolicyIds.DROP_WORK_REQUEST:
- return OutOfQuotaPolicy.DROP_WORK_REQUEST;
- default:
- throw new IllegalArgumentException(
- "Could not convert " + value + " to OutOfQuotaPolicy");
- }
- }
-
- /**
- * Converts a list of {@link ContentUriTriggers.Trigger}s to byte array representation
- * @param triggers the list of {@link ContentUriTriggers.Trigger}s to convert
- * @return corresponding byte array representation
- */
- @TypeConverter
- @SuppressWarnings("CatchAndPrintStackTrace")
- public static byte[] contentUriTriggersToByteArray(ContentUriTriggers triggers) {
- if (triggers.size() == 0) {
- return null;
- }
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- ObjectOutputStream objectOutputStream = null;
- try {
- objectOutputStream = new ObjectOutputStream(outputStream);
- objectOutputStream.writeInt(triggers.size());
- for (ContentUriTriggers.Trigger trigger : triggers.getTriggers()) {
- objectOutputStream.writeUTF(trigger.getUri().toString());
- objectOutputStream.writeBoolean(trigger.shouldTriggerForDescendants());
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- if (objectOutputStream != null) {
- try {
- objectOutputStream.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- try {
- outputStream.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- return outputStream.toByteArray();
- }
-
- /**
- * Converts a byte array to list of {@link ContentUriTriggers.Trigger}s
- * @param bytes byte array representation to convert
- * @return list of {@link ContentUriTriggers.Trigger}s
- */
- @TypeConverter
- @SuppressWarnings("CatchAndPrintStackTrace")
- public static ContentUriTriggers byteArrayToContentUriTriggers(byte[] bytes) {
- ContentUriTriggers triggers = new ContentUriTriggers();
- if (bytes == null) {
- // bytes will be null if there are no Content Uri Triggers
- return triggers;
- }
- ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
- ObjectInputStream objectInputStream = null;
- try {
- objectInputStream = new ObjectInputStream(inputStream);
- for (int i = objectInputStream.readInt(); i > 0; i--) {
- Uri uri = Uri.parse(objectInputStream.readUTF());
- boolean triggersForDescendants = objectInputStream.readBoolean();
- triggers.add(uri, triggersForDescendants);
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- if (objectInputStream != null) {
- try {
- objectInputStream.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- try {
- inputStream.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- return triggers;
- }
-
- private WorkTypeConverters() {
- }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTypeConverters.kt b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTypeConverters.kt
new file mode 100644
index 0000000..2d4ab79
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTypeConverters.kt
@@ -0,0 +1,282 @@
+/*
+ * Copyright 2018 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.work.impl.model
+
+import android.net.Uri
+import android.os.Build
+import androidx.room.TypeConverter
+import androidx.work.BackoffPolicy
+import androidx.work.ContentUriTriggers
+import androidx.work.NetworkType
+import androidx.work.OutOfQuotaPolicy
+import androidx.work.WorkInfo
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.io.ObjectInputStream
+import java.io.ObjectOutputStream
+import java.lang.IllegalArgumentException
+
+/**
+ * TypeConverters for WorkManager enums and classes.
+ */
+object WorkTypeConverters {
+ /**
+ * Integer identifiers that map to [WorkInfo.State].
+ */
+ object StateIds {
+ const val ENQUEUED = 0
+ const val RUNNING = 1
+ const val SUCCEEDED = 2
+ const val FAILED = 3
+ const val BLOCKED = 4
+ const val CANCELLED = 5
+ const val COMPLETED_STATES = "($SUCCEEDED, $FAILED, $CANCELLED)"
+ }
+
+ /**
+ * Integer identifiers that map to [BackoffPolicy].
+ */
+ private object BackoffPolicyIds {
+ const val EXPONENTIAL = 0
+ const val LINEAR = 1
+ }
+
+ /**
+ * Integer identifiers that map to [NetworkType].
+ */
+ private object NetworkTypeIds {
+ const val NOT_REQUIRED = 0
+ const val CONNECTED = 1
+ const val UNMETERED = 2
+ const val NOT_ROAMING = 3
+ const val METERED = 4
+ const val TEMPORARILY_UNMETERED = 5
+ }
+
+ /**
+ * Integer identifiers that map to [OutOfQuotaPolicy].
+ */
+ private object OutOfPolicyIds {
+ const val RUN_AS_NON_EXPEDITED_WORK_REQUEST = 0
+ const val DROP_WORK_REQUEST = 1
+ }
+
+ /**
+ * TypeConverter for a State to an int.
+ *
+ * @param state The input State
+ * @return The associated int constant
+ */
+ @JvmStatic
+ @TypeConverter
+ fun stateToInt(state: WorkInfo.State): Int {
+ return when (state) {
+ WorkInfo.State.ENQUEUED -> StateIds.ENQUEUED
+ WorkInfo.State.RUNNING -> StateIds.RUNNING
+ WorkInfo.State.SUCCEEDED -> StateIds.SUCCEEDED
+ WorkInfo.State.FAILED -> StateIds.FAILED
+ WorkInfo.State.BLOCKED -> StateIds.BLOCKED
+ WorkInfo.State.CANCELLED -> StateIds.CANCELLED
+ }
+ }
+
+ /**
+ * TypeConverter for an int to a State.
+ *
+ * @param value The input integer
+ * @return The associated State enum value
+ */
+ @JvmStatic
+ @TypeConverter
+ fun intToState(value: Int): WorkInfo.State {
+ return when (value) {
+ StateIds.ENQUEUED -> WorkInfo.State.ENQUEUED
+ StateIds.RUNNING -> WorkInfo.State.RUNNING
+ StateIds.SUCCEEDED -> WorkInfo.State.SUCCEEDED
+ StateIds.FAILED -> WorkInfo.State.FAILED
+ StateIds.BLOCKED -> WorkInfo.State.BLOCKED
+ StateIds.CANCELLED -> WorkInfo.State.CANCELLED
+ else -> throw IllegalArgumentException("Could not convert $value to State")
+ }
+ }
+
+ /**
+ * TypeConverter for a BackoffPolicy to an int.
+ *
+ * @param backoffPolicy The input BackoffPolicy
+ * @return The associated int constant
+ */
+ @JvmStatic
+ @TypeConverter
+ fun backoffPolicyToInt(backoffPolicy: BackoffPolicy): Int {
+ return when (backoffPolicy) {
+ BackoffPolicy.EXPONENTIAL -> BackoffPolicyIds.EXPONENTIAL
+ BackoffPolicy.LINEAR -> BackoffPolicyIds.LINEAR
+ }
+ }
+
+ /**
+ * TypeConverter for an int to a BackoffPolicy.
+ *
+ * @param value The input integer
+ * @return The associated BackoffPolicy enum value
+ */
+ @JvmStatic
+ @TypeConverter
+ fun intToBackoffPolicy(value: Int): BackoffPolicy {
+ return when (value) {
+ BackoffPolicyIds.EXPONENTIAL -> BackoffPolicy.EXPONENTIAL
+ BackoffPolicyIds.LINEAR -> BackoffPolicy.LINEAR
+ else -> throw IllegalArgumentException("Could not convert $value to BackoffPolicy")
+ }
+ }
+
+ /**
+ * TypeConverter for a NetworkType to an int.
+ *
+ * @param networkType The input NetworkType
+ * @return The associated int constant
+ */
+ @JvmStatic
+ @TypeConverter
+ fun networkTypeToInt(networkType: NetworkType): Int {
+ return when (networkType) {
+ NetworkType.NOT_REQUIRED -> NetworkTypeIds.NOT_REQUIRED
+ NetworkType.CONNECTED -> NetworkTypeIds.CONNECTED
+ NetworkType.UNMETERED -> NetworkTypeIds.UNMETERED
+ NetworkType.NOT_ROAMING -> NetworkTypeIds.NOT_ROAMING
+ NetworkType.METERED -> NetworkTypeIds.METERED
+ else -> {
+ if (Build.VERSION.SDK_INT >= 30 && networkType == NetworkType.TEMPORARILY_UNMETERED)
+ NetworkTypeIds.TEMPORARILY_UNMETERED
+ else
+ throw IllegalArgumentException("Could not convert $networkType to int")
+ }
+ }
+ }
+
+ /**
+ * TypeConverter for an int to a NetworkType.
+ *
+ * @param value The input integer
+ * @return The associated NetworkType enum value
+ */
+ @JvmStatic
+ @TypeConverter
+ fun intToNetworkType(value: Int): NetworkType {
+ return when (value) {
+ NetworkTypeIds.NOT_REQUIRED -> NetworkType.NOT_REQUIRED
+ NetworkTypeIds.CONNECTED -> NetworkType.CONNECTED
+ NetworkTypeIds.UNMETERED -> NetworkType.UNMETERED
+ NetworkTypeIds.NOT_ROAMING -> NetworkType.NOT_ROAMING
+ NetworkTypeIds.METERED -> NetworkType.METERED
+ else -> {
+ if (Build.VERSION.SDK_INT >= 30 && value == NetworkTypeIds.TEMPORARILY_UNMETERED) {
+ return NetworkType.TEMPORARILY_UNMETERED
+ } else throw IllegalArgumentException("Could not convert $value to NetworkType")
+ }
+ }
+ }
+
+ /**
+ * Converts a [OutOfQuotaPolicy] to an int.
+ *
+ * @param policy The [OutOfQuotaPolicy] policy being used
+ * @return the corresponding int representation.
+ */
+ @JvmStatic
+ @TypeConverter
+ fun outOfQuotaPolicyToInt(policy: OutOfQuotaPolicy): Int {
+ return when (policy) {
+ OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST ->
+ OutOfPolicyIds.RUN_AS_NON_EXPEDITED_WORK_REQUEST
+ OutOfQuotaPolicy.DROP_WORK_REQUEST -> OutOfPolicyIds.DROP_WORK_REQUEST
+ }
+ }
+
+ /**
+ * Converter from an int to a [OutOfQuotaPolicy].
+ *
+ * @param value The input integer
+ * @return An [OutOfQuotaPolicy]
+ */
+ @JvmStatic
+ @TypeConverter
+ fun intToOutOfQuotaPolicy(value: Int): OutOfQuotaPolicy {
+ return when (value) {
+ OutOfPolicyIds.RUN_AS_NON_EXPEDITED_WORK_REQUEST ->
+ OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST
+ OutOfPolicyIds.DROP_WORK_REQUEST -> OutOfQuotaPolicy.DROP_WORK_REQUEST
+ else -> throw IllegalArgumentException("Could not convert $value to OutOfQuotaPolicy")
+ }
+ }
+
+ /**
+ * Converts a list of [ContentUriTriggers.Trigger]s to byte array representation
+ * @param triggers the list of [ContentUriTriggers.Trigger]s to convert
+ * @return corresponding byte array representation
+ */
+ @JvmStatic
+ @TypeConverter
+ fun contentUriTriggersToByteArray(triggers: ContentUriTriggers): ByteArray? {
+ if (triggers.size() == 0) {
+ return null
+ }
+ val outputStream = ByteArrayOutputStream()
+ outputStream.use {
+ ObjectOutputStream(outputStream).use { objectOutputStream ->
+ objectOutputStream.writeInt(triggers.size())
+ for (trigger in triggers.triggers) {
+ objectOutputStream.writeUTF(trigger.uri.toString())
+ objectOutputStream.writeBoolean(trigger.shouldTriggerForDescendants())
+ }
+ }
+ }
+
+ return outputStream.toByteArray()
+ }
+
+ /**
+ * Converts a byte array to list of [ContentUriTriggers.Trigger]s
+ * @param bytes byte array representation to convert
+ * @return list of [ContentUriTriggers.Trigger]s
+ */
+ @JvmStatic
+ @TypeConverter
+ fun byteArrayToContentUriTriggers(bytes: ByteArray?): ContentUriTriggers {
+ val triggers = ContentUriTriggers()
+ if (bytes == null) {
+ // bytes will be null if there are no Content Uri Triggers
+ return triggers
+ }
+ val inputStream = ByteArrayInputStream(bytes)
+ inputStream.use {
+ try {
+ ObjectInputStream(inputStream).use { objectInputStream ->
+ repeat(objectInputStream.readInt()) {
+ val uri = Uri.parse(objectInputStream.readUTF())
+ val triggersForDescendants = objectInputStream.readBoolean()
+ triggers.add(uri, triggersForDescendants)
+ }
+ }
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+ return triggers
+ }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/package-info.java b/work/work-runtime/src/main/java/androidx/work/impl/package-info.java
new file mode 100644
index 0000000..3f9ab0d
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2018 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.
+ */
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.work.impl;
+
+import androidx.annotation.RestrictTo;
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/IdGenerator.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/IdGenerator.java
index dc71fce..994330f 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/IdGenerator.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/IdGenerator.java
@@ -18,7 +18,7 @@
import static android.content.Context.MODE_PRIVATE;
-import static androidx.work.impl.WorkDatabaseMigrations.INSERT_PREFERENCE;
+import static androidx.work.impl.utils.PreferenceUtils.INSERT_PREFERENCE;
import android.content.Context;
import android.content.SharedPreferences;
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/PreferenceUtils.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/PreferenceUtils.java
index 337f05a..02ea9f9 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/PreferenceUtils.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/PreferenceUtils.java
@@ -18,7 +18,6 @@
import static android.content.Context.MODE_PRIVATE;
-import static androidx.work.impl.WorkDatabaseMigrations.INSERT_PREFERENCE;
import android.content.Context;
import android.content.SharedPreferences;
@@ -39,6 +38,14 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class PreferenceUtils {
+ public static final String INSERT_PREFERENCE =
+ "INSERT OR REPLACE INTO `Preference`"
+ + " (`key`, `long_value`) VALUES"
+ + " (@key, @long_value)";
+
+ public static final String CREATE_PREFERENCE =
+ "CREATE TABLE IF NOT EXISTS `Preference` (`key` TEXT NOT NULL, `long_value` INTEGER, "
+ + "PRIMARY KEY(`key`))";
// For migration
public static final String PREFERENCES_FILE_NAME = "androidx.work.util.preferences";