Merge "Add explicit check for Step.lanesImage type validity" into androidx-main
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index f2553e3..8b81217 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -33,11 +33,17 @@
api("androidx.compose.runtime:runtime-saveable:1.0.1")
api(projectOrArtifact(":activity:activity-ktx"))
api("androidx.compose.ui:ui:1.0.1")
+ // old version of common-java8 conflicts with newer version, because both have
+ // DefaultLifecycleEventObserver.
+ // Outside of androidx this is resolved via constraint added to lifecycle-common,
+ // but it doesn't work in androidx.
+ // See aosp/1804059
+ implementation(project(":lifecycle:lifecycle-common-java8"))
androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
androidTestImplementation projectOrArtifact(":compose:material:material")
androidTestImplementation projectOrArtifact(":compose:test-utils")
- androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
+ androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-runtime-testing")
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testExtJunitKtx)
androidTestImplementation(libs.junit)
diff --git a/activity/activity-compose/samples/build.gradle b/activity/activity-compose/samples/build.gradle
index 74db5f1..0e3628e 100644
--- a/activity/activity-compose/samples/build.gradle
+++ b/activity/activity-compose/samples/build.gradle
@@ -33,6 +33,12 @@
implementation projectOrArtifact(":activity:activity-compose")
implementation projectOrArtifact(":activity:activity-ktx")
implementation "androidx.compose.material:material:1.0.1"
+ // old version of common-java8 conflicts with newer version, because both have
+ // DefaultLifecycleEventObserver.
+ // Outside of androidx this is resolved via constraint added to lifecycle-common,
+ // but it doesn't work in androidx.
+ // See aosp/1804059
+ implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
}
androidx {
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index cc7ef29..8b86605 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -29,16 +29,16 @@
api("androidx.core:core-ktx:1.1.0") {
because "Mirror activity dependency graph for -ktx artifacts"
}
- api("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1") {
+ api(projectOrArtifact(":lifecycle:lifecycle-runtime-ktx")) {
because 'Mirror activity dependency graph for -ktx artifacts'
}
- api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1")
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx"))
api(projectOrArtifact(":savedstate:savedstate-ktx")) {
because 'Mirror activity dependency graph for -ktx artifacts'
}
api(libs.kotlinStdlib)
- androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
+ androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
androidTestImplementation(libs.junit)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.testExtJunit)
@@ -60,12 +60,10 @@
// needed only while https://youtrack.jetbrains.com/issue/KT-47000 isn't resolved which is
// targeted to 1.6
-if (project.hasProperty("androidx.useMaxDepVersions")){
- tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=enable",
- ]
- }
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += [
+ "-Xjvm-default=enable",
+ ]
}
}
\ No newline at end of file
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index a2fa9b7..a5c92a8 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -18,14 +18,14 @@
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.collection:collection:1.0.0")
api(projectOrArtifact(":core:core"))
- api("androidx.lifecycle:lifecycle-runtime:2.3.1")
- api("androidx.lifecycle:lifecycle-viewmodel:2.3.1")
+ api(projectOrArtifact(":lifecycle:lifecycle-runtime"))
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
api(projectOrArtifact(":savedstate:savedstate"))
- api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1")
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
implementation("androidx.tracing:tracing:1.0.0")
api(libs.kotlinStdlib)
- androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
+ androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.espressoCore, excludes.espresso)
androidTestImplementation(libs.leakcanary)
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
index 814c66f..4d722e5 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/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index b5490be..dc7499a 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -36,14 +36,13 @@
import androidx.appsearch.app.SetSchemaResponse;
import androidx.appsearch.app.StorageInfo;
import androidx.appsearch.exceptions.AppSearchException;
-import androidx.appsearch.localstorage.converter.GenericDocumentToProtoConverter;
import androidx.appsearch.localstorage.stats.InitializeStats;
import androidx.appsearch.localstorage.stats.OptimizeStats;
import androidx.appsearch.localstorage.util.PrefixUtil;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
-import androidx.test.filters.FlakyTest;
import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.FlakyTest;
import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.GetOptimizeInfoResultProto;
@@ -53,14 +52,11 @@
import com.google.android.icing.proto.PutResultProto;
import com.google.android.icing.proto.SchemaProto;
import com.google.android.icing.proto.SchemaTypeConfigProto;
-import com.google.android.icing.proto.SearchResultProto;
-import com.google.android.icing.proto.SearchSpecProto;
import com.google.android.icing.proto.StatusProto;
import com.google.android.icing.proto.StorageInfoProto;
import com.google.android.icing.proto.StringIndexingConfig;
import com.google.android.icing.proto.TermMatchType;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.junit.After;
@@ -534,123 +530,6 @@
}
@Test
- public void testRewriteSearchSpec_oneInstance() throws Exception {
- SearchSpecProto.Builder searchSpecProto =
- SearchSpecProto.newBuilder().setQuery("");
-
- // Insert schema
- List<AppSearchSchema> schemas =
- Collections.singletonList(new AppSearchSchema.Builder("type").build());
- mAppSearchImpl.setSchema(
- "package",
- "database",
- schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
- /*forceOverride=*/ false,
- /*version=*/ 0,
- /* setSchemaStatsBuilder= */ null);
-
- // Insert document
- GenericDocument document = new GenericDocument.Builder<>("namespace", "id",
- "type").build();
- mAppSearchImpl.putDocument("package", "database", document, /*logger=*/ null);
-
- // Rewrite SearchSpec
- mAppSearchImpl.rewriteSearchSpecForPrefixesLocked(searchSpecProto,
- Collections.singleton(createPrefix("package", "database")),
- ImmutableSet.of("package$database/type"));
- assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
- "package$database/type");
- assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
- "package$database/namespace");
- }
-
- @Test
- public void testRewriteSearchSpec_twoInstances() throws Exception {
- SearchSpecProto.Builder searchSpecProto =
- SearchSpecProto.newBuilder().setQuery("");
-
- // Insert schema
- List<AppSearchSchema> schemas = ImmutableList.of(
- new AppSearchSchema.Builder("typeA").build(),
- new AppSearchSchema.Builder("typeB").build());
- mAppSearchImpl.setSchema(
- "package",
- "database1",
- schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
- /*forceOverride=*/ false,
- /*version=*/ 0,
- /* setSchemaStatsBuilder= */ null);
- mAppSearchImpl.setSchema(
- "package",
- "database2",
- schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
- /*forceOverride=*/ false,
- /*version=*/ 0,
- /* setSchemaStatsBuilder= */ null);
-
- // Insert documents
- GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id",
- "typeA").build();
- mAppSearchImpl.putDocument("package", "database1", document1, /*logger=*/ null);
-
- GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id",
- "typeB").build();
- mAppSearchImpl.putDocument("package", "database2", document2, /*logger=*/ null);
-
- // Rewrite SearchSpec
- mAppSearchImpl.rewriteSearchSpecForPrefixesLocked(searchSpecProto,
- ImmutableSet.of(createPrefix("package", "database1"),
- createPrefix("package", "database2")), ImmutableSet.of(
- "package$database1/typeA", "package$database1/typeB",
- "package$database2/typeA", "package$database2/typeB"));
- assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
- "package$database1/typeA", "package$database1/typeB", "package$database2/typeA",
- "package$database2/typeB");
- assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
- "package$database1/namespace", "package$database2/namespace");
- }
-
- @Test
- public void testRewriteSearchSpec_ignoresSearchSpecSchemaFilters() throws Exception {
- SearchSpecProto.Builder searchSpecProto =
- SearchSpecProto.newBuilder().setQuery("").addSchemaTypeFilters("type");
-
- // Insert schema
- List<AppSearchSchema> schemas =
- Collections.singletonList(new AppSearchSchema.Builder("type").build());
- mAppSearchImpl.setSchema(
- "package",
- "database",
- schemas,
- /*visibilityStore=*/ null,
- /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
- /*schemasVisibleToPackages=*/ Collections.emptyMap(),
- /*forceOverride=*/ false,
- /*version=*/ 0,
- /* setSchemaStatsBuilder= */ null);
-
- // Insert document
- GenericDocument document = new GenericDocument.Builder<>("namespace", "id",
- "type").build();
- mAppSearchImpl.putDocument("package", "database", document, /*logger=*/ null);
-
- // If 'allowedPrefixedSchemas' is empty, this returns false since there's nothing to
- // search over. Despite the searchSpecProto having schema type filters.
- assertThat(mAppSearchImpl.rewriteSearchSpecForPrefixesLocked(searchSpecProto,
- Collections.singleton(createPrefix("package", "database")),
- /*allowedPrefixedSchemas=*/ Collections.emptySet())).isFalse();
- }
-
- @Test
public void testQueryEmptyDatabase() throws Exception {
SearchSpec searchSpec =
new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
@@ -1623,47 +1502,6 @@
expectedMapping);
}
- @Test
- public void testRewriteSearchResultProto() throws Exception {
- final String prefix =
- "com.package.foo" + PrefixUtil.PACKAGE_DELIMITER + "databaseName"
- + PrefixUtil.DATABASE_DELIMITER;
- final String id = "id";
- final String namespace = prefix + "namespace";
- final String schemaType = prefix + "schema";
-
- // Building the SearchResult received from query.
- DocumentProto documentProto = DocumentProto.newBuilder()
- .setUri(id)
- .setNamespace(namespace)
- .setSchema(schemaType)
- .build();
- SearchResultProto.ResultProto resultProto = SearchResultProto.ResultProto.newBuilder()
- .setDocument(documentProto)
- .build();
- SearchResultProto searchResultProto = SearchResultProto.newBuilder()
- .addResults(resultProto)
- .build();
- SchemaTypeConfigProto schemaTypeConfigProto =
- SchemaTypeConfigProto.newBuilder()
- .setSchemaType(schemaType)
- .build();
- Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(prefix,
- ImmutableMap.of(schemaType, schemaTypeConfigProto));
-
- DocumentProto.Builder strippedDocumentProto = documentProto.toBuilder();
- removePrefixesFromDocument(strippedDocumentProto);
- SearchResultPage searchResultPage =
- AppSearchImpl.rewriteSearchResultProto(searchResultProto, schemaMap);
- for (SearchResult result : searchResultPage.getResults()) {
- assertThat(result.getPackageName()).isEqualTo("com.package.foo");
- assertThat(result.getDatabaseName()).isEqualTo("databaseName");
- assertThat(result.getGenericDocument()).isEqualTo(
- GenericDocumentToProtoConverter.toGenericDocument(
- strippedDocumentProto.build(), prefix, schemaMap.get(prefix)));
- }
- }
-
@FlakyTest(bugId = 204186664)
@Test
public void testReportUsage() throws Exception {
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
new file mode 100644
index 0000000..47462dc
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.appsearch.localstorage.converter;
+
+import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.SearchResult;
+import androidx.appsearch.app.SearchResultPage;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+
+import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.SchemaTypeConfigProto;
+import com.google.android.icing.proto.SearchResultProto;
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Test;
+
+import java.util.Map;
+
+public class SearchResultToProtoConverterTest {
+ @Test
+ public void testToSearchResultProto() throws Exception {
+ final String prefix =
+ "com.package.foo" + PrefixUtil.PACKAGE_DELIMITER + "databaseName"
+ + PrefixUtil.DATABASE_DELIMITER;
+ final String id = "id";
+ final String namespace = prefix + "namespace";
+ final String schemaType = prefix + "schema";
+
+ // Building the SearchResult received from query.
+ DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
+ .setUri(id)
+ .setNamespace(namespace)
+ .setSchema(schemaType);
+ SearchResultProto searchResultProto = SearchResultProto.newBuilder()
+ .addResults(SearchResultProto.ResultProto.newBuilder()
+ .setDocument(documentProtoBuilder))
+ .build();
+ SchemaTypeConfigProto schemaTypeConfigProto =
+ SchemaTypeConfigProto.newBuilder()
+ .setSchemaType(schemaType)
+ .build();
+ Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(prefix,
+ ImmutableMap.of(schemaType, schemaTypeConfigProto));
+
+ removePrefixesFromDocument(documentProtoBuilder);
+ SearchResultPage searchResultPage =
+ SearchResultToProtoConverter.toSearchResultPage(searchResultProto, schemaMap);
+ assertThat(searchResultPage.getResults()).hasSize(1);
+ SearchResult result = searchResultPage.getResults().get(0);
+ assertThat(result.getPackageName()).isEqualTo("com.package.foo");
+ assertThat(result.getDatabaseName()).isEqualTo("databaseName");
+ assertThat(result.getGenericDocument()).isEqualTo(
+ GenericDocumentToProtoConverter.toGenericDocument(
+ documentProtoBuilder.build(), prefix, schemaMap.get(prefix)));
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
new file mode 100644
index 0000000..29aeb96
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
@@ -0,0 +1,532 @@
+/*
+ * 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.appsearch.localstorage.converter;
+
+import static androidx.appsearch.app.SearchSpec.GROUPING_TYPE_PER_PACKAGE;
+import static androidx.appsearch.app.SearchSpec.ORDER_ASCENDING;
+import static androidx.appsearch.app.SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP;
+import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
+
+import com.google.android.icing.proto.ResultSpecProto;
+import com.google.android.icing.proto.SchemaTypeConfigProto;
+import com.google.android.icing.proto.ScoringSpecProto;
+import com.google.android.icing.proto.SearchSpecProto;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class SearchSpecToProtoConverterTest {
+
+ @Test
+ public void testToSearchSpecProto() throws Exception {
+ SearchSpec searchSpec = new SearchSpec.Builder().build();
+ String prefix1 = PrefixUtil.createPrefix("package", "database1");
+ String prefix2 = PrefixUtil.createPrefix("package", "database2");
+
+ SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+ /*namespaceMap=*/ImmutableMap.of(
+ prefix1, ImmutableSet.of(
+ prefix1 + "namespace1",
+ prefix1 + "namespace2"),
+ prefix2, ImmutableSet.of(
+ prefix2 + "namespace1",
+ prefix2 + "namespace2")),
+ /*schemaMap=*/ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ prefix1 + "typeA", configProto,
+ prefix1 + "typeB", configProto),
+ prefix2, ImmutableMap.of(
+ prefix2 + "typeA", configProto,
+ prefix2 + "typeB", configProto)));
+ // Convert SearchSpec to proto.
+ SearchSpecProto searchSpecProto = converter.toSearchSpecProto(
+ /*queryExpression=*/"query");
+
+ assertThat(searchSpecProto.getQuery()).isEqualTo("query");
+ assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
+ "package$database1/typeA", "package$database1/typeB", "package$database2/typeA",
+ "package$database2/typeB");
+ assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
+ "package$database1/namespace1", "package$database1/namespace2",
+ "package$database2/namespace1", "package$database2/namespace2");
+ }
+
+ @Test
+ public void testToScoringSpecProto() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setOrder(ORDER_ASCENDING)
+ .setRankingStrategy(RANKING_STRATEGY_CREATION_TIMESTAMP).build();
+
+ ScoringSpecProto scoringSpecProto = new SearchSpecToProtoConverter(searchSpec,
+ /*prefixes=*/ImmutableSet.of(),
+ /*namespaceMap=*/ImmutableMap.of(),
+ /*schemaMap=*/ImmutableMap.of()).toScoringSpecProto();
+
+ assertThat(scoringSpecProto.getOrderBy().getNumber())
+ .isEqualTo(ScoringSpecProto.Order.Code.ASC_VALUE);
+ assertThat(scoringSpecProto.getRankBy().getNumber())
+ .isEqualTo(ScoringSpecProto.RankingStrategy.Code.CREATION_TIMESTAMP_VALUE);
+ }
+
+ @Test
+ public void testToResultSpecProto() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setResultCountPerPage(123)
+ .setSnippetCount(234)
+ .setSnippetCountPerProperty(345)
+ .setMaxSnippetSize(456)
+ .build();
+
+ SearchSpecToProtoConverter convert = new SearchSpecToProtoConverter(searchSpec,
+ /*prefixes=*/ImmutableSet.of(),
+ /*namespaceMap=*/ImmutableMap.of(),
+ /*schemaMap=*/ImmutableMap.of());
+ ResultSpecProto resultSpecProto = convert.toResultSpecProto(
+ /*namespaceMap=*/ImmutableMap.of());
+
+ assertThat(resultSpecProto.getNumPerPage()).isEqualTo(123);
+ assertThat(resultSpecProto.getSnippetSpec().getNumToSnippet()).isEqualTo(234);
+ assertThat(resultSpecProto.getSnippetSpec().getNumMatchesPerProperty()).isEqualTo(345);
+ assertThat(resultSpecProto.getSnippetSpec().getMaxWindowBytes()).isEqualTo(456);
+ }
+
+ @Test
+ public void testToResultSpecProto_groupByPackage() {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 5)
+ .build();
+
+ String prefix1 = PrefixUtil.createPrefix("package1", "database");
+ String prefix2 = PrefixUtil.createPrefix("package2", "database");
+
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+ /*namespaceMap=*/ImmutableMap.of(),
+ /*schemaMap=*/ImmutableMap.of());
+ ResultSpecProto resultSpecProto = converter.toResultSpecProto(
+ /*namespaceMap=*/ImmutableMap.of(
+ prefix1, ImmutableSet.of(
+ prefix1 + "namespaceA",
+ prefix1 + "namespaceB"),
+ prefix2, ImmutableSet.of(
+ prefix2 + "namespaceA",
+ prefix2 + "namespaceB")));
+
+ assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
+ // First grouping should have same package name.
+ ResultSpecProto.ResultGrouping grouping1 = resultSpecProto.getResultGroupings(0);
+ assertThat(grouping1.getMaxResults()).isEqualTo(5);
+ assertThat(grouping1.getNamespacesCount()).isEqualTo(2);
+ assertThat(
+ PrefixUtil.getPackageName(grouping1.getNamespaces(0)))
+ .isEqualTo(
+ PrefixUtil.getPackageName(grouping1.getNamespaces(1)));
+
+ // Second grouping should have same package name.
+ ResultSpecProto.ResultGrouping grouping2 = resultSpecProto.getResultGroupings(1);
+ assertThat(grouping2.getMaxResults()).isEqualTo(5);
+ assertThat(grouping2.getNamespacesCount()).isEqualTo(2);
+ assertThat(
+ PrefixUtil.getPackageName(grouping2.getNamespaces(0)))
+ .isEqualTo(
+ PrefixUtil.getPackageName(grouping2.getNamespaces(1)));
+ }
+
+ @Test
+ public void testToResultSpecProto_groupByNamespace() throws Exception {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE, 5)
+ .build();
+
+ String prefix1 = PrefixUtil.createPrefix("package1", "database");
+ String prefix2 = PrefixUtil.createPrefix("package2", "database");
+
+ Map<String, Set<String>> namespaceMap = ImmutableMap.of(
+ prefix1, ImmutableSet.of(
+ prefix1 + "namespaceA",
+ prefix1 + "namespaceB"),
+ prefix2, ImmutableSet.of(
+ prefix2 + "namespaceA",
+ prefix2 + "namespaceB"));
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+ namespaceMap,
+ /*schemaMap=*/ImmutableMap.of());
+ ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap);
+
+ assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
+ // First grouping should have same namespace.
+ ResultSpecProto.ResultGrouping grouping1 = resultSpecProto.getResultGroupings(0);
+ assertThat(grouping1.getNamespacesCount()).isEqualTo(2);
+ assertThat(
+ PrefixUtil.removePrefix(grouping1.getNamespaces(0)))
+ .isEqualTo(
+ PrefixUtil.removePrefix(grouping1.getNamespaces(1)));
+
+ // Second grouping should have same namespace.
+ ResultSpecProto.ResultGrouping grouping2 = resultSpecProto.getResultGroupings(1);
+ assertThat(grouping2.getNamespacesCount()).isEqualTo(2);
+ assertThat(
+ PrefixUtil.removePrefix(grouping1.getNamespaces(0)))
+ .isEqualTo(
+ PrefixUtil.removePrefix(grouping1.getNamespaces(1)));
+ }
+
+ @Test
+ public void testToResultSpecProto_groupByNamespaceAndPackage() throws Exception {
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .setResultGrouping(GROUPING_TYPE_PER_PACKAGE
+ | SearchSpec.GROUPING_TYPE_PER_NAMESPACE , 5)
+ .build();
+
+ String prefix1 = PrefixUtil.createPrefix("package1", "database");
+ String prefix2 = PrefixUtil.createPrefix("package2", "database");
+ Map<String, Set<String>> namespaceMap = /*namespaceMap=*/ImmutableMap.of(
+ prefix1, ImmutableSet.of(
+ prefix1 + "namespaceA",
+ prefix1 + "namespaceB"),
+ prefix2, ImmutableSet.of(
+ prefix2 + "namespaceA",
+ prefix2 + "namespaceB"));
+
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+ namespaceMap, /*schemaMap=*/ImmutableMap.of());
+ ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap);
+
+ // All namespace should be separated.
+ assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(4);
+ assertThat(resultSpecProto.getResultGroupings(0).getNamespacesCount()).isEqualTo(1);
+ assertThat(resultSpecProto.getResultGroupings(1).getNamespacesCount()).isEqualTo(1);
+ assertThat(resultSpecProto.getResultGroupings(2).getNamespacesCount()).isEqualTo(1);
+ assertThat(resultSpecProto.getResultGroupings(3).getNamespacesCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testGetTargetNamespaceFilters_emptySearchingFilter() {
+ SearchSpec searchSpec = new SearchSpec.Builder().build();
+ String prefix1 = PrefixUtil.createPrefix("package", "database1");
+ String prefix2 = PrefixUtil.createPrefix("package", "database2");
+ // search both prefixes
+ Map<String, Set<String>> namespaceMap = ImmutableMap.of(
+ prefix1, ImmutableSet.of("package$database1/namespace1",
+ "package$database1/namespace2"),
+ prefix2, ImmutableSet.of("package$database2/namespace3",
+ "package$database2/namespace4"));
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+ namespaceMap, /*schemaMap=*/ImmutableMap.of());
+
+ SearchSpecProto searchSpecProto =
+ converter.toSearchSpecProto(/*queryExpression=*/"");
+
+ assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
+ "package$database1/namespace1", "package$database1/namespace2",
+ "package$database2/namespace3", "package$database2/namespace4");
+ }
+
+ @Test
+ public void testGetTargetNamespaceFilters_searchPartialPrefix() {
+ SearchSpec searchSpec = new SearchSpec.Builder().build();
+ String prefix1 = PrefixUtil.createPrefix("package", "database1");
+ String prefix2 = PrefixUtil.createPrefix("package", "database2");
+
+ // Only search for prefix1
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1),
+ /*namespaceMap=*/ImmutableMap.of(
+ prefix1, ImmutableSet.of("package$database1/namespace1",
+ "package$database1/namespace2"),
+ prefix2, ImmutableSet.of("package$database2/namespace3",
+ "package$database2/namespace4")),
+ /*schemaMap=*/ImmutableMap.of());
+
+ SearchSpecProto searchSpecProto =
+ converter.toSearchSpecProto(/*queryExpression=*/"");
+ // Only search prefix1 will return namespace 1 and 2.
+ assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
+ "package$database1/namespace1", "package$database1/namespace2");
+ }
+
+ @Test
+ public void testGetTargetNamespaceFilters_intersectionWithSearchingFilter() {
+ // Put some searching namespaces.
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .addFilterNamespaces("namespace1", "nonExist").build();
+ String prefix1 = PrefixUtil.createPrefix("package", "database1");
+
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1),
+ /*namespaceMap=*/ImmutableMap.of(
+ prefix1, ImmutableSet.of("package$database1/namespace1",
+ "package$database1/namespace2")),
+ /*schemaMap=*/ImmutableMap.of());
+ SearchSpecProto searchSpecProto =
+ converter.toSearchSpecProto(/*queryExpression=*/"");
+ // If the searching namespace filter is not empty, the target namespace filter will be the
+ // intersection of the searching namespace filters that users want to search over and
+ // those candidates which are stored in AppSearch.
+ assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
+ "package$database1/namespace1");
+ }
+
+ @Test
+ public void testGetTargetNamespaceFilters_intersectionWithNonExistFilter() {
+ // Search in non-exist namespaces
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .addFilterNamespaces("nonExist").build();
+ String prefix1 = PrefixUtil.createPrefix("package", "database1");
+
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1),
+ /*namespaceMap=*/ImmutableMap.of(
+ prefix1, ImmutableSet.of("package$database1/namespace1",
+ "package$database1/namespace2")),
+ /*schemaMap=*/ImmutableMap.of());
+ SearchSpecProto searchSpecProto =
+ converter.toSearchSpecProto(/*queryExpression=*/"");
+ // If the searching namespace filter is not empty, the target namespace filter will be the
+ // intersection of the searching namespace filters that users want to search over and
+ // those candidates which are stored in AppSearch.
+ assertThat(searchSpecProto.getNamespaceFiltersList()).isEmpty();
+ }
+
+ @Test
+ public void testGetTargetSchemaFilters_emptySearchingFilter() {
+ SearchSpec searchSpec = new SearchSpec.Builder().build();
+ String prefix1 = createPrefix("package", "database1");
+ String prefix2 = createPrefix("package", "database2");
+ SchemaTypeConfigProto schemaTypeConfigProto =
+ SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+ /*namespaceMap=*/ImmutableMap.of(
+ prefix1, ImmutableSet.of("package$database1/namespace1")),
+ /*schemaMap=*/ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ "package$database1/typeA", schemaTypeConfigProto,
+ "package$database1/typeB", schemaTypeConfigProto),
+ prefix2, ImmutableMap.of(
+ "package$database2/typeC", schemaTypeConfigProto,
+ "package$database2/typeD", schemaTypeConfigProto)));
+ SearchSpecProto searchSpecProto =
+ converter.toSearchSpecProto(/*queryExpression=*/"");
+ // Empty searching filter will get all types for target filter
+ assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
+ "package$database1/typeA", "package$database1/typeB",
+ "package$database2/typeC", "package$database2/typeD");
+ }
+
+ @Test
+ public void testGetTargetSchemaFilters_searchPartialFilter() {
+ SearchSpec searchSpec = new SearchSpec.Builder().build();
+ String prefix1 = createPrefix("package", "database1");
+ String prefix2 = createPrefix("package", "database2");
+ SchemaTypeConfigProto schemaTypeConfigProto =
+ SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
+ // only search in prefix1
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1),
+ /*namespaceMap=*/ImmutableMap.of(
+ prefix1, ImmutableSet.of("package$database1/namespace1")),
+ /*schemaMap=*/ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ "package$database1/typeA", schemaTypeConfigProto,
+ "package$database1/typeB", schemaTypeConfigProto),
+ prefix2, ImmutableMap.of(
+ "package$database2/typeC", schemaTypeConfigProto,
+ "package$database2/typeD", schemaTypeConfigProto)));
+ SearchSpecProto searchSpecProto =
+ converter.toSearchSpecProto(/*queryExpression=*/"");
+ // Only search prefix1 will return typeA and B.
+ assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
+ "package$database1/typeA", "package$database1/typeB");
+ }
+
+ @Test
+ public void testGetTargetSchemaFilters_intersectionWithSearchingFilter() {
+ // Put some searching schemas.
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .addFilterSchemas("typeA", "nonExist").build();
+ String prefix1 = createPrefix("package", "database1");
+ SchemaTypeConfigProto schemaTypeConfigProto =
+ SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1),
+ /*namespaceMap=*/ImmutableMap.of(
+ prefix1, ImmutableSet.of("package$database1/namespace1")),
+ /*schemaMap=*/ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ "package$database1/typeA", schemaTypeConfigProto,
+ "package$database1/typeB", schemaTypeConfigProto)));
+ SearchSpecProto searchSpecProto =
+ converter.toSearchSpecProto(/*queryExpression=*/"");
+ // If the searching schema filter is not empty, the target schema filter will be the
+ // intersection of the schema filters that users want to search over and those candidates
+ // which are stored in AppSearch.
+ assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
+ "package$database1/typeA");
+ }
+
+ @Test
+ public void testGetTargetSchemaFilters_intersectionWithNonExistFilter() {
+ // Put non-exist searching schema.
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .addFilterSchemas("nonExist").build();
+ String prefix1 = createPrefix("package", "database1");
+ SchemaTypeConfigProto schemaTypeConfigProto =
+ SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix1),
+ /*namespaceMap=*/ImmutableMap.of(
+ prefix1, ImmutableSet.of("package$database1/namespace1")),
+ /*schemaMap=*/ImmutableMap.of(
+ prefix1, ImmutableMap.of(
+ "package$database1/typeA", schemaTypeConfigProto,
+ "package$database1/typeB", schemaTypeConfigProto)));
+ SearchSpecProto searchSpecProto =
+ converter.toSearchSpecProto(/*queryExpression=*/"");
+
+ // If there is no intersection of the schema filters that user want to search over and
+ // those filters which are stored in AppSearch, return empty.
+ assertThat(searchSpecProto.getSchemaTypeFiltersList()).isEmpty();
+ }
+
+ @Test
+ public void testRemoveInaccessibleSchemaFilter() {
+ String prefix = PrefixUtil.createPrefix("package", "database");
+
+ SearchSpec searchSpec = new SearchSpec.Builder().build();
+
+ SchemaTypeConfigProto schemaTypeConfigProto =
+ SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
+ SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+ /*prefixes=*/ImmutableSet.of(prefix),
+ /*namespaceMap=*/ImmutableMap.of(
+ prefix, ImmutableSet.of("package$database/namespace1")),
+ /*schemaMap=*/ImmutableMap.of(
+ prefix, ImmutableMap.of(
+ "package$database/schema1", schemaTypeConfigProto,
+ "package$database/schema2", schemaTypeConfigProto,
+ "package$database/schema3", schemaTypeConfigProto)));
+
+ converter.removeInaccessibleSchemaFilter(
+ /*callerPackageName=*/"otherPackageName",
+ new VisibilityStore() {
+ @Override
+ public void setVisibility(@NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull Set<String> schemasNotDisplayedBySystem,
+ @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages)
+ throws AppSearchException {
+
+ }
+
+ @Override
+ public boolean isSchemaSearchableByCaller(@NonNull String packageName,
+ @NonNull String databaseName, @NonNull String prefixedSchema,
+ int callerUid, boolean callerHasSystemAccess) {
+ // filter out schema 2 which is not searchable for user.
+ return !prefixedSchema.equals(prefix + "schema2");
+ }
+ },
+ /*callerUid=*/-1,
+ /*callerHasSystemAccess=*/true);
+
+ SearchSpecProto searchSpecProto =
+ converter.toSearchSpecProto(/*queryExpression=*/"");
+ // schema 2 is filtered out since it is not searchable for user.
+ assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
+ prefix + "schema1", prefix + "schema3");
+ }
+
+ @Test
+ public void testIsNothingToSearch() {
+ String prefix = PrefixUtil.createPrefix("package", "database");
+ SearchSpec searchSpec = new SearchSpec.Builder()
+ .addFilterSchemas("schema").addFilterNamespaces("namespace").build();
+
+ // build maps
+ SchemaTypeConfigProto schemaTypeConfigProto =
+ SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
+ Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+ prefix, ImmutableMap.of(
+ "package$database/schema", schemaTypeConfigProto)
+ );
+ Map<String, Set<String>> namespaceMap = ImmutableMap.of(
+ prefix, ImmutableSet.of("package$database/namespace")
+ );
+
+ SearchSpecToProtoConverter emptySchemaConverter =
+ new SearchSpecToProtoConverter(searchSpec, /*prefixes=*/ImmutableSet.of(prefix),
+ /*namespaceMap=*/namespaceMap,
+ /*schemaMap=*/ImmutableMap.of());
+ assertThat(emptySchemaConverter.isNothingToSearch()).isTrue();
+
+ SearchSpecToProtoConverter emptyNamespaceConverter =
+ new SearchSpecToProtoConverter(searchSpec, /*prefixes=*/ImmutableSet.of(prefix),
+ /*namespaceMap=*/ImmutableMap.of(),
+ schemaMap);
+ assertThat(emptyNamespaceConverter.isNothingToSearch()).isTrue();
+
+ SearchSpecToProtoConverter nonEmptyConverter =
+ new SearchSpecToProtoConverter(searchSpec, /*prefixes=*/ImmutableSet.of(prefix),
+ namespaceMap, schemaMap);
+ assertThat(nonEmptyConverter.isNothingToSearch()).isFalse();
+
+ // remove all target schema filter, and the query becomes nothing to search.
+ nonEmptyConverter.removeInaccessibleSchemaFilter(
+ /*callerPackageName=*/"otherPackageName",
+ new VisibilityStore() {
+ @Override
+ public void setVisibility(@NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull Set<String> schemasNotDisplayedBySystem,
+ @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages)
+ throws AppSearchException {
+
+ }
+
+ @Override
+ public boolean isSchemaSearchableByCaller(@NonNull String packageName,
+ @NonNull String databaseName, @NonNull String prefixedSchema,
+ int callerUid, boolean callerHasSystemAccess) {
+ // filter out all schema.
+ return false;
+ }
+ },
+ /*callerUid=*/-1,
+ /*callerHasSystemAccess=*/true);
+ assertThat(nonEmptyConverter.isNothingToSearch()).isTrue();
+ }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
index 1f33ff0..fd150c0 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
@@ -35,21 +35,21 @@
import java.util.Map;
public class SnippetTest {
- private static final String SCHEMA_TYPE = "schema1";
private static final String PACKAGE_NAME = "packageName";
private static final String DATABASE_NAME = "databaseName";
private static final String PREFIX = PrefixUtil.createPrefix(PACKAGE_NAME, DATABASE_NAME);
+ private static final String PREFIXED_SCHEMA_TYPE = PREFIX + "schema1";
+ private static final String PREFIXED_NAMESPACE = PREFIX + "";
private static final SchemaTypeConfigProto SCHEMA_TYPE_CONFIG_PROTO =
SchemaTypeConfigProto.newBuilder()
- .setSchemaType(PREFIX + SCHEMA_TYPE)
+ .setSchemaType(PREFIXED_SCHEMA_TYPE)
.build();
private static final Map<String, Map<String, SchemaTypeConfigProto>> SCHEMA_MAP =
Collections.singletonMap(PREFIX,
- Collections.singletonMap(PREFIX + SCHEMA_TYPE,
- SCHEMA_TYPE_CONFIG_PROTO));
+ Collections.singletonMap(PREFIXED_SCHEMA_TYPE, SCHEMA_TYPE_CONFIG_PROTO));
@Test
- public void testSingleStringSnippet() {
+ public void testSingleStringSnippet() throws Exception {
final String propertyKeyString = "content";
final String propertyValueString = "A commonly used fake word is foo.\n"
+ " Another nonsense word that’s used a lot\n"
@@ -61,7 +61,8 @@
// Building the SearchResult received from query.
DocumentProto documentProto = DocumentProto.newBuilder()
.setUri(id)
- .setSchema(SCHEMA_TYPE)
+ .setNamespace(PREFIXED_NAMESPACE)
+ .setSchema(PREFIXED_SCHEMA_TYPE)
.addProperties(PropertyProto.newBuilder()
.setName(propertyKeyString)
.addStringValues(propertyValueString))
@@ -90,8 +91,6 @@
// Making ResultReader and getting Snippet values.
SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
searchResultProto,
- Collections.singletonList(PACKAGE_NAME),
- Collections.singletonList(DATABASE_NAME),
SCHEMA_MAP);
assertThat(searchResultPage.getResults()).hasSize(1);
SearchResult.MatchInfo match = searchResultPage.getResults().get(0).getMatchInfos().get(0);
@@ -110,7 +109,7 @@
}
@Test
- public void testNoSnippets() {
+ public void testNoSnippets() throws Exception {
final String propertyKeyString = "content";
final String propertyValueString = "A commonly used fake word is foo.\n"
+ " Another nonsense word that’s used a lot\n"
@@ -120,7 +119,8 @@
// Building the SearchResult received from query.
DocumentProto documentProto = DocumentProto.newBuilder()
.setUri(id)
- .setSchema(SCHEMA_TYPE)
+ .setNamespace(PREFIXED_NAMESPACE)
+ .setSchema(PREFIXED_SCHEMA_TYPE)
.addProperties(PropertyProto.newBuilder()
.setName(propertyKeyString)
.addStringValues(propertyValueString))
@@ -131,19 +131,18 @@
SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
searchResultProto,
- Collections.singletonList(PACKAGE_NAME),
- Collections.singletonList(DATABASE_NAME),
SCHEMA_MAP);
assertThat(searchResultPage.getResults()).hasSize(1);
assertThat(searchResultPage.getResults().get(0).getMatchInfos()).isEmpty();
}
@Test
- public void testMultipleStringSnippet() {
+ public void testMultipleStringSnippet() throws Exception {
// Building the SearchResult received from query.
DocumentProto documentProto = DocumentProto.newBuilder()
.setUri("uri1")
- .setSchema(SCHEMA_TYPE)
+ .setNamespace(PREFIXED_NAMESPACE)
+ .setSchema(PREFIXED_SCHEMA_TYPE)
.addProperties(PropertyProto.newBuilder()
.setName("senderName")
.addStringValues("Test Name Jr."))
@@ -188,8 +187,6 @@
// Making ResultReader and getting Snippet values.
SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
searchResultProto,
- Collections.singletonList(PACKAGE_NAME),
- Collections.singletonList(DATABASE_NAME),
SCHEMA_MAP);
assertThat(searchResultPage.getResults()).hasSize(1);
SearchResult.MatchInfo match1 = searchResultPage.getResults().get(0).getMatchInfos().get(0);
@@ -220,14 +217,17 @@
}
@Test
- public void testNestedDocumentSnippet() {
+ public void testNestedDocumentSnippet() throws Exception {
// Building the SearchResult received from query.
DocumentProto documentProto = DocumentProto.newBuilder()
.setUri("id1")
- .setSchema(SCHEMA_TYPE)
+ .setNamespace(PREFIXED_NAMESPACE)
+ .setSchema(PREFIXED_SCHEMA_TYPE)
.addProperties(PropertyProto.newBuilder()
.setName("sender")
.addDocumentValues(DocumentProto.newBuilder()
+ .setNamespace(PREFIXED_NAMESPACE)
+ .setSchema(PREFIXED_SCHEMA_TYPE)
.addProperties(PropertyProto.newBuilder()
.setName("name")
.addStringValues("Test Name Jr."))
@@ -273,8 +273,6 @@
// Making ResultReader and getting Snippet values.
SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
searchResultProto,
- Collections.singletonList(PACKAGE_NAME),
- Collections.singletonList(DATABASE_NAME),
SCHEMA_MAP);
assertThat(searchResultPage.getResults()).hasSize(1);
SearchResult.MatchInfo match1 = searchResultPage.getResults().get(0).getMatchInfos().get(0);
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 0b13263..d6978c4 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -21,7 +21,6 @@
import static androidx.appsearch.localstorage.util.PrefixUtil.getDatabaseName;
import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName;
import static androidx.appsearch.localstorage.util.PrefixUtil.getPrefix;
-import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefix;
import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
import android.os.Bundle;
@@ -106,7 +105,6 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -783,22 +781,22 @@
try {
throwIfClosedLocked();
String prefix = createPrefix(packageName, databaseName);
- List<TypePropertyMask> nonPrefixedPropertyMasks =
- TypePropertyPathToProtoConverter.toTypePropertyMaskList(typePropertyPaths);
+ List<TypePropertyMask.Builder> nonPrefixedPropertyMaskBuilders =
+ TypePropertyPathToProtoConverter
+ .toTypePropertyMaskBuilderList(typePropertyPaths);
List<TypePropertyMask> prefixedPropertyMasks =
- new ArrayList<>(nonPrefixedPropertyMasks.size());
- for (int i = 0; i < nonPrefixedPropertyMasks.size(); ++i) {
- TypePropertyMask typePropertyMask = nonPrefixedPropertyMasks.get(i);
- String nonPrefixedType = typePropertyMask.getSchemaType();
+ new ArrayList<>(nonPrefixedPropertyMaskBuilders.size());
+ for (int i = 0; i < nonPrefixedPropertyMaskBuilders.size(); ++i) {
+ String nonPrefixedType = nonPrefixedPropertyMaskBuilders.get(i).getSchemaType();
String prefixedType = nonPrefixedType.equals(
GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD)
? nonPrefixedType : prefix + nonPrefixedType;
prefixedPropertyMasks.add(
- typePropertyMask.toBuilder().setSchemaType(prefixedType).build());
+ nonPrefixedPropertyMaskBuilders.get(i).setSchemaType(prefixedType).build());
}
GetResultSpecProto getResultSpec =
- GetResultSpecProto.newBuilder().addAllTypePropertyMasks(prefixedPropertyMasks
- ).build();
+ GetResultSpecProto.newBuilder().addAllTypePropertyMasks(prefixedPropertyMasks)
+ .build();
String finalNamespace = createPrefix(packageName, databaseName) + namespace;
if (mLogUtil.isPiiTraceEnabled()) {
@@ -867,13 +865,19 @@
}
String prefix = createPrefix(packageName, databaseName);
- Set<String> allowedPrefixedSchemas = getAllowedPrefixSchemasLocked(prefix, searchSpec);
+ SearchSpecToProtoConverter searchSpecToProtoConverter =
+ new SearchSpecToProtoConverter(searchSpec, Collections.singleton(prefix),
+ mNamespaceMapLocked, mSchemaMapLocked);
+ if (searchSpecToProtoConverter.isNothingToSearch()) {
+ // there is nothing to search over given their search filters, so we can return an
+ // empty SearchResult and skip sending request to Icing.
+ return new SearchResultPage(Bundle.EMPTY);
+ }
SearchResultPage searchResultPage =
- doQueryLocked(Collections.singleton(createPrefix(packageName, databaseName)),
- allowedPrefixedSchemas,
+ doQueryLocked(
queryExpression,
- searchSpec,
+ searchSpecToProtoConverter,
sStatsBuilder);
addNextPageToken(packageName, searchResultPage.getNextPageToken());
return searchResultPage;
@@ -946,56 +950,22 @@
}
}
}
-
- // Convert schema filters to prefixed schema filters
- ArraySet<String> prefixedSchemaFilters = new ArraySet<>();
- for (String prefix : prefixFilters) {
- List<String> schemaFilters = searchSpec.getFilterSchemas();
- if (schemaFilters.isEmpty()) {
- // Client didn't specify certain schemas to search over, check all schemas
- prefixedSchemaFilters.addAll(mSchemaMapLocked.get(prefix).keySet());
- } else {
- // Client specified some schemas to search over, check each one
- for (int i = 0; i < schemaFilters.size(); i++) {
- prefixedSchemaFilters.add(prefix + schemaFilters.get(i));
- }
- }
+ SearchSpecToProtoConverter searchSpecToProtoConverter =
+ new SearchSpecToProtoConverter(searchSpec, prefixFilters, mNamespaceMapLocked,
+ mSchemaMapLocked);
+ // Remove those inaccessible schemas.
+ searchSpecToProtoConverter.removeInaccessibleSchemaFilter(callerPackageName,
+ visibilityStore, callerUid, callerHasSystemAccess);
+ if (searchSpecToProtoConverter.isNothingToSearch()) {
+ // there is nothing to search over given their search filters, so we can return an
+ // empty SearchResult and skip sending request to Icing.
+ return new SearchResultPage(Bundle.EMPTY);
}
-
- // Remove the schemas the client is not allowed to search over
- Iterator<String> prefixedSchemaIt = prefixedSchemaFilters.iterator();
- while (prefixedSchemaIt.hasNext()) {
- String prefixedSchema = prefixedSchemaIt.next();
- String packageName = getPackageName(prefixedSchema);
-
- boolean allow;
- if (packageName.equals(callerPackageName)) {
- // Callers can always retrieve their own data
- allow = true;
- } else if (visibilityStore == null) {
- // If there's no visibility store, there's no extra access
- allow = false;
- } else {
- String databaseName = getDatabaseName(prefixedSchema);
- allow = visibilityStore.isSchemaSearchableByCaller(
- packageName,
- databaseName,
- prefixedSchema,
- callerUid,
- callerHasSystemAccess);
- }
-
- if (!allow) {
- prefixedSchemaIt.remove();
- }
- }
-
- SearchResultPage searchResultPage = doQueryLocked(
- prefixFilters,
- prefixedSchemaFilters,
- queryExpression,
- searchSpec,
- sStatsBuilder);
+ SearchResultPage searchResultPage =
+ doQueryLocked(
+ queryExpression,
+ searchSpecToProtoConverter,
+ sStatsBuilder);
addNextPageToken(callerPackageName, searchResultPage.getNextPageToken());
return searchResultPage;
} finally {
@@ -1009,6 +979,65 @@
}
}
+ @GuardedBy("mReadWriteLock")
+ private SearchResultPage doQueryLocked(
+ @NonNull String queryExpression,
+ @NonNull SearchSpecToProtoConverter searchSpecToProtoConverter,
+ @Nullable SearchStats.Builder sStatsBuilder)
+ throws AppSearchException {
+ // Rewrite the given SearchSpec into SearchSpecProto, ResultSpecProto and ScoringSpecProto.
+ // All processes are counted in rewriteSearchSpecLatencyMillis
+ long rewriteSearchSpecLatencyStartMillis = SystemClock.elapsedRealtime();
+ SearchSpecProto finalSearchSpec =
+ searchSpecToProtoConverter.toSearchSpecProto(queryExpression);
+ ResultSpecProto finalResultSpec = searchSpecToProtoConverter.toResultSpecProto(
+ mNamespaceMapLocked);
+ ScoringSpecProto scoringSpec = searchSpecToProtoConverter.toScoringSpecProto();
+ if (sStatsBuilder != null) {
+ sStatsBuilder.setRewriteSearchSpecLatencyMillis((int)
+ (SystemClock.elapsedRealtime() - rewriteSearchSpecLatencyStartMillis));
+ }
+
+ // Send request to Icing.
+ SearchResultProto searchResultProto = searchInIcingLocked(
+ finalSearchSpec, finalResultSpec, scoringSpec, sStatsBuilder);
+
+ long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime();
+ // Rewrite search result before we return.
+ SearchResultPage searchResultPage = SearchResultToProtoConverter
+ .toSearchResultPage(searchResultProto, mSchemaMapLocked);
+ if (sStatsBuilder != null) {
+ sStatsBuilder.setRewriteSearchResultLatencyMillis(
+ (int) (SystemClock.elapsedRealtime()
+ - rewriteSearchResultLatencyStartMillis));
+ }
+ return searchResultPage;
+ }
+
+ @GuardedBy("mReadWriteLock")
+ private SearchResultProto searchInIcingLocked(
+ @NonNull SearchSpecProto searchSpec,
+ @NonNull ResultSpecProto resultSpec,
+ @NonNull ScoringSpecProto scoringSpec,
+ @Nullable SearchStats.Builder sStatsBuilder) throws AppSearchException {
+ if (mLogUtil.isPiiTraceEnabled()) {
+ mLogUtil.piiTrace(
+ "search, request",
+ searchSpec.getQuery(),
+ searchSpec + ", " + scoringSpec + ", " + resultSpec);
+ }
+ SearchResultProto searchResultProto = mIcingSearchEngineLocked.search(
+ searchSpec, scoringSpec, resultSpec);
+ mLogUtil.piiTrace(
+ "search, response", searchResultProto.getResultsCount(), searchResultProto);
+ if (sStatsBuilder != null) {
+ sStatsBuilder.setStatusCode(statusProtoToResultCode(searchResultProto.getStatus()));
+ AppSearchLoggerHelper.copyNativeStats(searchResultProto.getQueryStats(), sStatsBuilder);
+ }
+ checkSuccess(searchResultProto.getStatus());
+ return searchResultProto;
+ }
+
/**
* Returns a mapping of package names to all the databases owned by that package.
*
@@ -1038,89 +1067,6 @@
}
}
- @GuardedBy("mReadWriteLock")
- private SearchResultPage doQueryLocked(
- @NonNull Set<String> prefixes,
- @NonNull Set<String> allowedPrefixedSchemas,
- @NonNull String queryExpression,
- @NonNull SearchSpec searchSpec,
- @Nullable SearchStats.Builder sStatsBuilder)
- throws AppSearchException {
- long rewriteSearchSpecLatencyStartMillis = SystemClock.elapsedRealtime();
-
- SearchSpecProto.Builder searchSpecBuilder =
- SearchSpecToProtoConverter.toSearchSpecProto(searchSpec).toBuilder().setQuery(
- queryExpression);
- // rewriteSearchSpecForPrefixesLocked will return false if there is nothing to search
- // over given their search filters, so we can return an empty SearchResult and skip
- // sending request to Icing.
- if (!rewriteSearchSpecForPrefixesLocked(searchSpecBuilder, prefixes,
- allowedPrefixedSchemas)) {
- if (sStatsBuilder != null) {
- sStatsBuilder.setRewriteSearchSpecLatencyMillis(
- (int) (SystemClock.elapsedRealtime()
- - rewriteSearchSpecLatencyStartMillis));
- }
- return new SearchResultPage(Bundle.EMPTY);
- }
-
- // rewriteSearchSpec, rewriteResultSpec and convertScoringSpec are all counted in
- // rewriteSearchSpecLatencyMillis
- ResultSpecProto.Builder resultSpecBuilder =
- SearchSpecToProtoConverter.toResultSpecProto(searchSpec).toBuilder();
-
- int groupingType = searchSpec.getResultGroupingTypeFlags();
- if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0
- && (groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) {
- addPerPackagePerNamespaceResultGroupingsLocked(resultSpecBuilder, prefixes,
- searchSpec.getResultGroupingLimit());
- } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0) {
- addPerPackageResultGroupingsLocked(resultSpecBuilder, prefixes,
- searchSpec.getResultGroupingLimit());
- } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) {
- addPerNamespaceResultGroupingsLocked(resultSpecBuilder, prefixes,
- searchSpec.getResultGroupingLimit());
- }
-
- rewriteResultSpecForPrefixesLocked(resultSpecBuilder, prefixes, allowedPrefixedSchemas);
- ScoringSpecProto scoringSpec = SearchSpecToProtoConverter.toScoringSpecProto(searchSpec);
- SearchSpecProto finalSearchSpec = searchSpecBuilder.build();
- ResultSpecProto finalResultSpec = resultSpecBuilder.build();
-
- long rewriteSearchSpecLatencyEndMillis = SystemClock.elapsedRealtime();
-
- if (mLogUtil.isPiiTraceEnabled()) {
- mLogUtil.piiTrace(
- "search, request",
- finalSearchSpec.getQuery(),
- finalSearchSpec + ", " + scoringSpec + ", " + finalResultSpec);
- }
- SearchResultProto searchResultProto = mIcingSearchEngineLocked.search(
- finalSearchSpec, scoringSpec, finalResultSpec);
- mLogUtil.piiTrace(
- "search, response", searchResultProto.getResultsCount(), searchResultProto);
-
- if (sStatsBuilder != null) {
- sStatsBuilder
- .setStatusCode(statusProtoToResultCode(searchResultProto.getStatus()))
- .setRewriteSearchSpecLatencyMillis((int) (rewriteSearchSpecLatencyEndMillis
- - rewriteSearchSpecLatencyStartMillis));
- AppSearchLoggerHelper.copyNativeStats(searchResultProto.getQueryStats(), sStatsBuilder);
- }
-
- checkSuccess(searchResultProto.getStatus());
-
- long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime();
- SearchResultPage resultPage = rewriteSearchResultProto(searchResultProto, mSchemaMapLocked);
- if (sStatsBuilder != null) {
- sStatsBuilder.setRewriteSearchResultLatencyMillis(
- (int) (SystemClock.elapsedRealtime()
- - rewriteSearchResultLatencyStartMillis));
- }
-
- return resultPage;
- }
-
/**
* Fetches the next page of results of a previously executed query. Results can be empty if
* next-page token is invalid or all pages have been returned.
@@ -1169,14 +1115,15 @@
}
}
long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime();
- SearchResultPage resultPage = rewriteSearchResultProto(searchResultProto,
- mSchemaMapLocked);
+ // Rewrite search result before we return.
+ SearchResultPage searchResultPage = SearchResultToProtoConverter
+ .toSearchResultPage(searchResultProto, mSchemaMapLocked);
if (statsBuilder != null) {
statsBuilder.setRewriteSearchResultLatencyMillis(
(int) (SystemClock.elapsedRealtime()
- rewriteSearchResultLatencyStartMillis));
}
- return resultPage;
+ return searchResultPage;
} finally {
mReadWriteLock.readLock().unlock();
if (statsBuilder != null) {
@@ -1350,27 +1297,32 @@
return;
}
- SearchSpecProto searchSpecProto =
- SearchSpecToProtoConverter.toSearchSpecProto(searchSpec);
- SearchSpecProto.Builder searchSpecBuilder = searchSpecProto.toBuilder()
- .setQuery(queryExpression);
-
String prefix = createPrefix(packageName, databaseName);
- Set<String> allowedPrefixedSchemas = getAllowedPrefixSchemasLocked(prefix, searchSpec);
-
- // rewriteSearchSpecForPrefixesLocked will return false if there is nothing to search
- // over given their search filters, so we can return early and skip sending request
- // to Icing.
- if (!rewriteSearchSpecForPrefixesLocked(searchSpecBuilder,
- Collections.singleton(prefix), allowedPrefixedSchemas)) {
+ if (!mNamespaceMapLocked.containsKey(prefix)) {
+ // The target database is empty so we can return early and skip sending request to
+ // Icing.
return;
}
- SearchSpecProto finalSearchSpec = searchSpecBuilder.build();
+
+ SearchSpecToProtoConverter searchSpecToProtoConverter =
+ new SearchSpecToProtoConverter(searchSpec, Collections.singleton(prefix),
+ mNamespaceMapLocked, mSchemaMapLocked);
+ if (searchSpecToProtoConverter.isNothingToSearch()) {
+ // there is nothing to search over given their search filters, so we can return
+ // early and skip sending request to Icing.
+ return;
+ }
+
+ SearchSpecProto finalSearchSpec =
+ searchSpecToProtoConverter.toSearchSpecProto(queryExpression);
Set<String> prefixedObservedSchemas = null;
if (mObserverManager.isPackageObserved(packageName)) {
prefixedObservedSchemas = new ArraySet<>();
- for (String prefixedType : allowedPrefixedSchemas) {
+ List<String> prefixedTargetSchemaTypes =
+ finalSearchSpec.getSchemaTypeFiltersList();
+ for (int i = 0; i < prefixedTargetSchemaTypes.size(); i++) {
+ String prefixedType = prefixedTargetSchemaTypes.get(i);
String shortTypeName = PrefixUtil.removePrefix(prefixedType);
if (mObserverManager.isSchemaTypeObserved(packageName, shortTypeName)) {
prefixedObservedSchemas.add(prefixedType);
@@ -1399,8 +1351,8 @@
* Executes removeByQuery, creating change notifications for removal.
*
* @param packageName The package name that owns the documents.
- * @param rewrittenSearchSpec A search spec that has been run through
- * {@link #rewriteSearchSpecForPrefixesLocked}.
+ * @param finalSearchSpec The final search spec that has been written through
+ * {@link SearchSpecToProtoConverter}.
* @param prefixedObservedSchemas The set of prefixed schemas that have valid registered
* observers. Only changes to schemas in this set will be queued.
*/
@@ -1409,13 +1361,13 @@
@GuardedBy("mReadWriteLock")
private void doRemoveByQueryWithChangeNotificationLocked(
@NonNull String packageName,
- @NonNull SearchSpecProto rewrittenSearchSpec,
+ @NonNull SearchSpecProto finalSearchSpec,
@NonNull Set<String> prefixedObservedSchemas,
@Nullable RemoveStats.Builder removeStatsBuilder) throws AppSearchException {
mLogUtil.piiTrace(
- "removeByQuery.withChangeNotification, query request", rewrittenSearchSpec);
+ "removeByQuery.withChangeNotification, query request", finalSearchSpec);
SearchResultProto searchResultProto = mIcingSearchEngineLocked.search(
- rewrittenSearchSpec,
+ finalSearchSpec,
ScoringSpecProto.getDefaultInstance(),
RESULT_SPEC_NO_PROPERTIES);
mLogUtil.piiTrace(
@@ -1952,302 +1904,6 @@
return rewrittenSchemaResults;
}
- /**
- * Rewrites the search spec filters with {@code prefixes}.
- *
- * <p>This method should be only called in query methods and get the READ lock to keep thread
- * safety.
- *
- * @param searchSpecBuilder Client-provided SearchSpec
- * @param prefixes Prefixes that we should prepend to all our filters
- * @param allowedPrefixedSchemas Prefixed schemas that the client is allowed to query over. This
- * supersedes the schema filters that may exist on the {@code
- * searchSpecBuilder}.
- * @return false if none there would be nothing to search over.
- */
- @VisibleForTesting
- @GuardedBy("mReadWriteLock")
- boolean rewriteSearchSpecForPrefixesLocked(
- @NonNull SearchSpecProto.Builder searchSpecBuilder,
- @NonNull Set<String> prefixes,
- @NonNull Set<String> allowedPrefixedSchemas) {
- // Create a copy since retainAll() modifies the original set.
- Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
- existingPrefixes.retainAll(prefixes);
-
- if (existingPrefixes.isEmpty()) {
- // None of the prefixes exist, empty query.
- return false;
- }
-
- if (allowedPrefixedSchemas.isEmpty()) {
- // Not allowed to search over any schemas, empty query.
- return false;
- }
-
- // Clear the schema type filters since we'll be rewriting them with the
- // allowedPrefixedSchemas.
- searchSpecBuilder.clearSchemaTypeFilters();
- searchSpecBuilder.addAllSchemaTypeFilters(allowedPrefixedSchemas);
-
- // Cache the namespaces before clearing everything.
- List<String> namespaceFilters = searchSpecBuilder.getNamespaceFiltersList();
- searchSpecBuilder.clearNamespaceFilters();
-
- // Rewrite non-schema filters to include a prefix.
- for (String prefix : existingPrefixes) {
- // TODO(b/169883602): We currently grab every namespace for every prefix. We can
- // optimize this by checking if a prefix has any allowedSchemaTypes. If not, that
- // means we don't want to query over anything in that prefix anyways, so we don't
- // need to grab its namespaces either.
-
- // Empty namespaces on the search spec means to query over all namespaces.
- Set<String> existingNamespaces = mNamespaceMapLocked.get(prefix);
- if (existingNamespaces != null) {
- if (namespaceFilters.isEmpty()) {
- // Include all namespaces
- searchSpecBuilder.addAllNamespaceFilters(existingNamespaces);
- } else {
- // Prefix the given namespaces.
- for (int i = 0; i < namespaceFilters.size(); i++) {
- String prefixedNamespace = prefix + namespaceFilters.get(i);
- if (existingNamespaces.contains(prefixedNamespace)) {
- searchSpecBuilder.addNamespaceFilters(prefixedNamespace);
- }
- }
- }
- }
- }
- if (searchSpecBuilder.getNamespaceFiltersCount() == 0) {
- // None of the user wanted namespace exist, empty query.
- return false;
- }
- return true;
- }
-
- /**
- * Returns the set of allowed prefixed schemas that the {@code prefix} can query while taking
- * into account the {@code searchSpec} schema filters.
- *
- * <p>This only checks intersection of schema filters on the search spec with those that the
- * prefix owns itself. This does not check global query permissions.
- */
- @GuardedBy("mReadWriteLock")
- private Set<String> getAllowedPrefixSchemasLocked(@NonNull String prefix,
- @NonNull SearchSpec searchSpec) {
- Set<String> allowedPrefixedSchemas = new ArraySet<>();
-
- List<String> schemaFilters = searchSpec.getFilterSchemas();
- Map<String, SchemaTypeConfigProto> prefixedSchemaMap = mSchemaMapLocked.get(prefix);
- if (prefixedSchemaMap == null) {
- // The db is empty, return early;
- return allowedPrefixedSchemas;
- }
- if (schemaFilters.isEmpty()) {
- // If the client didn't specify any schema filters, search over all of their schemas
- allowedPrefixedSchemas.addAll(prefixedSchemaMap.keySet());
- } else {
- // Check all client specified schemas, add them if they exist in AppSearch.
- for (int i = 0; i < schemaFilters.size(); i++) {
- String prefixedSchemaType = prefix + schemaFilters.get(i);
- if (prefixedSchemaMap.containsKey(prefixedSchemaType)) {
- allowedPrefixedSchemas.add(prefixedSchemaType);
- }
- }
- }
- return allowedPrefixedSchemas;
- }
-
- /**
- * Rewrites the typePropertyMasks that exist in {@code prefixes}.
- *
- * <p>This method should be only called in query methods and get the READ lock to keep thread
- * safety.
- *
- * @param resultSpecBuilder ResultSpecs as specified by client
- * @param prefixes Prefixes that we should prepend to all our filters
- * @param allowedPrefixedSchemas Prefixed schemas that the client is allowed to query over.
- */
- @VisibleForTesting
- @GuardedBy("mReadWriteLock")
- void rewriteResultSpecForPrefixesLocked(
- @NonNull ResultSpecProto.Builder resultSpecBuilder,
- @NonNull Set<String> prefixes, @NonNull Set<String> allowedPrefixedSchemas) {
- // Create a copy since retainAll() modifies the original set.
- Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
- existingPrefixes.retainAll(prefixes);
-
- List<TypePropertyMask> prefixedTypePropertyMasks = new ArrayList<>();
- // Rewrite filters to include a database prefix.
- for (String prefix : existingPrefixes) {
- // Qualify the given schema types
- for (TypePropertyMask typePropertyMask :
- resultSpecBuilder.getTypePropertyMasksList()) {
- String unprefixedType = typePropertyMask.getSchemaType();
- boolean isWildcard =
- unprefixedType.equals(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD);
- String prefixedType = isWildcard ? unprefixedType : prefix + unprefixedType;
- if (isWildcard || allowedPrefixedSchemas.contains(prefixedType)) {
- prefixedTypePropertyMasks.add(
- typePropertyMask.toBuilder().setSchemaType(prefixedType).build());
- }
- }
- }
- resultSpecBuilder.clearTypePropertyMasks().addAllTypePropertyMasks(
- prefixedTypePropertyMasks);
- }
-
- /**
- * Adds result groupings for each namespace in each package being queried for.
- *
- * <p>This method should be only called in query methods and get the READ lock to keep thread
- * safety.
- *
- * @param resultSpecBuilder ResultSpecs as specified by client
- * @param prefixes Prefixes that we should prepend to all our filters
- * @param maxNumResults The maximum number of results for each grouping to support.
- */
- @GuardedBy("mReadWriteLock")
- private void addPerPackagePerNamespaceResultGroupingsLocked(
- @NonNull ResultSpecProto.Builder resultSpecBuilder,
- @NonNull Set<String> prefixes, int maxNumResults) {
- Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
- existingPrefixes.retainAll(prefixes);
-
- // Create a map for package+namespace to prefixedNamespaces. This is NOT necessarily the
- // same as the list of namespaces. If one package has multiple databases, each with the same
- // namespace, then those should be grouped together.
- Map<String, List<String>> packageAndNamespaceToNamespaces = new ArrayMap<>();
- for (String prefix : existingPrefixes) {
- Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix);
- if (prefixedNamespaces == null) {
- continue;
- }
- String packageName = getPackageName(prefix);
- // Create a new prefix without the database name. This will allow us to group namespaces
- // that have the same name and package but a different database name together.
- String emptyDatabasePrefix = createPrefix(packageName, /*databaseName*/"");
- for (String prefixedNamespace : prefixedNamespaces) {
- String namespace;
- try {
- namespace = removePrefix(prefixedNamespace);
- } catch (AppSearchException e) {
- // This should never happen. Skip this namespace if it does.
- Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
- continue;
- }
- String emptyDatabasePrefixedNamespace = emptyDatabasePrefix + namespace;
- List<String> namespaceList =
- packageAndNamespaceToNamespaces.get(emptyDatabasePrefixedNamespace);
- if (namespaceList == null) {
- namespaceList = new ArrayList<>();
- packageAndNamespaceToNamespaces.put(emptyDatabasePrefixedNamespace,
- namespaceList);
- }
- namespaceList.add(prefixedNamespace);
- }
- }
-
- for (List<String> namespaces : packageAndNamespaceToNamespaces.values()) {
- resultSpecBuilder.addResultGroupings(
- ResultSpecProto.ResultGrouping.newBuilder()
- .addAllNamespaces(namespaces).setMaxResults(maxNumResults));
- }
- }
-
- /**
- * Adds result groupings for each package being queried for.
- *
- * <p>This method should be only called in query methods and get the READ lock to keep thread
- * safety.
- *
- * @param resultSpecBuilder ResultSpecs as specified by client
- * @param prefixes Prefixes that we should prepend to all our filters
- * @param maxNumResults The maximum number of results for each grouping to support.
- */
- @GuardedBy("mReadWriteLock")
- private void addPerPackageResultGroupingsLocked(
- @NonNull ResultSpecProto.Builder resultSpecBuilder,
- @NonNull Set<String> prefixes, int maxNumResults) {
- Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
- existingPrefixes.retainAll(prefixes);
-
- // Build up a map of package to namespaces.
- Map<String, List<String>> packageToNamespacesMap = new ArrayMap<>();
- for (String prefix : existingPrefixes) {
- Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix);
- if (prefixedNamespaces == null) {
- continue;
- }
- String packageName = getPackageName(prefix);
- List<String> packageNamespaceList = packageToNamespacesMap.get(packageName);
- if (packageNamespaceList == null) {
- packageNamespaceList = new ArrayList<>();
- packageToNamespacesMap.put(packageName, packageNamespaceList);
- }
- packageNamespaceList.addAll(prefixedNamespaces);
- }
-
- for (List<String> prefixedNamespaces : packageToNamespacesMap.values()) {
- resultSpecBuilder.addResultGroupings(
- ResultSpecProto.ResultGrouping.newBuilder()
- .addAllNamespaces(prefixedNamespaces).setMaxResults(maxNumResults));
- }
- }
-
- /**
- * Adds result groupings for each namespace being queried for.
- *
- * <p>This method should be only called in query methods and get the READ lock to keep thread
- * safety.
- *
- * @param resultSpecBuilder ResultSpecs as specified by client
- * @param prefixes Prefixes that we should prepend to all our filters
- * @param maxNumResults The maximum number of results for each grouping to support.
- */
- @GuardedBy("mReadWriteLock")
- private void addPerNamespaceResultGroupingsLocked(
- @NonNull ResultSpecProto.Builder resultSpecBuilder,
- @NonNull Set<String> prefixes, int maxNumResults) {
- Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
- existingPrefixes.retainAll(prefixes);
-
- // Create a map of namespace to prefixedNamespaces. This is NOT necessarily the
- // same as the list of namespaces. If a namespace exists under different packages and/or
- // different databases, they should still be grouped together.
- Map<String, List<String>> namespaceToPrefixedNamespaces = new ArrayMap<>();
- for (String prefix : existingPrefixes) {
- Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix);
- if (prefixedNamespaces == null) {
- continue;
- }
- for (String prefixedNamespace : prefixedNamespaces) {
- String namespace;
- try {
- namespace = removePrefix(prefixedNamespace);
- } catch (AppSearchException e) {
- // This should never happen. Skip this namespace if it does.
- Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
- continue;
- }
- List<String> groupedPrefixedNamespaces =
- namespaceToPrefixedNamespaces.get(namespace);
- if (groupedPrefixedNamespaces == null) {
- groupedPrefixedNamespaces = new ArrayList<>();
- namespaceToPrefixedNamespaces.put(namespace,
- groupedPrefixedNamespaces);
- }
- groupedPrefixedNamespaces.add(prefixedNamespace);
- }
- }
-
- for (List<String> namespaces : namespaceToPrefixedNamespaces.values()) {
- resultSpecBuilder.addResultGroupings(
- ResultSpecProto.ResultGrouping.newBuilder()
- .addAllNamespaces(namespaces).setMaxResults(maxNumResults));
- }
- }
-
@VisibleForTesting
@GuardedBy("mReadWriteLock")
SchemaProto getSchemaProtoLocked() throws AppSearchException {
@@ -2474,34 +2130,6 @@
}
}
- /** Remove the rewritten schema types from any result documents. */
- @NonNull
- @VisibleForTesting
- static SearchResultPage rewriteSearchResultProto(
- @NonNull SearchResultProto searchResultProto,
- @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)
- throws AppSearchException {
- // Parallel array of package names for each document search result.
- List<String> packageNames = new ArrayList<>(searchResultProto.getResultsCount());
-
- // Parallel array of database names for each document search result.
- List<String> databaseNames = new ArrayList<>(searchResultProto.getResultsCount());
-
- SearchResultProto.Builder resultsBuilder = searchResultProto.toBuilder();
- for (int i = 0; i < searchResultProto.getResultsCount(); i++) {
- SearchResultProto.ResultProto.Builder resultBuilder =
- searchResultProto.getResults(i).toBuilder();
- DocumentProto.Builder documentBuilder = resultBuilder.getDocument().toBuilder();
- String prefix = removePrefixesFromDocument(documentBuilder);
- packageNames.add(getPackageName(prefix));
- databaseNames.add(getDatabaseName(prefix));
- resultBuilder.setDocument(documentBuilder);
- resultsBuilder.setResults(i, resultBuilder);
- }
- return SearchResultToProtoConverter.toSearchResultPage(resultsBuilder, packageNames,
- databaseNames, schemaMap);
- }
-
@GuardedBy("mReadWriteLock")
@VisibleForTesting
GetOptimizeInfoResultProto getOptimizeInfoResultLocked() {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
index b51959c..a139d3b 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
@@ -23,6 +23,7 @@
import androidx.core.util.Preconditions;
import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.DocumentProtoOrBuilder;
import com.google.android.icing.proto.PropertyProto;
import com.google.android.icing.proto.SchemaTypeConfigProto;
import com.google.android.icing.protobuf.ByteString;
@@ -129,7 +130,7 @@
* that has all empty values.
*/
@NonNull
- public static GenericDocument toGenericDocument(@NonNull DocumentProto proto,
+ public static GenericDocument toGenericDocument(@NonNull DocumentProtoOrBuilder proto,
@NonNull String prefix,
@NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap) {
Preconditions.checkNotNull(proto);
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
index a1842e9..de42623 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
@@ -16,7 +16,9 @@
package androidx.appsearch.localstorage.converter;
-import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
+import static androidx.appsearch.localstorage.util.PrefixUtil.getDatabaseName;
+import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName;
+import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
import android.os.Bundle;
@@ -25,16 +27,15 @@
import androidx.appsearch.app.GenericDocument;
import androidx.appsearch.app.SearchResult;
import androidx.appsearch.app.SearchResultPage;
-import androidx.core.util.Preconditions;
+import androidx.appsearch.exceptions.AppSearchException;
+import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.SchemaTypeConfigProto;
import com.google.android.icing.proto.SearchResultProto;
-import com.google.android.icing.proto.SearchResultProtoOrBuilder;
import com.google.android.icing.proto.SnippetMatchProto;
import com.google.android.icing.proto.SnippetProto;
import java.util.ArrayList;
-import java.util.List;
import java.util.Map;
/**
@@ -51,32 +52,19 @@
* Translate a {@link SearchResultProto} into {@link SearchResultPage}.
*
* @param proto The {@link SearchResultProto} containing results.
- * @param packageNames A parallel array of package names. The package name at index 'i' of
- * this list should be the package that indexed the document at index 'i'
- * of proto.getResults(i).
- * @param databaseNames A parallel array of database names. The database name at index 'i' of
- * this list shold be the database that indexed the document at index 'i'
- * of proto.getResults(i).
- * @param schemaMap A map of prefixes to an inner-map of prefixed schema type to
- * SchemaTypeConfigProtos, used for setting a default value for results
- * with DocumentProtos that have empty values.
+ * @param schemaMap The cached Map of <Prefix, Map<PrefixedSchemaType, schemaProto>>
+ * stores all existing prefixed schema type.
* @return {@link SearchResultPage} of results.
*/
@NonNull
- public static SearchResultPage toSearchResultPage(@NonNull SearchResultProtoOrBuilder proto,
- @NonNull List<String> packageNames, @NonNull List<String> databaseNames,
- @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
- Preconditions.checkArgument(
- proto.getResultsCount() == packageNames.size(),
- "Size of results does not match the number of package names.");
+ public static SearchResultPage toSearchResultPage(@NonNull SearchResultProto proto,
+ @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)
+ throws AppSearchException {
Bundle bundle = new Bundle();
bundle.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, proto.getNextPageToken());
ArrayList<Bundle> resultBundles = new ArrayList<>(proto.getResultsCount());
for (int i = 0; i < proto.getResultsCount(); i++) {
- String prefix = createPrefix(packageNames.get(i), databaseNames.get(i));
- Map<String, SchemaTypeConfigProto> schemaTypeMap = schemaMap.get(prefix);
- SearchResult result = toSearchResult(
- proto.getResults(i), packageNames.get(i), databaseNames.get(i), schemaTypeMap);
+ SearchResult result = toUnprefixedSearchResult(proto.getResults(i), schemaMap);
resultBundles.add(result.getBundle());
}
bundle.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, resultBundles);
@@ -84,28 +72,28 @@
}
/**
- * Translate a {@link SearchResultProto.ResultProto} into {@link SearchResult}.
+ * Translate a {@link SearchResultProto.ResultProto} into {@link SearchResult}. The package and
+ * database prefix will be removed from {@link GenericDocument}.
*
- * @param proto The proto to be converted.
- * @param packageName The package name associated with the document in {@code proto}.
- * @param databaseName The database name associated with the document in {@code proto}.
- * @param schemaTypeToProtoMap A map of prefixed schema types to their corresponding
- * SchemaTypeConfigProto, used for setting a default value for
- * results with DocumentProtos that have empty values.
- * @return A {@link SearchResult} bundle.
+ * @param proto The proto to be converted.
+ * @param schemaMap The cached Map of <Prefix, Map<PrefixedSchemaType, schemaProto>>
+ * stores all existing prefixed schema type.
+ * @return A {@link SearchResult}.
*/
@NonNull
- private static SearchResult toSearchResult(
- @NonNull SearchResultProto.ResultProtoOrBuilder proto,
- @NonNull String packageName,
- @NonNull String databaseName,
- @NonNull Map<String, SchemaTypeConfigProto> schemaTypeToProtoMap) {
- String prefix = createPrefix(packageName, databaseName);
+ private static SearchResult toUnprefixedSearchResult(
+ @NonNull SearchResultProto.ResultProto proto,
+ @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)
+ throws AppSearchException {
+
+ DocumentProto.Builder documentBuilder = proto.getDocument().toBuilder();
+ String prefix = removePrefixesFromDocument(documentBuilder);
+ Map<String, SchemaTypeConfigProto> schemaTypeMap = schemaMap.get(prefix);
GenericDocument document =
- GenericDocumentToProtoConverter.toGenericDocument(proto.getDocument(), prefix,
- schemaTypeToProtoMap);
+ GenericDocumentToProtoConverter.toGenericDocument(documentBuilder, prefix,
+ schemaTypeMap);
SearchResult.Builder builder =
- new SearchResult.Builder(packageName, databaseName)
+ new SearchResult.Builder(getPackageName(prefix), getDatabaseName(prefix))
.setGenericDocument(document).setRankingSignal(proto.getScore());
if (proto.hasSnippet()) {
for (int i = 0; i < proto.getSnippet().getEntriesCount(); i++) {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
index abc97f3..af24c98 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
@@ -16,15 +16,35 @@
package androidx.appsearch.localstorage.converter;
+import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
+import static androidx.appsearch.localstorage.util.PrefixUtil.getDatabaseName;
+import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName;
+import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefix;
+
+import android.util.Log;
+
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
import com.google.android.icing.proto.ResultSpecProto;
+import com.google.android.icing.proto.SchemaTypeConfigProto;
import com.google.android.icing.proto.ScoringSpecProto;
import com.google.android.icing.proto.SearchSpecProto;
import com.google.android.icing.proto.TermMatchType;
+import com.google.android.icing.proto.TypePropertyMask;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
* Translates a {@link SearchSpec} into icing search protos.
@@ -33,18 +53,158 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class SearchSpecToProtoConverter {
- private SearchSpecToProtoConverter() {
+ private static final String TAG = "AppSearchSearchSpecConv";
+ private final SearchSpec mSearchSpec;
+ private final Set<String> mPrefixes;
+ /** Prefixed namespaces that the client is allowed to query over */
+ private final Set<String> mTargetPrefixedNamespaceFilters = new ArraySet<>();
+ /** Prefixed schemas that the client is allowed to query over */
+ private final Set<String> mTargetPrefixedSchemaFilters = new ArraySet<>();
+
+ /**
+ * Creates a {@link SearchSpecToProtoConverter} for given {@link SearchSpec}.
+ *
+ * @param searchSpec The spec we need to convert from.
+ * @param prefixes Set of database prefix which the caller want to access.
+ * @param namespaceMap The cached Map of {@code <Prefix, Set<PrefixedNamespace>>} stores
+ * all prefixed namespace filters which are stored in AppSearch.
+ * @param schemaMap The cached Map of {@code <Prefix, Map<PrefixedSchemaType, schemaProto>>}
+ * stores all prefixed schema filters which are stored inAppSearch.
+ */
+ public SearchSpecToProtoConverter(@NonNull SearchSpec searchSpec,
+ @NonNull Set<String> prefixes,
+ @NonNull Map<String, Set<String>> namespaceMap,
+ @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+ mSearchSpec = Preconditions.checkNotNull(searchSpec);
+ mPrefixes = Preconditions.checkNotNull(prefixes);
+ Preconditions.checkNotNull(namespaceMap);
+ Preconditions.checkNotNull(schemaMap);
+ generateTargetNamespaceFilters(namespaceMap);
+ if (!mTargetPrefixedNamespaceFilters.isEmpty()) {
+ // Skip generate the target schema filter if the target namespace filter is empty. We
+ // have nothing to search anyway.
+ generateTargetSchemaFilters(schemaMap);
+ }
}
- /** Extracts {@link SearchSpecProto} information from a {@link SearchSpec}. */
- @NonNull
- public static SearchSpecProto toSearchSpecProto(@NonNull SearchSpec spec) {
- Preconditions.checkNotNull(spec);
- SearchSpecProto.Builder protoBuilder = SearchSpecProto.newBuilder()
- .addAllSchemaTypeFilters(spec.getFilterSchemas())
- .addAllNamespaceFilters(spec.getFilterNamespaces());
+ /**
+ * Add prefix to the given namespace filters that user want to search over and find the
+ * intersection set with those prefixed namespace candidates that are stored in AppSearch.
+ *
+ * @param namespaceMap The cached Map of {@code <Prefix, Set<PrefixedNamespace>>} stores
+ * all prefixed namespace filters which are stored in AppSearch.
+ */
+ private void generateTargetNamespaceFilters(
+ @NonNull Map<String, Set<String>> namespaceMap) {
+ // Convert namespace filters to prefixed namespace filters
+ for (String prefix : mPrefixes) {
+ // Step1: find all prefixed namespace candidates that are stored in AppSearch.
+ Set<String> prefixedNamespaceCandidates = namespaceMap.get(prefix);
+ if (prefixedNamespaceCandidates == null) {
+ // This is should never happen. All prefixes should be verified before reach
+ // here.
+ continue;
+ }
+ // Step2: get the intersection of user searching filters and those candidates which are
+ // stored in AppSearch.
+ getIntersectedFilters(prefix, prefixedNamespaceCandidates,
+ mSearchSpec.getFilterNamespaces(), mTargetPrefixedNamespaceFilters);
+ }
+ }
- @SearchSpec.TermMatch int termMatchCode = spec.getTermMatch();
+ /**
+ * Add prefix to the given schema filters that user want to search over and find the
+ * intersection set with those prefixed schema candidates that are stored in AppSearch.
+ *
+ * @param schemaMap The cached Map of
+ * <Prefix, Map<PrefixedSchemaType, schemaProto>>
+ * stores all prefixed schema filters which are stored in
+ * AppSearch.
+ */
+ private void generateTargetSchemaFilters(
+ @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+ // Append prefix to input schema filters and get the intersection of existing schema filter.
+ for (String prefix : mPrefixes) {
+ // Step1: find all prefixed schema candidates that are stored in AppSearch.
+ Map<String, SchemaTypeConfigProto> prefixedSchemaMap = schemaMap.get(prefix);
+ if (prefixedSchemaMap == null) {
+ // This is should never happen. All prefixes should be verified before reach
+ // here.
+ continue;
+ }
+ Set<String> prefixedSchemaCandidates = prefixedSchemaMap.keySet();
+ // Step2: get the intersection of user searching filters and those candidates which are
+ // stored in AppSearch.
+ getIntersectedFilters(prefix, prefixedSchemaCandidates, mSearchSpec.getFilterSchemas(),
+ mTargetPrefixedSchemaFilters);
+ }
+ }
+
+ /**
+ * @return whether this search's target filters are empty. If any target filter is empty, we
+ * should skip send request to Icing.
+ */
+ public boolean isNothingToSearch() {
+ return mTargetPrefixedNamespaceFilters.isEmpty() || mTargetPrefixedSchemaFilters.isEmpty();
+ }
+
+ /**
+ * For each target schema, we will check visibility store is that accessible to the caller. And
+ * remove this schemas if it is not allowed for caller to query.
+ *
+ * @param callerPackageName The package name of caller
+ * @param visibilityStore The visibility store which holds all visibility settings.
+ * If you pass null, all schemas that don't belong to the
+ * caller package will be removed.
+ * @param callerUid The uid of the caller.
+ * @param callerHasSystemAccess Whether the caller has system access.
+ */
+ public void removeInaccessibleSchemaFilter(@NonNull String callerPackageName,
+ @Nullable VisibilityStore visibilityStore,
+ int callerUid,
+ boolean callerHasSystemAccess) {
+ Iterator<String> targetPrefixedSchemaFilterIterator =
+ mTargetPrefixedSchemaFilters.iterator();
+ while (targetPrefixedSchemaFilterIterator.hasNext()) {
+ String targetPrefixedSchemaFilter = targetPrefixedSchemaFilterIterator.next();
+ String packageName = getPackageName(targetPrefixedSchemaFilter);
+
+ boolean allow;
+ if (packageName.equals(callerPackageName)) {
+ // Callers can always retrieve their own data
+ allow = true;
+ } else if (visibilityStore == null) {
+ // If there's no visibility store, there's no extra access
+ allow = false;
+ } else {
+ String databaseName = getDatabaseName(targetPrefixedSchemaFilter);
+ allow = visibilityStore.isSchemaSearchableByCaller(packageName, databaseName,
+ targetPrefixedSchemaFilter, callerUid, callerHasSystemAccess);
+ }
+ if (!allow) {
+ targetPrefixedSchemaFilterIterator.remove();
+ }
+ }
+ }
+
+ /**
+ * Extracts {@link SearchSpecProto} information from a {@link SearchSpec}.
+ *
+ * @param queryExpression Query String to search.
+ */
+ @NonNull
+ public SearchSpecProto toSearchSpecProto(
+ @NonNull String queryExpression) {
+ Preconditions.checkNotNull(queryExpression);
+
+ // set query to SearchSpecProto and override schema and namespace filter by
+ // targetPrefixedFilters which is
+ SearchSpecProto.Builder protoBuilder = SearchSpecProto.newBuilder()
+ .setQuery(queryExpression)
+ .addAllNamespaceFilters(mTargetPrefixedNamespaceFilters)
+ .addAllSchemaTypeFilters(mTargetPrefixedSchemaFilters);
+
+ @SearchSpec.TermMatch int termMatchCode = mSearchSpec.getTermMatch();
TermMatchType.Code termMatchCodeProto = TermMatchType.Code.forNumber(termMatchCode);
if (termMatchCodeProto == null || termMatchCodeProto.equals(TermMatchType.Code.UNKNOWN)) {
throw new IllegalArgumentException("Invalid term match type: " + termMatchCode);
@@ -54,35 +214,73 @@
return protoBuilder.build();
}
- /** Extracts {@link ResultSpecProto} information from a {@link SearchSpec}. */
+ /**
+ * Extracts {@link ResultSpecProto} information from a {@link SearchSpec}.
+ *
+ * @param namespaceMap The cached Map of {@code <Prefix, Set<PrefixedNamespace>>} stores
+ * all existing prefixed namespace.
+ */
@NonNull
- public static ResultSpecProto toResultSpecProto(@NonNull SearchSpec spec) {
- Preconditions.checkNotNull(spec);
- return ResultSpecProto.newBuilder()
- .setNumPerPage(spec.getResultCountPerPage())
+ public ResultSpecProto toResultSpecProto(
+ @NonNull Map<String, Set<String>> namespaceMap) {
+ ResultSpecProto.Builder resultSpecBuilder = ResultSpecProto.newBuilder()
+ .setNumPerPage(mSearchSpec.getResultCountPerPage())
.setSnippetSpec(
ResultSpecProto.SnippetSpecProto.newBuilder()
- .setNumToSnippet(spec.getSnippetCount())
- .setNumMatchesPerProperty(spec.getSnippetCountPerProperty())
- .setMaxWindowBytes(spec.getMaxSnippetSize()))
- .addAllTypePropertyMasks(TypePropertyPathToProtoConverter.toTypePropertyMaskList(
- spec.getProjections())).build();
+ .setNumToSnippet(mSearchSpec.getSnippetCount())
+ .setNumMatchesPerProperty(mSearchSpec.getSnippetCountPerProperty())
+ .setMaxWindowBytes(mSearchSpec.getMaxSnippetSize()));
+
+ // Rewrites the typePropertyMasks that exist in {@code prefixes}.
+ int groupingType = mSearchSpec.getResultGroupingTypeFlags();
+ if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0
+ && (groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) {
+ addPerPackagePerNamespaceResultGroupings(mPrefixes,
+ mSearchSpec.getResultGroupingLimit(),
+ namespaceMap, resultSpecBuilder);
+ } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0) {
+ addPerPackageResultGroupings(mPrefixes, mSearchSpec.getResultGroupingLimit(),
+ namespaceMap, resultSpecBuilder);
+ } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) {
+ addPerNamespaceResultGroupings(mPrefixes, mSearchSpec.getResultGroupingLimit(),
+ namespaceMap, resultSpecBuilder);
+ }
+
+ List<TypePropertyMask.Builder> typePropertyMaskBuilders =
+ TypePropertyPathToProtoConverter
+ .toTypePropertyMaskBuilderList(mSearchSpec.getProjections());
+ // Rewrite filters to include a database prefix.
+ resultSpecBuilder.clearTypePropertyMasks();
+ for (int i = 0; i < typePropertyMaskBuilders.size(); i++) {
+ String unprefixedType = typePropertyMaskBuilders.get(i).getSchemaType();
+ boolean isWildcard =
+ unprefixedType.equals(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD);
+ // Qualify the given schema types
+ for (String prefix : mPrefixes) {
+ String prefixedType = isWildcard ? unprefixedType : prefix + unprefixedType;
+ if (isWildcard || mTargetPrefixedSchemaFilters.contains(prefixedType)) {
+ resultSpecBuilder.addTypePropertyMasks(typePropertyMaskBuilders.get(i)
+ .setSchemaType(prefixedType).build());
+ }
+ }
+ }
+
+ return resultSpecBuilder.build();
}
/** Extracts {@link ScoringSpecProto} information from a {@link SearchSpec}. */
@NonNull
- public static ScoringSpecProto toScoringSpecProto(@NonNull SearchSpec spec) {
- Preconditions.checkNotNull(spec);
+ public ScoringSpecProto toScoringSpecProto() {
ScoringSpecProto.Builder protoBuilder = ScoringSpecProto.newBuilder();
- @SearchSpec.Order int orderCode = spec.getOrder();
+ @SearchSpec.Order int orderCode = mSearchSpec.getOrder();
ScoringSpecProto.Order.Code orderCodeProto =
ScoringSpecProto.Order.Code.forNumber(orderCode);
if (orderCodeProto == null) {
throw new IllegalArgumentException("Invalid result ranking order: " + orderCode);
}
protoBuilder.setOrderBy(orderCodeProto).setRankBy(
- toProtoRankingStrategy(spec.getRankingStrategy()));
+ toProtoRankingStrategy(mSearchSpec.getRankingStrategy()));
return protoBuilder.build();
}
@@ -111,4 +309,174 @@
+ rankingStrategyCode);
}
}
+
+
+ /**
+ * Adds result groupings for each namespace in each package being queried for.
+ *
+ * @param prefixes Prefixes that we should prepend to all our filters
+ * @param maxNumResults The maximum number of results for each grouping to support.
+ * @param namespaceMap The namespace map contains all prefixed existing namespaces.
+ * @param resultSpecBuilder ResultSpecs as specified by client
+ */
+ private static void addPerPackagePerNamespaceResultGroupings(
+ @NonNull Set<String> prefixes,
+ int maxNumResults,
+ @NonNull Map<String, Set<String>> namespaceMap,
+ @NonNull ResultSpecProto.Builder resultSpecBuilder) {
+ // Create a map for package+namespace to prefixedNamespaces. This is NOT necessarily the
+ // same as the list of namespaces. If one package has multiple databases, each with the same
+ // namespace, then those should be grouped together.
+ Map<String, List<String>> packageAndNamespaceToNamespaces = new ArrayMap<>();
+ for (String prefix : prefixes) {
+ Set<String> prefixedNamespaces = namespaceMap.get(prefix);
+ if (prefixedNamespaces == null) {
+ continue;
+ }
+ String packageName = getPackageName(prefix);
+ // Create a new prefix without the database name. This will allow us to group namespaces
+ // that have the same name and package but a different database name together.
+ String emptyDatabasePrefix = createPrefix(packageName, /*databaseName*/"");
+ for (String prefixedNamespace : prefixedNamespaces) {
+ String namespace;
+ try {
+ namespace = removePrefix(prefixedNamespace);
+ } catch (AppSearchException e) {
+ // This should never happen. Skip this namespace if it does.
+ Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
+ continue;
+ }
+ String emptyDatabasePrefixedNamespace = emptyDatabasePrefix + namespace;
+ List<String> namespaceList =
+ packageAndNamespaceToNamespaces.get(emptyDatabasePrefixedNamespace);
+ if (namespaceList == null) {
+ namespaceList = new ArrayList<>();
+ packageAndNamespaceToNamespaces.put(emptyDatabasePrefixedNamespace,
+ namespaceList);
+ }
+ namespaceList.add(prefixedNamespace);
+ }
+ }
+
+ for (List<String> namespaces : packageAndNamespaceToNamespaces.values()) {
+ resultSpecBuilder.addResultGroupings(
+ ResultSpecProto.ResultGrouping.newBuilder()
+ .addAllNamespaces(namespaces).setMaxResults(maxNumResults));
+ }
+ }
+
+ /**
+ * Adds result groupings for each package being queried for.
+ *
+ * @param prefixes Prefixes that we should prepend to all our filters
+ * @param maxNumResults The maximum number of results for each grouping to support.
+ * @param namespaceMap The namespace map contains all prefixed existing namespaces.
+ * @param resultSpecBuilder ResultSpecs as specified by client
+ */
+ private static void addPerPackageResultGroupings(
+ @NonNull Set<String> prefixes,
+ int maxNumResults,
+ @NonNull Map<String, Set<String>> namespaceMap,
+ @NonNull ResultSpecProto.Builder resultSpecBuilder) {
+ // Build up a map of package to namespaces.
+ Map<String, List<String>> packageToNamespacesMap = new ArrayMap<>();
+ for (String prefix : prefixes) {
+ Set<String> prefixedNamespaces = namespaceMap.get(prefix);
+ if (prefixedNamespaces == null) {
+ continue;
+ }
+ String packageName = getPackageName(prefix);
+ List<String> packageNamespaceList = packageToNamespacesMap.get(packageName);
+ if (packageNamespaceList == null) {
+ packageNamespaceList = new ArrayList<>();
+ packageToNamespacesMap.put(packageName, packageNamespaceList);
+ }
+ packageNamespaceList.addAll(prefixedNamespaces);
+ }
+
+ for (List<String> prefixedNamespaces : packageToNamespacesMap.values()) {
+ resultSpecBuilder.addResultGroupings(
+ ResultSpecProto.ResultGrouping.newBuilder()
+ .addAllNamespaces(prefixedNamespaces).setMaxResults(maxNumResults));
+ }
+ }
+
+ /**
+ * Adds result groupings for each namespace being queried for.
+ *
+ * @param prefixes Prefixes that we should prepend to all our filters
+ * @param maxNumResults The maximum number of results for each grouping to support.
+ * @param namespaceMap The namespace map contains all prefixed existing namespaces.
+ * @param resultSpecBuilder ResultSpecs as specified by client
+ */
+ private static void addPerNamespaceResultGroupings(
+ @NonNull Set<String> prefixes,
+ int maxNumResults,
+ @NonNull Map<String, Set<String>> namespaceMap,
+ @NonNull ResultSpecProto.Builder resultSpecBuilder) {
+ // Create a map of namespace to prefixedNamespaces. This is NOT necessarily the
+ // same as the list of namespaces. If a namespace exists under different packages and/or
+ // different databases, they should still be grouped together.
+ Map<String, List<String>> namespaceToPrefixedNamespaces = new ArrayMap<>();
+ for (String prefix : prefixes) {
+ Set<String> prefixedNamespaces = namespaceMap.get(prefix);
+ if (prefixedNamespaces == null) {
+ continue;
+ }
+ for (String prefixedNamespace : prefixedNamespaces) {
+ String namespace;
+ try {
+ namespace = removePrefix(prefixedNamespace);
+ } catch (AppSearchException e) {
+ // This should never happen. Skip this namespace if it does.
+ Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
+ continue;
+ }
+ List<String> groupedPrefixedNamespaces =
+ namespaceToPrefixedNamespaces.get(namespace);
+ if (groupedPrefixedNamespaces == null) {
+ groupedPrefixedNamespaces = new ArrayList<>();
+ namespaceToPrefixedNamespaces.put(namespace,
+ groupedPrefixedNamespaces);
+ }
+ groupedPrefixedNamespaces.add(prefixedNamespace);
+ }
+ }
+
+ for (List<String> namespaces : namespaceToPrefixedNamespaces.values()) {
+ resultSpecBuilder.addResultGroupings(
+ ResultSpecProto.ResultGrouping.newBuilder()
+ .addAllNamespaces(namespaces).setMaxResults(maxNumResults));
+ }
+ }
+
+ /**
+ * Find the intersection set of candidates existing in AppSearch and user specified filters.
+ *
+ * @param prefix The package and database's identifier.
+ * @param prefixedCandidates The set contains all prefixed candidates which are existing
+ * in a database.
+ * @param inputFilters The set contains all desired but un-prefixed filters of user.
+ * @param prefixedTargetFilters The output set contains all desired prefixed filters which
+ * are existing in the database.
+ */
+ private static void getIntersectedFilters(
+ @NonNull String prefix,
+ @NonNull Set<String> prefixedCandidates,
+ @NonNull List<String> inputFilters,
+ @NonNull Set<String> prefixedTargetFilters) {
+ if (inputFilters.isEmpty()) {
+ // Client didn't specify certain schemas to search over, add all candidates.
+ prefixedTargetFilters.addAll(prefixedCandidates);
+ } else {
+ // Client specified some filters to search over, check and only add those are
+ // existing in the database.
+ for (int i = 0; i < inputFilters.size(); i++) {
+ String prefixedTargetFilter = prefix + inputFilters.get(i);
+ if (prefixedCandidates.contains(prefixedTargetFilter)) {
+ prefixedTargetFilters.add(prefixedTargetFilter);
+ }
+ }
+ }
+ }
}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/TypePropertyPathToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/TypePropertyPathToProtoConverter.java
index 98f5642..1e81145 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/TypePropertyPathToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/TypePropertyPathToProtoConverter.java
@@ -37,15 +37,16 @@
/** Extracts {@link TypePropertyMask} information from a {@link Map}. */
@NonNull
- public static List<TypePropertyMask> toTypePropertyMaskList(@NonNull Map<String,
- List<String>> typePropertyPaths) {
+ public static List<TypePropertyMask.Builder> toTypePropertyMaskBuilderList(
+ @NonNull Map<String, List<String>> typePropertyPaths) {
Preconditions.checkNotNull(typePropertyPaths);
- List<TypePropertyMask> typePropertyMasks = new ArrayList<>(typePropertyPaths.size());
+ List<TypePropertyMask.Builder> typePropertyMaskBuilders =
+ new ArrayList<>(typePropertyPaths.size());
for (Map.Entry<String, List<String>> e : typePropertyPaths.entrySet()) {
- typePropertyMasks.add(
+ typePropertyMaskBuilders.add(
TypePropertyMask.newBuilder().setSchemaType(
- e.getKey()).addAllPaths(e.getValue()).build());
+ e.getKey()).addAllPaths(e.getValue()));
}
- return typePropertyMasks;
+ return typePropertyMaskBuilders;
}
}
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/camera/integration-tests/extensionstestapp/build.gradle b/camera/integration-tests/extensionstestapp/build.gradle
index f049cea..af4a3b4 100644
--- a/camera/integration-tests/extensionstestapp/build.gradle
+++ b/camera/integration-tests/extensionstestapp/build.gradle
@@ -52,11 +52,12 @@
implementation(libs.kotlinStdlib)
implementation("androidx.appcompat:appcompat:1.2.0")
implementation("androidx.activity:activity-ktx:1.2.0")
- implementation('androidx.concurrent:concurrent-futures-ktx:1.1.0')
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
+ implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
// Guava
implementation(libs.guavaAndroid)
+ implementation("androidx.viewpager2:viewpager2:1.0.0")
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultActivity.kt
index 8394543..52fcd2b 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultActivity.kt
@@ -193,13 +193,15 @@
companion object {
- fun getLensFacingStringFromId(lensFacing: Int): String = when (lensFacing) {
+ fun getLensFacingStringFromInt(lensFacing: Int): String = when (lensFacing) {
CameraMetadata.LENS_FACING_BACK -> "BACK"
CameraMetadata.LENS_FACING_FRONT -> "FRONT"
CameraMetadata.LENS_FACING_EXTERNAL -> "EXTERNAL"
else -> throw IllegalArgumentException("Invalid lens facing!!")
}
+ const val INVALID_LENS_FACING = -1
+
const val INTENT_EXTRA_KEY_CAMERA_ID = "CameraId"
const val INTENT_EXTRA_KEY_LENS_FACING = "LensFacing"
const val INTENT_EXTRA_KEY_EXTENSION_MODE = "ExtensionMode"
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultAdapter.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultAdapter.kt
index 8027ec5..43520df 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultAdapter.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultAdapter.kt
@@ -22,7 +22,7 @@
import android.widget.BaseAdapter
import android.widget.TextView
import androidx.camera.integration.extensions.R
-import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.getLensFacingStringFromId
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.getLensFacingStringFromInt
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_FAILED
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_NOT_SUPPORTED
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_NOT_TESTED
@@ -72,7 +72,7 @@
}
val padding = 10
- val lensFacingName = cameraLensFacingMap[item.key]?.let { getLensFacingStringFromId(it) }
+ val lensFacingName = cameraLensFacingMap[item.key]?.let { getLensFacingStringFromInt(it) }
textView.text = "Camera ${item.key} [$lensFacingName]"
textView.setPadding(padding, 0, padding, 0)
textView.compoundDrawablePadding = padding
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt
index 93bee76..c3a2051 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt
@@ -32,14 +32,13 @@
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_REQUEST_CODE
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_RESULT_MAP
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_TEST_RESULT
-import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.getLensFacingStringFromId
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INVALID_LENS_FACING
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.getLensFacingStringFromInt
import androidx.camera.integration.extensions.validation.TestResults.Companion.INVALID_EXTENSION_MODE
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_NOT_SUPPORTED
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_NOT_TESTED
import androidx.core.app.ActivityCompat
-private const val INVALID_LENS_FACING = -1
-
class ExtensionValidationResultActivity : AppCompatActivity() {
private val extensionTestResultMap = linkedMapOf<Int, Int>()
private val result = Intent()
@@ -69,7 +68,7 @@
setResult(requestCode, result)
supportActionBar?.title = "${resources.getString(R.string.extensions_validator)}"
- supportActionBar!!.subtitle = "Camera $cameraId [${getLensFacingStringFromId(lensFacing)}]"
+ supportActionBar!!.subtitle = "Camera $cameraId [${getLensFacingStringFromInt(lensFacing)}]"
val layoutInflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
adapter = ExtensionValidationResultAdapter(layoutInflater, extensionTestResultMap)
@@ -113,6 +112,7 @@
private fun startCaptureValidationActivity(cameraId: String, mode: Int) {
val intent = Intent(this, ImageValidationActivity::class.java)
intent.putExtra(INTENT_EXTRA_KEY_CAMERA_ID, cameraId)
+ intent.putExtra(INTENT_EXTRA_KEY_LENS_FACING, lensFacing)
intent.putExtra(INTENT_EXTRA_KEY_EXTENSION_MODE, mode)
intent.putExtra(INTENT_EXTRA_KEY_REQUEST_CODE, imageValidationActivityRequestCode)
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
index 6272aae..325b2d7 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
@@ -54,7 +54,10 @@
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_EXTENSION_MODE
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_IMAGE_URI
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_LENS_FACING
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_REQUEST_CODE
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INVALID_LENS_FACING
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.getLensFacingStringFromInt
import androidx.camera.integration.extensions.validation.TestResults.Companion.INVALID_EXTENSION_MODE
import androidx.camera.integration.extensions.validation.TestResults.Companion.createCameraSelectorById
import androidx.camera.integration.extensions.validation.TestResults.Companion.getExtensionModeStringFromId
@@ -79,6 +82,7 @@
private var extensionMode = INVALID_EXTENSION_MODE
private var extensionEnabled = true
private val result = Intent()
+ private var lensFacing = INVALID_LENS_FACING
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var extensionsManager: ExtensionsManager
private lateinit var cameraId: String
@@ -105,6 +109,7 @@
setContentView(R.layout.image_capture_activity)
cameraId = intent?.getStringExtra(INTENT_EXTRA_KEY_CAMERA_ID)!!
+ lensFacing = intent.getIntExtra(INTENT_EXTRA_KEY_LENS_FACING, INVALID_LENS_FACING)
extensionMode = intent.getIntExtra(INTENT_EXTRA_KEY_EXTENSION_MODE, INVALID_EXTENSION_MODE)
result.putExtra(INTENT_EXTRA_KEY_EXTENSION_MODE, extensionMode)
@@ -114,7 +119,8 @@
supportActionBar?.title = "${resources.getString(R.string.extensions_validator)}"
supportActionBar!!.subtitle =
- "Camera $cameraId [${getExtensionModeStringFromId(extensionMode)}]"
+ "Camera $cameraId [${getLensFacingStringFromInt(lensFacing)}]" +
+ "[${getExtensionModeStringFromId(extensionMode)}]"
viewFinder = findViewById(R.id.view_finder)
@@ -213,9 +219,17 @@
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
+ val filenamePrefix =
+ "[Camera-$cameraId][${getLensFacingStringFromInt(lensFacing)}]" +
+ "[${getExtensionModeStringFromId(extensionMode)}]"
+ val filename = if (extensionEnabled) {
+ "$filenamePrefix[Enabled]"
+ } else {
+ "$filenamePrefix[Disabled]"
+ }
val tempFile = File.createTempFile(
- getExtensionModeStringFromId(extensionMode),
- ".jpg",
+ filename,
+ "",
codeCacheDir
)
val outputStream = FileOutputStream(tempFile)
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
index 2a62393..1f5f738 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
@@ -16,14 +16,23 @@
package androidx.camera.integration.extensions.validation
+import android.content.ContentValues
import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
+import android.content.res.Configuration
import android.net.Uri
+import android.os.Build
import android.os.Bundle
+import android.provider.MediaStore
import android.util.Log
+import android.view.GestureDetector
+import android.view.Menu
+import android.view.MenuItem
+import android.view.MotionEvent
+import android.view.ScaleGestureDetector
+import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
+import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.integration.extensions.R
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_CAMERA_ID
@@ -31,18 +40,35 @@
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_EXTENSION_MODE
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_IMAGE_URI
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_LENS_FACING
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_REQUEST_CODE
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_TEST_RESULT
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INVALID_LENS_FACING
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.getLensFacingStringFromInt
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_BIND_FAIL
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_NONE
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_TAKE_PICTURE_FAILED
+import androidx.camera.integration.extensions.validation.PhotoFragment.Companion.decodeImageToBitmap
import androidx.camera.integration.extensions.validation.TestResults.Companion.INVALID_EXTENSION_MODE
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_FAILED
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_NOT_TESTED
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_PASSED
import androidx.camera.integration.extensions.validation.TestResults.Companion.getExtensionModeStringFromId
import androidx.core.app.ActivityCompat
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import androidx.viewpager2.widget.ViewPager2
+import java.io.File
+import java.io.FileInputStream
+import java.io.OutputStream
+import java.text.Format
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+import kotlin.math.max
+import kotlin.math.min
private const val TAG = "ImageValidationActivity"
@@ -50,18 +76,27 @@
private var extensionMode = INVALID_EXTENSION_MODE
private val result = Intent()
+ private var lensFacing = INVALID_LENS_FACING
private lateinit var cameraId: String
- private lateinit var photoViewer: ImageView
private lateinit var failButton: ImageButton
private lateinit var passButton: ImageButton
private lateinit var captureButton: ImageButton
+ private lateinit var viewPager: ViewPager2
+ private lateinit var photoImageView: ImageView
private val imageCaptureActivityRequestCode = ImageCaptureActivity::class.java.hashCode() % 1000
+ private val imageUris = arrayListOf<Pair<Uri, Int>>()
+ private var scaledBitmapWidth = 0
+ private var scaledBitmapHeight = 0
+ private var currentScale = 1.0f
+ private var translationX = 0.0f
+ private var translationY = 0.0f
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.image_validation_activity)
cameraId = intent?.getStringExtra(INTENT_EXTRA_KEY_CAMERA_ID)!!
+ lensFacing = intent.getIntExtra(INTENT_EXTRA_KEY_LENS_FACING, INVALID_LENS_FACING)
extensionMode = intent.getIntExtra(INTENT_EXTRA_KEY_EXTENSION_MODE, INVALID_EXTENSION_MODE)
result.putExtra(INTENT_EXTRA_KEY_EXTENSION_MODE, extensionMode)
@@ -71,10 +106,14 @@
supportActionBar?.title = "${resources.getString(R.string.extensions_validator)}"
supportActionBar!!.subtitle =
- "Camera $cameraId [${getExtensionModeStringFromId(extensionMode)}]"
+ "Camera $cameraId [${getLensFacingStringFromInt(lensFacing)}]" +
+ "[${getExtensionModeStringFromId(extensionMode)}]"
- photoViewer = findViewById(R.id.photo_viewer)
+ viewPager = findViewById(R.id.photo_view_pager)
+ photoImageView = findViewById(R.id.photo_image_view)
+
setupButtonControls()
+ setupGestureControls()
startCaptureImageActivity(cameraId, extensionMode)
}
@@ -103,14 +142,118 @@
// Returns without capturing a picture
if (uri == null) {
- finish()
+ // Closes the activity if there is no image captured.
+ if (imageUris.isEmpty()) {
+ finish()
+ }
return
}
val rotationDegrees = data?.getIntExtra(INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES, 0)!!
+ imageUris.add(Pair(uri, rotationDegrees))
- photoViewer.setImageBitmap(decodeImageFromUri(uri))
- photoViewer.rotation = rotationDegrees.toFloat()
+ viewPager.adapter = PhotoPagerAdapter(this)
+ viewPager.currentItem = imageUris.size - 1
+ viewPager.visibility = View.VISIBLE
+ viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
+ override fun onPageSelected(position: Int) {
+ super.onPageSelected(position)
+ updatePhotoImageView()
+ }
+ })
+
+ updatePhotoImageView()
+ resetAndHidePhotoImageView()
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ resetAndHidePhotoImageView()
+ updateScaledBitmapDims(scaledBitmapWidth, scaledBitmapHeight)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.image_validation_menu, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.menu_save_image -> {
+ saveCurrentImage()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ private fun saveCurrentImage() {
+ val formatter: Format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
+ val savedFileName =
+ "${imageUris[viewPager.currentItem].first.lastPathSegment}" +
+ "[${formatter.format(Calendar.getInstance().time)}].jpg"
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val contentValues = ContentValues()
+ contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, savedFileName)
+ contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+ contentValues.put(
+ MediaStore.MediaColumns.RELATIVE_PATH,
+ "Pictures/ExtensionsValidation"
+ )
+ contentValues.put(MediaStore.Images.Media.IS_PENDING, 1)
+
+ val outputUri =
+ contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+ val resultToastMsg: String
+
+ if (outputUri == null) {
+ resultToastMsg = "Failed to export image - $savedFileName!"
+ } else {
+ if (copyTempFileToOutputUri(imageUris[viewPager.currentItem].first, outputUri)) {
+ resultToastMsg =
+ "Image is saved as Pictures/ExtensionsValidation/$savedFileName."
+ } else {
+ resultToastMsg = "Failed to export image - $savedFileName!"
+ }
+ contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
+ contentResolver.update(outputUri, contentValues, null, null)
+ }
+ Toast.makeText(this, resultToastMsg, Toast.LENGTH_LONG).show()
+ } else {
+ Log.e(TAG, "The known devices which support Extensions should be at least" +
+ " Android Q!")
+ }
+ }
+
+ /**
+ * Copies temp file to output [Uri].
+ *
+ * @return false if the [Uri] is not writable.
+ */
+ private fun copyTempFileToOutputUri(tempFileUri: Uri, uri: Uri): Boolean {
+ contentResolver.openOutputStream(uri).use { outputStream ->
+ if (tempFileUri.path == null || outputStream == null) {
+ return false
+ }
+
+ val tempFile = File(tempFileUri.path!!)
+ copyTempFileToOutputStream(tempFile, outputStream)
+ }
+ return true
+ }
+
+ private fun copyTempFileToOutputStream(
+ tempFile: File,
+ outputStream: OutputStream
+ ) {
+ FileInputStream(tempFile).use { `in` ->
+ val buf = ByteArray(1024)
+ var len: Int
+ while (`in`.read(buf).also { len = it } > 0) {
+ outputStream.write(buf, 0, len)
+ }
+ }
}
private fun setupButtonControls() {
@@ -135,6 +278,7 @@
private fun startCaptureImageActivity(cameraId: String, mode: Int) {
val intent = Intent(this, ImageCaptureActivity::class.java)
intent.putExtra(INTENT_EXTRA_KEY_CAMERA_ID, cameraId)
+ intent.putExtra(INTENT_EXTRA_KEY_LENS_FACING, lensFacing)
intent.putExtra(INTENT_EXTRA_KEY_EXTENSION_MODE, mode)
intent.putExtra(INTENT_EXTRA_KEY_REQUEST_CODE, imageCaptureActivityRequestCode)
@@ -146,10 +290,186 @@
)
}
- private fun decodeImageFromUri(uri: Uri): Bitmap {
- val parcelFileDescriptor = this.contentResolver.openFileDescriptor(uri, "r")
- val bitmap = BitmapFactory.decodeFileDescriptor(parcelFileDescriptor?.fileDescriptor)
- parcelFileDescriptor?.close()
- return bitmap
+ /** Adapter class used to present a fragment containing one photo or video as a page */
+ inner class PhotoPagerAdapter(fragmentActivity: FragmentActivity) :
+ FragmentStateAdapter(fragmentActivity) {
+ override fun getItemCount(): Int {
+ return imageUris.size
+ }
+
+ override fun createFragment(position: Int): Fragment {
+ // Set scale gesture listener to the fragments inside the ViewPager2 so that we can
+ // switch to another photo view which supports the translation function in the X
+ // direction. Otherwise, the fragments inside the ViewPager2 will eat the X direction
+ // movement events for the ViewPager2's page switch function. But we'll need the
+ // translation function in X direction after the photo is zoomed in.
+ val scaleGestureListener: ScaleGestureDetector.SimpleOnScaleGestureListener =
+ object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
+ override fun onScale(detector: ScaleGestureDetector): Boolean {
+ updatePhotoViewScale(detector.scaleFactor)
+ return true
+ }
+ }
+
+ return PhotoFragment(
+ imageUris[position].first,
+ imageUris[position].second,
+ scaleGestureListener
+ )
+ }
+ }
+
+ private fun setupGestureControls() {
+ // Registers the scale gesture event to allow the users to scale the photo image view
+ // between 1.0 and 3.0 times.
+ val scaleGestureListener: ScaleGestureDetector.SimpleOnScaleGestureListener =
+ object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
+ override fun onScale(detector: ScaleGestureDetector): Boolean {
+ updatePhotoViewScale(detector.scaleFactor)
+ return true
+ }
+ }
+
+ // Registers double tap event to reset and hide the photo image view.
+ val onDoubleTapGestureListener: GestureDetector.OnGestureListener =
+ object : GestureDetector.SimpleOnGestureListener() {
+ override fun onDoubleTap(e: MotionEvent): Boolean {
+ resetAndHidePhotoImageView()
+ return true
+ }
+ }
+
+ val scaleDetector = ScaleGestureDetector(this, scaleGestureListener)
+ val doubleTapDetector = GestureDetector(this, onDoubleTapGestureListener)
+ var previousX = 0.0f
+ var previousY = 0.0f
+
+ photoImageView.setOnTouchListener { _, e: MotionEvent? ->
+ if (photoImageView.visibility != View.VISIBLE) {
+ return@setOnTouchListener false
+ }
+
+ val doubleTapProcessed = doubleTapDetector.onTouchEvent(e)
+ val scaleGestureProcessed = scaleDetector.onTouchEvent(e)
+
+ when (e?.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ previousX = e.x
+ previousY = e.y
+ }
+ MotionEvent.ACTION_MOVE -> {
+ updatePhotoImageViewTranslation(e.x, e.y, previousX, previousY)
+ }
+ }
+
+ doubleTapProcessed || scaleGestureProcessed
+ }
+ }
+
+ internal fun updatePhotoViewScale(scaleFactor: Float) {
+ currentScale *= scaleFactor
+
+ // Don't let the object get too small or too large.
+ currentScale = max(1.0f, min(currentScale, 3.0f))
+
+ photoImageView.scaleX = currentScale
+ photoImageView.scaleY = currentScale
+
+ // Shows the photoImageView when the scale is larger than 1.0f. Hides the photoImageView
+ // when the scale has been reduced as 1.0f.
+ if (photoImageView.visibility != View.VISIBLE && currentScale > 1.0f) {
+ photoImageView.visibility = View.VISIBLE
+ viewPager.visibility = View.INVISIBLE
+ } else if (photoImageView.visibility == View.VISIBLE && currentScale == 1.0f) {
+ resetAndHidePhotoImageView()
+ }
+ }
+
+ private fun updatePhotoImageViewTranslation(
+ x: Float,
+ y: Float,
+ previousX: Float,
+ previousY: Float
+ ) {
+ val newTranslationX = translationX + x - previousX
+
+ if (scaledBitmapWidth * currentScale > photoImageView.width) {
+ val maxTranslationX = (scaledBitmapWidth * currentScale - photoImageView.width) / 2
+
+ translationX = if (newTranslationX >= 0) {
+ if (maxTranslationX - newTranslationX >= 0) {
+ newTranslationX
+ } else {
+ maxTranslationX
+ }
+ } else {
+ if (maxTranslationX + newTranslationX >= 0) {
+ newTranslationX
+ } else {
+ -maxTranslationX
+ }
+ }
+ photoImageView.translationX = translationX
+ }
+
+ val newTranslationY = translationY + y - previousY
+
+ if (scaledBitmapHeight * currentScale > photoImageView.height) {
+ val maxTranslationY = (scaledBitmapHeight * currentScale - photoImageView.height) / 2
+
+ translationY = if (newTranslationY >= 0) {
+ if (maxTranslationY - newTranslationY >= 0) {
+ newTranslationY
+ } else {
+ maxTranslationY
+ }
+ } else {
+ if (maxTranslationY + newTranslationY >= 0) {
+ newTranslationY
+ } else {
+ -maxTranslationY
+ }
+ }
+ photoImageView.translationY = translationY
+ }
+ }
+
+ internal fun updatePhotoImageView() {
+ val bitmap = decodeImageToBitmap(
+ this@ImageValidationActivity.contentResolver,
+ imageUris[viewPager.currentItem].first,
+ imageUris[viewPager.currentItem].second
+ )
+
+ photoImageView.setImageBitmap(bitmap)
+ updateScaledBitmapDims(bitmap.width, bitmap.height)
+
+ // Updates the index and file name to the subtitle
+ supportActionBar!!.subtitle = "[${viewPager.currentItem + 1}/${imageUris.size}]" +
+ "${imageUris[viewPager.currentItem].first.lastPathSegment}"
+ }
+
+ private fun updateScaledBitmapDims(width: Int, height: Int) {
+ val scale: Float
+ if (width * photoImageView.height / photoImageView.width > height) {
+ scale = photoImageView.width.toFloat() / width
+ } else {
+ scale = photoImageView.height.toFloat() / height
+ }
+
+ scaledBitmapWidth = (width * scale).toInt()
+ scaledBitmapHeight = (height * scale).toInt()
+ }
+
+ internal fun resetAndHidePhotoImageView() {
+ viewPager.visibility = View.VISIBLE
+ photoImageView.visibility = View.INVISIBLE
+ photoImageView.scaleX = 1.0f
+ photoImageView.scaleY = 1.0f
+ photoImageView.translationX = 0.0f
+ photoImageView.translationY = 0.0f
+ currentScale = 1.0f
+ translationX = 0.0f
+ translationY = 0.0f
}
}
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/PhotoFragment.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/PhotoFragment.kt
new file mode 100644
index 0000000..6c3a5c7
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/PhotoFragment.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.extensions.validation
+
+import android.app.Activity
+import android.content.ContentResolver
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Matrix
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.ScaleGestureDetector
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.camera.integration.extensions.R
+import androidx.fragment.app.Fragment
+import androidx.viewpager2.widget.ViewPager2
+
+/**
+ * Fragment used for each individual page showing a photo inside [ImageValidationActivity].
+ *
+ * @param imageUri The image uri to be displayed in this photo fragment
+ * @param rotationDegrees The rotation degrees to rotate the image to the upright direction
+ * @param scaleGestureListener The scale gesture listener which allow the caller activity to
+ * receive the scale events to switch to another photo view which supports the translation
+ * function in the X direction. It is because this fragment will be put inside a [ViewPager2]
+ * and it will eat the X direction movement events for the [ViewPager2]'s page switch function. But
+ * we'll need the translation function in X direction after the photo is zoomed in.
+ */
+class PhotoFragment constructor(
+ private val imageUri: Uri,
+ private val rotationDegrees: Int,
+ private val scaleGestureListener: ScaleGestureDetector.SimpleOnScaleGestureListener?
+) :
+ Fragment() {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = inflater.inflate(R.layout.single_photo_viewer, container, false)
+
+ private lateinit var photoViewer: ImageView
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ photoViewer = view.findViewById(R.id.imageView)
+ photoViewer.setImageBitmap(
+ decodeImageToBitmap(
+ (requireActivity() as Activity).contentResolver,
+ imageUri,
+ rotationDegrees
+ )
+ )
+
+ setPhotoViewerScaleGestureListener()
+ }
+
+ private fun setPhotoViewerScaleGestureListener() {
+ scaleGestureListener?.let {
+ val scaleDetector = ScaleGestureDetector(requireContext(), scaleGestureListener)
+ photoViewer.setOnTouchListener { _, e: MotionEvent? ->
+ scaleDetector.onTouchEvent(e)
+ }
+ }
+ }
+
+ companion object {
+ fun decodeImageToBitmap(
+ contentResolver: ContentResolver,
+ imageUri: Uri,
+ rotationDegrees: Int
+ ): Bitmap {
+ val parcelFileDescriptor = contentResolver.openFileDescriptor(imageUri, "r")
+ val bitmap = BitmapFactory.decodeFileDescriptor(parcelFileDescriptor?.fileDescriptor)
+ parcelFileDescriptor?.close()
+
+ // Rotates the bitmap to the correct orientation
+ val matrix = Matrix()
+ matrix.postRotate(rotationDegrees.toFloat())
+ return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/image_validation_activity.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/image_validation_activity.xml
index cc0a543..b8d0119 100644
--- a/camera/integration-tests/extensionstestapp/src/main/res/layout/image_validation_activity.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/image_validation_activity.xml
@@ -22,11 +22,18 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <ImageView
- android:id="@+id/photo_viewer"
+ <androidx.viewpager2.widget.ViewPager2
+ android:id="@+id/photo_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
+ <ImageView
+ android:id="@+id/photo_image_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="invisible"
+ />
+
<ImageButton
android:id="@+id/fail_button"
android:layout_width="@dimen/round_button_medium"
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/single_photo_viewer.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/single_photo_viewer.xml
new file mode 100644
index 0000000..f4bc18f
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/single_photo_viewer.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/constraintLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ImageView
+ android:id="@+id/imageView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/menu/image_validation_menu.xml b/camera/integration-tests/extensionstestapp/src/main/res/menu/image_validation_menu.xml
new file mode 100644
index 0000000..aca8d62
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/menu/image_validation_menu.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_save_image"
+ android:title="Save image" />
+</menu>
\ No newline at end of file
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/build.gradle b/compose/foundation/foundation-layout/build.gradle
index 27abb26..fa9377e 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -51,6 +51,12 @@
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.0.0")
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation("androidx.activity:activity-compose:1.3.1")
+ // old version of common-java8 conflicts with newer version, because both have
+ // DefaultLifecycleEventObserver.
+ // Outside of androidx this is resolved via constraint added to lifecycle-common,
+ // but it doesn't work in androidx.
+ // See aosp/1804059
+ androidTestImplementation(project(":lifecycle:lifecycle-common-java8"))
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.junit)
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/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
index dc008b7..ff02ea8 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
@@ -18,15 +18,18 @@
import android.os.Build
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.TextFieldScrollerPosition
import androidx.compose.foundation.text.TextLayoutResultProxy
import androidx.compose.foundation.text.maxLinesHeight
@@ -53,11 +56,17 @@
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.isPopup
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipe
import androidx.compose.ui.test.swipeDown
@@ -225,7 +234,7 @@
}
}
- rule.runOnIdle {}
+ rule.waitForIdle()
rule.onNodeWithTag(tag)
.captureToImage()
@@ -260,7 +269,7 @@
}
}
- rule.runOnIdle {}
+ rule.waitForIdle()
rule.onNodeWithTag(tag)
.captureToImage()
@@ -545,6 +554,118 @@
}
}
+ @Test
+ fun textField_cursorHandle_hidden_whenScrolledOutOfView() {
+ val size = 100
+ val tag = "Text"
+
+ with(rule.density) {
+ rule.setContent {
+ BasicTextField(
+ value = longText,
+ onValueChange = {},
+ modifier = Modifier
+ .padding(size.toDp())
+ .size(size.toDp())
+ .testTag(tag)
+ )
+ }
+ }
+
+ // Click to focus and show handle.
+ rule.onNodeWithTag(tag)
+ .performClick()
+
+ // Check that handle is displayed (if it's not, we can't check if it gets hidden).
+ rule.onNode(isPopup())
+ .assertIsDisplayed()
+ // TODO(b/139861182) Assert handle position is within field bounds?
+
+ // Scroll up by twice the height to move the cursor out of the visible area.
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ down(center)
+ moveBy(Offset(x = 0f, y = -size * 2f))
+ }
+
+ // Check that cursor is hidden.
+ rule.onNode(isPopup()).assertDoesNotExist()
+
+ // Scroll back and make sure the handles are shown again.
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ moveBy(Offset(x = 0f, y = size * 2f))
+ }
+
+ rule.onNode(isPopup()).assertIsDisplayed()
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun textField_selectionHandles_hidden_whenScrolledOutOfView() {
+ val size = 200
+ val tag = "Text"
+
+ fun assertHandlesDisplayed() {
+ rule.onAllNodes(isPopup())
+ .assertCountEquals(2)
+ .apply {
+ (0 until 2)
+ .map(::get)
+ .forEach {
+ it.assertIsDisplayed()
+ // TODO(b/139861182) Assert handle position is within field bounds?
+ }
+ }
+ }
+
+ with(rule.density) {
+ rule.setContent {
+ BasicTextField(
+ value = longText,
+ onValueChange = {},
+ modifier = Modifier
+ .border(0.dp, Color.Gray)
+ .padding(size.toDp())
+ .border(0.dp, Color.Black)
+ .size(size.toDp())
+ .testTag(tag)
+ )
+ }
+ }
+
+ // Select something to ensure both handles are visible.
+ rule.onNodeWithTag(tag)
+ // TODO(b/209698586) Use performTextInputSelection once that method actually updates
+ // the text handles.
+ // .performTextInputSelection(TextRange(0, 1))
+ .performTouchInput {
+ longClick()
+ }
+
+ // Check that both handles are displayed (if not, we can't check that they get hidden).
+ assertHandlesDisplayed()
+
+ // Scroll up by twice the height to move the cursor out of the visible area.
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ down(center)
+ moveBy(Offset(x = 0f, y = -size * 2f))
+ }
+
+ // Check that cursor is hidden.
+ rule.onAllNodes(isPopup())
+ .assertCountEquals(0)
+
+ // Scroll back and make sure the handles are shown again.
+ rule.onNodeWithTag(tag)
+ .performTouchInput {
+ moveBy(Offset(x = 0f, y = size * 2f))
+ }
+
+ assertHandlesDisplayed()
+ }
+
private fun ComposeContentTestRule.setupHorizontallyScrollableContent(
scrollerPosition: TextFieldScrollerPosition,
text: String,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 048a88f..babaf59 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -344,8 +344,13 @@
} else {
manager.hideSelectionToolbar()
}
- state.showSelectionHandleStart = manager.isSelectionHandleInVisibleBound(true)
- state.showSelectionHandleEnd = manager.isSelectionHandleInVisibleBound(false)
+ state.showSelectionHandleStart =
+ manager.isSelectionHandleInVisibleBound(isStartHandle = true)
+ state.showSelectionHandleEnd =
+ manager.isSelectionHandleInVisibleBound(isStartHandle = false)
+ } else if (state.handleState == HandleState.Cursor) {
+ state.showCursorHandle =
+ manager.isSelectionHandleInVisibleBound(isStartHandle = true)
}
state.layoutResult?.let { layoutResult ->
state.inputSession?.let { inputSession ->
@@ -703,15 +708,23 @@
var showFloatingToolbar = false
/**
- * A flag to check if the start selection handle should show.
+ * True if the position of the selection start handle is within a visible part of the window
+ * (i.e. not scrolled out of view) and the handle should be drawn.
*/
var showSelectionHandleStart by mutableStateOf(false)
/**
- * A flag to check if the end selection handle should show.
+ * True if the position of the selection end handle is within a visible part of the window
+ * (i.e. not scrolled out of view) and the handle should be drawn.
*/
var showSelectionHandleEnd by mutableStateOf(false)
+ /**
+ * True if the position of the cursor is within a visible part of the window (i.e. not scrolled
+ * out of view) and the handle should be drawn.
+ */
+ var showCursorHandle by mutableStateOf(false)
+
val keyboardActionRunner: KeyboardActionRunner = KeyboardActionRunner()
var onValueChange: (TextFieldValue) -> Unit = {}
@@ -849,17 +862,11 @@
@Composable
internal fun TextFieldCursorHandle(manager: TextFieldSelectionManager) {
- val offset = manager.offsetMapping.originalToTransformed(manager.value.selection.start)
- val observer = remember(manager) { manager.cursorDragObserver() }
- manager.state?.layoutResult?.value?.let {
- val cursorRect = it.getCursorRect(
- offset.coerceIn(0, it.layoutInput.text.length)
- )
- val x = with(LocalDensity.current) {
- cursorRect.left + DefaultCursorThickness.toPx() / 2
- }
+ if (manager.state?.showCursorHandle == true) {
+ val observer = remember(manager) { manager.cursorDragObserver() }
+ val position = manager.getCursorPosition(LocalDensity.current)
CursorHandle(
- handlePosition = Offset(x, cursorRect.bottom),
+ handlePosition = position,
modifier = Modifier.pointerInput(observer) {
detectDragGesturesWithObserver(observer)
},
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
index 353a734..63e6ce0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
@@ -132,7 +132,7 @@
false
}
}
- .selectionMagnifier(this)
+ .then(if (shouldShowMagnifier) Modifier.selectionMagnifier(this) else Modifier)
private var previousPosition: Offset? = null
/**
@@ -189,6 +189,8 @@
var draggingHandle: Handle? by mutableStateOf(null)
private set
+ private val shouldShowMagnifier get() = draggingHandle != null
+
init {
selectionRegistrar.onPositionChangeCallback = { selectableId ->
if (
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
index d973268..1c9dc08 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.text.selection
+import androidx.compose.foundation.text.DefaultCursorThickness
import androidx.compose.foundation.text.Handle
import androidx.compose.foundation.text.HandleState
import androidx.compose.foundation.text.InternalFoundationTextApi
@@ -49,6 +50,7 @@
import androidx.compose.ui.text.input.getTextAfterSelection
import androidx.compose.ui.text.input.getTextBeforeSelection
import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import kotlin.math.max
import kotlin.math.min
@@ -557,6 +559,18 @@
)
}
+ internal fun getCursorPosition(density: Density): Offset {
+ val offset = offsetMapping.originalToTransformed(value.selection.start)
+ val layoutResult = state?.layoutResult!!.value
+ val cursorRect = layoutResult.getCursorRect(
+ offset.coerceIn(0, layoutResult.layoutInput.text.length)
+ )
+ val x = with(density) {
+ cursorRect.left + DefaultCursorThickness.toPx() / 2
+ }
+ return Offset(x, cursorRect.bottom)
+ }
+
/**
* This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
* to make the FloatingToolbar show up in the proper place. In addition, this function passes
diff --git a/compose/integration-tests/docs-snippets/build.gradle b/compose/integration-tests/docs-snippets/build.gradle
index d41ce68..b6c04ee 100644
--- a/compose/integration-tests/docs-snippets/build.gradle
+++ b/compose/integration-tests/docs-snippets/build.gradle
@@ -44,6 +44,13 @@
implementation(project(":navigation:navigation-compose"))
implementation("androidx.activity:activity-compose:1.3.1")
implementation(project(":lifecycle:lifecycle-viewmodel-compose"))
+ // old version of common-java8 conflicts with newer version, because both have
+ // DefaultLifecycleEventObserver.
+ // Outside of androidx this is resolved via constraint added to lifecycle-common,
+ // but it doesn't work in androidx.
+ // See aosp/1804059
+ implementation(project(":lifecycle:lifecycle-common-java8"))
+ implementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
implementation(project(":paging:paging-compose"))
implementation(libs.kotlinStdlib)
diff --git a/compose/integration-tests/macrobenchmark-target/build.gradle b/compose/integration-tests/macrobenchmark-target/build.gradle
index 623a666a..3b4567f 100644
--- a/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -20,6 +20,12 @@
implementation(libs.kotlinStdlib)
implementation(project(":activity:activity-compose"))
+ // old version of common-java8 conflicts with newer version, because both have
+ // DefaultLifecycleEventObserver.
+ // Outside of androidx this is resolved via constraint added to lifecycle-common,
+ // but it doesn't work in androidx.
+ // See aosp/1804059
+ implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
implementation(project(":compose:foundation:foundation-layout"))
implementation(project(":compose:material:material"))
implementation(project(":compose:runtime:runtime"))
diff --git a/compose/integration-tests/material-catalog/build.gradle b/compose/integration-tests/material-catalog/build.gradle
index 49d9f2c..65d3a75 100644
--- a/compose/integration-tests/material-catalog/build.gradle
+++ b/compose/integration-tests/material-catalog/build.gradle
@@ -52,6 +52,12 @@
implementation project(":compose:material3:material3:integration-tests:material3-catalog")
implementation "androidx.activity:activity-compose:1.3.1"
implementation project(":navigation:navigation-compose")
+ // old version of common-java8 conflicts with newer version, because both have
+ // DefaultLifecycleEventObserver.
+ // Outside of androidx this is resolved via constraint added to lifecycle-common,
+ // but it doesn't work in androidx.
+ // See aosp/1804059
+ implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
implementation "com.google.accompanist:accompanist-insets:0.18.0"
}
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
index a569c881..db462d1 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
+++ b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
@@ -52,6 +52,12 @@
androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
androidTestImplementation("androidx.activity:activity:1.2.0")
androidTestImplementation(projectOrArtifact(":activity:activity-compose"))
+ // old version of common-java8 conflicts with newer version, because both have
+ // DefaultLifecycleEventObserver.
+ // Outside of androidx this is resolved via constraint added to lifecycle-common,
+ // but it doesn't work in androidx.
+ // See aosp/1804059
+ androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
}
androidx {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index b012099..c985b20 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -689,17 +689,19 @@
return InvalidationResult.IGNORED // The scope was removed from the composition
if (!scope.canRecompose)
return InvalidationResult.IGNORED // The scope isn't able to be recomposed/invalidated
- if (isComposing && composer.tryImminentInvalidation(scope, instance)) {
- // The invalidation was redirected to the composer.
- return InvalidationResult.IMMINENT
- }
+ synchronized(lock) {
+ if (isComposing && composer.tryImminentInvalidation(scope, instance)) {
+ // The invalidation was redirected to the composer.
+ return InvalidationResult.IMMINENT
+ }
- // invalidations[scope] containing an explicit null means it was invalidated
- // unconditionally.
- if (instance == null) {
- invalidations[scope] = null
- } else {
- invalidations.addValue(scope, instance)
+ // invalidations[scope] containing an explicit null means it was invalidated
+ // unconditionally.
+ if (instance == null) {
+ invalidations[scope] = null
+ } else {
+ invalidations.addValue(scope, instance)
+ }
}
parent.invalidate(this)
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
index 25b9fb5..ca50efb 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -45,6 +45,7 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineDispatcher
@@ -3193,6 +3194,41 @@
thread.interrupt()
}
+ @Test
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun avoidRaceConditionWhenInvalidating() = compositionTest {
+ var scope: RecomposeScope? = null
+ var count = 0
+ var threadException: Exception? = null
+ val thread = thread {
+ try {
+ while (!Thread.interrupted()) {
+ scope?.invalidate()
+ count++
+ }
+ } catch (e: Exception) {
+ threadException = e
+ }
+ }
+
+ compose {
+ scope = currentRecomposeScope
+ Text("Some text")
+ Text("Count $count")
+ }
+
+ repeat(20) {
+ advance(ignorePendingWork = true)
+ delay(1)
+ }
+
+ thread.interrupt()
+ @Suppress("BlockingMethodInNonBlockingContext")
+ thread.join()
+ delay(10)
+ threadException?.let { throw it }
+ }
+
@Test // b/197064250 and others
fun canInvalidateDuringApplyChanges() = compositionTest {
var value by mutableStateOf(0)
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index e792129..e8f5622 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -44,6 +44,12 @@
implementation(projectOrArtifact(":compose:ui:ui-unit"))
implementation(projectOrArtifact(":compose:ui:ui-graphics"))
implementation(projectOrArtifact(":activity:activity-compose"))
+ // old version of common-java8 conflicts with newer version, because both have
+ // DefaultLifecycleEventObserver.
+ // Outside of androidx this is resolved via constraint added to lifecycle-common,
+ // but it doesn't work in androidx.
+ // See aosp/1804059
+ implementation(projectOrArtifact(":lifecycle:lifecycle-common-java8"))
implementation(libs.testCore)
implementation(libs.testRules)
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index f1e7484..7b39480 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -102,6 +102,13 @@
androidAndroidTest.dependencies {
implementation(project(":compose:ui:ui-test-junit4"))
+ // old version of common-java8 conflicts with newer version, because both have
+ // DefaultLifecycleEventObserver.
+ // Outside of androidx this is resolved via constraint added to lifecycle-common,
+ // but it doesn't work in androidx.
+ // See aosp/1804059
+ implementation(project(":lifecycle:lifecycle-common-java8"))
+ implementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
implementation(libs.junit)
implementation(libs.testRunner)
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt
index c26798e..db86c40 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt
@@ -127,6 +127,56 @@
}
@Test
+ fun getAnimatedPropertiesWithNotSyncedTime() {
+ var rotationAnimation: ComposeAnimation? = null
+ var offsetAnimation: ComposeAnimation? = null
+ var animatedVisibility: Transition<Any>? = null
+
+ composeRule.setContent {
+ rotationAnimation = setUpRotationColorScenario()
+ offsetAnimation = setUpOffsetScenario()
+ animatedVisibility = createAnimationVisibility(1000)
+ }
+ composeRule.waitForIdle()
+ testClock.trackAnimatedVisibility(animatedVisibility!!)
+ composeRule.waitForIdle()
+ val animatedVisibilityComposeAnimation = testClock.trackedAnimatedVisibility.single()
+ testClock.setClockTimes(
+ mapOf(
+ rotationAnimation!! to 500,
+ offsetAnimation!! to 200,
+ animatedVisibilityComposeAnimation to 800
+ )
+ )
+ composeRule.waitForIdle()
+
+ var animatedProperties = testClock.getAnimatedProperties(rotationAnimation!!)
+ val rotation = animatedProperties.single { it.label == "myRotation" }
+ // We're animating from RC1 (0 degrees) to RC3 (360 degrees). There is a transition of
+ // 1000ms defined for the rotation, and we set the clock to 50% of this time.
+ assertEquals(180f, rotation.value as Float, eps)
+
+ animatedProperties = testClock.getAnimatedProperties(offsetAnimation!!)
+ val offset = animatedProperties.single { it.label == "myOffset" }
+ // We're animating from O1 (0) to O2 (100). There is a transition of 800ms defined for
+ // the offset, and we set the clock to 25% of this time.
+ assertEquals(25f, offset.value as Float, eps)
+
+ animatedProperties = testClock.getAnimatedProperties(animatedVisibilityComposeAnimation)
+ val scale = animatedProperties.single { it.label == "box scale" }
+ // We're animating from invisible to visible, which means PreEnter (scale 0.5f) to
+ // Visible (scale 1f). Animation duration is 1000ms, so the current clock time
+ // corresponds to 80% of it.
+ assertEquals(0.9f, scale.value as Float, 0.0001f)
+
+ animatedProperties = testClock.getAnimatedProperties(animatedVisibilityComposeAnimation)
+ val alpha = animatedProperties.single { it.label == "Built-in alpha" }
+ // We're animating from invisible (Built-in alpha 0f) to visible (Built-in alpha 1f),
+ // 1000ms being the animation duration, clock time corresponds to 80% of it.
+ assertEquals(0.8f, alpha.value)
+ }
+
+ @Test
fun getAnimatedPropertiesReturnsAllDescendantAnimations() {
var transitionAnimation: ComposeAnimation? = null
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt
index a9932f0..573818c 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt
@@ -284,22 +284,29 @@
* via reflection from Android Studio.
*/
fun setClockTime(animationTimeMs: Long) {
- val timeNs = TimeUnit.MILLISECONDS.toNanos(animationTimeMs)
- trackedTransitions.forEach { composeAnimation ->
- composeAnimation.animationObject.let {
- val states = transitionStates[it] ?: return@let
- it.setPlaytimeAfterInitialAndTargetStateEstablished(
- states.current,
- states.target,
- timeNs
- )
- }
- }
- trackedAnimatedVisibility.forEach { composeAnimation ->
- composeAnimation.animationObject.let {
- val (current, target) =
- animatedVisibilityStates[it]?.toCurrentTargetPair() ?: return@let
- it.setPlaytimeAfterInitialAndTargetStateEstablished(current, target, timeNs)
+ setClockTimes((trackedTransitions + trackedAnimatedVisibility)
+ .associateWith { animationTimeMs })
+ }
+
+ /**
+ * Seeks each animation being tracked to the given [animationTimeMillis]. Expected to be called
+ * via reflection from Android Studio.
+ */
+ fun setClockTimes(animationTimeMillis: Map<ComposeAnimation, Long>) {
+ animationTimeMillis.forEach { (composeAnimation, millis) ->
+ val timeNs = TimeUnit.MILLISECONDS.toNanos(millis)
+ if (trackedTransitions.contains(composeAnimation)) {
+ (composeAnimation as TransitionComposeAnimation).animationObject.let {
+ val states = transitionStates[it] ?: return@let
+ it.setPlaytimeAfterInitialAndTargetStateEstablished(
+ states.current, states.target, timeNs)
+ }
+ } else if (trackedAnimatedVisibility.contains(composeAnimation)) {
+ (composeAnimation as AnimatedVisibilityComposeAnimation).animationObject.let {
+ val (current, target) =
+ animatedVisibilityStates[it]?.toCurrentTargetPair() ?: return@let
+ it.setPlaytimeAfterInitialAndTargetStateEstablished(current, target, timeNs)
+ }
}
}
setAnimationsTimeCallback.invoke()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
index 2c6747b..27ce9ff 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
@@ -116,7 +116,6 @@
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
-import org.junit.Assert.fail
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -505,21 +504,6 @@
validateSquareColors(outerColor = green, innerColor = white, size = 20)
}
- // Tests that calling measure multiple times on the same Measurable causes an exception
- @Test
- fun multipleMeasureCall() {
- val latch = CountDownLatch(1)
- activityTestRule.runOnUiThreadIR {
- activity.setContent {
- TwoMeasureLayout(50, latch) {
- AtLeastSize(50) {
- }
- }
- }
- }
- assertTrue(latch.await(1, TimeUnit.SECONDS))
- }
-
@Test
fun multiChildLayoutTest() {
val childrenCount = 3
@@ -4019,33 +4003,6 @@
}
@Composable
-fun TwoMeasureLayout(
- size: Int,
- latch: CountDownLatch,
- modifier: Modifier = Modifier,
- content: @Composable () -> Unit
-) {
- Layout(modifier = modifier, content = content) { measurables, _ ->
- val testConstraints = Constraints()
- measurables.forEach { it.measure(testConstraints) }
- val childConstraints = Constraints.fixed(size, size)
- try {
- val placeables2 = measurables.map { it.measure(childConstraints) }
- fail("Measuring twice on the same Measurable should throw an exception")
- layout(size, size) {
- placeables2.forEach { child ->
- child.placeRelative(0, 0)
- }
- }
- } catch (_: IllegalStateException) {
- // expected
- latch.countDown()
- }
- layout(0, 0) { }
- }
-}
-
-@Composable
fun Wrap(
modifier: Modifier = Modifier,
minWidth: Int = 0,
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
index dd0f380..b615ca8 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
@@ -50,6 +50,9 @@
on { snapshotObserver } doAnswer {
OwnerSnapshotObserver { it.invoke() }
}
+ on { forceMeasureTheSubtree(any()) } doAnswer {
+ delegate.forceMeasureTheSubtree(it.arguments[0] as LayoutNode)
+ }
}
)
if (firstMeasureCompleted) {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
index 9136657..484e2ce 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.layout
+import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.LayoutNode.LayoutState
import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
import androidx.compose.ui.unit.Constraints
@@ -1230,4 +1231,55 @@
assertThat(root.first.first.isPlaced).isFalse()
assertThat(root.first.first.isPlaced).isFalse()
}
+
+ @Test
+ fun remeasuringNodeSecondTimeWithinTheSameIteration() {
+ lateinit var node1: LayoutNode
+ lateinit var node2: LayoutNode
+ lateinit var node3: LayoutNode
+ lateinit var node4: LayoutNode
+ lateinit var node5: LayoutNode
+ val root = root {
+ size = 100
+ add(node {
+ node1 = this
+ size = 50
+ add(node {
+ node2 = this
+ add(node { node3 = this })
+ })
+ add(node { node4 = this })
+ })
+ add(node { node5 = this })
+ }
+
+ val delegate = createDelegate(root)
+
+ delegate.requestRemeasure(root)
+ // we change the root size so now node1 and node5 will be remeasured for the new size
+ root.size = 50
+ // we also want to remeasure node3. as node2 is not scheduled for remeasure node3 will
+ // be remeasured via owner.forceMeasureTheSubtree() logic.
+ delegate.requestRemeasure(node3)
+ // we also want node5 to synchronously request remeasure for already measured node1
+ node5.runDuringMeasure {
+ delegate.requestRemeasure(node1)
+ }
+ node2.toString()
+ node4.toString()
+ // this was crashing and reported as b/208675143
+ assertRemeasured(root) {
+ assertRemeasured(node1, times = 2) {
+ assertNotRemeasured(node2) {
+ assertRemeasured(node3) {
+ assertNotRemeasured(node4) {
+ assertRemeasured(node5) {
+ delegate.measureAndLayout()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasuringPlacingTwiceIsNotAllowedTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasuringPlacingTwiceIsNotAllowedTest.kt
new file mode 100644
index 0000000..a21ac3e
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasuringPlacingTwiceIsNotAllowedTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.layout
+
+import androidx.compose.ui.AtLeastSize
+import androidx.compose.ui.layout.Placeable.PlacementScope
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Constraints
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class MeasuringPlacingTwiceIsNotAllowedTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun measureTwiceInMeasureBlock() {
+ assertException(measureBlock = { measurable, constraints ->
+ measurable.measure(constraints)
+ measurable.measure(constraints)
+ })
+ }
+
+ @Test
+ fun measureTwiceInMeasureBlockWithDifferentConstraints() {
+ assertException(measureBlock = { measurable, _ ->
+ measurable.measure(Constraints.fixed(100, 100))
+ measurable.measure(Constraints.fixed(200, 200))
+ })
+ }
+
+ @Test
+ fun measureTwiceInLayoutBlock() {
+ assertException(layoutBlock = { measurable, constraints ->
+ measurable.measure(constraints)
+ measurable.measure(constraints)
+ })
+ }
+
+ @Test
+ fun measureInBothStages() {
+ assertException(
+ measureBlock = { measurable, constraints ->
+ measurable.measure(constraints)
+ },
+ layoutBlock = { measurable, constraints ->
+ measurable.measure(constraints)
+ }
+ )
+ }
+
+ @Test
+ fun placeTwiceWithTheSamePosition() {
+ assertException(
+ layoutBlock = { measurable, constraints ->
+ measurable.measure(constraints).also {
+ it.place(0, 0)
+ it.place(0, 0)
+ }
+ }
+ )
+ }
+
+ @Test
+ fun placeTwiceWithDifferentPositions() {
+ assertException(
+ layoutBlock = { measurable, constraints ->
+ measurable.measure(constraints).also {
+ it.place(0, 0)
+ it.place(10, 10)
+ }
+ }
+ )
+ }
+
+ @Test
+ fun placeTwiceWithLayer() {
+ assertException(
+ layoutBlock = { measurable, constraints ->
+ measurable.measure(constraints).also {
+ it.placeWithLayer(0, 0)
+ it.placeWithLayer(0, 0)
+ }
+ }
+ )
+ }
+
+ private fun assertException(
+ measureBlock: (Measurable, Constraints) -> Unit = { _, _ -> },
+ layoutBlock: PlacementScope.(Measurable, Constraints) -> Unit = { _, _ -> }
+ ) {
+ var exception: Exception? = null
+ rule.setContent {
+ Layout(content = {
+ AtLeastSize(50)
+ }) { measurables, constraints ->
+ try {
+ measureBlock(measurables.first(), constraints)
+ } catch (e: Exception) {
+ exception = e
+ }
+ layout(100, 100) {
+ try {
+ layoutBlock(measurables.first(), constraints)
+ } catch (e: Exception) {
+ exception = e
+ }
+ }
+ }
+ }
+ assertThat(exception).isNotNull()
+ }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt
index 28bb807..623b031 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt
@@ -147,6 +147,84 @@
assertThat(remeasurements).isEqualTo(2)
}
}
+
+ @Test
+ fun remeasuringChildDuringLayoutWithExtraLayer() {
+ val height = mutableStateOf(10)
+ var remeasurements = 0
+
+ rule.setContent {
+ WrapChildMeasureDuringLayout(onMeasured = { actualHeight ->
+ assertThat(actualHeight).isEqualTo(height.value)
+ remeasurements++
+ }) {
+ WrapChild {
+ Child(height)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasurements).isEqualTo(1)
+ height.value = 20
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasurements).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun remeasuringChildDuringLayout() {
+ val height = mutableStateOf(10)
+ var expectedHeight = 10
+ var remeasurements = 0
+
+ rule.setContent {
+ WrapChildMeasureDuringLayout(onMeasured = { actualHeight ->
+ assertThat(actualHeight).isEqualTo(expectedHeight)
+ remeasurements++
+ }) {
+ Child(height)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasurements).isEqualTo(1)
+ expectedHeight = 20
+ height.value = 20
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasurements).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun remeasuringChildDuringLayoutWithExtraLayerUsingIntrinsics() {
+ val height = mutableStateOf(10)
+ var remeasurements = 0
+
+ rule.setContent {
+ IntrinsicSizeAndMeasureDuringLayout(onMeasured = { actualHeight ->
+ assertThat(actualHeight).isEqualTo(height.value)
+ remeasurements++
+ }) {
+ WrapChild {
+ Child(height)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasurements).isEqualTo(1)
+ height.value = 20
+ }
+
+ rule.runOnIdle {
+ assertThat(remeasurements).isEqualTo(2)
+ }
+ }
}
@Composable
@@ -162,6 +240,38 @@
}
@Composable
+private fun WrapChildMeasureDuringLayout(
+ onMeasured: (Int) -> Unit = {},
+ content: @Composable () -> Unit
+) {
+ Layout(content = content) { measurables, constraints ->
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ val placeable = measurables.first()
+ .measure(constraints.copy(minHeight = 0, maxHeight = Constraints.Infinity))
+ onMeasured(placeable.height)
+ placeable.place(0, 0)
+ }
+ }
+}
+
+@Composable
+private fun IntrinsicSizeAndMeasureDuringLayout(
+ onMeasured: (Int) -> Unit = {},
+ content: @Composable () -> Unit
+) {
+ Layout(content = content) { measurables, constraints ->
+ val width = measurables.first().maxIntrinsicWidth(constraints.maxWidth)
+ val height = measurables.first().maxIntrinsicHeight(constraints.maxHeight)
+ layout(width, height) {
+ val placeable = measurables.first()
+ .measure(constraints.copy(minHeight = 0, maxHeight = Constraints.Infinity))
+ onMeasured(placeable.height)
+ placeable.place(0, 0)
+ }
+ }
+}
+
+@Composable
private fun NotPlaceChild(height: State<Int>, content: @Composable () -> Unit) {
Layout(content = content) { measurables, constraints ->
layout(constraints.maxWidth, height.value) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
index e7bac4b..90866fc 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
@@ -47,6 +47,10 @@
override val measureScope get() = layoutNode.measureScope
override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
+ // before rerunning the user's measure block reset previous measuredByParent for children
+ layoutNode._children.forEach {
+ it.measuredByParent = LayoutNode.UsageByParent.NotUsed
+ }
val measureResult = with(layoutNode.measurePolicy) {
layoutNode.measureScope.measure(layoutNode.children, constraints)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 71b3960..665a66e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -175,9 +175,6 @@
*/
internal var layoutState = Ready
- internal val wasMeasuredDuringThisIteration: Boolean
- get() = requireOwner().measureIteration == outerMeasurablePlaceable.measureIteration
-
/**
* A cache of modifiers to be used when setting and reusing previous modifiers.
*/
@@ -853,9 +850,21 @@
* Place this layout node again on the same position it was placed last time
*/
internal fun replace() {
- outerMeasurablePlaceable.replace()
+ try {
+ relayoutWithoutParentInProgress = true
+ outerMeasurablePlaceable.replace()
+ } finally {
+ relayoutWithoutParentInProgress = false
+ }
}
+ /**
+ * Is true during [replace] invocation. Helps to differentiate between the cases when our
+ * parent is measuring us during the measure block, and when we are remeasured individually
+ * because of some change. This could be useful to know if we need to record the placing order.
+ */
+ private var relayoutWithoutParentInProgress = false
+
internal fun draw(canvas: Canvas) = outerLayoutNodeWrapper.draw(canvas)
/**
@@ -936,7 +945,7 @@
}
if (parent != null) {
- if (parent.layoutState == LayingOut) {
+ if (!relayoutWithoutParentInProgress && parent.layoutState == LayingOut) {
// the parent is currently placing its children
check(placeOrder == NotPlacedPlaceOrder) {
"Place was called on a node which was placed already"
@@ -944,8 +953,9 @@
placeOrder = parent.nextChildPlaceOrder
parent.nextChildPlaceOrder++
}
- // if parent is not laying out we were asked to be relaid out without affecting the
- // parent. this means our placeOrder didn't change since the last time parent placed us
+ // if relayoutWithoutParentInProgress is true we were asked to be relaid out without
+ // affecting the parent. this means our placeOrder didn't change since the last time
+ // parent placed us.
} else {
// parent is null for the root node
placeOrder = 0
@@ -973,6 +983,11 @@
child.previousPlaceOrder = child.placeOrder
child.placeOrder = NotPlacedPlaceOrder
child.alignmentLines.usedDuringParentLayout = false
+ // before rerunning the user's layout block reset previous measuredByParent
+ // for children which we measured in the layout block during the last run.
+ if (child.measuredByParent == UsageByParent.InLayoutBlock) {
+ child.measuredByParent = UsageByParent.NotUsed
+ }
}
innerLayoutNodeWrapper.measureResult.placeChildren()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
index 775bdd3..3dac820 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
@@ -124,15 +124,11 @@
false
}
NeedsRelayout, Ready -> {
- if (duringMeasureLayout && layoutNode.wasMeasuredDuringThisIteration) {
- postponedMeasureRequests.add(layoutNode)
- } else {
- layoutNode.layoutState = NeedsRemeasure
- if (layoutNode.isPlaced || layoutNode.canAffectParent) {
- val parentLayoutState = layoutNode.parent?.layoutState
- if (parentLayoutState != NeedsRemeasure) {
- relayoutNodes.add(layoutNode)
- }
+ layoutNode.layoutState = NeedsRemeasure
+ if (layoutNode.isPlaced || layoutNode.canAffectParent) {
+ val parentLayoutState = layoutNode.parent?.layoutState
+ if (parentLayoutState != NeedsRemeasure) {
+ relayoutNodes.add(layoutNode)
}
}
!duringMeasureLayout
@@ -240,7 +236,6 @@
onPositionedDispatcher.onNodePositioned(layoutNode)
consistencyChecker?.assertConsistent()
}
- measureIteration++
// execute postponed `onRequestMeasure`
if (postponedMeasureRequests.isNotEmpty()) {
postponedMeasureRequests.fastForEach {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt
index c35cec7..4f01beb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt
@@ -44,13 +44,6 @@
private var lastLayerBlock: (GraphicsLayerScope.() -> Unit)? = null
private var lastZIndex: Float = 0f
- /**
- * A local version of [Owner.measureIteration] to ensure that [MeasureBlocks.measure]
- * is not called multiple times within a measure pass.
- */
- var measureIteration = -1L
- private set
-
override var parentData: Any? = null
private set
@@ -59,14 +52,25 @@
*/
override fun measure(constraints: Constraints): Placeable {
// when we measure the root it is like the virtual parent is currently laying out
- val parentState = layoutNode.parent?.layoutState ?: LayoutState.LayingOut
- layoutNode.measuredByParent = when (parentState) {
- LayoutState.Measuring -> LayoutNode.UsageByParent.InMeasureBlock
- LayoutState.LayingOut -> LayoutNode.UsageByParent.InLayoutBlock
- else -> throw IllegalStateException(
- "Measurable could be only measured from the parent's measure or layout block." +
- "Parents state is $parentState"
- )
+ val parent = layoutNode.parent
+ if (parent != null) {
+ check(
+ layoutNode.measuredByParent == LayoutNode.UsageByParent.NotUsed ||
+ @Suppress("DEPRECATION") layoutNode.canMultiMeasure
+ ) {
+ "measure() may not be called multiple times on the same Measurable. Current " +
+ "state ${layoutNode.measuredByParent}. Parent state ${parent.layoutState}."
+ }
+ layoutNode.measuredByParent = when (parent.layoutState) {
+ LayoutState.Measuring -> LayoutNode.UsageByParent.InMeasureBlock
+ LayoutState.LayingOut -> LayoutNode.UsageByParent.InLayoutBlock
+ else -> throw IllegalStateException(
+ "Measurable could be only measured from the parent's measure or layout block." +
+ "Parents state is ${parent.layoutState}"
+ )
+ }
+ } else {
+ layoutNode.measuredByParent = LayoutNode.UsageByParent.NotUsed
}
remeasure(constraints)
return this
@@ -77,16 +81,11 @@
*/
fun remeasure(constraints: Constraints): Boolean {
val owner = layoutNode.requireOwner()
- val iteration = owner.measureIteration
val parent = layoutNode.parent
@Suppress("Deprecation")
layoutNode.canMultiMeasure = layoutNode.canMultiMeasure ||
(parent != null && parent.canMultiMeasure)
@Suppress("Deprecation")
- check(measureIteration != iteration || layoutNode.canMultiMeasure) {
- "measure() may not be called multiple times on the same Measurable"
- }
- measureIteration = owner.measureIteration
if (layoutNode.layoutState == LayoutState.NeedsRemeasure ||
measurementConstraints != constraints
) {
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-ktx/build.gradle b/fragment/fragment-ktx/build.gradle
index fdc614d..de40fab 100644
--- a/fragment/fragment-ktx/build.gradle
+++ b/fragment/fragment-ktx/build.gradle
@@ -26,7 +26,7 @@
dependencies {
api(project(":fragment:fragment"))
- api("androidx.activity:activity-ktx:1.2.3") {
+ api(projectOrArtifact(":activity:activity-ktx")) {
because "Mirror fragment dependency graph for -ktx artifacts"
}
api("androidx.core:core-ktx:1.2.0") {
@@ -35,10 +35,10 @@
api("androidx.collection:collection-ktx:1.1.0") {
because "Mirror fragment dependency graph for -ktx artifacts"
}
- api("androidx.lifecycle:lifecycle-livedata-core-ktx:2.3.1") {
+ api(projectOrArtifact(":lifecycle:lifecycle-livedata-core-ktx")) {
because 'Mirror fragment dependency graph for -ktx artifacts'
}
- api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1")
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx"))
api(projectOrArtifact(":savedstate:savedstate-ktx")) {
because 'Mirror fragment dependency graph for -ktx artifacts'
}
@@ -64,12 +64,10 @@
// needed only while https://youtrack.jetbrains.com/issue/KT-47000 isn't resolved which is
// targeted to 1.6
-if (project.hasProperty("androidx.useMaxDepVersions")){
- tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=enable",
- ]
- }
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += [
+ "-Xjvm-default=enable",
+ ]
}
}
\ No newline at end of file
diff --git a/fragment/fragment-testing/build.gradle b/fragment/fragment-testing/build.gradle
index 93569ee..0177bdf 100644
--- a/fragment/fragment-testing/build.gradle
+++ b/fragment/fragment-testing/build.gradle
@@ -51,12 +51,10 @@
// needed only while https://youtrack.jetbrains.com/issue/KT-47000 isn't resolved which is
// targeted to 1.6
-if (project.hasProperty("androidx.useMaxDepVersions")){
- tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=enable",
- ]
- }
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += [
+ "-Xjvm-default=enable",
+ ]
}
}
\ No newline at end of file
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 39c0ddd..60abcaf 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -30,9 +30,9 @@
api("androidx.viewpager:viewpager:1.0.0")
api("androidx.loader:loader:1.0.0")
api(projectOrArtifact(":activity:activity"))
- api("androidx.lifecycle:lifecycle-livedata-core:2.3.1")
- api("androidx.lifecycle:lifecycle-viewmodel:2.3.1")
- api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1")
+ api(projectOrArtifact(":lifecycle:lifecycle-livedata-core"))
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
api(projectOrArtifact(":savedstate:savedstate"))
api("androidx.annotation:annotation-experimental:1.0.0")
api(libs.kotlinStdlib)
@@ -76,12 +76,10 @@
// needed only while https://youtrack.jetbrains.com/issue/KT-47000 isn't resolved which is
// targeted to 1.6
-if (project.hasProperty("androidx.useMaxDepVersions")){
- tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=enable",
- ]
- }
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += [
+ "-Xjvm-default=enable",
+ ]
}
-}
\ 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..4c1d460 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/glance/glance-appwidget/api/current.txt b/glance/glance-appwidget/api/current.txt
index 607ed48..acbfe95 100644
--- a/glance/glance-appwidget/api/current.txt
+++ b/glance/glance-appwidget/api/current.txt
@@ -54,7 +54,7 @@
method @androidx.compose.runtime.Composable public abstract void Content();
method public androidx.glance.appwidget.SizeMode getSizeMode();
method public androidx.glance.state.GlanceStateDefinition<?>? getStateDefinition();
- method public suspend Object? onDelete(androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public suspend Object? onDelete(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public final suspend Object? update(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
property public androidx.glance.appwidget.SizeMode sizeMode;
property public androidx.glance.state.GlanceStateDefinition<?>? stateDefinition;
@@ -148,30 +148,30 @@
public final class ApplyActionKt {
}
- public final class LaunchActivityIntentActionKt {
- method public static androidx.glance.action.Action actionLaunchActivity(android.content.Intent intent, optional androidx.glance.action.ActionParameters parameters);
- }
-
- public final class LaunchBroadcastActionKt {
- method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(String action, optional android.content.ComponentName? componentName);
- method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(android.content.Intent intent);
- method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(android.content.ComponentName componentName);
- method public static <T extends android.content.BroadcastReceiver> androidx.glance.action.Action actionLaunchBroadcastReceiver(Class<T> receiver);
- method public static inline <reified T extends android.content.BroadcastReceiver> androidx.glance.action.Action! actionLaunchBroadcastReceiver();
- }
-
- public final class LaunchServiceActionKt {
- method public static androidx.glance.action.Action actionLaunchService(android.content.Intent intent, optional boolean isForegroundService);
- method public static androidx.glance.action.Action actionLaunchService(android.content.ComponentName componentName, optional boolean isForegroundService);
- method public static <T extends android.app.Service> androidx.glance.action.Action actionLaunchService(Class<T> service, optional boolean isForegroundService);
- method public static inline <reified T extends android.app.Service> androidx.glance.action.Action! actionLaunchService(optional boolean isForegroundService);
- }
-
public final class RunCallbackActionKt {
method public static <T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action actionRunCallback(Class<T> callbackClass, optional androidx.glance.action.ActionParameters parameters);
method public static inline <reified T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action! actionRunCallback(optional androidx.glance.action.ActionParameters parameters);
}
+ public final class SendBroadcastActionKt {
+ method public static androidx.glance.action.Action actionSendBroadcast(String action, optional android.content.ComponentName? componentName);
+ method public static androidx.glance.action.Action actionSendBroadcast(android.content.Intent intent);
+ method public static androidx.glance.action.Action actionSendBroadcast(android.content.ComponentName componentName);
+ method public static <T extends android.content.BroadcastReceiver> androidx.glance.action.Action actionSendBroadcast(Class<T> receiver);
+ method public static inline <reified T extends android.content.BroadcastReceiver> androidx.glance.action.Action! actionSendBroadcast();
+ }
+
+ public final class StartActivityIntentActionKt {
+ method public static androidx.glance.action.Action actionStartActivity(android.content.Intent intent, optional androidx.glance.action.ActionParameters parameters);
+ }
+
+ public final class StartServiceActionKt {
+ method public static androidx.glance.action.Action actionStartService(android.content.Intent intent, optional boolean isForegroundService);
+ method public static androidx.glance.action.Action actionStartService(android.content.ComponentName componentName, optional boolean isForegroundService);
+ method public static <T extends android.app.Service> androidx.glance.action.Action actionStartService(Class<T> service, optional boolean isForegroundService);
+ method public static inline <reified T extends android.app.Service> androidx.glance.action.Action! actionStartService(optional boolean isForegroundService);
+ }
+
public final class ToggleableKt {
method public static androidx.glance.action.ActionParameters.Key<java.lang.Boolean> getToggleableStateKey();
property public static final androidx.glance.action.ActionParameters.Key<java.lang.Boolean> ToggleableStateKey;
diff --git a/glance/glance-appwidget/api/public_plus_experimental_current.txt b/glance/glance-appwidget/api/public_plus_experimental_current.txt
index 607ed48..acbfe95 100644
--- a/glance/glance-appwidget/api/public_plus_experimental_current.txt
+++ b/glance/glance-appwidget/api/public_plus_experimental_current.txt
@@ -54,7 +54,7 @@
method @androidx.compose.runtime.Composable public abstract void Content();
method public androidx.glance.appwidget.SizeMode getSizeMode();
method public androidx.glance.state.GlanceStateDefinition<?>? getStateDefinition();
- method public suspend Object? onDelete(androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public suspend Object? onDelete(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public final suspend Object? update(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
property public androidx.glance.appwidget.SizeMode sizeMode;
property public androidx.glance.state.GlanceStateDefinition<?>? stateDefinition;
@@ -148,30 +148,30 @@
public final class ApplyActionKt {
}
- public final class LaunchActivityIntentActionKt {
- method public static androidx.glance.action.Action actionLaunchActivity(android.content.Intent intent, optional androidx.glance.action.ActionParameters parameters);
- }
-
- public final class LaunchBroadcastActionKt {
- method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(String action, optional android.content.ComponentName? componentName);
- method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(android.content.Intent intent);
- method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(android.content.ComponentName componentName);
- method public static <T extends android.content.BroadcastReceiver> androidx.glance.action.Action actionLaunchBroadcastReceiver(Class<T> receiver);
- method public static inline <reified T extends android.content.BroadcastReceiver> androidx.glance.action.Action! actionLaunchBroadcastReceiver();
- }
-
- public final class LaunchServiceActionKt {
- method public static androidx.glance.action.Action actionLaunchService(android.content.Intent intent, optional boolean isForegroundService);
- method public static androidx.glance.action.Action actionLaunchService(android.content.ComponentName componentName, optional boolean isForegroundService);
- method public static <T extends android.app.Service> androidx.glance.action.Action actionLaunchService(Class<T> service, optional boolean isForegroundService);
- method public static inline <reified T extends android.app.Service> androidx.glance.action.Action! actionLaunchService(optional boolean isForegroundService);
- }
-
public final class RunCallbackActionKt {
method public static <T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action actionRunCallback(Class<T> callbackClass, optional androidx.glance.action.ActionParameters parameters);
method public static inline <reified T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action! actionRunCallback(optional androidx.glance.action.ActionParameters parameters);
}
+ public final class SendBroadcastActionKt {
+ method public static androidx.glance.action.Action actionSendBroadcast(String action, optional android.content.ComponentName? componentName);
+ method public static androidx.glance.action.Action actionSendBroadcast(android.content.Intent intent);
+ method public static androidx.glance.action.Action actionSendBroadcast(android.content.ComponentName componentName);
+ method public static <T extends android.content.BroadcastReceiver> androidx.glance.action.Action actionSendBroadcast(Class<T> receiver);
+ method public static inline <reified T extends android.content.BroadcastReceiver> androidx.glance.action.Action! actionSendBroadcast();
+ }
+
+ public final class StartActivityIntentActionKt {
+ method public static androidx.glance.action.Action actionStartActivity(android.content.Intent intent, optional androidx.glance.action.ActionParameters parameters);
+ }
+
+ public final class StartServiceActionKt {
+ method public static androidx.glance.action.Action actionStartService(android.content.Intent intent, optional boolean isForegroundService);
+ method public static androidx.glance.action.Action actionStartService(android.content.ComponentName componentName, optional boolean isForegroundService);
+ method public static <T extends android.app.Service> androidx.glance.action.Action actionStartService(Class<T> service, optional boolean isForegroundService);
+ method public static inline <reified T extends android.app.Service> androidx.glance.action.Action! actionStartService(optional boolean isForegroundService);
+ }
+
public final class ToggleableKt {
method public static androidx.glance.action.ActionParameters.Key<java.lang.Boolean> getToggleableStateKey();
property public static final androidx.glance.action.ActionParameters.Key<java.lang.Boolean> ToggleableStateKey;
diff --git a/glance/glance-appwidget/api/restricted_current.txt b/glance/glance-appwidget/api/restricted_current.txt
index 607ed48..acbfe95 100644
--- a/glance/glance-appwidget/api/restricted_current.txt
+++ b/glance/glance-appwidget/api/restricted_current.txt
@@ -54,7 +54,7 @@
method @androidx.compose.runtime.Composable public abstract void Content();
method public androidx.glance.appwidget.SizeMode getSizeMode();
method public androidx.glance.state.GlanceStateDefinition<?>? getStateDefinition();
- method public suspend Object? onDelete(androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public suspend Object? onDelete(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public final suspend Object? update(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
property public androidx.glance.appwidget.SizeMode sizeMode;
property public androidx.glance.state.GlanceStateDefinition<?>? stateDefinition;
@@ -148,30 +148,30 @@
public final class ApplyActionKt {
}
- public final class LaunchActivityIntentActionKt {
- method public static androidx.glance.action.Action actionLaunchActivity(android.content.Intent intent, optional androidx.glance.action.ActionParameters parameters);
- }
-
- public final class LaunchBroadcastActionKt {
- method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(String action, optional android.content.ComponentName? componentName);
- method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(android.content.Intent intent);
- method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(android.content.ComponentName componentName);
- method public static <T extends android.content.BroadcastReceiver> androidx.glance.action.Action actionLaunchBroadcastReceiver(Class<T> receiver);
- method public static inline <reified T extends android.content.BroadcastReceiver> androidx.glance.action.Action! actionLaunchBroadcastReceiver();
- }
-
- public final class LaunchServiceActionKt {
- method public static androidx.glance.action.Action actionLaunchService(android.content.Intent intent, optional boolean isForegroundService);
- method public static androidx.glance.action.Action actionLaunchService(android.content.ComponentName componentName, optional boolean isForegroundService);
- method public static <T extends android.app.Service> androidx.glance.action.Action actionLaunchService(Class<T> service, optional boolean isForegroundService);
- method public static inline <reified T extends android.app.Service> androidx.glance.action.Action! actionLaunchService(optional boolean isForegroundService);
- }
-
public final class RunCallbackActionKt {
method public static <T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action actionRunCallback(Class<T> callbackClass, optional androidx.glance.action.ActionParameters parameters);
method public static inline <reified T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action! actionRunCallback(optional androidx.glance.action.ActionParameters parameters);
}
+ public final class SendBroadcastActionKt {
+ method public static androidx.glance.action.Action actionSendBroadcast(String action, optional android.content.ComponentName? componentName);
+ method public static androidx.glance.action.Action actionSendBroadcast(android.content.Intent intent);
+ method public static androidx.glance.action.Action actionSendBroadcast(android.content.ComponentName componentName);
+ method public static <T extends android.content.BroadcastReceiver> androidx.glance.action.Action actionSendBroadcast(Class<T> receiver);
+ method public static inline <reified T extends android.content.BroadcastReceiver> androidx.glance.action.Action! actionSendBroadcast();
+ }
+
+ public final class StartActivityIntentActionKt {
+ method public static androidx.glance.action.Action actionStartActivity(android.content.Intent intent, optional androidx.glance.action.ActionParameters parameters);
+ }
+
+ public final class StartServiceActionKt {
+ method public static androidx.glance.action.Action actionStartService(android.content.Intent intent, optional boolean isForegroundService);
+ method public static androidx.glance.action.Action actionStartService(android.content.ComponentName componentName, optional boolean isForegroundService);
+ method public static <T extends android.app.Service> androidx.glance.action.Action actionStartService(Class<T> service, optional boolean isForegroundService);
+ method public static inline <reified T extends android.app.Service> androidx.glance.action.Action! actionStartService(optional boolean isForegroundService);
+ }
+
public final class ToggleableKt {
method public static androidx.glance.action.ActionParameters.Key<java.lang.Boolean> getToggleableStateKey();
property public static final androidx.glance.action.ActionParameters.Key<java.lang.Boolean> ToggleableStateKey;
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt
index 038d797..450ba72 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt
@@ -39,17 +39,17 @@
import androidx.glance.GlanceModifier
import androidx.glance.LocalContext
import androidx.glance.action.ActionParameters
-import androidx.glance.action.actionLaunchActivity
import androidx.glance.action.actionParametersOf
+import androidx.glance.action.actionStartActivity
import androidx.glance.action.clickable
import androidx.glance.action.toParametersKey
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.action.ActionCallback
-import androidx.glance.appwidget.action.actionLaunchActivity
-import androidx.glance.appwidget.action.actionLaunchBroadcastReceiver
-import androidx.glance.appwidget.action.actionLaunchService
import androidx.glance.appwidget.action.actionRunCallback
+import androidx.glance.appwidget.action.actionSendBroadcast
+import androidx.glance.appwidget.action.actionStartActivity
+import androidx.glance.appwidget.action.actionStartService
import androidx.glance.appwidget.appWidgetBackground
import androidx.glance.appwidget.cornerRadius
import androidx.glance.appwidget.state.updateAppWidgetState
@@ -86,9 +86,9 @@
}
when (currentState<Preferences>()[selectedItemKey] ?: 0) {
- 0 -> LaunchActivityActions()
- 1 -> LaunchServiceActions()
- 2 -> LaunchBroadcastReceiverActions()
+ 0 -> StartActivityActions()
+ 1 -> StartServiceActions()
+ 2 -> SendBroadcastActions()
else -> throw IllegalArgumentException("Wrong index selected")
}
}
@@ -96,7 +96,7 @@
}
private val selectedItemKey = intPreferencesKey("selectedItemKey")
-private val launchMessageKey = ActionParameters.Key<String>("launchMessageKey")
+private val startMessageKey = ActionParameters.Key<String>("launchMessageKey")
@Composable
private fun SelectableActionItem(label: String, index: Int) {
@@ -133,87 +133,87 @@
}
@Composable
-private fun LaunchActivityActions() {
+private fun StartActivityActions() {
Button(
text = "Intent",
- onClick = actionLaunchActivity(
+ onClick = actionStartActivity(
Intent(LocalContext.current, ActionDemoActivity::class.java)
)
)
Button(
text = "Target class",
- onClick = actionLaunchActivity<ActionDemoActivity>(),
+ onClick = actionStartActivity<ActionDemoActivity>(),
)
Button(
text = "Target class with params",
- onClick = actionLaunchActivity<ActionDemoActivity>(
+ onClick = actionStartActivity<ActionDemoActivity>(
actionParametersOf(
- launchMessageKey to "Launch activity by target class"
+ startMessageKey to "Start activity by target class"
)
)
)
Button(
text = "Component name",
- onClick = actionLaunchActivity(
+ onClick = actionStartActivity(
ComponentName(LocalContext.current, ActionDemoActivity::class.java)
)
)
Button(
text = "Component name with params",
- onClick = actionLaunchActivity(
+ onClick = actionStartActivity(
ComponentName(LocalContext.current, ActionDemoActivity::class.java),
actionParametersOf(
- launchMessageKey to "Launch activity by component name"
+ startMessageKey to "Start activity by component name"
)
)
)
}
@Composable
-private fun LaunchServiceActions() {
+private fun StartServiceActions() {
Button(
text = "Intent",
- onClick = actionLaunchService(
+ onClick = actionStartService(
Intent(LocalContext.current, ActionDemoService::class.java)
)
)
Button(
text = "Target class",
- onClick = actionLaunchService<ActionDemoService>()
+ onClick = actionStartService<ActionDemoService>()
)
Button(
text = "In foreground",
- onClick = actionLaunchService<ActionDemoService>(isForegroundService = true)
+ onClick = actionStartService<ActionDemoService>(isForegroundService = true)
)
Button(
text = "Component name",
- onClick = actionLaunchService(
+ onClick = actionStartService(
ComponentName(LocalContext.current, ActionDemoService::class.java)
)
)
}
@Composable
-private fun LaunchBroadcastReceiverActions() {
+private fun SendBroadcastActions() {
Button(
text = "Intent",
- onClick = actionLaunchBroadcastReceiver(
+ onClick = actionSendBroadcast(
Intent(LocalContext.current, ActionAppWidgetReceiver::class.java)
)
)
Button(
text = "Action",
- onClick = actionLaunchBroadcastReceiver(
+ onClick = actionSendBroadcast(
AppWidgetManager.ACTION_APPWIDGET_UPDATE
)
)
Button(
text = "Target class",
- onClick = actionLaunchBroadcastReceiver<ActionAppWidgetReceiver>()
+ onClick = actionSendBroadcast<ActionAppWidgetReceiver>()
)
Button(
text = "Component name",
- onClick = actionLaunchBroadcastReceiver(
+ onClick = actionSendBroadcast(
ComponentName(LocalContext.current, ActionAppWidgetReceiver::class.java)
)
)
@@ -235,7 +235,7 @@
}
/**
- * Placeholder activity to launch via [actionLaunchActivity]
+ * Placeholder activity to launch via [actionStartActivity]
*/
class ActionDemoActivity : ComponentActivity() {
@@ -246,7 +246,7 @@
modifier = Modifier.fillMaxSize(),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
- val message = intent.getStringExtra(launchMessageKey.name) ?: "Not found"
+ val message = intent.getStringExtra(startMessageKey.name) ?: "Not found"
androidx.compose.material.Text(message)
}
}
@@ -255,7 +255,7 @@
}
/**
- * Placeholder service to launch via [actionLaunchService]
+ * Placeholder service to launch via [actionStartService]
*/
class ActionDemoService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ResponsiveAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ResponsiveAppWidget.kt
index 9599aa9..4c9c6f7 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ResponsiveAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ResponsiveAppWidget.kt
@@ -28,12 +28,12 @@
import androidx.glance.GlanceModifier
import androidx.glance.LocalSize
import androidx.glance.action.ActionParameters
-import androidx.glance.appwidget.action.ActionCallback
-import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.action.actionParametersOf
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.SizeMode
+import androidx.glance.appwidget.action.ActionCallback
+import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.background
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt
index 16c26a3..5c46dae 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt
@@ -44,8 +44,8 @@
import androidx.glance.appwidget.SizeMode
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.ToggleableStateKey
-import androidx.glance.appwidget.action.actionLaunchActivity
import androidx.glance.appwidget.action.actionRunCallback
+import androidx.glance.appwidget.action.actionStartActivity
import androidx.glance.appwidget.demos.ScrollableAppWidget.Companion.checkboxKey
import androidx.glance.appwidget.lazy.LazyColumn
import androidx.glance.appwidget.lazy.itemsIndexed
@@ -139,7 +139,7 @@
) {
Button(
text = "Activity ${index + 1}",
- onClick = actionLaunchActivity(
+ onClick = actionStartActivity(
Intent(
LocalContext.current,
activityClass
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
index 978172a..d64f0a1 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
@@ -28,7 +28,7 @@
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.LocalContext
-import androidx.glance.action.actionLaunchActivity
+import androidx.glance.action.actionStartActivity
import androidx.glance.appwidget.test.R
import androidx.glance.appwidget.unit.ColorProvider
import androidx.glance.background
@@ -210,13 +210,13 @@
Row(modifier = GlanceModifier.defaultWeight().fillMaxWidth()) {
Button(
"Start",
- onClick = actionLaunchActivity<Activity>(),
+ onClick = actionStartActivity<Activity>(),
modifier = GlanceModifier.defaultWeight().fillMaxHeight(),
style = TextStyle(textAlign = TextAlign.Start)
)
Button(
"End",
- onClick = actionLaunchActivity<Activity>(),
+ onClick = actionStartActivity<Activity>(),
modifier = GlanceModifier.defaultWeight().fillMaxHeight(),
style = TextStyle(textAlign = TextAlign.End)
)
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
index 9aed573..8a868ff 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
@@ -47,8 +47,8 @@
import androidx.glance.LocalContext
import androidx.glance.LocalSize
import androidx.glance.action.ActionParameters
-import androidx.glance.action.actionLaunchActivity
import androidx.glance.action.actionParametersOf
+import androidx.glance.action.actionStartActivity
import androidx.glance.action.clickable
import androidx.glance.action.toParametersKey
import androidx.glance.appwidget.action.ActionCallback
@@ -393,7 +393,7 @@
@Test
fun createButton() {
TestGlanceAppWidget.uiDefinition = {
- Button("Button", onClick = actionLaunchActivity<Activity>(), enabled = false)
+ Button("Button", onClick = actionStartActivity<Activity>(), enabled = false)
}
mHostRule.startHost()
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt
index 23acd1a..84e7151 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt
@@ -20,17 +20,17 @@
import android.os.Build
import android.view.Gravity
import android.view.View
-import android.widget.Button
import android.view.ViewGroup
+import android.widget.Button
import android.widget.FrameLayout
import android.widget.ListView
import android.widget.TextView
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import androidx.glance.Button
import androidx.core.view.children
+import androidx.glance.Button
import androidx.glance.GlanceModifier
-import androidx.glance.action.actionLaunchActivity
+import androidx.glance.action.actionStartActivity
import androidx.glance.action.clickable
import androidx.glance.appwidget.lazy.LazyColumn
import androidx.glance.appwidget.lazy.ReservedItemIdRangeEnd
@@ -323,11 +323,11 @@
item {
Text(
"Text",
- modifier = GlanceModifier.clickable(actionLaunchActivity<Activity>())
+ modifier = GlanceModifier.clickable(actionStartActivity<Activity>())
)
Button(
"Button",
- onClick = actionLaunchActivity<Activity>()
+ onClick = actionStartActivity<Activity>()
)
}
}
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
index 12ac3f0..89fc5f1 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
@@ -16,6 +16,7 @@
package androidx.glance.appwidget
+import android.content.Context
import androidx.compose.runtime.Composable
import androidx.glance.GlanceId
import androidx.glance.state.GlanceStateDefinition
@@ -45,7 +46,7 @@
onDeleteBlock = null
}
- override suspend fun onDelete(glanceId: GlanceId) {
+ override suspend fun onDelete(context: Context, glanceId: GlanceId) {
onDeleteBlock?.apply { this(glanceId) }
}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
index ddcb533..778b197 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -43,9 +43,9 @@
import androidx.glance.LocalSize
import androidx.glance.LocalState
import androidx.glance.appwidget.state.getAppWidgetState
-import kotlinx.coroutines.CancellationException
import androidx.glance.state.GlanceState
import androidx.glance.state.GlanceStateDefinition
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
@@ -89,7 +89,7 @@
*
* When the method returns, the state associated with the [glanceId] will be deleted.
*/
- public open suspend fun onDelete(glanceId: GlanceId) { }
+ public open suspend fun onDelete(context: Context, glanceId: GlanceId) {}
/**
* Triggers the composition of [Content] and sends the result to the [AppWidgetManager].
@@ -109,7 +109,7 @@
internal suspend fun deleted(context: Context, appWidgetId: Int) {
val glanceId = AppWidgetId(appWidgetId)
try {
- onDelete(glanceId)
+ onDelete(context, glanceId)
} catch (cancelled: CancellationException) {
// Nothing to do here
} catch (t: Throwable) {
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ApplyAction.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ApplyAction.kt
index 70cc6cd..eeb0379 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ApplyAction.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ApplyAction.kt
@@ -28,9 +28,9 @@
import androidx.core.os.bundleOf
import androidx.glance.action.Action
import androidx.glance.action.ActionParameters
-import androidx.glance.action.LaunchActivityAction
-import androidx.glance.action.LaunchActivityClassAction
-import androidx.glance.action.LaunchActivityComponentAction
+import androidx.glance.action.StartActivityAction
+import androidx.glance.action.StartActivityClassAction
+import androidx.glance.action.StartActivityComponentAction
import androidx.glance.action.toMutableParameters
import androidx.glance.appwidget.GlanceAppWidgetTag
import androidx.glance.appwidget.TranslationContext
@@ -71,10 +71,10 @@
editParams: (ActionParameters) -> ActionParameters = { it },
): PendingIntent {
when (action) {
- is LaunchActivityAction -> {
+ is StartActivityAction -> {
val params = editParams(action.parameters)
- val intent = getLaunchActivityIntent(action, translationContext, params)
- val finalIntent = if (action !is LaunchActivityIntentAction && !params.isEmpty()) {
+ val intent = getStartActivityIntent(action, translationContext, params)
+ val finalIntent = if (action !is StartActivityIntentAction && !params.isEmpty()) {
intent.applyTrampolineIntent(
translationContext,
viewId,
@@ -90,8 +90,8 @@
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
}
- is LaunchServiceAction -> {
- val intent = getLaunchServiceIntent(action, translationContext)
+ is StartServiceAction -> {
+ val intent = getServiceIntent(action, translationContext)
return if (action.isForegroundService &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
) {
@@ -108,11 +108,11 @@
)
}
}
- is LaunchBroadcastReceiverAction -> {
+ is SendBroadcastAction -> {
return PendingIntent.getBroadcast(
translationContext.context,
0,
- getLaunchBroadcastReceiverIntent(action, translationContext),
+ getBroadcastReceiverIntent(action, translationContext),
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
}
@@ -154,8 +154,8 @@
@IdRes viewId: Int,
editParams: (ActionParameters) -> ActionParameters = { it }
): Intent = when (action) {
- is LaunchActivityAction -> {
- getLaunchActivityIntent(
+ is StartActivityAction -> {
+ getStartActivityIntent(
action = action,
translationContext = translationContext,
params = editParams(action.parameters)
@@ -165,8 +165,8 @@
type = ActionTrampolineType.ACTIVITY,
)
}
- is LaunchServiceAction -> {
- getLaunchServiceIntent(
+ is StartServiceAction -> {
+ getServiceIntent(
action = action,
translationContext = translationContext
).applyTrampolineIntent(
@@ -179,8 +179,8 @@
},
)
}
- is LaunchBroadcastReceiverAction -> {
- getLaunchBroadcastReceiverIntent(
+ is SendBroadcastAction -> {
+ getBroadcastReceiverIntent(
action = action,
translationContext = translationContext
).applyTrampolineIntent(
@@ -223,37 +223,37 @@
}
}
-private fun getLaunchBroadcastReceiverIntent(
- action: LaunchBroadcastReceiverAction,
+private fun getBroadcastReceiverIntent(
+ action: SendBroadcastAction,
translationContext: TranslationContext,
): Intent = when (action) {
- is LaunchBroadcastReceiverComponentAction -> Intent().setComponent(action.componentName)
- is LaunchBroadcastReceiverClassAction ->
+ is SendBroadcastComponentAction -> Intent().setComponent(action.componentName)
+ is SendBroadcastClassAction ->
Intent(translationContext.context, action.receiverClass)
- is LaunchBroadcastReceiverIntentAction -> action.intent
- is LaunchBroadcastReceiverActionAction ->
+ is SendBroadcastIntentAction -> action.intent
+ is SendBroadcastActionAction ->
Intent(action.action).setComponent(action.componentName)
}
-private fun getLaunchServiceIntent(
- action: LaunchServiceAction,
+private fun getServiceIntent(
+ action: StartServiceAction,
translationContext: TranslationContext,
): Intent = when (action) {
- is LaunchServiceComponentAction -> Intent().setComponent(action.componentName)
- is LaunchServiceClassAction ->
+ is StartServiceComponentAction -> Intent().setComponent(action.componentName)
+ is StartServiceClassAction ->
Intent(translationContext.context, action.serviceClass)
- is LaunchServiceIntentAction -> action.intent
+ is StartServiceIntentAction -> action.intent
}
-private fun getLaunchActivityIntent(
- action: LaunchActivityAction,
+private fun getStartActivityIntent(
+ action: StartActivityAction,
translationContext: TranslationContext,
params: ActionParameters,
): Intent {
val activityIntent = when (action) {
- is LaunchActivityComponentAction -> Intent().setComponent(action.componentName)
- is LaunchActivityClassAction -> Intent(translationContext.context, action.activityClass)
- is LaunchActivityIntentAction -> action.intent
+ is StartActivityComponentAction -> Intent().setComponent(action.componentName)
+ is StartActivityClassAction -> Intent(translationContext.context, action.activityClass)
+ is StartActivityIntentAction -> action.intent
else -> error("Action type not defined in app widget package: $action")
}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchBroadcastAction.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/SendBroadcastAction.kt
similarity index 66%
rename from glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchBroadcastAction.kt
rename to glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/SendBroadcastAction.kt
index c4a050f..a2edcd0 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchBroadcastAction.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/SendBroadcastAction.kt
@@ -21,24 +21,24 @@
import android.content.Intent
import androidx.glance.action.Action
-internal sealed interface LaunchBroadcastReceiverAction : Action
+internal sealed interface SendBroadcastAction : Action
-internal class LaunchBroadcastReceiverActionAction(
+internal class SendBroadcastActionAction(
val action: String,
val componentName: ComponentName? = null,
-) : LaunchBroadcastReceiverAction
+) : SendBroadcastAction
-internal class LaunchBroadcastReceiverComponentAction(
+internal class SendBroadcastComponentAction(
val componentName: ComponentName,
-) : LaunchBroadcastReceiverAction
+) : SendBroadcastAction
-internal class LaunchBroadcastReceiverClassAction(
+internal class SendBroadcastClassAction(
val receiverClass: Class<out BroadcastReceiver>,
-) : LaunchBroadcastReceiverAction
+) : SendBroadcastAction
-internal class LaunchBroadcastReceiverIntentAction(
+internal class SendBroadcastIntentAction(
val intent: Intent,
-) : LaunchBroadcastReceiverAction
+) : SendBroadcastAction
/**
* Creates an [Action] that launches the [BroadcastReceiver] specified by the given action.
@@ -46,10 +46,10 @@
* @param action of the BroadcastReceiver to launch
* @param componentName optional [ComponentName] of the target BroadcastReceiver
*/
-public fun actionLaunchBroadcastReceiver(
+public fun actionSendBroadcast(
action: String,
componentName: ComponentName? = null
-): Action = LaunchBroadcastReceiverActionAction(action, componentName)
+): Action = SendBroadcastActionAction(action, componentName)
/**
* Creates an [Action] that launches a [BroadcastReceiver] from the given [Intent] when triggered.
@@ -57,28 +57,28 @@
*
* @param intent the [Intent] used to launch the [BroadcastReceiver]
*/
-public fun actionLaunchBroadcastReceiver(intent: Intent): Action =
- LaunchBroadcastReceiverIntentAction(intent)
+public fun actionSendBroadcast(intent: Intent): Action =
+ SendBroadcastIntentAction(intent)
/**
* Creates an [Action] that launches the [BroadcastReceiver] specified by the given [ComponentName].
*
* @param componentName component of the [BroadcastReceiver] to launch
*/
-public fun actionLaunchBroadcastReceiver(componentName: ComponentName): Action =
- LaunchBroadcastReceiverComponentAction(componentName)
+public fun actionSendBroadcast(componentName: ComponentName): Action =
+ SendBroadcastComponentAction(componentName)
/**
* Creates an [Action] that launches the specified [BroadcastReceiver] when triggered.
*
* @param receiver class of the [BroadcastReceiver] to launch
*/
-public fun <T : BroadcastReceiver> actionLaunchBroadcastReceiver(receiver: Class<T>): Action =
- LaunchBroadcastReceiverClassAction(receiver)
+public fun <T : BroadcastReceiver> actionSendBroadcast(receiver: Class<T>): Action =
+ SendBroadcastClassAction(receiver)
/**
* Creates an [Action] that launches the specified [BroadcastReceiver] when triggered.
*/
@Suppress("MissingNullability") // Shouldn't need to specify @NonNull. b/199284086
-public inline fun <reified T : BroadcastReceiver> actionLaunchBroadcastReceiver(): Action =
- actionLaunchBroadcastReceiver(T::class.java)
+public inline fun <reified T : BroadcastReceiver> actionSendBroadcast(): Action =
+ actionSendBroadcast(T::class.java)
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentAction.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/StartActivityIntentAction.kt
similarity index 87%
rename from glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentAction.kt
rename to glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/StartActivityIntentAction.kt
index 7e7f48a..1fd2a0f 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentAction.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/StartActivityIntentAction.kt
@@ -20,13 +20,13 @@
import android.content.Intent
import androidx.glance.action.Action
import androidx.glance.action.ActionParameters
-import androidx.glance.action.LaunchActivityAction
+import androidx.glance.action.StartActivityAction
import androidx.glance.action.actionParametersOf
-internal class LaunchActivityIntentAction(
+internal class StartActivityIntentAction(
val intent: Intent,
override val parameters: ActionParameters = actionParametersOf()
-) : LaunchActivityAction
+) : StartActivityAction
/**
* Creates an [Action] that launches an [Activity] from the given [Intent] when triggered. The
@@ -38,7 +38,7 @@
* @param parameters the parameters associated with the action. Parameter values will be added to
* the activity intent, keyed by the parameter key name string.
*/
-public fun actionLaunchActivity(
+public fun actionStartActivity(
intent: Intent,
parameters: ActionParameters = actionParametersOf()
-): Action = LaunchActivityIntentAction(intent, parameters)
+): Action = StartActivityIntentAction(intent, parameters)
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchServiceAction.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/StartServiceAction.kt
similarity index 77%
rename from glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchServiceAction.kt
rename to glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/StartServiceAction.kt
index 183e9bf..5b2541d 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchServiceAction.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/StartServiceAction.kt
@@ -21,24 +21,24 @@
import android.content.Intent
import androidx.glance.action.Action
-internal sealed interface LaunchServiceAction : Action {
+internal sealed interface StartServiceAction : Action {
val isForegroundService: Boolean
}
-internal class LaunchServiceComponentAction(
+internal class StartServiceComponentAction(
val componentName: ComponentName,
override val isForegroundService: Boolean
-) : LaunchServiceAction
+) : StartServiceAction
-internal class LaunchServiceClassAction(
+internal class StartServiceClassAction(
val serviceClass: Class<out Service>,
override val isForegroundService: Boolean
-) : LaunchServiceAction
+) : StartServiceAction
-internal class LaunchServiceIntentAction(
+internal class StartServiceIntentAction(
val intent: Intent,
override val isForegroundService: Boolean
-) : LaunchServiceAction
+) : StartServiceAction
/**
* Creates an [Action] that launches a [Service] from the given [Intent] when triggered. The
@@ -49,8 +49,8 @@
* is only used for device versions after [android.os.Build.VERSION_CODES.O] that requires
* foreground service to be launched differently
*/
-public fun actionLaunchService(intent: Intent, isForegroundService: Boolean = false): Action =
- LaunchServiceIntentAction(intent, isForegroundService)
+public fun actionStartService(intent: Intent, isForegroundService: Boolean = false): Action =
+ StartServiceIntentAction(intent, isForegroundService)
/**
* Creates an [Action] that launches the [Service] specified by the given [ComponentName].
@@ -60,10 +60,10 @@
* is only used for device versions after [android.os.Build.VERSION_CODES.O] that requires
* foreground service to be launched differently
*/
-public fun actionLaunchService(
+public fun actionStartService(
componentName: ComponentName,
isForegroundService: Boolean = false
-): Action = LaunchServiceComponentAction(componentName, isForegroundService)
+): Action = StartServiceComponentAction(componentName, isForegroundService)
/**
* Creates an [Action] that launches the specified [Service] when triggered.
@@ -73,11 +73,11 @@
* is only used for device versions after [android.os.Build.VERSION_CODES.O] that requires
* foreground service to be launched differently
*/
-public fun <T : Service> actionLaunchService(
+public fun <T : Service> actionStartService(
service: Class<T>,
isForegroundService: Boolean = false
): Action =
- LaunchServiceClassAction(service, isForegroundService)
+ StartServiceClassAction(service, isForegroundService)
/**
* Creates an [Action] that launches the specified [Service] when triggered.
@@ -86,7 +86,8 @@
* is only used for device versions after [android.os.Build.VERSION_CODES.O] that requires
* foreground service to be launched differently.
*/
-@Suppress("MissingNullability") /* Shouldn't need to specify @NonNull. b/199284086 */
-public inline fun <reified T : Service> actionLaunchService(
+@Suppress("MissingNullability")
+/* Shouldn't need to specify @NonNull. b/199284086 */
+public inline fun <reified T : Service> actionStartService(
isForegroundService: Boolean = false
-): Action = actionLaunchService(T::class.java, isForegroundService)
+): Action = actionStartService(T::class.java, isForegroundService)
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt
index 4501b4c..f22391fffd 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt
@@ -58,8 +58,8 @@
"Glance does not support nested list views."
}
// TODO(b/205868100): Remove [FILL_IN_COMPONENT] flag and set target component here when all
- // click actions on descendants are exclusively [LaunchActivityAction] or exclusively not
- // [LaunchActivityAction].
+ // click actions on descendants are exclusively [StartActivityAction] or exclusively not
+ // [StartActivityAction].
setPendingIntentTemplate(
viewDef.mainViewId,
PendingIntent.getActivity(
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
index 412938b..349f28f 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
@@ -39,7 +39,7 @@
import androidx.glance.Button
import androidx.glance.GlanceModifier
import androidx.glance.Visibility
-import androidx.glance.action.actionLaunchActivity
+import androidx.glance.action.actionStartActivity
import androidx.glance.action.clickable
import androidx.glance.appwidget.FrameLayoutSubject.Companion.assertThat
import androidx.glance.appwidget.LinearLayoutSubject.Companion.assertThat
@@ -71,9 +71,9 @@
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowLog
import kotlin.test.assertFailsWith
import kotlin.test.assertIs
-import org.robolectric.shadows.ShadowLog
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
@@ -694,7 +694,7 @@
val rv = context.runAndTranslate {
Button(
"Button",
- onClick = actionLaunchActivity<Activity>(),
+ onClick = actionStartActivity<Activity>(),
enabled = true
)
}
@@ -710,7 +710,7 @@
val rv = context.runAndTranslate {
Button(
"Button",
- onClick = actionLaunchActivity<Activity>(),
+ onClick = actionStartActivity<Activity>(),
enabled = false
)
}
@@ -846,9 +846,9 @@
Text(
"text1",
modifier = GlanceModifier.clickable(
- actionLaunchActivity(ComponentName("package", "class"))
+ actionStartActivity(ComponentName("package", "class"))
).clickable(
- actionLaunchActivity(ComponentName("package", "class2"))
+ actionStartActivity(ComponentName("package", "class2"))
)
)
}
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/WidgetLayoutTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/WidgetLayoutTest.kt
index a154511..5aa780f 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/WidgetLayoutTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/WidgetLayoutTest.kt
@@ -23,19 +23,19 @@
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.ImageProvider
-import androidx.glance.action.actionLaunchActivity
+import androidx.glance.action.actionStartActivity
import androidx.glance.appwidget.lazy.LazyColumn
+import androidx.glance.appwidget.proto.LayoutProto
import androidx.glance.appwidget.test.R
import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.ContentScale
+import androidx.glance.layout.Row
import androidx.glance.layout.fillMaxHeight
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.width
import androidx.glance.layout.wrapContentWidth
-import androidx.glance.appwidget.proto.LayoutProto
-import androidx.glance.layout.Box
-import androidx.glance.layout.Row
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -67,7 +67,7 @@
onCheckedChange = null,
modifier = GlanceModifier.fillMaxSize()
)
- Button(text = "test", onClick = actionLaunchActivity<Activity>())
+ Button(text = "test", onClick = actionStartActivity<Activity>())
Image(
ImageProvider(R.drawable.oval),
"description",
@@ -108,10 +108,10 @@
Column {
CheckBox(
checked = true,
-onCheckedChange = null,
+ onCheckedChange = null,
modifier = GlanceModifier.fillMaxSize()
)
- Button(text = "test", onClick = actionLaunchActivity<Activity>())
+ Button(text = "test", onClick = actionStartActivity<Activity>())
}
}
val root2 = runTestingComposition {
@@ -121,7 +121,7 @@
onCheckedChange = null,
modifier = GlanceModifier.wrapContentWidth().fillMaxHeight()
)
- Button(text = "test", onClick = actionLaunchActivity<Activity>())
+ Button(text = "test", onClick = actionStartActivity<Activity>())
}
}
@@ -338,7 +338,7 @@
onCheckedChange = null,
modifier = GlanceModifier.fillMaxSize()
)
- Button(text = "test", onClick = actionLaunchActivity<Activity>())
+ Button(text = "test", onClick = actionStartActivity<Activity>())
}
}
val root2 = runTestingComposition {
@@ -348,7 +348,7 @@
onCheckedChange = null,
modifier = GlanceModifier.fillMaxSize()
)
- Button(text = "testtesttest", onClick = actionLaunchActivity<Activity>())
+ Button(text = "testtesttest", onClick = actionStartActivity<Activity>())
}
}
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentActionTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentActionTest.kt
index 0284863..acded2e 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentActionTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentActionTest.kt
@@ -24,12 +24,12 @@
import androidx.glance.action.clickable
import androidx.glance.findModifier
import androidx.test.core.app.ApplicationProvider
-import org.junit.Test
-import kotlin.test.assertIs
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
+import kotlin.test.assertIs
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
@@ -41,9 +41,9 @@
fun testLaunchIntent() {
val intentActionString = "test_action"
val intent = Intent(context, TestActivity::class.java).setAction(intentActionString)
- val modifiers = GlanceModifier.clickable(actionLaunchActivity(intent))
+ val modifiers = GlanceModifier.clickable(actionStartActivity(intent))
val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
- val action = assertIs<LaunchActivityIntentAction>(modifier.action)
+ val action = assertIs<StartActivityIntentAction>(modifier.action)
assertThat(action.intent).isEqualTo(intent)
assertThat(action.intent.action).isEqualTo(intentActionString)
}
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchBroadcastReceiverActionTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/SendBroadcastActionTest.kt
similarity index 79%
rename from glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchBroadcastReceiverActionTest.kt
rename to glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/SendBroadcastActionTest.kt
index 79215f4..061d7ef 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchBroadcastReceiverActionTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/SendBroadcastActionTest.kt
@@ -34,25 +34,25 @@
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
-class LaunchBroadcastReceiverActionTest {
+class SendBroadcastActionTest {
private val context = ApplicationProvider.getApplicationContext<Context>()
@Test
fun testLaunchClass() {
val modifiers =
- GlanceModifier.clickable(actionLaunchBroadcastReceiver<TestBroadcastReceiver>())
+ GlanceModifier.clickable(actionSendBroadcast<TestBroadcastReceiver>())
val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
- val action = assertIs<LaunchBroadcastReceiverClassAction>(modifier.action)
+ val action = assertIs<SendBroadcastClassAction>(modifier.action)
assertThat(action.receiverClass).isEqualTo(TestBroadcastReceiver::class.java)
}
@Test
fun testLaunchAction() {
val intentActionString = "test_action"
- val modifiers = GlanceModifier.clickable(actionLaunchBroadcastReceiver(intentActionString))
+ val modifiers = GlanceModifier.clickable(actionSendBroadcast(intentActionString))
val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
- val action = assertIs<LaunchBroadcastReceiverActionAction>(modifier.action)
+ val action = assertIs<SendBroadcastActionAction>(modifier.action)
assertThat(action.action).isEqualTo(intentActionString)
assertThat(action.componentName).isNull()
}
@@ -65,13 +65,13 @@
"androidx.glance.appwidget.action.TestBroadcastReceiver"
)
val modifiers = GlanceModifier.clickable(
- actionLaunchBroadcastReceiver(
+ actionSendBroadcast(
intentActionString,
componentName
)
)
val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
- val action = assertIs<LaunchBroadcastReceiverActionAction>(modifier.action)
+ val action = assertIs<SendBroadcastActionAction>(modifier.action)
assertThat(action.action).isEqualTo(intentActionString)
assertThat(action.componentName).isEqualTo(componentName)
}
@@ -81,9 +81,9 @@
val intentActionString = "test_action"
val intent =
Intent(context, TestBroadcastReceiver::class.java).setAction(intentActionString)
- val modifiers = GlanceModifier.clickable(actionLaunchBroadcastReceiver(intent))
+ val modifiers = GlanceModifier.clickable(actionSendBroadcast(intent))
val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
- val action = assertIs<LaunchBroadcastReceiverIntentAction>(modifier.action)
+ val action = assertIs<SendBroadcastIntentAction>(modifier.action)
assertThat(action.intent).isEqualTo(intent)
assertThat(action.intent.action).isEqualTo(intentActionString)
}
@@ -94,9 +94,9 @@
"androidx.glance.appwidget.action",
"androidx.glance.appwidget.action.TestBroadcastReceiver"
)
- val modifiers = GlanceModifier.clickable(actionLaunchBroadcastReceiver(componentName))
+ val modifiers = GlanceModifier.clickable(actionSendBroadcast(componentName))
val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
- val action = assertIs<LaunchBroadcastReceiverComponentAction>(modifier.action)
+ val action = assertIs<SendBroadcastComponentAction>(modifier.action)
assertThat(action.componentName).isEqualTo(componentName)
}
}
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchServiceActionTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/StartServiceActionTest.kt
similarity index 83%
rename from glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchServiceActionTest.kt
rename to glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/StartServiceActionTest.kt
index 5dbffd7..431e9f0 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchServiceActionTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/StartServiceActionTest.kt
@@ -35,15 +35,15 @@
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
-class LaunchServiceActionTest {
+class StartServiceActionTest {
private val context = ApplicationProvider.getApplicationContext<Context>()
@Test
fun testLaunchClass() {
- val modifiers = GlanceModifier.clickable(actionLaunchService<TestService>())
+ val modifiers = GlanceModifier.clickable(actionStartService<TestService>())
val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
- val action = assertIs<LaunchServiceClassAction>(modifier.action)
+ val action = assertIs<StartServiceClassAction>(modifier.action)
assertThat(action.serviceClass).isEqualTo(TestService::class.java)
assertThat(action.isForegroundService).isEqualTo(false)
}
@@ -51,12 +51,12 @@
@Test
fun testLaunchClassWithForeground() {
val modifiers = GlanceModifier.clickable(
- actionLaunchService<TestService>(
+ actionStartService<TestService>(
isForegroundService = true
)
)
val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
- val action = assertIs<LaunchServiceClassAction>(modifier.action)
+ val action = assertIs<StartServiceClassAction>(modifier.action)
assertThat(action.serviceClass).isEqualTo(TestService::class.java)
assertThat(action.isForegroundService).isEqualTo(true)
}
@@ -65,9 +65,9 @@
fun testLaunchIntent() {
val intentActionString = "test_action"
val intent = Intent(context, TestService::class.java).setAction(intentActionString)
- val modifiers = GlanceModifier.clickable(actionLaunchService(intent))
+ val modifiers = GlanceModifier.clickable(actionStartService(intent))
val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
- val action = assertIs<LaunchServiceIntentAction>(modifier.action)
+ val action = assertIs<StartServiceIntentAction>(modifier.action)
assertThat(action.intent).isEqualTo(intent)
assertThat(action.intent.action).isEqualTo(intentActionString)
assertThat(action.isForegroundService).isEqualTo(false)
@@ -79,9 +79,9 @@
"androidx.glance.appwidget.action",
"androidx.glance.appwidget.action.TestService"
)
- val modifiers = GlanceModifier.clickable(actionLaunchService(componentName))
+ val modifiers = GlanceModifier.clickable(actionStartService(componentName))
val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
- val action = assertIs<LaunchServiceComponentAction>(modifier.action)
+ val action = assertIs<StartServiceComponentAction>(modifier.action)
assertThat(action.componentName).isEqualTo(componentName)
assertThat(action.isForegroundService).isEqualTo(false)
}
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
index 28ff657..e336057 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
@@ -33,9 +33,9 @@
import androidx.glance.GlanceModifier
import androidx.glance.VisibilityModifier
import androidx.glance.action.ActionModifier
-import androidx.glance.action.LaunchActivityAction
-import androidx.glance.action.LaunchActivityClassAction
-import androidx.glance.action.LaunchActivityComponentAction
+import androidx.glance.action.StartActivityAction
+import androidx.glance.action.StartActivityClassAction
+import androidx.glance.action.StartActivityComponentAction
import androidx.glance.findModifier
import androidx.glance.layout.Alignment
import androidx.glance.layout.ContentScale
@@ -164,21 +164,21 @@
}
// TODO: handle parameters
-private fun LaunchActivityAction.toProto(context: Context): ActionBuilders.LaunchAction =
+private fun StartActivityAction.toProto(context: Context): ActionBuilders.LaunchAction =
ActionBuilders.LaunchAction.Builder()
.setAndroidActivity(
ActionBuilders.AndroidActivity.Builder()
.setPackageName(
when (this) {
- is LaunchActivityComponentAction -> componentName.packageName
- is LaunchActivityClassAction -> context.packageName
+ is StartActivityComponentAction -> componentName.packageName
+ is StartActivityClassAction -> context.packageName
else -> error("Action type not defined in wear package: $this")
}
)
.setClassName(
when (this) {
- is LaunchActivityComponentAction -> componentName.className
- is LaunchActivityClassAction -> activityClass.name
+ is StartActivityComponentAction -> componentName.className
+ is StartActivityClassAction -> activityClass.name
else -> error("Action type not defined in wear package: $this")
}
)
@@ -190,7 +190,7 @@
val builder = ModifiersBuilders.Clickable.Builder()
when (val action = this.action) {
- is LaunchActivityAction -> {
+ is StartActivityAction -> {
builder.setOnClick(action.toProto(context))
} else -> {
Log.e(GlanceWearTileTag, "Unknown Action $this, skipped")
diff --git a/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt b/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt
index a31eab3..eec788c 100644
--- a/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt
+++ b/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt
@@ -29,7 +29,7 @@
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.ImageProvider
-import androidx.glance.action.actionLaunchActivity
+import androidx.glance.action.actionStartActivity
import androidx.glance.action.clickable
import androidx.glance.background
import androidx.glance.layout.Alignment
@@ -500,7 +500,7 @@
fun canInflateLaunchAction() = fakeCoroutineScope.runBlockingTest {
val content = runAndTranslate {
Text(
- modifier = GlanceModifier.clickable(actionLaunchActivity(TestActivity::class.java)),
+ modifier = GlanceModifier.clickable(actionStartActivity(TestActivity::class.java)),
text = "Hello World"
)
}.layout
@@ -533,7 +533,7 @@
)
Button(
"Hello World",
- onClick = actionLaunchActivity(TestActivity::class.java),
+ onClick = actionStartActivity(TestActivity::class.java),
modifier = GlanceModifier.padding(1.dp),
style = style
)
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index b4a5861..8893409 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -121,12 +121,6 @@
method public static <T> androidx.glance.action.ActionParameters.Key<T> toParametersKey(androidx.datastore.preferences.core.Preferences.Key<T>);
}
- public final class LaunchActivityActionKt {
- method public static androidx.glance.action.Action actionLaunchActivity(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
- method public static <T extends android.app.Activity> androidx.glance.action.Action actionLaunchActivity(Class<T> activity, optional androidx.glance.action.ActionParameters parameters);
- method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionLaunchActivity(optional androidx.glance.action.ActionParameters parameters);
- }
-
public final class MutableActionParameters extends androidx.glance.action.ActionParameters {
method public java.util.Map<androidx.glance.action.ActionParameters.Key<?>,java.lang.Object> asMap();
method public void clear();
@@ -138,6 +132,12 @@
method public operator <T> T? set(androidx.glance.action.ActionParameters.Key<T> key, T? value);
}
+ public final class StartActivityActionKt {
+ method public static androidx.glance.action.Action actionStartActivity(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
+ method public static <T extends android.app.Activity> androidx.glance.action.Action actionStartActivity(Class<T> activity, optional androidx.glance.action.ActionParameters parameters);
+ method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionStartActivity(optional androidx.glance.action.ActionParameters parameters);
+ }
+
}
package androidx.glance.layout {
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index b4a5861..8893409 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -121,12 +121,6 @@
method public static <T> androidx.glance.action.ActionParameters.Key<T> toParametersKey(androidx.datastore.preferences.core.Preferences.Key<T>);
}
- public final class LaunchActivityActionKt {
- method public static androidx.glance.action.Action actionLaunchActivity(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
- method public static <T extends android.app.Activity> androidx.glance.action.Action actionLaunchActivity(Class<T> activity, optional androidx.glance.action.ActionParameters parameters);
- method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionLaunchActivity(optional androidx.glance.action.ActionParameters parameters);
- }
-
public final class MutableActionParameters extends androidx.glance.action.ActionParameters {
method public java.util.Map<androidx.glance.action.ActionParameters.Key<?>,java.lang.Object> asMap();
method public void clear();
@@ -138,6 +132,12 @@
method public operator <T> T? set(androidx.glance.action.ActionParameters.Key<T> key, T? value);
}
+ public final class StartActivityActionKt {
+ method public static androidx.glance.action.Action actionStartActivity(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
+ method public static <T extends android.app.Activity> androidx.glance.action.Action actionStartActivity(Class<T> activity, optional androidx.glance.action.ActionParameters parameters);
+ method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionStartActivity(optional androidx.glance.action.ActionParameters parameters);
+ }
+
}
package androidx.glance.layout {
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index b4a5861..8893409 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -121,12 +121,6 @@
method public static <T> androidx.glance.action.ActionParameters.Key<T> toParametersKey(androidx.datastore.preferences.core.Preferences.Key<T>);
}
- public final class LaunchActivityActionKt {
- method public static androidx.glance.action.Action actionLaunchActivity(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
- method public static <T extends android.app.Activity> androidx.glance.action.Action actionLaunchActivity(Class<T> activity, optional androidx.glance.action.ActionParameters parameters);
- method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionLaunchActivity(optional androidx.glance.action.ActionParameters parameters);
- }
-
public final class MutableActionParameters extends androidx.glance.action.ActionParameters {
method public java.util.Map<androidx.glance.action.ActionParameters.Key<?>,java.lang.Object> asMap();
method public void clear();
@@ -138,6 +132,12 @@
method public operator <T> T? set(androidx.glance.action.ActionParameters.Key<T> key, T? value);
}
+ public final class StartActivityActionKt {
+ method public static androidx.glance.action.Action actionStartActivity(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
+ method public static <T extends android.app.Activity> androidx.glance.action.Action actionStartActivity(Class<T> activity, optional androidx.glance.action.ActionParameters parameters);
+ method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionStartActivity(optional androidx.glance.action.ActionParameters parameters);
+ }
+
}
package androidx.glance.layout {
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt
index f363143..6348d86 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt
@@ -22,7 +22,7 @@
/**
* An Action defines the actions a user can take. Implementations specify what operation will be
- * performed in response to the action, eg. [actionLaunchActivity] creates an Action that launches
+ * performed in response to the action, eg. [actionStartActivity] creates an Action that launches
* the specified [Activity].
*/
public interface Action
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/action/LaunchActivityAction.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/action/StartActivityAction.kt
similarity index 79%
rename from glance/glance/src/androidMain/kotlin/androidx/glance/action/LaunchActivityAction.kt
rename to glance/glance/src/androidMain/kotlin/androidx/glance/action/StartActivityAction.kt
index ff1e90b..657da3bc 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/action/LaunchActivityAction.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/action/StartActivityAction.kt
@@ -17,28 +17,28 @@
package androidx.glance.action
import android.app.Activity
-import androidx.annotation.RestrictTo
import android.content.ComponentName
+import androidx.annotation.RestrictTo
/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public interface LaunchActivityAction : Action {
+public interface StartActivityAction : Action {
abstract val parameters: ActionParameters
}
/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class LaunchActivityComponentAction(
+public class StartActivityComponentAction(
public val componentName: ComponentName,
public override val parameters: ActionParameters
-) : LaunchActivityAction
+) : StartActivityAction
/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class LaunchActivityClassAction(
+public class StartActivityClassAction(
public val activityClass: Class<out Activity>,
public override val parameters: ActionParameters
-) : LaunchActivityAction
+) : StartActivityAction
/**
* Creates an [Action] that launches the [Activity] specified by the given [ComponentName].
@@ -47,10 +47,10 @@
* @param parameters the parameters associated with the action. Parameter values will be added to
* the activity intent, keyed by the parameter key name string.
*/
-public fun actionLaunchActivity(
+public fun actionStartActivity(
componentName: ComponentName,
parameters: ActionParameters = actionParametersOf()
-): Action = LaunchActivityComponentAction(componentName, parameters)
+): Action = StartActivityComponentAction(componentName, parameters)
/**
* Creates an [Action] that launches the specified [Activity] when triggered.
@@ -59,18 +59,19 @@
* @param parameters the parameters associated with the action. Parameter values will be added to
* the activity intent, keyed by the parameter key name string.
*/
-public fun <T : Activity> actionLaunchActivity(
+public fun <T : Activity> actionStartActivity(
activity: Class<T>,
parameters: ActionParameters = actionParametersOf()
-): Action = LaunchActivityClassAction(activity, parameters)
+): Action = StartActivityClassAction(activity, parameters)
-@Suppress("MissingNullability") /* Shouldn't need to specify @NonNull. b/199284086 */
+@Suppress("MissingNullability")
+/* Shouldn't need to specify @NonNull. b/199284086 */
/**
* Creates an [Action] that launches the specified [Activity] when triggered.
*
* @param parameters the parameters associated with the action. Parameter values will be added to
* the activity intent, keyed by the parameter key name string.
*/
-public inline fun <reified T : Activity> actionLaunchActivity(
+public inline fun <reified T : Activity> actionStartActivity(
parameters: ActionParameters = actionParametersOf()
-): Action = actionLaunchActivity(T::class.java, parameters)
+): Action = actionStartActivity(T::class.java, parameters)
diff --git a/glance/glance/src/test/kotlin/androidx/glance/ButtonTest.kt b/glance/glance/src/test/kotlin/androidx/glance/ButtonTest.kt
index 83909e2..e94dd92 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/ButtonTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/ButtonTest.kt
@@ -19,9 +19,9 @@
import androidx.compose.ui.unit.sp
import androidx.glance.action.ActionModifier
import androidx.glance.action.ActionParameters
-import androidx.glance.action.LaunchActivityAction
-import androidx.glance.action.actionLaunchActivity
+import androidx.glance.action.StartActivityAction
import androidx.glance.action.actionParametersOf
+import androidx.glance.action.actionStartActivity
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.runTestingComposition
import androidx.glance.text.TextStyle
@@ -51,7 +51,7 @@
val root = runTestingComposition {
Button(
- text = "button", onClick = actionLaunchActivity<Activity>(
+ text = "button", onClick = actionStartActivity<Activity>(
actionParametersOf(stringKey to string, intKey to int)
), enabled = true
)
@@ -61,7 +61,7 @@
val child = assertIs<EmittableButton>(root.children[0])
assertThat(child.text).isEqualTo("button")
val action =
- assertIs<LaunchActivityAction>(child.modifier.findModifier<ActionModifier>()?.action)
+ assertIs<StartActivityAction>(child.modifier.findModifier<ActionModifier>()?.action)
assertThat(child.enabled).isTrue()
assertThat(action.parameters.asMap()).hasSize(2)
assertThat(action.parameters[stringKey]).isEqualTo(string)
@@ -71,7 +71,7 @@
@Test
fun createDisabledButton() = fakeCoroutineScope.runBlockingTest {
val root = runTestingComposition {
- Button(text = "button", onClick = actionLaunchActivity<Activity>(), enabled = false)
+ Button(text = "button", onClick = actionStartActivity<Activity>(), enabled = false)
}
assertThat(root.children).hasSize(1)
@@ -86,7 +86,7 @@
val root = runTestingComposition {
Button(
text = "button",
- onClick = actionLaunchActivity<Activity>(),
+ onClick = actionStartActivity<Activity>(),
modifier = GlanceModifier.fillMaxSize(),
maxLines = 3,
style = TextStyle(fontSize = 12.sp)
diff --git a/glance/glance/src/test/kotlin/androidx/glance/action/ActionTest.kt b/glance/glance/src/test/kotlin/androidx/glance/action/ActionTest.kt
index 9543516..78b6bfb 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/action/ActionTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/action/ActionTest.kt
@@ -45,19 +45,19 @@
}
@Test
- fun testLaunchActivity() {
- val modifiers = GlanceModifier.clickable(actionLaunchActivity(TestActivity::class.java))
+ fun testStartActivity() {
+ val modifiers = GlanceModifier.clickable(actionStartActivity(TestActivity::class.java))
val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
- assertIs<LaunchActivityClassAction>(modifier.action)
+ assertIs<StartActivityClassAction>(modifier.action)
}
@Test
fun testLaunchFromComponent() = fakeCoroutineScope.runBlockingTest {
val c = ComponentName("androidx.glance.action", "androidx.glance.action.TestActivity")
- val modifiers = GlanceModifier.clickable(actionLaunchActivity(c))
+ val modifiers = GlanceModifier.clickable(actionStartActivity(c))
val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
- val action = assertIs<LaunchActivityComponentAction>(modifier.action)
+ val action = assertIs<StartActivityComponentAction>(modifier.action)
val component = assertNotNull(action.componentName)
assertThat(component).isEqualTo(c)
@@ -67,9 +67,9 @@
fun testLaunchFromComponentWithContext() = fakeCoroutineScope.runBlockingTest {
val c = ComponentName(context, "androidx.glance.action.TestActivity")
- val modifiers = GlanceModifier.clickable(actionLaunchActivity(c))
+ val modifiers = GlanceModifier.clickable(actionStartActivity(c))
val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
- val action = assertIs<LaunchActivityComponentAction>(modifier.action)
+ val action = assertIs<StartActivityComponentAction>(modifier.action)
val component = assertNotNull(action.componentName)
assertThat(component).isEqualTo(c)
diff --git a/lifecycle/lifecycle-viewmodel-compose/build.gradle b/lifecycle/lifecycle-viewmodel-compose/build.gradle
index 1285491..2932a3a 100644
--- a/lifecycle/lifecycle-viewmodel-compose/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/build.gradle
@@ -29,7 +29,7 @@
dependencies {
kotlinPlugin(projectOrArtifact(":compose:compiler:compiler"))
- api "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0"
+ api projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx")
api("androidx.compose.runtime:runtime:1.0.1")
api "androidx.compose.ui:ui:1.0.1"
@@ -43,6 +43,13 @@
androidTestImplementation(libs.truth)
androidTestImplementation "androidx.fragment:fragment:1.3.0"
androidTestImplementation "androidx.appcompat:appcompat:1.3.0"
+ // old version of common-java8 conflicts with newer version, because both have
+ // DefaultLifecycleEventObserver.
+ // Outside of androidx this is resolved via constraint added to lifecycle-common,
+ // but it doesn't work in androidx.
+ // See aosp/1804059
+ androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
+ androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate")
androidTestImplementation projectOrArtifact(":activity:activity-compose")
samples(projectOrArtifact(":lifecycle:lifecycle-viewmodel-compose:lifecycle-viewmodel-compose-samples"))
@@ -59,12 +66,10 @@
// needed only while https://youtrack.jetbrains.com/issue/KT-47000 isn't resolved which is
// targeted to 1.6
-if (project.hasProperty("androidx.useMaxDepVersions")){
- tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=enable",
- ]
- }
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += [
+ "-Xjvm-default=enable",
+ ]
}
}
\ No newline at end of file
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/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index a864da0..82a3f77 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -31,10 +31,10 @@
dependencies {
api("androidx.annotation:annotation:1.1.0")
- api("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
- api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1")
+ api(projectOrArtifact(":lifecycle:lifecycle-runtime-ktx"))
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx"))
api("androidx.savedstate:savedstate:1.0.0")
- api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1")
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
implementation("androidx.core:core-ktx:1.1.0")
implementation("androidx.collection:collection-ktx:1.1.0")
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index eadea17..ba33048e 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -30,12 +30,18 @@
implementation(libs.kotlinStdlib)
implementation("androidx.compose.foundation:foundation-layout:1.0.1")
- api("androidx.activity:activity-compose:1.3.1")
+ api(projectOrArtifact(":activity:activity-compose"))
api("androidx.compose.animation:animation:1.0.1")
api("androidx.compose.runtime:runtime:1.0.1")
api("androidx.compose.runtime:runtime-saveable:1.0.1")
api("androidx.compose.ui:ui:1.0.1")
- api("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0")
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-compose"))
+ // old version of common-java8 conflicts with newer version, because both have
+ // DefaultLifecycleEventObserver.
+ // Outside of androidx this is resolved via constraint added to lifecycle-common,
+ // but it doesn't work in androidx.
+ // See aosp/1804059
+ implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
api(projectOrArtifact(":navigation:navigation-runtime-ktx"))
androidTestImplementation(projectOrArtifact(":compose:material:material"))
diff --git a/navigation/navigation-dynamic-features-fragment/build.gradle b/navigation/navigation-dynamic-features-fragment/build.gradle
index a7a7afa..48c5b1f 100644
--- a/navigation/navigation-dynamic-features-fragment/build.gradle
+++ b/navigation/navigation-dynamic-features-fragment/build.gradle
@@ -73,12 +73,10 @@
// needed only while https://youtrack.jetbrains.com/issue/KT-47000 isn't resolved which is
// targeted to 1.6
-if (project.hasProperty("androidx.useMaxDepVersions")){
- tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += [
- "-Xjvm-default=enable",
- ]
- }
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += [
+ "-Xjvm-default=enable",
+ ]
}
}
\ No newline at end of file
diff --git a/navigation/navigation-fragment/build.gradle b/navigation/navigation-fragment/build.gradle
index 8108470..8d49434 100644
--- a/navigation/navigation-fragment/build.gradle
+++ b/navigation/navigation-fragment/build.gradle
@@ -24,7 +24,7 @@
}
dependencies {
- api("androidx.fragment:fragment-ktx:1.4.0")
+ api(projectOrArtifact(":fragment:fragment-ktx"))
api(project(":navigation:navigation-runtime"))
api(projectOrArtifact(":slidingpanelayout:slidingpanelayout"))
api(libs.kotlinStdlib)
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index 4d5e19f..f52b59c 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -26,13 +26,13 @@
dependencies {
api(project(":navigation:navigation-common"))
- api("androidx.activity:activity-ktx:1.2.3")
- api("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
- api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1")
+ api(projectOrArtifact(":activity:activity-ktx"))
+ api(projectOrArtifact(":lifecycle:lifecycle-runtime-ktx"))
+ api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx"))
api("androidx.annotation:annotation-experimental:1.1.0")
api(libs.kotlinStdlib)
- androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
+ androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
androidTestImplementation(project(":internal-testutils-navigation"))
androidTestImplementation(project(":internal-testutils-runtime"))
androidTestImplementation(libs.testExtJunit)
@@ -69,8 +69,6 @@
freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
// needed only while https://youtrack.jetbrains.com/issue/KT-47000 isn't resolved which is
// targeted to 1.6
- if (project.hasProperty("androidx.useMaxDepVersions")) {
- freeCompilerArgs += ["-Xjvm-default=enable"]
- }
+ freeCompilerArgs += ["-Xjvm-default=enable"]
}
}
\ No newline at end of file
diff --git a/playground-common/playground-plugin/src/main/kotlin/androidx/playground/PlaygroundExtension.kt b/playground-common/playground-plugin/src/main/kotlin/androidx/playground/PlaygroundExtension.kt
index 90a699a..4bf9938 100644
--- a/playground-common/playground-plugin/src/main/kotlin/androidx/playground/PlaygroundExtension.kt
+++ b/playground-common/playground-plugin/src/main/kotlin/androidx/playground/PlaygroundExtension.kt
@@ -168,6 +168,8 @@
if (name == ":compose:test-utils") return true
if (name == ":compose:lint:common-test") return true
if (name == ":test:screenshot:screenshot") return true
+ if (name == ":lifecycle:lifecycle-common") return true
+ if (name == ":lifecycle:lifecycle-common-java8") return true
return false
}
}
\ No newline at end of file
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index 1c4f9a7..ec2d6c4 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -25,7 +25,7 @@
kotlin.code.style=official
# Disable docs
androidx.enableDocumentation=false
-androidx.playground.snapshotBuildId=7967568
+androidx.playground.snapshotBuildId=7990267
androidx.playground.metalavaBuildId=7856580
androidx.playground.dokkaBuildId=7472101
androidx.studio.type=playground
diff --git a/preference/preference/src/main/java/androidx/preference/PreferenceInflater.java b/preference/preference/src/main/java/androidx/preference/PreferenceInflater.java
index df0b2b0..dcddf49 100644
--- a/preference/preference/src/main/java/androidx/preference/PreferenceInflater.java
+++ b/preference/preference/src/main/java/androidx/preference/PreferenceInflater.java
@@ -49,7 +49,7 @@
private PreferenceManager mPreferenceManager;
private String[] mDefaultPackages;
- PreferenceInflater(@NonNull Context context, PreferenceManager preferenceManager) {
+ public PreferenceInflater(@NonNull Context context, PreferenceManager preferenceManager) {
mContext = context;
init(preferenceManager);
}
diff --git a/security/security-identity-credential/src/main/java/androidx/security/identity/IdentityCredentialStore.java b/security/security-identity-credential/src/main/java/androidx/security/identity/IdentityCredentialStore.java
index 3054ad4..c2dc700 100644
--- a/security/security-identity-credential/src/main/java/androidx/security/identity/IdentityCredentialStore.java
+++ b/security/security-identity-credential/src/main/java/androidx/security/identity/IdentityCredentialStore.java
@@ -211,6 +211,10 @@
/**
* Creates a new credential.
*
+ * <p>Note that the credential is not persisted until calling
+ * {@link WritableIdentityCredential#personalize(PersonalizationData)} on the returned
+ * {@link WritableIdentityCredential} object.
+ *
* @param credentialName The name used to identify the credential.
* @param docType The document type for the credential.
* @return A @{link WritableIdentityCredential} that can be used to create a new credential.
diff --git a/security/security-identity-credential/src/main/java/androidx/security/identity/WritableIdentityCredential.java b/security/security-identity-credential/src/main/java/androidx/security/identity/WritableIdentityCredential.java
index 2b6aab0..7b2e394 100644
--- a/security/security-identity-credential/src/main/java/androidx/security/identity/WritableIdentityCredential.java
+++ b/security/security-identity-credential/src/main/java/androidx/security/identity/WritableIdentityCredential.java
@@ -25,8 +25,11 @@
/**
* Class used to personalize a new identity credential.
*
- * <p>Credentials cannot be updated or modified after creation; any changes require deletion and
- * re-creation.
+ * <p>Note that the credential is not persisted until calling
+ * {@link #personalize(PersonalizationData)}.
+ *
+ * <p>Once persisted, the PII in a credential can be updated using
+ * {@link IdentityCredential#update(PersonalizationData)}.
*
* Use {@link IdentityCredentialStore#createCredential(String, String)} to create a new credential.
*/
@@ -62,6 +65,9 @@
* authority doesn't care about the nature of the security hardware. If called, however, this
* method must be called before {@link #personalize(PersonalizationData)}.
*
+ * <p>Note that the credential is not persisted until calling
+ * {@link #personalize(PersonalizationData)}.
+ *
* @param challenge is a non-empty byte array whose contents should be unique, fresh and
* provided by the issuing authority. The value provided is embedded in the
* attestation extension and enables the issuing authority to verify that the
@@ -74,6 +80,8 @@
/**
* Stores all of the data in the credential, with the specified access control profiles.
*
+ * <p>The credential is persisted only after this method returns successfully.
+ *
* <p>This method returns a COSE_Sign1 data structure signed by the CredentialKey with payload
* set to {@code ProofOfProvisioning} as defined below.
*
diff --git a/settings.gradle b/settings.gradle
index 9577478..afb72af 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -536,7 +536,7 @@
includeProject(":lifecycle:lifecycle-compiler", "lifecycle/lifecycle-compiler", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:lifecycle-extensions", "lifecycle/lifecycle-extensions", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:lifecycle-livedata", "lifecycle/lifecycle-livedata", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-livedata-core", "lifecycle/lifecycle-livedata-core", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR])
+includeProject(":lifecycle:lifecycle-livedata-core", "lifecycle/lifecycle-livedata-core", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
includeProject(":lifecycle:lifecycle-livedata-core-ktx", "lifecycle/lifecycle-livedata-core-ktx", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:lifecycle-livedata-core-ktx-lint", "lifecycle/lifecycle-livedata-core-ktx-lint", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:lifecycle-livedata-core-truth", "lifecycle/lifecycle-livedata-core-truth", [BuildType.MAIN, BuildType.FLAN])
@@ -549,12 +549,12 @@
includeProject(":lifecycle:lifecycle-runtime-ktx-lint", "lifecycle/lifecycle-runtime-ktx-lint", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
includeProject(":lifecycle:lifecycle-runtime-testing", "lifecycle/lifecycle-runtime-testing", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
includeProject(":lifecycle:lifecycle-service", "lifecycle/lifecycle-service", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-viewmodel", "lifecycle/lifecycle-viewmodel", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR])
+includeProject(":lifecycle:lifecycle-viewmodel", "lifecycle/lifecycle-viewmodel", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
includeProject(":lifecycle:lifecycle-viewmodel-compose", "lifecycle/lifecycle-viewmodel-compose", [BuildType.COMPOSE])
includeProject(":lifecycle:lifecycle-viewmodel-compose:lifecycle-viewmodel-compose-samples", "lifecycle/lifecycle-viewmodel-compose/samples", [BuildType.COMPOSE])
includeProject(":lifecycle:lifecycle-viewmodel-compose:integration-tests:lifecycle-viewmodel-demos", "lifecycle/lifecycle-viewmodel-compose/integration-tests/lifecycle-viewmodel-demos", [BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-viewmodel-ktx", "lifecycle/lifecycle-viewmodel-ktx", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-viewmodel-savedstate", "lifecycle/lifecycle-viewmodel-savedstate", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR])
+includeProject(":lifecycle:lifecycle-viewmodel-ktx", "lifecycle/lifecycle-viewmodel-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-viewmodel-savedstate", "lifecycle/lifecycle-viewmodel-savedstate", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
includeProject(":lint-checks", "lint-checks")
includeProject(":lint-checks:integration-tests", "lint-checks/integration-tests")
includeProject(":lint-demos:lint-demo-appcompat", "lint-demos/lint-demo-appcompat", [BuildType.MAIN])
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/compose/integration-tests/navigation/build.gradle b/wear/compose/integration-tests/navigation/build.gradle
index 9c93c22..b2f8d46 100644
--- a/wear/compose/integration-tests/navigation/build.gradle
+++ b/wear/compose/integration-tests/navigation/build.gradle
@@ -58,4 +58,10 @@
implementation(project(":wear:compose:compose-foundation-samples"))
implementation(project(":wear:compose:compose-material-samples"))
implementation(project(':wear:compose:compose-navigation'))
+ // old version of common-java8 conflicts with newer version, because both have
+ // DefaultLifecycleEventObserver.
+ // Outside of androidx this is resolved via constraint added to lifecycle-common,
+ // but it doesn't work in androidx.
+ // See aosp/1804059
+ androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
}
\ No newline at end of file
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";