Merge "Make sure there is only 1 instance of each type element" into androidx-main
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index baf38e0..d5cae8d7 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -9,6 +9,9 @@
- name: Fragment
url: https://issuetracker.google.com/issues/new?component=460964&template=1182267
about: File a bug or feature request for Fragment
+ - name: Lifecycle
+ url: https://issuetracker.google.com/issues/new?component=413132&template=1096619
+ about: File a bug or feature request for Lifecycle
- name: Navigation
url: https://issuetracker.google.com/issues/new?component=409828&template=1093757
about: File a bug or feature request for Navigation
diff --git a/.github/workflows/presubmit.yml b/.github/workflows/presubmit.yml
index c1dee53d..891ae8d 100644
--- a/.github/workflows/presubmit.yml
+++ b/.github/workflows/presubmit.yml
@@ -191,6 +191,59 @@
if: always()
run: echo ::set-output name=status::${{ job.status }}
+ build-lifecycle:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ runs-on: ${{ matrix.os }}
+ needs: [setup, lint]
+ outputs:
+ status: ${{ steps.output-status.outputs.status }}
+ env:
+ group-id: "lifecycle"
+ steps:
+ - name: "Checkout androidx repo"
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 1
+
+ - name: "Setup JDK 11"
+ id: setup-java
+ uses: actions/setup-java@v1
+ with:
+ java-version: "11"
+
+ - name: "Set environment variables"
+ shell: bash
+ run: |
+ set -x
+ echo "ANDROID_SDK_ROOT=$HOME/Library/Android/sdk" >> $GITHUB_ENV
+ echo "DIST_DIR=$HOME/dist" >> $GITHUB_ENV
+
+ - name: "./gradlew buildOnServer"
+ uses: eskatos/gradle-command-action@v1
+ env:
+ JAVA_HOME: ${{ steps.setup-java.outputs.path }}
+ with:
+ arguments: buildOnServer
+ build-root-directory: ${{ env.group-id }}
+ gradle-executable: ${{ env.group-id }}/gradlew
+ wrapper-directory: ${{ env.group-id }}/gradle/wrapper
+
+ - name: "Upload build artifacts"
+ continue-on-error: true
+ if: always()
+ uses: actions/upload-artifact@v2
+ with:
+ name: artifacts_${{ env.group-id }}
+ path: ~/dist
+
+ - name: "Report job status"
+ id: output-status
+ if: always()
+ run: echo ::set-output name=status::${{ job.status }}
+
build-navigation:
strategy:
fail-fast: false
@@ -411,6 +464,7 @@
lint,
build-activity,
build-fragment,
+ build-lifecycle,
build-navigation,
build-paging,
build-room,
@@ -425,6 +479,7 @@
if [ "${{ needs.lint.outputs.status }}" == "success" ] && \
[ "${{ needs.build-activity.outputs.status }}" == "success" ] && \
[ "${{ needs.build-fragment.outputs.status }}" == "success" ] && \
+ [ "${{ needs.build-lifecycle.outputs.status }}" == "success" ] && \
[ "${{ needs.build-navigation.outputs.status }}" == "success" ] && \
[ "${{ needs.build-paging.outputs.status }}" == "success" ] && \
[ "${{ needs.build-room.outputs.status }}" == "success" ] && \
diff --git a/.github/workflows/prune_artifacts.yml b/.github/workflows/prune_artifacts.yml
new file mode 100644
index 0000000..6d4171e
--- /dev/null
+++ b/.github/workflows/prune_artifacts.yml
@@ -0,0 +1,14 @@
+name: "Prune old artifacts"
+on:
+ schedule:
+ - cron: "0 * * * *"
+
+jobs:
+ delete-artifacts:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: kolpav/purge-artifacts-action@v1
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ expire-in: 7days
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4fc8df1..433e83b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,6 +12,7 @@
- [Activity](https://developer.android.com/guide/components/activities/intro-activities)
- [Biometric](https://developer.android.com/training/sign-in/biometric-auth)
- [Fragment](https://developer.android.com/guide/components/fragments)
+ - [Lifecycle](https://developer.android.com/topic/libraries/architecture/lifecycle)
- [Navigation](https://developer.android.com/guide/navigation)
- [Paging](https://developer.android.com/topic/libraries/architecture/paging)
- [Room](https://developer.android.com/topic/libraries/architecture/room)
@@ -48,6 +49,7 @@
-- activity
-- biometric
-- fragment
+ -- lifecycle
-- navigation
-- paging
-- room
diff --git a/README.md b/README.md
index c9828cb9..dcc6d1a 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@
* [Activity](activity)
* [Biometric](biometric)
* [Fragment](fragment)
+* [Lifecycle](lifecycle)
* [Navigation](navigation)
* [Paging](paging)
* [Room](room)
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
index db721f3..d743f82 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
@@ -37,7 +37,7 @@
import java.util.Set;
public class SetSchemaRequestTest {
-
+// @exportToFramework:startStrip()
@AppSearchDocument
static class Card {
@AppSearchDocument.Uri
@@ -82,6 +82,7 @@
(indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
String mString;
}
+// @exportToFramework:endStrip()
private static Collection<String> getSchemaTypesFromSetSchemaRequest(SetSchemaRequest request) {
HashSet<String> schemaTypes = new HashSet<>();
@@ -132,6 +133,7 @@
assertThat(request.getSchemasNotVisibleToSystemUi()).containsExactly("Schema");
}
+// @exportToFramework:startStrip()
@Test
public void testDataClassVisibilityForSystemUi_visible() throws Exception {
// By default, the schema is visible.
@@ -154,6 +156,7 @@
Card.class, false).build();
assertThat(request.getSchemasNotVisibleToSystemUi()).containsExactly("Card");
}
+// @exportToFramework:endStrip()
@Test
public void testSchemaTypeVisibilityForPackage_visible() {
@@ -231,6 +234,7 @@
assertThat(request.getSchemasVisibleToPackages()).isEmpty();
}
+// @exportToFramework:startStrip()
@Test
public void testDataClassVisibilityForPackage_visible() throws Exception {
// By default, the schema is not visible.
@@ -329,4 +333,5 @@
assertThat(getSchemaTypesFromSetSchemaRequest(request)).containsExactly("Queen",
"King");
}
+// @exportToFramework:endStrip()
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
index 44d780c..3b4bdb5 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
@@ -45,6 +45,8 @@
import androidx.appsearch.localstorage.LocalStorage;
import androidx.test.core.app.ApplicationProvider;
+import com.google.common.util.concurrent.MoreExecutors;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -107,12 +109,15 @@
mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(emailSchema).build()).get();
}
+// @exportToFramework:startStrip()
@Test
public void testSetSchema_dataClass() throws Exception {
mDb1.setSchema(
new SetSchemaRequest.Builder().addDataClass(EmailDataClass.class).build()).get();
}
+// @exportToFramework:endStrip()
+// @exportToFramework:startStrip()
@Test
public void testGetSchema() throws Exception {
AppSearchSchema emailSchema1 = new AppSearchSchema.Builder("Email1")
@@ -158,6 +163,7 @@
assertThat(actual1).isEqualTo(request1.getSchemas());
assertThat(actual2).isEqualTo(request2.getSchemas());
}
+// @exportToFramework:endStrip()
@Test
public void testPutDocuments() throws Exception {
@@ -179,6 +185,7 @@
assertThat(result.getFailures()).isEmpty();
}
+// @exportToFramework:startStrip()
@Test
public void testPutDocuments_dataClass() throws Exception {
// Schema registration
@@ -196,6 +203,7 @@
assertThat(result.getSuccesses()).containsExactly("uri1", null);
assertThat(result.getFailures()).isEmpty();
}
+// @exportToFramework:endStrip()
@Test
public void testUpdateSchema() throws Exception {
@@ -437,6 +445,7 @@
.isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
}
+// @exportToFramework:startStrip()
@Test
public void testGetDocuments_dataClass() throws Exception {
// Schema registration
@@ -459,6 +468,7 @@
assertThat(inEmail.subject).isEqualTo(outEmail.subject);
assertThat(inEmail.body).isEqualTo(outEmail.body);
}
+// @exportToFramework:endStrip()
@Test
public void testQuery() throws Exception {
@@ -1285,4 +1295,72 @@
assertThat(getResult.getFailures().get("uri1").getResultCode())
.isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
}
+
+ @Test
+ public void testCloseAndReopen() throws Exception {
+ // Schema registration
+ mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build());
+
+ // Index a document
+ AppSearchEmail inEmail =
+ new AppSearchEmail.Builder("uri1")
+ .setFrom("from@example.com")
+ .setTo("to1@example.com", "to2@example.com")
+ .setSubject("testPut example")
+ .setBody("This is the body of the testPut email")
+ .build();
+ checkIsBatchResultSuccess(mDb1.putDocuments(
+ new PutDocumentsRequest.Builder().addGenericDocument(inEmail).build()));
+
+ // close and re-open the appSearchSession
+ mDb1.close();
+ Context context = ApplicationProvider.getApplicationContext();
+ mDb1 = LocalStorage.createSearchSession(
+ new LocalStorage.SearchContext.Builder(context)
+ .setDatabaseName(DB_NAME_1).build()).get();
+
+ // Query for the document
+ SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .build());
+ List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+ assertThat(documents).containsExactly(inEmail);
+ }
+
+ @Test
+ public void testCallAfterClose() throws Exception {
+
+ // Create a same-thread database by inject an executor which could help us maintain the
+ // execution order of those async tasks.
+ Context context = ApplicationProvider.getApplicationContext();
+ AppSearchSession sameThreadDb = LocalStorage.createSearchSession(
+ new LocalStorage.SearchContext.Builder(context)
+ .setDatabaseName("sameThreadDb").build(),
+ MoreExecutors.newDirectExecutorService()).get();
+
+ try {
+ // Schema registration -- just mutate something
+ sameThreadDb.setSchema(
+ new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
+
+ // Close the database. No further call will be allowed.
+ sameThreadDb.close();
+
+ // Try to query the closed database
+ // We are using the same-thread db here to make sure it has been closed.
+ IllegalStateException e = assertThrows(IllegalStateException.class, () ->
+ sameThreadDb.query("query", new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+ .build()));
+ assertThat(e).hasMessageThat().contains("AppSearchSession has already been closed");
+ } finally {
+ // To clean the data that has been added in the test, need to re-open the session and
+ // set an empty schema.
+ AppSearchSession reopen = LocalStorage.createSearchSession(
+ new LocalStorage.SearchContext.Builder(context)
+ .setDatabaseName("sameThreadDb").build(),
+ MoreExecutors.newDirectExecutorService()).get();
+ reopen.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+ }
+ }
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
index a2c188c..0f34d6a 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -19,6 +19,7 @@
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
import com.google.common.util.concurrent.ListenableFuture;
@@ -210,4 +211,13 @@
@NonNull
ListenableFuture<Void> removeByQuery(
@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+
+ /**
+ * Closes the SearchSessionImpl to persists all update/delete requests to the disk.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ // TODO(b/175637134) when unhide it, extends Closeable and remove this method.
+ void close();
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
index 28357af..049fc1f 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -450,6 +450,7 @@
}
}
+// @exportToFramework:startStrip()
/**
* Converts this GenericDocument into an instance of the provided data class.
*
@@ -471,6 +472,7 @@
DataClassFactory<T> factory = registry.getOrCreateFactory(dataClass);
return factory.fromGenericDocument(this);
}
+// @exportToFramework:endStrip()
@Override
public boolean equals(@Nullable Object other) {
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
index 7adee3c..1a481f1 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
@@ -69,6 +69,7 @@
return this;
}
+// @exportToFramework:startStrip()
/**
* Adds one or more documents to the request.
*
@@ -113,6 +114,7 @@
DataClassFactory<T> factory = registry.getOrCreateFactory(dataClass);
return factory.toGenericDocument(dataClass);
}
+// @exportToFramework:endStrip()
/** Builds a new {@link PutDocumentsRequest}. */
@NonNull
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
index f3c6a54..1cf99e8 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
@@ -299,6 +299,7 @@
return this;
}
+// @exportToFramework:startStrip()
/**
* Adds the Schema type of given data classes to the Schema type filter of
* {@link SearchSpec} Entry. Only search for documents that have the specified schema types.
@@ -323,7 +324,9 @@
addSchemaType(schemaTypes);
return this;
}
+// @exportToFramework:endStrip()
+// @exportToFramework:startStrip()
/**
* Adds the Schema type of given data classes to the Schema type filter of
* {@link SearchSpec} Entry. Only search for documents that have the specified schema types.
@@ -340,6 +343,7 @@
Preconditions.checkNotNull(dataClasses);
return addSchemaByDataClass(Arrays.asList(dataClasses));
}
+// @exportToFramework:endStrip()
/**
* Adds a namespace filter to {@link SearchSpec} Entry. Only search for documents that
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
index 9f65a80..07df364 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
@@ -139,6 +139,7 @@
return this;
}
+// @exportToFramework:startStrip()
/**
* Adds one or more types to the schema.
*
@@ -181,6 +182,7 @@
}
return addSchema(schemas);
}
+// @exportToFramework:endStrip()
/**
* Sets visibility on system UI surfaces for the given {@code schemaType}.
@@ -243,6 +245,7 @@
return this;
}
+// @exportToFramework:startStrip()
/**
* Sets visibility on system UI surfaces for the given {@code dataClass}.
*
@@ -287,6 +290,7 @@
return setSchemaTypeVisibilityForPackage(factory.getSchemaType(), visible,
packageIdentifier);
}
+// @exportToFramework:endStrip()
/**
* Configures the {@link SetSchemaRequest} to delete any existing documents that don't
diff --git a/appsearch/exportToFramework.py b/appsearch/exportToFramework.py
index ab2e767..69bdb81 100755
--- a/appsearch/exportToFramework.py
+++ b/appsearch/exportToFramework.py
@@ -58,11 +58,12 @@
print('Prune: remove "%s"' % abs_path)
os.remove(abs_path)
- def _TransformAndCopyFile(self, source_path, dest_path, transform_func=None):
+ def _TransformAndCopyFile(
+ self, source_path, dest_path, transform_func=None, ignore_skips=False):
with open(source_path, 'r') as fh:
contents = fh.read()
- if '@exportToFramework:skipFile()' in contents:
+ if not ignore_skips and '@exportToFramework:skipFile()' in contents:
print('Skipping: "%s" -> "%s"' % (source_path, dest_path), file=sys.stderr)
return
@@ -79,6 +80,12 @@
subprocess.check_call(google_java_format_cmd, cwd=self._framework_appsearch_root)
def _TransformCommonCode(self, contents):
+ # Apply strips
+ contents = re.sub(
+ r'\/\/ @exportToFramework:startStrip\(\).*?\/\/ @exportToFramework:endStrip\(\)',
+ '',
+ contents,
+ flags=re.DOTALL)
return (contents
.replace('androidx.appsearch.app', 'android.app.appsearch')
.replace(
@@ -91,6 +98,7 @@
.replace(
'androidx.annotation.VisibleForTesting',
'com.android.internal.annotations.VisibleForTesting')
+ .replace('androidx.annotation.', 'android.annotation.')
.replace('androidx.collection.ArrayMap', 'android.util.ArrayMap')
.replace('androidx.collection.ArraySet', 'android.util.ArraySet')
.replace(
@@ -100,19 +108,48 @@
'androidx.core.util.Preconditions',
'com.android.internal.util.Preconditions')
.replace('import androidx.annotation.RestrictTo;', '')
+ .replace('@RestrictTo(RestrictTo.Scope.LIBRARY)', '')
.replace('@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)', '')
.replace('ObjectsCompat.', 'Objects.')
- .replace('androidx.', 'android.')
+ .replace('// @exportToFramework:skipFile()', '')
)
def _TransformTestCode(self, contents):
contents = (contents
- .replace('org.junit.Assert.assertThrows',
- 'org.testng.Assert.expectThrows')
+ .replace('org.junit.Assert.assertThrows', 'org.testng.Assert.expectThrows')
.replace('assertThrows(', 'expectThrows(')
+ .replace('androidx.appsearch.app.util.', 'com.android.server.appsearch.testing.')
+ .replace(
+ 'package androidx.appsearch.app.util;',
+ 'package com.android.server.appsearch.testing;')
+ .replace('AppSearchSession;', 'AppSearchSessionShim;')
+ .replace('AppSearchSession ', 'AppSearchSessionShim ')
+ .replace('GlobalSearchSession;', 'GlobalSearchSessionShim;')
+ .replace('GlobalSearchSession ', 'GlobalSearchSessionShim ')
+ .replace('SearchResults;', 'SearchResultsShim;')
+ .replace('SearchResults ', 'SearchResultsShim ')
+ .replace(
+ 'LocalStorage.createSearchSession(',
+ 'AppSearchSessionShimImpl.createSearchSession(')
+ .replace(
+ 'LocalStorage.createGlobalSearchSession(',
+ 'GlobalSearchSessionShimImpl.createGlobalSearchSession(')
+ .replace(
+ 'new LocalStorage.SearchContext.Builder(context)',
+ 'new LocalStorage.SearchContext.Builder()')
+ .replace(
+ 'new LocalStorage.GlobalSearchContext.Builder(context).build()',
+ '')
+ .replace('LocalStorage.', 'AppSearchManager.')
)
return self._TransformCommonCode(contents)
+ def _TransformCtsTestCode(self, contents):
+ contents = self._TransformTestCode(contents)
+ return (contents
+ .replace('android.app.appsearch.test', 'com.android.cts.appsearch')
+ )
+
def _TransformAndCopyFolder(self, source_dir, dest_dir, transform_func=None):
for currentpath, folders, files in os.walk(source_dir):
dir_rel_to_root = os.path.relpath(currentpath, source_dir)
@@ -180,12 +217,20 @@
# Copy CTS tests
print('~~~ Copying CTS tests ~~~')
self._TransformAndCopyFolder(
- cts_test_source_dir, cts_test_dest_dir, transform_func=self._TransformTestCode)
+ cts_test_source_dir, cts_test_dest_dir, transform_func=self._TransformCtsTestCode)
# Copy test utils
print('~~~ Copying test utils ~~~')
self._TransformAndCopyFolder(
test_util_source_dir, test_util_dest_dir, transform_func=self._TransformTestCode)
+ for iface_file in (
+ 'AppSearchSession.java', 'GlobalSearchSession.java', 'SearchResults.java'):
+ dest_file_name = os.path.splitext(iface_file)[0] + 'Shim.java'
+ self._TransformAndCopyFile(
+ os.path.join(api_source_dir, 'app/' + iface_file),
+ os.path.join(test_util_dest_dir, dest_file_name),
+ transform_func=self._TransformTestCode,
+ ignore_skips=True)
def _ExportImplCode(self):
impl_source_dir = os.path.join(self._jetpack_appsearch_root, JETPACK_IMPL_ROOT)
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 627e87a..1a20e58 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -47,6 +47,7 @@
import com.google.android.icing.proto.IcingSearchEngineOptions;
import com.google.android.icing.proto.InitializeResultProto;
import com.google.android.icing.proto.OptimizeResultProto;
+import com.google.android.icing.proto.PersistToDiskResultProto;
import com.google.android.icing.proto.PropertyConfigProto;
import com.google.android.icing.proto.PropertyProto;
import com.google.android.icing.proto.PutResultProto;
@@ -628,6 +629,25 @@
}
/**
+ * Persists all update/delete requests to the disk.
+ *
+ * <p>If the app crashes after a call to PersistToDisk(), Icing would be able to fully recover
+ * all data written up to this point without a costly recovery process.
+ *
+ * <p>If the app crashes before a call to PersistToDisk(), Icing would trigger a costly
+ * recovery process in next initialization. After that, Icing would still be able to recover
+ * all written data.
+ *
+ * @throws AppSearchException
+ */
+ public void persistToDisk() throws AppSearchException {
+ PersistToDiskResultProto persistToDiskResultProto =
+ mIcingSearchEngineLocked.persistToDisk();
+ checkSuccess(persistToDiskResultProto.getStatus());
+ }
+
+
+ /**
* Clears documents and schema across all packages and databaseNames.
*
* <p>This method also clear all data in {@link VisibilityStore}, an
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
index e06f75d..8f6cfde 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
@@ -175,9 +175,30 @@
public static ListenableFuture<AppSearchSession> createSearchSession(
@NonNull SearchContext context) {
Preconditions.checkNotNull(context);
- return FutureUtil.execute(EXECUTOR_SERVICE, () -> {
+ return createSearchSession(context, EXECUTOR_SERVICE);
+ }
+
+ /**
+ * Opens a new {@link AppSearchSession} on this storage with executor.
+ *
+ * <p>This process requires a native search library. If it's not created, the initialization
+ * process will create one.
+ *
+ * @param context The {@link SearchContext} contains all information to create a new
+ * {@link AppSearchSession}
+ * @param executor The executor of where tasks will execute.
+ * @hide
+ */
+ @NonNull
+ @VisibleForTesting
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static ListenableFuture<AppSearchSession> createSearchSession(
+ @NonNull SearchContext context, @NonNull ExecutorService executor) {
+ Preconditions.checkNotNull(context);
+ Preconditions.checkNotNull(executor);
+ return FutureUtil.execute(executor, () -> {
LocalStorage instance = getOrCreateInstance(context.mContext);
- return instance.doCreateSearchSession(context);
+ return instance.doCreateSearchSession(context, executor);
});
}
@@ -197,7 +218,7 @@
Preconditions.checkNotNull(context);
return FutureUtil.execute(EXECUTOR_SERVICE, () -> {
LocalStorage instance = getOrCreateInstance(context.mContext);
- return instance.doCreateGlobalSearchSession();
+ return instance.doCreateGlobalSearchSession(EXECUTOR_SERVICE);
});
}
@@ -230,13 +251,14 @@
}
@NonNull
- private AppSearchSession doCreateSearchSession(@NonNull SearchContext context) {
- return new SearchSessionImpl(mAppSearchImpl, EXECUTOR_SERVICE,
+ private AppSearchSession doCreateSearchSession(@NonNull SearchContext context,
+ @NonNull ExecutorService executor) {
+ return new SearchSessionImpl(mAppSearchImpl, executor,
context.mContext.getPackageName(), context.mDatabaseName);
}
@NonNull
- private GlobalSearchSession doCreateGlobalSearchSession() {
- return new GlobalSearchSessionImpl(mAppSearchImpl, EXECUTOR_SERVICE);
+ private GlobalSearchSession doCreateGlobalSearchSession(@NonNull ExecutorService executor) {
+ return new GlobalSearchSessionImpl(mAppSearchImpl, executor);
}
}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
index c846667..e11f1f0 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
@@ -53,6 +53,8 @@
private boolean mIsFirstLoad = true;
+ private boolean mIsClosed = false;
+
SearchResultsImpl(
@NonNull AppSearchImpl appSearchImpl,
@NonNull ExecutorService executorService,
@@ -71,6 +73,7 @@
@Override
@NonNull
public ListenableFuture<List<SearchResult>> getNextPage() {
+ Preconditions.checkState(!mIsClosed, "SearchResults has already been closed");
return FutureUtil.execute(mExecutorService, () -> {
SearchResultPage searchResultPage;
if (mIsFirstLoad) {
@@ -104,9 +107,12 @@
public void close() {
// Checking the future result is not needed here since this is a cleanup step which is not
// critical to the correct functioning of the system; also, the return value is void.
- FutureUtil.execute(mExecutorService, () -> {
- mAppSearchImpl.invalidateNextPageToken(mNextPageToken);
- return null;
- });
+ if (!mIsClosed) {
+ FutureUtil.execute(mExecutorService, () -> {
+ mAppSearchImpl.invalidateNextPageToken(mNextPageToken);
+ mIsClosed = true;
+ return null;
+ });
+ }
}
}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index 8bc6eb9..973b724 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -53,6 +53,8 @@
private final ExecutorService mExecutorService;
private final String mPackageName;
private final String mDatabaseName;
+ private boolean mIsMutated = false;
+ private boolean mIsClosed = false;
SearchSessionImpl(
@NonNull AppSearchImpl appSearchImpl,
@@ -69,6 +71,7 @@
@NonNull
public ListenableFuture<Void> setSchema(@NonNull SetSchemaRequest request) {
Preconditions.checkNotNull(request);
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
return execute(() -> {
mAppSearchImpl.setSchema(
mPackageName,
@@ -76,6 +79,7 @@
new ArrayList<>(request.getSchemas()),
new ArrayList<>(request.getSchemasNotVisibleToSystemUi()),
request.isForceOverride());
+ mIsMutated = true;
return null;
});
}
@@ -83,6 +87,7 @@
@Override
@NonNull
public ListenableFuture<Set<AppSearchSchema>> getSchema() {
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
return execute(() -> {
List<AppSearchSchema> schemas = mAppSearchImpl.getSchema(mPackageName, mDatabaseName);
return new ArraySet<>(schemas);
@@ -94,6 +99,7 @@
public ListenableFuture<AppSearchBatchResult<String, Void>> putDocuments(
@NonNull PutDocumentsRequest request) {
Preconditions.checkNotNull(request);
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
return execute(() -> {
AppSearchBatchResult.Builder<String, Void> resultBuilder =
new AppSearchBatchResult.Builder<>();
@@ -106,6 +112,7 @@
resultBuilder.setResult(document.getUri(), throwableToFailedResult(t));
}
}
+ mIsMutated = true;
return resultBuilder.build();
});
}
@@ -115,6 +122,7 @@
public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByUri(
@NonNull GetByUriRequest request) {
Preconditions.checkNotNull(request);
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
return execute(() -> {
AppSearchBatchResult.Builder<String, GenericDocument> resultBuilder =
new AppSearchBatchResult.Builder<>();
@@ -140,6 +148,7 @@
@NonNull SearchSpec searchSpec) {
Preconditions.checkNotNull(queryExpression);
Preconditions.checkNotNull(searchSpec);
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
return new SearchResultsImpl(
mAppSearchImpl,
mExecutorService,
@@ -154,6 +163,7 @@
public ListenableFuture<AppSearchBatchResult<String, Void>> removeByUri(
@NonNull RemoveByUriRequest request) {
Preconditions.checkNotNull(request);
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
return execute(() -> {
AppSearchBatchResult.Builder<String, Void> resultBuilder =
new AppSearchBatchResult.Builder<>();
@@ -165,6 +175,7 @@
resultBuilder.setResult(uri, throwableToFailedResult(t));
}
}
+ mIsMutated = true;
return resultBuilder.build();
});
}
@@ -175,12 +186,27 @@
@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
Preconditions.checkNotNull(queryExpression);
Preconditions.checkNotNull(searchSpec);
+ Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
return execute(() -> {
mAppSearchImpl.removeByQuery(mPackageName, mDatabaseName, queryExpression, searchSpec);
+ mIsMutated = true;
return null;
});
}
+ @Override
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void close() {
+ if (mIsMutated && !mIsClosed) {
+ // No future is needed here since the method is void.
+ FutureUtil.execute(mExecutorService, () -> {
+ mAppSearchImpl.persistToDisk();
+ mIsClosed = true;
+ return null;
+ });
+ }
+ }
+
private <T> ListenableFuture<T> execute(Callable<T> callable) {
return FutureUtil.execute(mExecutorService, callable);
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index 1e2c5ac..d3040c6 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -99,7 +99,7 @@
const val REACTIVE_STREAMS = "org.reactivestreams:reactive-streams:1.0.0"
const val RX_JAVA = "io.reactivex.rxjava2:rxjava:2.2.9"
const val RX_JAVA3 = "io.reactivex.rxjava3:rxjava:3.0.0"
-val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.1.18"
+val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.1.21"
val SKIKO = "org.jetbrains.skiko:skiko-jvm:$SKIKO_VERSION"
val SKIKO_LINUX_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-linux-x64:$SKIKO_VERSION"
val SKIKO_MACOS_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-macos-x64:$SKIKO_VERSION"
diff --git a/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt
index 01510147..0954725 100644
--- a/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt
@@ -210,21 +210,14 @@
val docsRuntimeClasspath = project.configurations.create("docs-runtime-classpath") {
it.setResolveClasspathForUsage(Usage.JAVA_RUNTIME)
}
- docsCompileClasspath.resolutionStrategy {
- val buildVersions = (project.rootProject.property("ext") as ExtraPropertiesExtension)
- .let { it.get("build_versions") as Map<*, *> }
- it.eachDependency { details ->
- if (details.requested.group == "org.jetbrains.kotlin") {
- details.useVersion(buildVersions["kotlin"] as String)
- }
- }
- }
- docsRuntimeClasspath.resolutionStrategy {
- val buildVersions = (project.rootProject.property("ext") as ExtraPropertiesExtension)
- .let { it.get("build_versions") as Map<*, *> }
- it.eachDependency { details ->
- if (details.requested.group == "org.jetbrains.kotlin") {
- details.useVersion(buildVersions["kotlin"] as String)
+ listOf(docsCompileClasspath, docsRuntimeClasspath).forEach { config ->
+ config.resolutionStrategy {
+ val versions = (project.rootProject.property("ext") as ExtraPropertiesExtension)
+ .let { it.get("build_versions") as Map<*, *> }
+ it.eachDependency { details ->
+ if (details.requested.group == "org.jetbrains.kotlin") {
+ details.useVersion(versions["kotlin"] as String)
+ }
}
}
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt b/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
index c92d780..049dc31 100644
--- a/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
@@ -128,6 +128,9 @@
"dokkaKotlinDocs",
"zipDokkaDocs",
+ // Flakily not up-to-date, b/176120659
+ "doclavaDocs",
+
// We should be able to remove these entries when b/160392650 is fixed
"lint",
"lintDebug",
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 628cc89..c2e6423 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
@@ -20,7 +20,7 @@
import android.graphics.Rect
import android.hardware.camera2.CameraCharacteristics
import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Log.warn
+import androidx.camera.camera2.pipe.core.Log.warn
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.camera2.pipe.integration.impl.CameraProperties
import androidx.camera.camera2.pipe.integration.impl.EvCompControl
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
index 120f8b9..65f755f 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
@@ -17,11 +17,11 @@
import android.content.Context
import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.impl.Debug
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.camera2.pipe.impl.Timestamps
-import androidx.camera.camera2.pipe.impl.Timestamps.measureNow
-import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log.debug
+import androidx.camera.camera2.pipe.core.Timestamps
+import androidx.camera.camera2.pipe.core.Timestamps.measureNow
+import androidx.camera.camera2.pipe.core.Timestamps.formatMs
import androidx.camera.camera2.pipe.integration.config.CameraConfig
import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
import androidx.camera.camera2.pipe.integration.config.CameraAppConfig
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index 6c6d6da..0bf359a 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -20,7 +20,7 @@
import android.hardware.camera2.CameraCharacteristics
import android.view.Surface
import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.core.Log
import androidx.camera.camera2.pipe.integration.config.CameraConfig
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.camera2.pipe.integration.impl.CameraCallbackMap
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
index d3dcff7..7e753b2 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
@@ -17,8 +17,8 @@
package androidx.camera.camera2.pipe.integration.adapter
import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.camera2.pipe.impl.Log.warn
+import androidx.camera.camera2.pipe.core.Log.debug
+import androidx.camera.camera2.pipe.core.Log.warn
import androidx.camera.camera2.pipe.integration.config.CameraConfig
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.camera2.pipe.integration.impl.UseCaseManager
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
index d026e65..4b9e616 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
@@ -19,7 +19,7 @@
import android.graphics.ImageFormat
import android.util.Size
import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
import androidx.camera.core.impl.CameraDeviceSurfaceManager
import androidx.camera.core.impl.SurfaceConfig
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
index dcbcba5..a276b2c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
@@ -22,8 +22,8 @@
import android.util.Size
import android.view.Display
import android.view.WindowManager
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.camera2.pipe.impl.Log.info
+import androidx.camera.camera2.pipe.core.Log.debug
+import androidx.camera.camera2.pipe.core.Log.info
import androidx.camera.camera2.pipe.integration.impl.asLandscape
import androidx.camera.camera2.pipe.integration.impl.minByArea
import androidx.camera.camera2.pipe.integration.impl.toSize
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt
index 63f471f..0da56f5 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt
@@ -21,7 +21,7 @@
import androidx.camera.camera2.pipe.FrameInfo
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.RequestMetadata
-import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.core.Log
import androidx.camera.camera2.pipe.integration.impl.CAMERAX_TAG_BUNDLE
import androidx.camera.core.impl.CameraCaptureMetaData.AeState
import androidx.camera.core.impl.CameraCaptureMetaData.AfMode
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
index 93ff9e7..082989e 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
@@ -16,18 +16,15 @@
package androidx.camera.camera2.pipe.integration.impl
-import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CaptureRequest
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.FrameNumber
-import androidx.camera.camera2.pipe.RequestTemplate
-import androidx.camera.camera2.pipe.StreamConfig
+import androidx.camera.camera2.pipe.CameraStream
+import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.StreamFormat
import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.StreamType
import androidx.camera.camera2.pipe.TorchState
-import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.integration.config.CameraConfig
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraScope
import androidx.camera.core.UseCase
@@ -51,7 +48,7 @@
fun <T> setParametersAsync(values: Map<CaptureRequest.Key<*>, Any>): Deferred<Unit>
// 3A
- suspend fun setTorchAsync(enabled: Boolean): Deferred<FrameNumber>
+ suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A>
// Capture
@@ -90,7 +87,7 @@
cameraGraph.close()
}
- override suspend fun setTorchAsync(enabled: Boolean): Deferred<FrameNumber> {
+ override suspend fun setTorchAsync(enabled: Boolean): Deferred<Result3A> {
return cameraGraph.acquireSession().use {
it.setTorch(
when (enabled) {
@@ -152,31 +149,28 @@
callbackMap: CameraCallbackMap,
coroutineScope: CoroutineScope,
): UseCaseCamera {
- val streamConfigs = mutableListOf<StreamConfig>()
- val useCaseMap = mutableMapOf<StreamConfig, UseCase>()
+ val streamConfigs = mutableListOf<CameraStream.Config>()
+ val useCaseMap = mutableMapOf<CameraStream.Config, UseCase>()
// TODO: This may need to combine outputs that are (or will) share the same output
// imageReader or surface. Right now, each UseCase gets its own [StreamConfig]
// TODO: useCases only have a single `attachedSurfaceResolution`, yet they have a
// list of deferrableSurfaces.
for (useCase in useCases) {
- val config = StreamConfig(
+ val outputConfig = CameraStream.Config.create(
size = useCase.attachedSurfaceResolution!!,
format = StreamFormat(useCase.imageFormat),
- camera = cameraConfig.cameraId,
- type = StreamType.SURFACE,
- deferrable = false
+ camera = cameraConfig.cameraId
)
- streamConfigs.add(config)
- useCaseMap[config] = useCase
+ streamConfigs.add(outputConfig)
+ useCaseMap[outputConfig] = useCase
}
// Build up a config (using TEMPLATE_PREVIEW by default)
val config = CameraGraph.Config(
camera = cameraConfig.cameraId,
streams = streamConfigs,
- listeners = listOf(callbackMap),
- template = RequestTemplate(CameraDevice.TEMPLATE_PREVIEW)
+ defaultListeners = listOf(callbackMap),
)
val graph = cameraPipe.create(config)
@@ -189,7 +183,7 @@
// this code assumes only a single surface per UseCase.
val deferredSurfaces = useCaseSessionConfig?.surfaces
if (stream != null && deferredSurfaces != null && deferredSurfaces.size == 1) {
- val deferredSurface = deferredSurfaces[0]
+ val deferredSurface = deferredSurfaces.first()
graph.setSurface(stream.id, deferredSurface.surface.get())
surfaceToStreamMap[deferredSurface] = stream.id
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt
index 09607b0..90f9b42 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt
@@ -166,8 +166,8 @@
request = Request(
template = currentTemplate,
streams = currentStreams.toList(),
- requestParameters = currentParameters.toMap(),
- extraRequestParameters = currentInternalParameters.toMap(),
+ parameters = currentParameters.toMap(),
+ extras = currentInternalParameters.toMap()
)
result = updateSignal
updateSignal = null
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 1b76c6ff..2071570 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -16,7 +16,7 @@
package androidx.camera.camera2.pipe.integration.impl
-import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.core.Log
import androidx.camera.camera2.pipe.integration.config.CameraConfig
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraComponent
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeMetadata.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeMetadata.kt
index 1e8c988..bb9e816 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeMetadata.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeMetadata.kt
@@ -97,13 +97,13 @@
*/
public class FakeRequestMetadata(
private val requestParameters: Map<CaptureRequest.Key<*>, Any?> = emptyMap(),
- extraRequestParameters: Map<Metadata.Key<*>, Any?> = emptyMap(),
+ metadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
override val template: RequestTemplate = RequestTemplate(0),
override val streams: Map<StreamId, Surface> = mapOf(),
override val repeating: Boolean = false,
override val request: Request = Request(listOf()),
override val requestNumber: RequestNumber = RequestNumber(4321)
-) : FakeMetadata(request.extraRequestParameters.plus(extraRequestParameters)), RequestMetadata {
+) : FakeMetadata(request.extras.plus(metadata)), RequestMetadata {
override fun <T> get(key: CaptureRequest.Key<T>): T? = requestParameters[key] as T?
override fun <T> getOrDefault(key: CaptureRequest.Key<T>, default: T): T = get(key) ?: default
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
index 06a49c1..4333589 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
@@ -76,8 +76,8 @@
requestParameters = mapOf(CaptureRequest.JPEG_QUALITY to 95),
request = Request(
streams = listOf(),
- requestParameters = mapOf(CaptureRequest.JPEG_QUALITY to 20),
- extraRequestParameters = mapOf(FakeMetadata.TEST_KEY to 42)
+ parameters = mapOf(CaptureRequest.JPEG_QUALITY to 20),
+ extras = mapOf(FakeMetadata.TEST_KEY to 42)
)
)
@@ -124,8 +124,8 @@
requestParameters = mapOf(CaptureRequest.JPEG_QUALITY to 95),
request = Request(
streams = listOf(),
- requestParameters = mapOf(CaptureRequest.JPEG_QUALITY to 20),
- extraRequestParameters = mapOf(FakeMetadata.TEST_KEY to 42)
+ parameters = mapOf(CaptureRequest.JPEG_QUALITY to 20),
+ extras = mapOf(FakeMetadata.TEST_KEY to 42)
)
)
diff --git a/camera/camera-camera2-pipe/lint-baseline.xml b/camera/camera-camera2-pipe/lint-baseline.xml
deleted file mode 100644
index c8dcd32..0000000
--- a/camera/camera-camera2-pipe/lint-baseline.xml
+++ /dev/null
@@ -1,114 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-alpha15" client="gradle" variant="debug" version="4.2.0-alpha15">
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 23, the call containing class androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice is not annotated with @RequiresApi(x) where x is at least 23. Either annotate the containing class with at least @RequiresApi(23) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(23)."
- errorLine1=" cameraDevice.createReprocessableCaptureSession("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt"
- line="159"
- column="22"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 23, the call containing class androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice is not annotated with @RequiresApi(x) where x is at least 23. Either annotate the containing class with at least @RequiresApi(23) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(23)."
- errorLine1=" cameraDevice.createConstrainedHighSpeedCaptureSession("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt"
- line="177"
- column="22"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 24, the call containing class androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice is not annotated with @RequiresApi(x) where x is at least 24. Either annotate the containing class with at least @RequiresApi(24) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(24)."
- errorLine1=" cameraDevice.createCaptureSessionByOutputConfigurations("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt"
- line="194"
- column="22"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 24, the call containing class androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice is not annotated with @RequiresApi(x) where x is at least 24. Either annotate the containing class with at least @RequiresApi(24) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(24)."
- errorLine1=" cameraDevice.createReprocessableCaptureSessionByConfigurations("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt"
- line="212"
- column="22"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 23, the call containing class androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice is not annotated with @RequiresApi(x) where x is at least 23. Either annotate the containing class with at least @RequiresApi(23) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(23)."
- errorLine1=" InputConfiguration(inputConfig.width, inputConfig.height, inputConfig.format),"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt"
- line="213"
- column="13"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.camera.camera2.pipe.impl.CameraMetadataImpl is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" characteristics.physicalCameraIds.orEmpty().map { CameraId(it) }.toSet()"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt"
- line="108"
- column="41"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.camera.camera2.pipe.impl.CameraMetadataImpl is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" characteristics.availablePhysicalCameraRequestKeys.orEmpty().toSet()"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt"
- line="124"
- column="41"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.camera.camera2.pipe.impl.CameraMetadataImpl is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" characteristics.availableSessionKeys.orEmpty().toSet()"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt"
- line="140"
- column="41"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.camera.camera2.pipe.impl.AndroidFrameInfo is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" val physicalResults = totalCaptureResult.physicalCameraResults"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/camera2/pipe/impl/FrameMetadata.kt"
- line="105"
- column="54"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.camera.camera2.pipe.impl.VirtualCameraManager is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" instance.openCamera("
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/java/androidx/camera/camera2/pipe/impl/VirtualCameraManager.kt"
- line="228"
- column="34"/>
- </issue>
-
-</issues>
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Cameras.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraDevices.kt
similarity index 69%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Cameras.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraDevices.kt
index 4fc0990..cd2f1aa 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Cameras.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraDevices.kt
@@ -14,12 +14,14 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE")
+
package androidx.camera.camera2.pipe
/**
* Methods for querying, iterating, and selecting the Cameras that are available on the device.
*/
-public interface Cameras {
+public interface CameraDevices {
/**
* Iterate and return a list of CameraId's on the device that are capable of being opened. Some
* camera devices may be hidden or un-openable if they are included as part of a logical camera
@@ -39,4 +41,20 @@
* available.
*/
public fun awaitMetadata(camera: CameraId): CameraMetadata
+}
+
+@Suppress("EXPERIMENTAL_FEATURE_WARNING")
+public inline class CameraId(public val value: String) {
+ public companion object {
+ public inline fun fromCamera2Id(value: String): CameraId = CameraId(value)
+ public inline fun fromCamera1Id(value: Int): CameraId = CameraId("$value")
+ }
+
+ /**
+ * Attempt to parse an camera1 id from a camera2 id.
+ *
+ * @return The parsed Camera1 id, or null if the value cannot be parsed as a Camera1 id.
+ */
+ public inline fun toCamera1Id(): Int? = value.toIntOrNull()
+ public override fun toString(): String = "Camera $value"
}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
index 10e7b5c..865e8c0 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
@@ -16,6 +16,7 @@
package androidx.camera.camera2.pipe
+import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.params.MeteringRectangle
import android.view.Surface
import kotlinx.coroutines.Deferred
@@ -25,7 +26,7 @@
* A CameraGraph represents the combined configuration and state of a camera.
*/
public interface CameraGraph : Closeable {
- public val streams: Map<StreamConfig, Stream>
+ public val streams: StreamGraph
/**
* This will cause the CameraGraph to start opening the camera and configuring the Camera2
@@ -61,28 +62,23 @@
public fun setSurface(stream: StreamId, surface: Surface?)
/**
- * This defines the configuration, flags, and pre-defined behavior of a CameraGraph instance.
+ * This defines the configuration, flags, and pre-defined structure of a CameraGraph instance.
*/
public data class Config(
val camera: CameraId,
- val streams: List<StreamConfig>,
- val template: RequestTemplate,
- val defaultParameters: Map<Any?, Any> = emptyMap(),
- val inputStream: InputStreamConfig? = null,
- val operatingMode: OperatingMode = OperatingMode.NORMAL,
- val listeners: List<Request.Listener> = listOf(),
+ val streams: List<CameraStream.Config>,
+ val streamSharingGroups: List<List<CameraStream.Config>> = listOf(),
+ val input: InputStream.Config? = null,
+ val sessionTemplate: RequestTemplate = RequestTemplate(1),
+ val sessionParameters: Map<CaptureRequest.Key<*>, Any> = emptyMap(),
+ val sessionMode: OperatingMode = OperatingMode.NORMAL,
+ val defaultTemplate: RequestTemplate = RequestTemplate(1),
+ val defaultParameters: Map<*, Any> = emptyMap<Any, Any>(),
+ val defaultListeners: List<Request.Listener> = listOf(),
val metadataTransform: MetadataTransform = MetadataTransform(),
val flags: Flags = Flags()
- )
- /**
- * Configuration for defining the properties of a Camera2 InputStream for reprocessing
- * requests.
- */
- public data class InputStreamConfig(
- val width: Int,
- val height: Int,
- val format: Int
+ // TODO: Internal error handling. May be better at the CameraPipe level.
)
/**
@@ -171,10 +167,18 @@
/**
* Turns the torch to ON or OFF.
*
+ * This method has a side effect on the currently set AE mode. Ref:
+ * https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#FLASH_MODE
+ * To use the flash control, AE mode must be set to ON or OFF. So if the AE mode is
+ * already not either ON or OFF, we will need to update the AE mode to one of those states,
+ * here we will choose ON. It is the responsibility of the application layer above
+ * CameraPipe to restore the AE mode after the torch control has been used. The
+ * [update3A] method can be used to restore the AE state to a previous value.
+ *
* @return the FrameNumber at which the turn was fully turned on if switch was ON, or the
* FrameNumber at which it was completely turned off when the switch was OFF.
*/
- public fun setTorch(torchState: TorchState): Deferred<FrameNumber>
+ public fun setTorch(torchState: TorchState): Deferred<Result3A>
/**
* Locks the auto-exposure, auto-focus and auto-whitebalance as per the given desired
@@ -248,7 +252,7 @@
*
* @param frameLimit the maximum number of frames to wait before we give up waiting for
* this operation to complete.
- * @param timeLimitMs the maximum time limit in ms we wait before we give up waiting for
+ * @param timeLimitNs the maximum time limit in ms we wait before we give up waiting for
* this operation to complete.
*
* @return [Result3A], which will contain the latest frame number at which the locks were
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraId.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraId.kt
deleted file mode 100644
index 4fcb4cb..0000000
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraId.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("NOTHING_TO_INLINE")
-
-package androidx.camera.camera2.pipe
-
-@Suppress("EXPERIMENTAL_FEATURE_WARNING")
-public inline class CameraId(public val value: String) {
- public companion object {
- public inline fun fromCamera2Id(value: String): CameraId = CameraId(value)
- public inline fun fromCamera1Id(value: Int): CameraId = CameraId("$value")
- }
-
- /**
- * Attempt to parse an camera1 id from a camera2 id.
- *
- * @return The parsed Camera1 id, or null if the value cannot be parsed as a Camera1 id.
- */
- public inline fun toCamera1Id(): Int? = value.toIntOrNull()
- public override fun toString(): String = "Camera $value"
-}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
index 4c4b458..2668286 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
@@ -55,7 +55,7 @@
/**
* This provides access to information about the available cameras on the device.
*/
- public fun cameras(): Cameras {
+ public fun cameras(): CameraDevices {
return component.cameras()
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/FlashMode.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/FlashMode.kt
new file mode 100644
index 0000000..5fac6d3
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/FlashMode.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe
+
+import android.hardware.camera2.CameraMetadata
+
+/**
+ * An enum to match the CameraMetadata.FLASH_MODE_* constants.
+ */
+public enum class FlashMode(public val value: Int) {
+ OFF(CameraMetadata.FLASH_MODE_OFF),
+ SINGLE(CameraMetadata.FLASH_MODE_SINGLE),
+ TORCH(CameraMetadata.FLASH_MODE_TORCH);
+
+ public companion object {
+ @JvmStatic
+ public fun fromIntOrNull(value: Int): FlashMode? = values().firstOrNull {
+ it.value == value
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
index ef80195..5272007 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
@@ -243,7 +243,7 @@
/**
* Utility function to help deal with the unsafe nature of the typed Key/Value pairs.
*/
-public fun CaptureRequest.Builder.writeParameter(key: Any?, value: Any?) {
+public fun CaptureRequest.Builder.writeParameter(key: Any?, value: Any) {
if (key != null && key is CaptureRequest.Key<*>) {
@Suppress("UNCHECKED_CAST")
this.set(key as CaptureRequest.Key<Any>, value)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt
index 79ed2c0..0e150b0 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt
@@ -27,8 +27,8 @@
*/
public data class Request(
val streams: List<StreamId>,
- val requestParameters: Map<CaptureRequest.Key<*>, Any> = emptyMap(),
- val extraRequestParameters: Map<Metadata.Key<*>, Any> = emptyMap(),
+ val parameters: Map<CaptureRequest.Key<*>, Any> = emptyMap(),
+ val extras: Map<Metadata.Key<*>, Any> = emptyMap(),
val listeners: List<Listener> = emptyList(),
val template: RequestTemplate? = null
) {
@@ -212,11 +212,11 @@
@Suppress("UNCHECKED_CAST")
private fun <T> Request.getUnchecked(key: Metadata.Key<T>): T? =
- this.extraRequestParameters[key] as T?
+ this.extras[key] as T?
@Suppress("UNCHECKED_CAST")
private fun <T> Request.getUnchecked(key: CaptureRequest.Key<T>): T? =
- this.requestParameters[key] as T?
+ this.parameters[key] as T?
}
public fun <T> Request.getOrDefault(key: Metadata.Key<T>, default: T): T = this[key] ?: default
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Stream.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Stream.kt
deleted file mode 100644
index c40476d..0000000
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Stream.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.pipe
-
-import android.util.Size
-
-@Suppress("EXPERIMENTAL_FEATURE_WARNING")
-public inline class StreamId(public val value: Int) {
- override fun toString(): String = "Stream-$value"
-}
-
-/**
- * A Stream is an identifier for a specific stream as well as the properties that were used to
- * create this stream instance. This allows StreamConfig's to be used to build multiple
- * [CameraGraph]'s while enforcing that each [Stream] will only work with that specific camera
- * [CameraGraph] instance.
- */
-public interface Stream {
- public val id: StreamId
- public val size: Size
- public val format: StreamFormat
- public val camera: CameraId
- public val type: StreamType
-}
-
-/**
- * Configuration object that provides the parameters for a specific input / output stream on Camera.
- */
-public class StreamConfig(
- public val size: Size,
- public val format: StreamFormat,
- public val camera: CameraId,
- public val type: StreamType,
- public val deferrable: Boolean = true
-)
-
-/**
- * Camera2 allows the camera to be configured with outputs that are not immediately available.
- * This allows the camera to configure the internal pipeline with additional information about
- * the surface that has not yet been provided to the camera.
- */
-public enum class StreamType {
- SURFACE,
- SURFACE_VIEW,
- SURFACE_TEXTURE
-}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt
index 9b55e47..c8f0bf2 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt
@@ -21,6 +21,7 @@
*
* Using this inline class prevents missing values on platforms where the format is not present
* or not listed.
+ * // TODO: Consider adding data-space as a separate property, or finding a way to work it in.
*/
@Suppress("EXPERIMENTAL_FEATURE_WARNING")
public inline class StreamFormat(public val value: Int) {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamGraph.kt
new file mode 100644
index 0000000..a161543
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamGraph.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe
+
+/**
+ * This defines a fixed set of inputs and outputs for a single [CameraGraph] instance.
+ *
+ * [CameraStream]s can be used to build [Request]s that are sent to a [CameraGraph].
+ */
+public interface StreamGraph {
+ public val streams: List<CameraStream>
+ public val input: InputStream?
+ public val outputs: List<OutputStream>
+
+ public operator fun get(config: CameraStream.Config): CameraStream?
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
new file mode 100644
index 0000000..663ae3f
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe
+
+import android.hardware.camera2.params.OutputConfiguration
+import android.util.Size
+
+/**
+ * A [CameraStream] is used on a [CameraGraph] to control what outputs that graph produces.
+ *
+ * - Each [CameraStream] must have a surface associated with it in the [CameraGraph]. This surface
+ * may be changed, although this may cause the camera to stall and reconfigure.
+ * - [CameraStream]'s may be added to [Request]'s that are sent to the [CameraGraph]. This causes
+ * the associated surface to be used by the Camera to produce one or more of the outputs (defined
+ * by outputs.
+ *
+ * [CameraStream] may be configured in several different ways with the requirement that each
+ * [CameraStream] may only represent a single surface that is sent to Camera2, and that each
+ * [CameraStream] must produce one or more distinct outputs.
+ *
+ * There are three main components that will be wired together, the [CameraStream], the
+ * Camera2 [OutputConfiguration], and the [OutputStream]'s. In each of these examples a
+ * [CameraStream] is associated with a distinct surface that may be sent to camera2 to produce 1
+ * or more distinct outputs defined in the list of [OutputStream]'s.
+ *
+ * Simple 1:1 configuration
+ * ```
+ * CameraStream-1 -> OutputConfig-1 -> OutputStream-1
+ * CameraStream-2 -> OutputConfig-2 -> OutputStream-2
+ * ```
+ *
+ * Stream sharing (Multiple surfaces use the same OutputConfig object)
+ * ```
+ * CameraStream-1 --------------------> OutputStream-1
+ * >- OutputConfig-1 -<
+ * CameraStream-2 --------------------> OutputStream-2
+ * ```
+ *
+ * Multi-Output / External OutputConfiguration (Camera2 may produce one or more of the outputs)
+ * ```
+ * CameraStream-1 -> OutputConfig-1 -> OutputStream-1
+ * \-> OutputConfig-2 -> OutputStream-2
+ * ```
+ */
+public class CameraStream internal constructor(
+ public val id: StreamId,
+ public val outputs: List<OutputStream>
+) {
+ override fun toString(): String = id.toString()
+
+ /** Configuration that may be used to define a [CameraStream] on a [CameraGraph] */
+ public class Config internal constructor(
+ val outputs: List<OutputStream.Config>
+ ) {
+ companion object {
+ /** Create a simple [CameraStream] to [OutputStream] configuration */
+ fun create(
+ size: Size,
+ format: StreamFormat,
+ camera: CameraId? = null,
+ outputType: OutputStream.OutputType = OutputStream.OutputType.SURFACE
+ ): Config = create(
+ OutputStream.Config.create(size, format, camera, outputType)
+ )
+
+ /**
+ * Create a simple [CameraStream] using a previously defined [OutputStream.Config].
+ * This allows multiple [CameraStream]s to share the same [OutputConfiguration].
+ */
+ fun create(output: OutputStream.Config) = Config(listOf(output))
+
+ /**
+ * Create a [CameraStream] from multiple [OutputStream.Config]s. This is used to to
+ * define a [CameraStream] that may produce one or more of the outputs when used in a
+ * request to the camera.
+ */
+ fun create(outputs: List<OutputStream.Config>) = Config(outputs)
+ }
+ }
+}
+
+/**
+ * This identifies a single surface that is used to tell the camera to produce one or more outputs.
+ */
+@Suppress("EXPERIMENTAL_FEATURE_WARNING")
+public inline class StreamId(public val value: Int) {
+ override fun toString(): String = "Stream-$value"
+}
+
+/**
+ * A [OutputStream] represents one of the possible outputs that may be produced from a
+ * [CameraStream]. Because some sensors are capable of producing images at different resolutions,
+ * the underlying HAL on the device may produce different sized images for the same request.
+ * This represents one of those potential outputs.
+ */
+public interface OutputStream {
+ // Every output comes from one, and exactly one, CameraStream
+ public val stream: CameraStream
+
+ public val id: OutputId
+ public val size: Size
+ public val format: StreamFormat
+ public val camera: CameraId
+ // TODO: Consider adding sensor mode and/or other metadata
+
+ /**
+ * Configuration object that provides the parameters for a specific input / output stream on Camera.
+ */
+ sealed class Config(
+ public val size: Size,
+ public val format: StreamFormat,
+ public val camera: CameraId?
+ ) {
+ companion object {
+ fun create(
+ size: Size,
+ format: StreamFormat,
+ camera: CameraId? = null,
+ outputType: OutputType = OutputType.SURFACE,
+ externalOutputConfig: OutputConfiguration? = null
+ ): Config =
+ if (externalOutputConfig != null) {
+ ExternalOutputConfig(size, format, camera, output = externalOutputConfig)
+ } else if (
+ outputType == OutputType.SURFACE_TEXTURE ||
+ outputType == OutputType.SURFACE_VIEW
+ ) {
+ LazyOutputConfig(size, format, camera, outputType)
+ } else {
+ SimpleOutputConfig(size, format, camera)
+ }
+ }
+
+ /**
+ * Most outputs only need to define size, format, and cameraId.
+ */
+ internal class SimpleOutputConfig(
+ size: Size,
+ format: StreamFormat,
+ camera: CameraId?
+ ) : Config(size, format, camera)
+
+ /**
+ * Used to configure an output with a surface that may be provided after the camera is running.
+ *
+ * This behavior is allowed on newer versions of the OS and allows the camera to start
+ * running before the UI is fully available. This configuration mode is only allowed for
+ * SurfaceHolder and SurfaceTexture output targets, and must be defined ahead of time (along
+ * with the size, and format) for these [OutputConfiguration]'s to be created.
+ */
+ internal class LazyOutputConfig(
+ size: Size,
+ format: StreamFormat,
+ camera: CameraId?,
+ internal val outputType: OutputType
+ ) : Config(size, format, camera)
+
+ /**
+ * Used to define an output that comes from an externally managed OutputConfiguration object.
+ *
+ * The configuration logic has the following behavior:
+ * - Assumes [OutputConfiguration] has a valid surface
+ * - Assumes [OutputConfiguration] surfaces will not be added / removed / changed.
+ * - If the CameraCaptureSession must be recreated, the [OutputConfiguration] will be reused.
+ */
+ internal class ExternalOutputConfig(
+ size: Size,
+ format: StreamFormat,
+ camera: CameraId?,
+ val output: OutputConfiguration,
+ ) : Config(size, format, camera)
+ }
+
+ enum class OutputType {
+ SURFACE,
+ SURFACE_VIEW,
+ SURFACE_TEXTURE,
+ }
+}
+
+/**
+ * This identifies a single output.
+ */
+@Suppress("EXPERIMENTAL_FEATURE_WARNING")
+public inline class OutputId(public val value: Int) {
+ override fun toString(): String = "Output-$value"
+}
+
+/**
+ * Configuration for defining the properties of a Camera2 InputStream for reprocessing
+ * requests.
+ */
+public interface InputStream {
+ public val id: InputId
+ public val format: StreamFormat
+ // TODO: This may accept
+
+ public class Config(val stream: CameraStream.Config)
+}
+
+/**
+ * This identifies a single input.
+ */
+@Suppress("EXPERIMENTAL_FEATURE_WARNING")
+public inline class InputId(public val value: Int) {
+ override fun toString(): String = "Input-$value"
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Debug.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
similarity index 64%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Debug.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
index 4f219ea..6852aa6 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Debug.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
@@ -16,7 +16,7 @@
@file:Suppress("NOTHING_TO_INLINE")
-package androidx.camera.camera2.pipe.impl
+package androidx.camera.camera2.pipe.core
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraCharacteristics.LENS_FACING
@@ -69,66 +69,68 @@
}
}
- internal fun logConfiguration(
- graphId: String,
+ public fun formatCameraGraphProperties(
metadata: CameraMetadata,
graphConfig: CameraGraph.Config,
- streamMap: StreamMap
- ) {
- Log.info {
- val lensFacing = when (metadata[LENS_FACING]) {
- CameraCharacteristics.LENS_FACING_FRONT -> "Front"
- CameraCharacteristics.LENS_FACING_BACK -> "Back"
- CameraCharacteristics.LENS_FACING_EXTERNAL -> "External"
- else -> "Unknown"
- }
+ cameraGraph: CameraGraph
+ ): String {
+ val lensFacing = when (metadata[LENS_FACING]) {
+ CameraCharacteristics.LENS_FACING_FRONT -> "Front"
+ CameraCharacteristics.LENS_FACING_BACK -> "Back"
+ CameraCharacteristics.LENS_FACING_EXTERNAL -> "External"
+ else -> "Unknown"
+ }
- val operatingMode = when (graphConfig.operatingMode) {
- CameraGraph.OperatingMode.HIGH_SPEED -> "High Speed"
- CameraGraph.OperatingMode.NORMAL -> "Normal"
- }
+ val operatingMode = when (graphConfig.sessionMode) {
+ CameraGraph.OperatingMode.HIGH_SPEED -> "High Speed"
+ CameraGraph.OperatingMode.NORMAL -> "Normal"
+ }
- val capabilities = metadata[REQUEST_AVAILABLE_CAPABILITIES]
- val cameraType = if (capabilities != null &&
- capabilities.contains(
- REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
- )
- ) {
- "Logical"
- } else {
- "Physical"
- }
+ val capabilities = metadata[REQUEST_AVAILABLE_CAPABILITIES]
+ val cameraType = if (capabilities != null &&
+ capabilities.contains(REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)
+ ) {
+ "Logical"
+ } else {
+ "Physical"
+ }
- StringBuilder().apply {
- append("$graphId (Camera ${graphConfig.camera.value})\n")
- append(" Facing: $lensFacing ($cameraType)\n")
- append(" Mode: $operatingMode\n")
- append("Streams:\n")
- for (stream in streamMap.streamConfigMap) {
+ return StringBuilder().apply {
+ append("$cameraGraph (Camera ${graphConfig.camera.value})\n")
+ append(" Facing: $lensFacing ($cameraType)\n")
+ append(" Mode: $operatingMode\n")
+ append("Outputs:\n")
+ for (stream in cameraGraph.streams.streams) {
+ stream.outputs.forEachIndexed { i, output ->
append(" ")
- append(stream.value.id.toString().padEnd(12, ' '))
- append(stream.value.size.toString().padEnd(12, ' '))
- append(stream.value.format.name.padEnd(16, ' '))
- append(stream.value.type.toString().padEnd(16, ' '))
+ val streamId = if (i == 0) output.stream.id.toString() else ""
+ append(streamId.padEnd(10, ' '))
+ append(output.id.toString().padEnd(10, ' '))
+ append(output.size.toString().padEnd(12, ' '))
+ append(output.format.name.padEnd(16, ' '))
+ if (output.camera != graphConfig.camera) {
+ append(" [")
+ append(output.camera)
+ append("]")
+ }
append("\n")
}
+ }
- if (graphConfig.defaultParameters.isEmpty()) {
- append("Default Parameters: (None)")
- } else {
- append("Default Parameters:\n")
- for (
- parameter in graphConfig.defaultParameters.filter {
- it is CaptureRequest.Key<*>
- }
- ) {
- append(" ")
- append((parameter.key as CaptureRequest.Key<*>).name.padEnd(50, ' '))
- append(parameter.value)
- }
+ if (graphConfig.defaultParameters.isEmpty()) {
+ append("Session Parameters: (None)")
+ } else {
+ append("Session Parameters:\n")
+ val captureRequestParameters = graphConfig.sessionParameters.filter {
+ it is CaptureRequest.Key<*>
}
- }.toString()
- }
+ for (parameter in captureRequestParameters) {
+ append(" ")
+ append((parameter.key).name.padEnd(50, ' '))
+ append(parameter.value)
+ }
+ }
+ }.toString()
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Log.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Log.kt
similarity index 98%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Log.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Log.kt
index d2e1d19..a024de2 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Log.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Log.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.camera.camera2.pipe.impl
+package androidx.camera.camera2.pipe.core
import android.util.Log
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Timestamps.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Timestamps.kt
similarity index 97%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Timestamps.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Timestamps.kt
index 6138029..abb9ba8 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Timestamps.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Timestamps.kt
@@ -16,7 +16,7 @@
@file:Suppress("NOTHING_TO_INLINE")
-package androidx.camera.camera2.pipe.impl
+package androidx.camera.camera2.pipe.core
import android.os.SystemClock
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CamerasImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraDevicesImpl.kt
similarity index 93%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CamerasImpl.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraDevicesImpl.kt
index 2bd2445..38d80e3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CamerasImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraDevicesImpl.kt
@@ -19,8 +19,8 @@
import android.hardware.camera2.CameraManager
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CameraMetadata
-import androidx.camera.camera2.pipe.Cameras
-import androidx.camera.camera2.pipe.impl.Debug.trace
+import androidx.camera.camera2.pipe.CameraDevices
+import androidx.camera.camera2.pipe.core.Debug.trace
import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton
@@ -29,10 +29,10 @@
* Provides utilities for querying cameras and accessing metadata about those cameras.
*/
@Singleton
-internal class CamerasImpl @Inject constructor(
+internal class CameraDevicesImpl @Inject constructor(
private val cameraManager: Provider<CameraManager>,
private val metadata: CameraMetadataCache
-) : Cameras {
+) : CameraDevices {
private val cameras = lazy(LazyThreadSafetyMode.PUBLICATION) {
// NOTE: Publication safety mode may cause this method to be invoked more than once if there
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt
index ea94610..a024cb3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt
@@ -72,7 +72,7 @@
@Binds
abstract fun bindRequestProcessorFactory(
factory: StandardRequestProcessorFactory
- ): RequestProcessor.Factory
+ ): RequestProcessorFactory
@Binds
abstract fun bindGraphState(graphState: GraphStateImpl): GraphState
@@ -107,7 +107,7 @@
// Listeners in CameraGraph.Config can de defined outside of the CameraPipe library,
// and since we iterate thought the listeners in order and invoke them, it appears
// beneficial to add the internal listeners first and then the graph config listeners.
- listeners.addAll(graphConfig.listeners)
+ listeners.addAll(graphConfig.defaultListeners)
return listeners
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt
index 8f376f5..4d7f06d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt
@@ -19,9 +19,10 @@
import android.view.Surface
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraMetadata
-import androidx.camera.camera2.pipe.Stream
-import androidx.camera.camera2.pipe.StreamConfig
+import androidx.camera.camera2.pipe.StreamGraph
import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log
import kotlinx.atomicfu.atomic
import javax.inject.Inject
@@ -32,7 +33,7 @@
graphConfig: CameraGraph.Config,
metadata: CameraMetadata,
private val graphProcessor: GraphProcessor,
- private val streamMap: StreamMap,
+ private val streamGraph: StreamGraphImpl,
private val graphState: GraphState,
private val graphState3A: GraphState3A,
private val listener3A: Listener3A
@@ -46,11 +47,11 @@
init {
// Log out the configuration of the camera graph when it is created.
- Debug.logConfiguration(this.toString(), metadata, graphConfig, streamMap)
+ Debug.formatCameraGraphProperties(metadata, graphConfig, this)
}
- override val streams: Map<StreamConfig, Stream>
- get() = streamMap.streamConfigMap
+ override val streams: StreamGraph
+ get() = streamGraph
override fun start() {
Debug.traceStart { "$this#start" }
@@ -84,7 +85,7 @@
override fun setSurface(stream: StreamId, surface: Surface?) {
Debug.traceStart { "$stream#setSurface" }
- streamMap[stream] = surface
+ streamGraph[stream] = surface
Debug.traceStop()
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
index 32710c3..0b632a3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
@@ -67,7 +67,14 @@
afRegions: List<MeteringRectangle>?,
awbRegions: List<MeteringRectangle>?
): Deferred<Result3A> {
- return controller3A.update3A(aeMode, afMode, awbMode, aeRegions, afRegions, awbRegions)
+ return controller3A.update3A(
+ aeMode = aeMode,
+ afMode = afMode,
+ awbMode = awbMode,
+ aeRegions = aeRegions,
+ afRegions = afRegions,
+ awbRegions = awbRegions
+ )
}
override suspend fun submit3A(
@@ -81,8 +88,10 @@
return controller3A.submit3A(aeMode, afMode, awbMode, aeRegions, afRegions, awbRegions)
}
- override fun setTorch(torchState: TorchState): Deferred<FrameNumber> {
- TODO("Implement setTorch")
+ override fun setTorch(torchState: TorchState): Deferred<Result3A> {
+ // TODO(sushilnath): First check whether the camera device has a flash unit. Ref:
+ // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#FLASH_INFO_AVAILABLE
+ return controller3A.setTorch(torchState)
}
override suspend fun lock3A(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt
index 25d9346..f1b51b7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt
@@ -22,7 +22,10 @@
import androidx.annotation.GuardedBy
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CameraMetadata
-import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.core.Timestamps
+import androidx.camera.camera2.pipe.core.Timestamps.formatMs
import kotlinx.coroutines.withContext
import java.lang.IllegalStateException
import javax.inject.Inject
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt
index aabc119..dfdfe8e 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt
@@ -25,7 +25,11 @@
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CameraMetadata
import androidx.camera.camera2.pipe.Metadata
-import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.core.Timestamps
+import androidx.camera.camera2.pipe.core.Timestamps.formatMs
+import androidx.camera.camera2.pipe.wrapper.Api28Compat
/**
* This implementation provides access to CameraCharacteristics and lazy caching of properties
@@ -70,7 +74,7 @@
characteristics.keys.orEmpty().toSet()
}
} catch (ignored: AssertionError) {
- emptySet<CameraCharacteristics.Key<*>>()
+ emptySet()
}
}
@@ -82,7 +86,7 @@
characteristics.availableCaptureRequestKeys.orEmpty().toSet()
}
} catch (ignored: AssertionError) {
- emptySet<CaptureRequest.Key<*>>()
+ emptySet()
}
}
@@ -94,7 +98,7 @@
characteristics.availableCaptureResultKeys.orEmpty().toSet()
}
} catch (ignored: AssertionError) {
- emptySet<CaptureResult.Key<*>>()
+ emptySet()
}
}
@@ -110,7 +114,7 @@
characteristics.physicalCameraIds.orEmpty().map { CameraId(it) }.toSet()
}
} catch (ignored: AssertionError) {
- emptySet<CameraId>()
+ emptySet()
}
}
}
@@ -122,11 +126,12 @@
} else {
try {
Debug.trace("Camera-${camera.value}#availablePhysicalCameraRequestKeys") {
- @Suppress("UselessCallOnNotNull")
- characteristics.availablePhysicalCameraRequestKeys.orEmpty().toSet()
+ Api28Compat.getAvailablePhysicalCameraRequestKeys(characteristics)
+ .orEmpty()
+ .toSet()
}
} catch (ignored: AssertionError) {
- emptySet<CaptureRequest.Key<*>>()
+ emptySet()
}
}
}
@@ -138,11 +143,10 @@
} else {
try {
Debug.trace("Camera-${camera.value}#availableSessionKeys") {
- @Suppress("UselessCallOnNotNull")
- characteristics.availableSessionKeys.orEmpty().toSet()
+ Api28Compat.getAvailableSessionKeys(characteristics).orEmpty().toSet()
}
} catch (ignored: AssertionError) {
- emptySet<CaptureRequest.Key<*>>()
+ emptySet()
}
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt
index 587b799..e5f794d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt
@@ -22,7 +22,7 @@
import android.os.HandlerThread
import android.os.Process
import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.Cameras
+import androidx.camera.camera2.pipe.CameraDevices
import dagger.Binds
import dagger.Component
import dagger.Module
@@ -48,7 +48,7 @@
)
internal interface CameraPipeComponent {
fun cameraGraphComponentBuilder(): CameraGraphComponent.Builder
- fun cameras(): Cameras
+ fun cameras(): CameraDevices
}
@Module(
@@ -62,7 +62,7 @@
@Module
internal abstract class CameraPipeModules {
@Binds
- abstract fun bindCameras(impl: CamerasImpl): Cameras
+ abstract fun bindCameras(impl: CameraDevicesImpl): CameraDevices
companion object {
@Provides
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CaptureSequence.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CaptureSequence.kt
new file mode 100644
index 0000000..8dde9ab
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CaptureSequence.kt
@@ -0,0 +1,310 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.impl
+
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CaptureFailure
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
+import android.view.Surface
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraTimestamp
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.RequestNumber
+import androidx.camera.camera2.pipe.StreamId
+
+/**
+ * This class responds to events from a set of one or more requests. It uses the tag field on
+ * a CaptureRequest object to lookup and invoke per-request listeners so that a listener can be
+ * defined on a specific request within a burst.
+ */
+internal class CaptureSequence(
+ private val internalListeners: List<Request.Listener>,
+ private val requests: Map<RequestNumber, RequestInfo>,
+ private val captureRequests: List<CaptureRequest>,
+ private val surfaceMap: Map<Surface, StreamId>,
+ private val inFlightRequests: MutableList<CaptureSequence>,
+ private val camera: CameraId
+) : CameraCaptureSession.CaptureCallback() {
+ private val debugId = requestSequenceDebugIds.incrementAndGet()
+
+ @Volatile
+ private var _sequenceNumber: Int? = null
+ var sequenceNumber: Int
+ get() {
+ if (_sequenceNumber == null) {
+ // If the sequence id has not been submitted, it means the call to capture or
+ // setRepeating has not yet returned. The callback methods should never be synchronously
+ // invoked, so the only case this should happen is if a second thread attempted to
+ // invoke one of the callbacks before the initial call completed. By locking against the
+ // captureSequence object here and in the capture call, we can block the callback thread
+ // until the sequenceId is available.
+ synchronized(this) {
+ return checkNotNull(_sequenceNumber) {
+ "SequenceNumber has not been set for $this!"
+ }
+ }
+ }
+ return checkNotNull(_sequenceNumber) {
+ "SequenceNumber has not been set for $this!"
+ }
+ }
+ set(value) {
+ _sequenceNumber = value
+ }
+
+ override fun onCaptureStarted(
+ captureSession: CameraCaptureSession,
+ captureRequest: CaptureRequest,
+ captureTimestamp: Long,
+ captureFrameNumber: Long
+ ) {
+ val requestNumber = readRequestNumber(captureRequest)
+ val timestamp = CameraTimestamp(captureTimestamp)
+ val frameNumber = FrameNumber(captureFrameNumber)
+
+ // Load the request and throw if we are not able to find an associated request. Under
+ // normal circumstances this should never happen.
+ val request = readRequest(requestNumber)
+
+ invokeOnRequest(request) {
+ it.onStarted(
+ request,
+ frameNumber,
+ timestamp
+ )
+ }
+ }
+
+ override fun onCaptureProgressed(
+ captureSession: CameraCaptureSession,
+ captureRequest: CaptureRequest,
+ partialCaptureResult: CaptureResult
+ ) {
+ val requestNumber = readRequestNumber(captureRequest)
+ val frameNumber = FrameNumber(partialCaptureResult.frameNumber)
+ val frameMetadata = AndroidFrameMetadata(partialCaptureResult, camera)
+
+ // Load the request and throw if we are not able to find an associated request. Under
+ // normal circumstances this should never happen.
+ val request = readRequest(requestNumber)
+
+ invokeOnRequest(request) {
+ it.onPartialCaptureResult(
+ request,
+ frameNumber,
+ frameMetadata
+ )
+ }
+ }
+
+ override fun onCaptureCompleted(
+ captureSession: CameraCaptureSession,
+ captureRequest: CaptureRequest,
+ captureResult: TotalCaptureResult
+ ) {
+ // Remove this request from the set of requests that are currently tracked.
+ synchronized(inFlightRequests) {
+ inFlightRequests.remove(this)
+ }
+
+ val requestNumber = readRequestNumber(captureRequest)
+ val frameNumber = FrameNumber(captureResult.frameNumber)
+
+ // Load the request and throw if we are not able to find an associated request. Under
+ // normal circumstances this should never happen.
+ val request = readRequest(requestNumber)
+
+ val frameInfo = AndroidFrameInfo(
+ captureResult,
+ camera,
+ request
+ )
+
+ invokeOnRequest(request) {
+ it.onTotalCaptureResult(
+ request,
+ frameNumber,
+ frameInfo
+ )
+ }
+ }
+
+ override fun onCaptureFailed(
+ captureSession: CameraCaptureSession,
+ captureRequest: CaptureRequest,
+ captureFailure: CaptureFailure
+ ) {
+ // Remove this request from the set of requests that are currently tracked.
+ synchronized(inFlightRequests) {
+ inFlightRequests.remove(this)
+ }
+
+ val requestNumber = readRequestNumber(captureRequest)
+ val frameNumber = FrameNumber(captureFailure.frameNumber)
+
+ // Load the request and throw if we are not able to find an associated request. Under
+ // normal circumstances this should never happen.
+ val request = readRequest(requestNumber)
+
+ invokeOnRequest(request) {
+ it.onFailed(
+ request,
+ frameNumber,
+ captureFailure
+ )
+ }
+ }
+
+ override fun onCaptureBufferLost(
+ captureSession: CameraCaptureSession,
+ captureRequest: CaptureRequest,
+ surface: Surface,
+ frameId: Long
+ ) {
+ val requestNumber = readRequestNumber(captureRequest)
+ val frameNumber = FrameNumber(frameId)
+ val streamId = checkNotNull(surfaceMap[surface]) {
+ "Unable to find the streamId for $surface on frame $frameNumber"
+ }
+
+ // Load the request and throw if we are not able to find an associated request. Under
+ // normal circumstances this should never happen.
+ val request = readRequest(requestNumber)
+
+ invokeOnRequest(request) {
+ it.onBufferLost(
+ request,
+ frameNumber,
+ streamId
+ )
+ }
+ }
+
+ /**
+ * Custom implementation that informs all listeners that the request had not completed when
+ * abort was called.
+ */
+ fun invokeOnAborted() {
+ invokeOnRequests { request, _, listener ->
+ listener.onAborted(request.request)
+ }
+ }
+
+ fun invokeOnRequestSequenceCreated() {
+ invokeOnRequests { request, _, listener ->
+ listener.onRequestSequenceCreated(request)
+ }
+ }
+
+ fun invokeOnRequestSequenceSubmitted() {
+ invokeOnRequests { request, _, listener ->
+ listener.onRequestSequenceSubmitted(request)
+ }
+ }
+
+ override fun onCaptureSequenceCompleted(
+ captureSession: CameraCaptureSession,
+ captureSequenceId: Int,
+ captureFrameNumber: Long
+ ) {
+ check(sequenceNumber == captureSequenceId) {
+ "Complete was invoked on $sequenceNumber, but the sequence was not fully submitted!"
+ }
+ synchronized(inFlightRequests) {
+ inFlightRequests.remove(this)
+ }
+
+ val frameNumber = FrameNumber(captureFrameNumber)
+ invokeOnRequests { request, _, listener ->
+ listener.onRequestSequenceCompleted(request, frameNumber)
+ }
+ }
+
+ override fun onCaptureSequenceAborted(
+ captureSession: CameraCaptureSession,
+ captureSequenceId: Int
+ ) {
+ check(sequenceNumber == captureSequenceId) {
+ "Abort was invoked on $sequenceNumber, but the sequence was not fully submitted!"
+ }
+
+ // Remove this request from the set of requests that are currently tracked.
+ synchronized(inFlightRequests) {
+ inFlightRequests.remove(this)
+ }
+
+ invokeOnRequests { request, _, listener ->
+ listener.onRequestSequenceAborted(request)
+ }
+ }
+
+ private fun readRequestNumber(request: CaptureRequest): RequestNumber =
+ checkNotNull(request.tag as RequestNumber)
+
+ private fun readRequest(requestNumber: RequestNumber): RequestInfo {
+ return checkNotNull(requests[requestNumber]) {
+ "Unable to find the request for $requestNumber!"
+ }
+ }
+
+ private inline fun invokeOnRequests(
+ crossinline fn: (RequestMetadata, Int, Request.Listener) -> Any
+ ) {
+
+ // Always invoke the internal listener first on all of the internal listeners for the
+ // entire sequence before invoking the listeners specified in the specific requests
+ for (i in captureRequests.indices) {
+ val requestNumber = readRequestNumber(captureRequests[i])
+ val request = checkNotNull(requests[requestNumber])
+
+ for (listenerIndex in internalListeners.indices) {
+ fn(request, i, internalListeners[listenerIndex])
+ }
+ }
+
+ for (i in captureRequests.indices) {
+ val requestNumber = readRequestNumber(captureRequests[i])
+ val request = checkNotNull(requests[requestNumber])
+
+ for (listenerIndex in request.request.listeners.indices) {
+ fn(request, i, request.request.listeners[listenerIndex])
+ }
+ }
+ }
+
+ private inline fun invokeOnRequest(
+ request: RequestInfo,
+ crossinline fn: (Request.Listener) -> Any
+ ) {
+ // Always invoke the internal listener first so that internal sate can be updated before
+ // other listeners ask for it.
+ for (i in internalListeners.indices) {
+ fn(internalListeners[i])
+ }
+
+ // Invoke the listeners that were defined on this request.
+ for (i in request.request.listeners.indices) {
+ fn(request.request.listeners[i])
+ }
+ }
+
+ override fun toString(): String = "CaptureSequence-$debugId"
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt
index e5d67a5..3133d88 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt
@@ -33,10 +33,12 @@
import androidx.camera.camera2.pipe.AwbMode
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraGraph.Constants3A.FRAME_NUMBER_INVALID
+import androidx.camera.camera2.pipe.FlashMode
import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.Status3A
-import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.TorchState
+import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.shouldUnlockAe
import androidx.camera.camera2.pipe.shouldUnlockAf
import androidx.camera.camera2.pipe.shouldUnlockAwb
@@ -135,26 +137,27 @@
aeMode: AeMode? = null,
afMode: AfMode? = null,
awbMode: AwbMode? = null,
+ flashMode: FlashMode? = null,
aeRegions: List<MeteringRectangle>? = null,
afRegions: List<MeteringRectangle>? = null,
awbRegions: List<MeteringRectangle>? = null
): Deferred<Result3A> {
// Add the listener to a global pool of 3A listeners to monitor the state change to the
// desired one.
- val listener = createListenerFor3AParams(aeMode, afMode, awbMode)
+ val listener = createListenerFor3AParams(aeMode, afMode, awbMode, flashMode)
graphListener3A.addListener(listener)
// Update the 3A state of the graph. This will make sure then when GraphProcessor builds
// the next request it will apply the 3A parameters corresponding to the updated 3A state
// to the request.
- graphState3A.update(aeMode, afMode, awbMode, aeRegions, afRegions, awbRegions, null, null)
+ graphState3A.update(aeMode, afMode, awbMode, flashMode, aeRegions, afRegions, awbRegions)
// Try submitting a new repeating request with the 3A parameters corresponding to the new
// 3A state and corresponding listeners.
graphProcessor.invalidate()
val result = listener.getDeferredResult()
synchronized(this) {
- lastUpdate3AResult?.cancel("A newer update3A call initiated.")
+ lastUpdate3AResult?.cancel("A newer call for 3A state update initiated.")
lastUpdate3AResult = result
}
return result
@@ -228,7 +231,7 @@
afLockBehavior: Lock3ABehavior? = null,
awbLockBehavior: Lock3ABehavior? = null,
frameLimit: Int = CameraGraph.DEFAULT_FRAME_LIMIT,
- timeLimitMsNs: Long? = CameraGraph.DEFAULT_TIME_LIMIT_NS
+ timeLimitNs: Long? = CameraGraph.DEFAULT_TIME_LIMIT_NS
): Deferred<Result3A> {
// If we explicitly need to unlock af first before proceeding to lock it, we need to send
// a single request with TRIGGER = TRIGGER_CANCEL so that af can start a fresh scan.
@@ -252,7 +255,7 @@
val listener = Result3AStateListenerImpl(
converged3AExitConditions,
frameLimit,
- timeLimitMsNs
+ timeLimitNs
)
graphListener3A.addListener(listener)
@@ -291,7 +294,7 @@
}
}
- return lock3ANow(aeLockBehavior, afLockBehavior, awbLockBehavior, frameLimit, timeLimitMsNs)
+ return lock3ANow(aeLockBehavior, afLockBehavior, awbLockBehavior, frameLimit, timeLimitNs)
}
/**
@@ -446,12 +449,22 @@
return listener.getDeferredResult()
}
+ fun setTorch(torchState: TorchState): Deferred<Result3A> {
+ // Determine the flash mode based on the torch state.
+ val flashMode = if (torchState == TorchState.ON) FlashMode.TORCH else FlashMode.OFF
+ // To use the flash control, AE mode must be set to ON or OFF.
+ val currAeMode = graphState3A.aeMode
+ val desiredAeMode = if (currAeMode == AeMode.ON || currAeMode == AeMode.OFF) null else
+ AeMode.ON
+ return update3A(aeMode = desiredAeMode, flashMode = flashMode)
+ }
+
private suspend fun lock3ANow(
aeLockBehavior: Lock3ABehavior?,
afLockBehavior: Lock3ABehavior?,
awbLockBehavior: Lock3ABehavior?,
frameLimit: Int?,
- timeLimitMsNs: Long?
+ timeLimitNs: Long?
): Deferred<Result3A> {
val finalAeLockValue = if (aeLockBehavior == null) null else true
val finalAwbLockValue = if (awbLockBehavior == null) null else true
@@ -466,7 +479,7 @@
val listener = Result3AStateListenerImpl(
locked3AExitConditions,
frameLimit,
- timeLimitMsNs
+ timeLimitNs
)
graphListener3A.addListener(listener)
graphState3A.update(aeLock = finalAeLockValue, awbLock = finalAwbLockValue)
@@ -566,14 +579,16 @@
// exact match between the metering regions sent in the capture request and the metering
// regions received from the camera device.
private fun createListenerFor3AParams(
- aeMode: AeMode?,
- afMode: AfMode?,
- awbMode: AwbMode?
+ aeMode: AeMode? = null,
+ afMode: AfMode? = null,
+ awbMode: AwbMode? = null,
+ flashMode: FlashMode? = null,
): Result3AStateListenerImpl {
val resultModesMap = mutableMapOf<CaptureResult.Key<*>, List<Any>>()
aeMode?.let { resultModesMap.put(CaptureResult.CONTROL_AE_MODE, listOf(it.value)) }
afMode?.let { resultModesMap.put(CaptureResult.CONTROL_AF_MODE, listOf(it.value)) }
awbMode?.let { resultModesMap.put(CaptureResult.CONTROL_AWB_MODE, listOf(it.value)) }
+ flashMode?.let { resultModesMap.put(CaptureResult.FLASH_MODE, listOf(it.value)) }
return Result3AStateListenerImpl(resultModesMap.toMap())
}
}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/FrameMetadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/FrameMetadata.kt
index 65e1ef0..c5cf660 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/FrameMetadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/FrameMetadata.kt
@@ -26,6 +26,7 @@
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.Metadata
import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.wrapper.Api28Compat
/**
* An implementation of [FrameMetadata] that retrieves values from a [CaptureResult] object
@@ -102,8 +103,8 @@
// Metadata for physical cameras was introduced in Android P so that it can be used to
// determine state of the physical lens and sensor in a multi-camera configuration.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- val physicalResults = totalCaptureResult.physicalCameraResults
- if (physicalResults.isNotEmpty()) {
+ val physicalResults = Api28Compat.getPhysicalCaptureResults(totalCaptureResult)
+ if (physicalResults != null && physicalResults.isNotEmpty()) {
val map = ArrayMap<CameraId, AndroidFrameMetadata>(physicalResults.size)
for (entry in physicalResults) {
val physicalCamera = CameraId(entry.key)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessor.kt
index 801c88a..856a94e 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessor.kt
@@ -16,12 +16,13 @@
package androidx.camera.camera2.pipe.impl
-import android.hardware.camera2.CaptureRequest
import androidx.annotation.GuardedBy
+import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log.debug
+import androidx.camera.camera2.pipe.core.Log.warn
import androidx.camera.camera2.pipe.formatForLogs
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.camera2.pipe.impl.Log.warn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -36,7 +37,7 @@
fun setRepeating(request: Request)
fun submit(request: Request)
fun submit(requests: List<Request>)
- suspend fun submit(parameters: Map<CaptureRequest.Key<*>, Any>): Boolean
+ suspend fun submit(parameters: Map<*, Any>): Boolean
/**
* Abort all submitted requests that have not yet been submitted to the [RequestProcessor] as
@@ -61,6 +62,7 @@
@CameraGraphScope
internal class GraphProcessorImpl @Inject constructor(
private val threads: Threads,
+ private val cameraGraphConfig: CameraGraph.Config,
@ForCameraGraph private val graphScope: CoroutineScope,
@ForCameraGraph private val graphListeners: java.util.ArrayList<Request.Listener>
) : GraphProcessor {
@@ -176,7 +178,7 @@
/**
* Submit a request to the camera using only the current repeating request.
*/
- override suspend fun submit(parameters: Map<CaptureRequest.Key<*>, Any>): Boolean =
+ override suspend fun submit(parameters: Map<*, Any>): Boolean =
withContext(threads.ioDispatcher) {
val processor: RequestProcessor?
val request: Request?
@@ -191,8 +193,8 @@
processor == null || request == null -> false
else -> processor.submit(
request,
- parameters,
- requireSurfacesForAllStreams = false
+ defaultParameters = cameraGraphConfig.defaultParameters,
+ requiredParameters = parameters
)
}
}
@@ -246,11 +248,6 @@
}
}
- private fun read3AState(): Map<CaptureRequest.Key<*>, Any> {
- // TODO: Build extras from 3A state
- return mapOf()
- }
-
private fun abortBurst(requests: List<Request>) {
for (request in requests) {
abortRequest(request)
@@ -281,10 +278,13 @@
if (processor != null && request != null) {
Debug.traceStart { "$this#setRepeating" }
- val extras: Map<CaptureRequest.Key<*>, Any> = read3AState()
-
synchronized(processor) {
- if (processor.setRepeating(request, extras, requireSurfacesForAllStreams = true)) {
+ if (processor.setRepeating(
+ request,
+ cameraGraphConfig.defaultParameters,
+ emptyMap<Any, Any>()
+ )
+ ) {
// ONLY update the current repeating request if the update succeeds
synchronized(lock) {
if (processor === _requestProcessor) {
@@ -332,12 +332,19 @@
var submitted = false
Debug.traceStart { "$this#submit" }
try {
- val extras: Map<CaptureRequest.Key<*>, Any> = read3AState()
submitted = synchronized(processor) {
if (burst.size == 1) {
- processor.submit(burst[0], extras, true)
+ processor.submit(
+ burst[0],
+ cameraGraphConfig.defaultParameters,
+ emptyMap<Any, Any>()
+ )
} else {
- processor.submit(burst, extras, true)
+ processor.submit(
+ burst,
+ cameraGraphConfig.defaultParameters,
+ emptyMap<Any, Any>()
+ )
}
}
} finally {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState.kt
index 9504ebe..c59b50f 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState.kt
@@ -43,9 +43,9 @@
private val config: CameraGraph.Config,
private val graphProcessor: GraphProcessor,
private val sessionFactory: SessionFactory,
- private val requestProcessorFactory: RequestProcessor.Factory,
+ private val requestProcessorFactory: RequestProcessorFactory,
private val virtualCameraManager: VirtualCameraManager,
- private val streamMap: StreamMap
+ private val streamGraph: StreamGraphImpl
) : GraphState {
private var currentCamera: VirtualCamera? = null
private var currentSession: VirtualSessionState? = null
@@ -120,7 +120,7 @@
}
if (camera != null && session != null) {
- streamMap.listener = session
+ streamGraph.listener = session
camera.state.collect {
if (it is CameraStateOpen) {
session.cameraDevice = it.cameraDevice
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt
index a660f54..6e8a0f2 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt
@@ -21,6 +21,7 @@
import androidx.camera.camera2.pipe.AeMode
import androidx.camera.camera2.pipe.AfMode
import androidx.camera.camera2.pipe.AwbMode
+import androidx.camera.camera2.pipe.FlashMode
import javax.inject.Inject
/**
@@ -37,19 +38,39 @@
*/
@CameraGraphScope
internal class GraphState3A @Inject constructor() {
- private var aeMode: AeMode? = null
- private var afMode: AfMode? = null
- private var awbMode: AwbMode? = null
- private var aeRegions: List<MeteringRectangle>? = null
- private var afRegions: List<MeteringRectangle>? = null
- private var awbRegions: List<MeteringRectangle>? = null
- private var aeLock: Boolean? = null
- private var awbLock: Boolean? = null
+ var aeMode: AeMode? = null
+ get() = synchronized(this) { field }
+ private set
+ var afMode: AfMode? = null
+ get() = synchronized(this) { field }
+ private set
+ var awbMode: AwbMode? = null
+ get() = synchronized(this) { field }
+ private set
+ var flashMode: FlashMode? = null
+ get() = synchronized(this) { field }
+ private set
+ var aeRegions: List<MeteringRectangle>? = null
+ get() = synchronized(this) { field }
+ private set
+ var afRegions: List<MeteringRectangle>? = null
+ get() = synchronized(this) { field }
+ private set
+ var awbRegions: List<MeteringRectangle>? = null
+ get() = synchronized(this) { field }
+ private set
+ var aeLock: Boolean? = null
+ get() = synchronized(this) { field }
+ private set
+ var awbLock: Boolean? = null
+ get() = synchronized(this) { field }
+ private set
fun update(
aeMode: AeMode? = null,
afMode: AfMode? = null,
awbMode: AwbMode? = null,
+ flashMode: FlashMode? = null,
aeRegions: List<MeteringRectangle>? = null,
afRegions: List<MeteringRectangle>? = null,
awbRegions: List<MeteringRectangle>? = null,
@@ -60,6 +81,7 @@
aeMode?.let { this.aeMode = it }
afMode?.let { this.afMode = it }
awbMode?.let { this.awbMode = it }
+ flashMode?.let { this.flashMode = it }
aeRegions?.let { this.aeRegions = it }
afRegions?.let { this.afRegions = it }
awbRegions?.let { this.awbRegions = it }
@@ -74,6 +96,7 @@
aeMode?.let { map.put(CaptureRequest.CONTROL_AE_MODE, it.value) }
afMode?.let { map.put(CaptureRequest.CONTROL_AF_MODE, it.value) }
awbMode?.let { map.put(CaptureRequest.CONTROL_AWB_MODE, it.value) }
+ flashMode?.let { map.put(CaptureRequest.FLASH_MODE, it.value) }
aeRegions?.let { map.put(CaptureRequest.CONTROL_AE_REGIONS, it.toTypedArray()) }
afRegions?.let { map.put(CaptureRequest.CONTROL_AF_REGIONS, it.toTypedArray()) }
awbRegions?.let { map.put(CaptureRequest.CONTROL_AWB_REGIONS, it.toTypedArray()) }
@@ -88,6 +111,7 @@
aeMode?.let { builder.set(CaptureRequest.CONTROL_AE_MODE, it.value) }
afMode?.let { builder.set(CaptureRequest.CONTROL_AF_MODE, it.value) }
awbMode?.let { builder.set(CaptureRequest.CONTROL_AWB_MODE, it.value) }
+ flashMode?.let { builder.set(CaptureRequest.FLASH_MODE, it.value) }
aeRegions?.let { builder.set(CaptureRequest.CONTROL_AE_REGIONS, it.toTypedArray()) }
afRegions?.let { builder.set(CaptureRequest.CONTROL_AF_REGIONS, it.toTypedArray()) }
awbRegions?.let { builder.set(CaptureRequest.CONTROL_AWB_REGIONS, it.toTypedArray()) }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Permissions.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Permissions.kt
index cabfe98..9271e28ec 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Permissions.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Permissions.kt
@@ -22,6 +22,7 @@
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build
import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.core.Debug
import javax.inject.Inject
import javax.inject.Singleton
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
index 6668b71..69b9ac0 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
@@ -16,32 +16,8 @@
package androidx.camera.camera2.pipe.impl
-import android.hardware.camera2.CameraAccessException
-import android.hardware.camera2.CameraCaptureSession
-import android.hardware.camera2.CaptureFailure
import android.hardware.camera2.CaptureRequest
-import android.hardware.camera2.CaptureResult
-import android.hardware.camera2.TotalCaptureResult
-import android.util.ArrayMap
-import android.view.Surface
-import androidx.annotation.GuardedBy
-import androidx.camera.camera2.pipe.CameraGraph
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraTimestamp
-import androidx.camera.camera2.pipe.FrameNumber
-import androidx.camera.camera2.pipe.Metadata
import androidx.camera.camera2.pipe.Request
-import androidx.camera.camera2.pipe.RequestMetadata
-import androidx.camera.camera2.pipe.RequestNumber
-import androidx.camera.camera2.pipe.RequestTemplate
-import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.wrapper.CameraCaptureSessionWrapper
-import androidx.camera.camera2.pipe.wrapper.ObjectUnavailableException
-import androidx.camera.camera2.pipe.writeParameters
-import kotlinx.atomicfu.atomic
-import java.util.Collections.singletonList
-import java.util.Collections.singletonMap
-import javax.inject.Inject
/**
* An instance of a RequestProcessor exists for the duration of a CameraCaptureSession and must be
@@ -55,47 +31,39 @@
* global and *are* thread safe.
* - Special care is taken to reduce the number objects and wrappers that are created, and to reduce
* the number of loops and overhead in wrapper objects.
- * - Callbacks are expected to be invoked at very high frequency.
+ * - Callbacks are expected to be invoked at *very* high frequency on the camera thread.
* - One RequestProcessor instance per CameraCaptureSession
*/
-internal interface RequestProcessor {
+interface RequestProcessor {
/**
* Submit a single [Request] with an optional set of extra parameters.
*
* @param request the request to submit to the camera.
- * @param extraRequestParameters extra parameters to apply to the request.
- * @param requireSurfacesForAllStreams if this flag is defined then this method will only submit
- * the request if all streamIds can be mapped to valid surfaces. At least one surface is
- * always required. This is useful if (for example) someone needs to quickly submit a
- * request with a specific trigger or mode key but does not care about modifying the list of
- * current surfaces.
+ * @param defaultParameters will not override parameters specified in the request.
+ * @param requiredParameters will override parameters specified in the request.
* @return false if this request failed to be submitted. If this method returns false, none of
* the callbacks on the Request(s) will be invoked.
*/
fun submit(
request: Request,
- extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
- requireSurfacesForAllStreams: Boolean
+ defaultParameters: Map<*, Any>,
+ requiredParameters: Map<*, Any>
): Boolean
/**
* Submit a list of [Request]s with an optional set of extra parameters.
*
* @param requests the requests to submit to the camera.
- * @param extraRequestParameters extra parameters to apply to the request.
- * @param requireSurfacesForAllStreams if this flag is defined then this method will only submit
- * the request if all streamIds can be mapped to valid surfaces. At least one surface is
- * always required. This is useful if (for example) someone needs to quickly submit a
- * request with a specific trigger or mode key but does not care about modifying the list of
- * current surfaces.
+ * @param defaultParameters will not override parameters specified in the request.
+ * @param requiredParameters will override parameters specified in the request.
* @return false if this request failed to be submitted. If this method returns false, none of
* the callbacks on the Request(s) will be invoked.
*/
fun submit(
requests: List<Request>,
- extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
- requireSurfacesForAllStreams: Boolean
+ defaultParameters: Map<*, Any>,
+ requiredParameters: Map<*, Any>
): Boolean
/**
@@ -106,19 +74,15 @@
* cause the request to be used to generate multiple [CaptureRequest]s to the camera.
*
* @param request the requests to set as the repeating request.
- * @param extraRequestParameters extra parameters to apply to the request.
- * @param requireSurfacesForAllStreams if this flag is defined then this method will only submit
- * the request if all streamIds can be mapped to valid surfaces. At least one surface is
- * always required. This is useful if (for example) someone needs to quickly submit a
- * request with a specific trigger or mode key but does not care about modifying the list of
- * current surfaces.
+ * @param defaultParameters will not override parameters specified in the request.
+ * @param requiredParameters will override parameters specified in the request.
* @return false if this request failed to be submitted. If this method returns false, none of
* the callbacks on the Request(s) will be invoked.
*/
fun setRepeating(
request: Request,
- extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
- requireSurfacesForAllStreams: Boolean
+ defaultParameters: Map<*, Any>,
+ requiredParameters: Map<*, Any>
): Boolean
/**
@@ -133,627 +97,7 @@
/**
* Puts the RequestProcessor into a closed state where it will reject all incoming requests.
+ * This does NOT call stopRepeating() or abortCaptures().
*/
fun close()
-
- interface Factory {
- fun create(
- session: CameraCaptureSessionWrapper,
- surfaceMap: Map<StreamId, Surface>
- ): RequestProcessor
- }
-}
-
-internal class StandardRequestProcessorFactory @Inject constructor(
- private val threads: Threads,
- private val graphConfig: CameraGraph.Config,
- @ForCameraGraph private val graphListeners: ArrayList<Request.Listener>,
- private val graphState3A: GraphState3A
-) : RequestProcessor.Factory {
- override fun create(
- session: CameraCaptureSessionWrapper,
- surfaceMap: Map<StreamId, Surface>
- ): RequestProcessor =
- StandardRequestProcessor(
- session,
- threads,
- graphConfig,
- surfaceMap,
- graphListeners,
- graphState3A
- )
-}
-
-internal val requestProcessorDebugIds = atomic(0)
-internal val requestSequenceDebugIds = atomic(0L)
-internal val requestTags = atomic(0L)
-internal fun nextRequestTag(): RequestNumber = RequestNumber(requestTags.incrementAndGet())
-
-/**
- * This class is designed to synchronously handle interactions with the Camera CaptureSession.
- */
-internal class StandardRequestProcessor(
- private val session: CameraCaptureSessionWrapper,
- private val threads: Threads,
- private val graphConfig: CameraGraph.Config,
- private val surfaceMap: Map<StreamId, Surface>,
- private val graphListeners: List<Request.Listener>,
- private val graphState3A: GraphState3A
-) : RequestProcessor {
-
- @GuardedBy("inFlightRequests")
- private val inFlightRequests = mutableListOf<CaptureSequence>()
- private val debugId = requestProcessorDebugIds.incrementAndGet()
- private val closed = atomic(false)
-
- override fun submit(
- request: Request,
- extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
- requireSurfacesForAllStreams: Boolean
- ): Boolean {
- return configureAndCapture(
- singletonList(request),
- extraRequestParameters,
- requireSurfacesForAllStreams,
- isRepeating = false
- )
- }
-
- override fun submit(
- requests: List<Request>,
- extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
- requireSurfacesForAllStreams: Boolean
- ): Boolean {
- return configureAndCapture(
- requests,
- extraRequestParameters,
- requireSurfacesForAllStreams,
- isRepeating = false
- )
- }
-
- override fun setRepeating(
- request: Request,
- extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
- requireSurfacesForAllStreams: Boolean
- ): Boolean {
- return configureAndCapture(
- singletonList(request),
- extraRequestParameters,
- requireSurfacesForAllStreams,
- isRepeating = true
- )
- }
-
- override fun abortCaptures() {
- val requestsToAbort = synchronized(inFlightRequests) {
- val copy = inFlightRequests.toList()
- inFlightRequests.clear()
- copy
- }
- for (sequence in requestsToAbort) {
- sequence.invokeOnAborted()
- }
- session.abortCaptures()
- }
-
- override fun stopRepeating() {
- session.stopRepeating()
- }
-
- override fun close() {
- closed.compareAndSet(expect = false, update = true)
- }
-
- private fun configureAndCapture(
- requests: List<Request>,
- extras: Map<CaptureRequest.Key<*>, Any>,
- requireStreams: Boolean,
- isRepeating: Boolean
- ): Boolean {
- // Reject incoming requests if this instance has been stopped or closed.
- if (closed.value) {
- return false
- }
-
- val requestMap = ArrayMap<RequestNumber, RequestInfo>(requests.size)
- val captureRequests = ArrayList<CaptureRequest>(requests.size)
-
- val surfaceToStreamMap = ArrayMap<Surface, StreamId>()
- val streamToSurfaceMap = ArrayMap<StreamId, Surface>()
-
- for (request in requests) {
- val requestTemplate = request.template ?: graphConfig.template
-
- Log.debug { "Building CaptureRequest for $request" }
-
- // Check to see if there is at least one valid surface for each stream.
- var hasSurface = false
- for (stream in request.streams) {
- if (streamToSurfaceMap.contains(stream)) {
- hasSurface = true
- continue
- }
-
- val surface = surfaceMap[stream]
- if (surface != null) {
- Log.debug { " Binding $stream to $surface" }
-
- // TODO(codelogic) There should be a more efficient way to do these lookups than
- // having two maps.
- surfaceToStreamMap[surface] = stream
- streamToSurfaceMap[stream] = surface
- hasSurface = true
- } else if (requireStreams) {
- Log.info { " Failed to bind surface to $stream" }
- // If requireStreams is set we are required to map every stream to a valid
- // Surface object for this request. If this condition is violated, then we
- // return false because we cannot submit these request(s) until there is a valid
- // StreamId -> Surface mapping for all streams.
- return false
- }
- }
-
- // If there are no surfaces on a particular request, camera2 will now allow us to
- // submit it.
- if (!hasSurface) {
- return false
- }
-
- // Create the request builder. There is a risk this will throw an exception or return null
- // if the CameraDevice has been closed or disconnected. If this fails, indicate that the
- // request was not submitted.
- val requestBuilder: CaptureRequest.Builder
- try {
- requestBuilder = session.device.createCaptureRequest(requestTemplate)
- } catch (exception: ObjectUnavailableException) {
- return false
- }
-
- // Apply the output surfaces to the requestBuilder
- hasSurface = false
- for (stream in request.streams) {
- val surface = streamToSurfaceMap[stream]
- if (surface != null) {
- requestBuilder.addTarget(surface)
- hasSurface = true
- }
- }
-
- // Soundness check to make sure we add at least one surface. This should be guaranteed
- // because we are supposed to exit early and return false if we cannot map at least one
- // surface per request.
- check(hasSurface)
-
- // Apply the parameters to the requestBuilder
- requestBuilder.writeParameters(request.requestParameters)
-
- // Apply the 3A parameters first. This gives the users of camerapipe the ability to
- // still override the 3A parameters for complicated use cases.
- //
- // TODO(sushilnath@): Implement one of the two options. (1) Apply the 3A parameters
- // from internal 3A state machine at last and provide a flag in the Request object to
- // specify when the clients want to explicitly override some of the 3A parameters
- // directly. Add code to handle the flag. (2) Let clients override the 3A parameters
- // freely and when that happens intercept those parameters from the request and keep the
- // internal 3A state machine in sync.
- graphState3A.writeTo(requestBuilder)
-
- // Write extra parameters to the request. These parameters will overwite parameters
- // defined in the Request (if they overlap)
- requestBuilder.writeParameters(extras)
-
- // The tag must be set for every request. We use it to lookup listeners for the
- // individual requests so that each request can specify individual listeners.
- val requestTag = nextRequestTag()
- requestBuilder.setTag(requestTag)
-
- // Create the camera2 captureRequest and add it to our list of requests.
- val captureRequest = requestBuilder.build()
- captureRequests.add(captureRequest)
-
- @Suppress("SyntheticAccessor")
- requestMap[requestTag] = RequestInfo(
- captureRequest,
- emptyMap(),
- streamToSurfaceMap,
- requestTemplate,
- isRepeating,
- request,
- requestTag
- )
- }
-
- // Create the captureSequence listener
- @Suppress("SyntheticAccessor")
- val captureSequence = CaptureSequence(
- graphListeners,
- if (requests.size == 1) {
- singletonMap(requestMap.keyAt(0), requestMap.valueAt(0))
- } else {
- requestMap
- },
- captureRequests,
- surfaceToStreamMap,
- inFlightRequests,
- session.device.cameraId
- )
-
- // Non-repeating requests must always be aware of abort calls.
- if (!isRepeating) {
- synchronized(inFlightRequests) {
- inFlightRequests.add(captureSequence)
- }
- }
-
- var captured = false
- return try {
-
- Log.debug { "Submitting $captureSequence" }
- capture(captureRequests, captureSequence, isRepeating)
- captured = true
- Log.debug { "Submitted $captureSequence" }
- true
- } catch (closedException: ObjectUnavailableException) {
- false
- } catch (accessException: CameraAccessException) {
- false
- } finally {
- // If ANY unhandled exception occurs, don't throw, but make sure we remove it from the
- // list of in-flight requests.
- if (!captured && !isRepeating) {
- captureSequence.invokeOnAborted()
- }
- }
- }
-
- private fun capture(
- captureRequests: List<CaptureRequest>,
- captureSequence: CaptureSequence,
- isRepeating: Boolean
- ) {
- captureSequence.invokeOnRequestSequenceCreated()
-
- // NOTE: This is a funny synchronization call. The purpose is to avoid a rare but possible
- // situation where calling capture causes one of the callback methods to be invoked before
- // sequenceNumber has been set on the callback. Both this call and the synchronized
- // behavior on the CaptureSequence listener have been designed to minimize the number of
- // synchronized calls.
- synchronized(lock = captureSequence) {
- // TODO: Update these calls to use executors on newer versions of the OS
- val sequenceNumber: Int = if (captureRequests.size == 1) {
- if (isRepeating) {
- session.setRepeatingRequest(
- captureRequests[0],
- captureSequence,
- threads.camera2Handler
- )
- } else {
- session.capture(captureRequests[0], captureSequence, threads.camera2Handler)
- }
- } else {
- if (isRepeating) {
- session.setRepeatingBurst(
- captureRequests,
- captureSequence,
- threads.camera2Handler
- )
- } else {
- session.captureBurst(captureRequests, captureSequence, threads.camera2Handler)
- }
- }
- captureSequence.sequenceNumber = sequenceNumber
- }
-
- // Invoke callbacks without holding a lock.
- captureSequence.invokeOnRequestSequenceSubmitted()
- }
-
- override fun toString(): String {
- return "RequestProcessor-$debugId"
- }
-}
-
-/**
- * This class packages together information about a request that was submitted to the camera.
- */
-@Suppress("SyntheticAccessor") // Using an inline class generates a synthetic constructor
-internal class RequestInfo(
- private val captureRequest: CaptureRequest,
- private val extraRequestParameters: Map<Metadata.Key<*>, Any?>,
- override val streams: Map<StreamId, Surface>,
- override val template: RequestTemplate,
- override val repeating: Boolean,
- override val request: Request,
- override val requestNumber: RequestNumber
-) : RequestMetadata {
- override fun <T> get(key: CaptureRequest.Key<T>): T? = captureRequest[key]
- override fun <T> getOrDefault(key: CaptureRequest.Key<T>, default: T): T =
- get(key) ?: default
-
- @Suppress("UNCHECKED_CAST")
- override fun <T> get(key: Metadata.Key<T>): T? = extraRequestParameters[key] as T?
-
- override fun <T> getOrDefault(key: Metadata.Key<T>, default: T): T = get(key) ?: default
-
- override fun unwrap(): CaptureRequest = captureRequest
-}
-
-/**
- * This class responds to events from a set of one or more requests. It uses the tag field on
- * a CaptureRequest object to lookup and invoke per-request listeners so that a listener can be
- * defined on a specific request within a burst.
- */
-internal class CaptureSequence(
- private val internalListeners: List<Request.Listener>,
- private val requests: Map<RequestNumber, RequestInfo>,
- private val captureRequests: List<CaptureRequest>,
- private val surfaceMap: Map<Surface, StreamId>,
- private val inFlightRequests: MutableList<CaptureSequence>,
- private val camera: CameraId
-) : CameraCaptureSession.CaptureCallback() {
- private val debugId = requestSequenceDebugIds.incrementAndGet()
-
- @Volatile
- private var _sequenceNumber: Int? = null
- var sequenceNumber: Int
- get() {
- if (_sequenceNumber == null) {
- // If the sequence id has not been submitted, it means the call to capture or
- // setRepeating has not yet returned. The callback methods should never be synchronously
- // invoked, so the only case this should happen is if a second thread attempted to
- // invoke one of the callbacks before the initial call completed. By locking against the
- // captureSequence object here and in the capture call, we can block the callback thread
- // until the sequenceId is available.
- synchronized(this) {
- return checkNotNull(_sequenceNumber) {
- "SequenceNumber has not been set for $this!"
- }
- }
- }
- return checkNotNull(_sequenceNumber) {
- "SequenceNumber has not been set for $this!"
- }
- }
- set(value) {
- _sequenceNumber = value
- }
-
- override fun onCaptureStarted(
- captureSession: CameraCaptureSession,
- captureRequest: CaptureRequest,
- captureTimestamp: Long,
- captureFrameNumber: Long
- ) {
- val requestNumber = readRequestNumber(captureRequest)
- val timestamp = CameraTimestamp(captureTimestamp)
- val frameNumber = FrameNumber(captureFrameNumber)
-
- // Load the request and throw if we are not able to find an associated request. Under
- // normal circumstances this should never happen.
- val request = readRequest(requestNumber)
-
- invokeOnRequest(request) {
- it.onStarted(
- request,
- frameNumber,
- timestamp
- )
- }
- }
-
- override fun onCaptureProgressed(
- captureSession: CameraCaptureSession,
- captureRequest: CaptureRequest,
- partialCaptureResult: CaptureResult
- ) {
- val requestNumber = readRequestNumber(captureRequest)
- val frameNumber = FrameNumber(partialCaptureResult.frameNumber)
- val frameMetadata = AndroidFrameMetadata(partialCaptureResult, camera)
-
- // Load the request and throw if we are not able to find an associated request. Under
- // normal circumstances this should never happen.
- val request = readRequest(requestNumber)
-
- invokeOnRequest(request) {
- it.onPartialCaptureResult(
- request,
- frameNumber,
- frameMetadata
- )
- }
- }
-
- override fun onCaptureCompleted(
- captureSession: CameraCaptureSession,
- captureRequest: CaptureRequest,
- captureResult: TotalCaptureResult
- ) {
- // Remove this request from the set of requests that are currently tracked.
- synchronized(inFlightRequests) {
- inFlightRequests.remove(this)
- }
-
- val requestNumber = readRequestNumber(captureRequest)
- val frameNumber = FrameNumber(captureResult.frameNumber)
-
- // Load the request and throw if we are not able to find an associated request. Under
- // normal circumstances this should never happen.
- val request = readRequest(requestNumber)
-
- val frameInfo = AndroidFrameInfo(
- captureResult,
- camera,
- request
- )
-
- invokeOnRequest(request) {
- it.onTotalCaptureResult(
- request,
- frameNumber,
- frameInfo
- )
- }
- }
-
- override fun onCaptureFailed(
- captureSession: CameraCaptureSession,
- captureRequest: CaptureRequest,
- captureFailure: CaptureFailure
- ) {
- // Remove this request from the set of requests that are currently tracked.
- synchronized(inFlightRequests) {
- inFlightRequests.remove(this)
- }
-
- val requestNumber = readRequestNumber(captureRequest)
- val frameNumber = FrameNumber(captureFailure.frameNumber)
-
- // Load the request and throw if we are not able to find an associated request. Under
- // normal circumstances this should never happen.
- val request = readRequest(requestNumber)
-
- invokeOnRequest(request) {
- it.onFailed(
- request,
- frameNumber,
- captureFailure
- )
- }
- }
-
- override fun onCaptureBufferLost(
- captureSession: CameraCaptureSession,
- captureRequest: CaptureRequest,
- surface: Surface,
- frameId: Long
- ) {
- val requestNumber = readRequestNumber(captureRequest)
- val frameNumber = FrameNumber(frameId)
- val streamId = checkNotNull(surfaceMap[surface]) {
- "Unable to find the streamId for $surface on frame $frameNumber"
- }
-
- // Load the request and throw if we are not able to find an associated request. Under
- // normal circumstances this should never happen.
- val request = readRequest(requestNumber)
-
- invokeOnRequest(request) {
- it.onBufferLost(
- request,
- frameNumber,
- streamId
- )
- }
- }
-
- /**
- * Custom implementation that informs all listeners that the request had not completed when
- * abort was called.
- */
- fun invokeOnAborted() {
- invokeOnRequests { request, _, listener ->
- listener.onAborted(request.request)
- }
- }
-
- fun invokeOnRequestSequenceCreated() {
- invokeOnRequests { request, _, listener ->
- listener.onRequestSequenceCreated(request)
- }
- }
-
- fun invokeOnRequestSequenceSubmitted() {
- invokeOnRequests { request, _, listener ->
- listener.onRequestSequenceSubmitted(request)
- }
- }
-
- override fun onCaptureSequenceCompleted(
- captureSession: CameraCaptureSession,
- captureSequenceId: Int,
- captureFrameNumber: Long
- ) {
- check(sequenceNumber == captureSequenceId) {
- "Complete was invoked on $sequenceNumber, but the sequence was not fully submitted!"
- }
- synchronized(inFlightRequests) {
- inFlightRequests.remove(this)
- }
-
- val frameNumber = FrameNumber(captureFrameNumber)
- invokeOnRequests { request, _, listener ->
- listener.onRequestSequenceCompleted(request, frameNumber)
- }
- }
-
- override fun onCaptureSequenceAborted(
- captureSession: CameraCaptureSession,
- captureSequenceId: Int
- ) {
- check(sequenceNumber == captureSequenceId) {
- "Abort was invoked on $sequenceNumber, but the sequence was not fully submitted!"
- }
-
- // Remove this request from the set of requests that are currently tracked.
- synchronized(inFlightRequests) {
- inFlightRequests.remove(this)
- }
-
- invokeOnRequests { request, _, listener ->
- listener.onRequestSequenceAborted(request)
- }
- }
-
- private fun readRequestNumber(request: CaptureRequest): RequestNumber =
- checkNotNull(request.tag as RequestNumber)
-
- private fun readRequest(requestNumber: RequestNumber): RequestInfo {
- return checkNotNull(requests[requestNumber]) {
- "Unable to find the request for $requestNumber!"
- }
- }
-
- private inline fun invokeOnRequests(
- crossinline fn: (RequestMetadata, Int, Request.Listener) -> Any
- ) {
-
- // Always invoke the internal listener first on all of the internal listeners for the
- // entire sequence before invoking the listeners specified in the specific requests
- for (i in captureRequests.indices) {
- val requestNumber = readRequestNumber(captureRequests[i])
- val request = checkNotNull(requests[requestNumber])
-
- for (listenerIndex in internalListeners.indices) {
- fn(request, i, internalListeners[listenerIndex])
- }
- }
-
- for (i in captureRequests.indices) {
- val requestNumber = readRequestNumber(captureRequests[i])
- val request = checkNotNull(requests[requestNumber])
-
- for (listenerIndex in request.request.listeners.indices) {
- fn(request, i, request.request.listeners[listenerIndex])
- }
- }
- }
-
- private inline fun invokeOnRequest(
- request: RequestInfo,
- crossinline fn: (Request.Listener) -> Any
- ) {
- // Always invoke the internal listener first so that internal sate can be updated before
- // other listeners ask for it.
- for (i in internalListeners.indices) {
- fn(internalListeners[i])
- }
-
- // Invoke the listeners that were defined on this request.
- for (i in request.request.listeners.indices) {
- fn(request.request.listeners[i])
- }
- }
-
- override fun toString(): String = "CaptureSequence-$debugId"
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/SessionFactory.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/SessionFactory.kt
index 9663b3b..e1840e8 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/SessionFactory.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/SessionFactory.kt
@@ -23,10 +23,12 @@
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.core.Log
import androidx.camera.camera2.pipe.wrapper.AndroidOutputConfiguration
import androidx.camera.camera2.pipe.wrapper.CameraDeviceWrapper
import androidx.camera.camera2.pipe.wrapper.InputConfigData
import androidx.camera.camera2.pipe.wrapper.OutputConfigurationWrapper
+import androidx.camera.camera2.pipe.wrapper.OutputConfigurationWrapper.Companion.SURFACE_GROUP_ID_NONE
import androidx.camera.camera2.pipe.wrapper.SessionConfigData
import dagger.Module
import dagger.Provides
@@ -64,7 +66,7 @@
return androidPProvider.get()
}
- if (graphConfig.operatingMode == CameraGraph.OperatingMode.HIGH_SPEED) {
+ if (graphConfig.sessionMode == CameraGraph.OperatingMode.HIGH_SPEED) {
check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
"Cannot use HighSpeed sessions below Android M"
}
@@ -82,7 +84,7 @@
check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
"CameraPipe is not supported below Android L"
}
- check(graphConfig.inputStream == null) {
+ check(graphConfig.input == null) {
"Reprocessing is not supported on Android L"
}
@@ -125,13 +127,14 @@
surfaces: Map<StreamId, Surface>,
virtualSessionState: VirtualSessionState
): Map<StreamId, OutputConfigurationWrapper> {
- if (graphConfig.inputStream != null) {
+ if (graphConfig.input != null) {
try {
+ val outputConfig = graphConfig.input.stream.outputs.single()
cameraDevice.createReprocessableCaptureSession(
InputConfiguration(
- graphConfig.inputStream.width,
- graphConfig.inputStream.height,
- graphConfig.inputStream.format
+ outputConfig.size.width,
+ outputConfig.size.height,
+ outputConfig.format.value
),
surfaces.map { it.value },
virtualSessionState,
@@ -178,7 +181,7 @@
@RequiresApi(Build.VERSION_CODES.N)
internal class AndroidNSessionFactory @Inject constructor(
private val threads: Threads,
- private val streamMap: StreamMap,
+ private val streamGraph: StreamGraphImpl,
private val graphConfig: CameraGraph.Config
) : SessionFactory {
override fun create(
@@ -188,23 +191,24 @@
): Map<StreamId, OutputConfigurationWrapper> {
val outputs = buildOutputConfigurations(
graphConfig,
- streamMap,
+ streamGraph,
surfaces
)
try {
- if (graphConfig.inputStream == null) {
+ if (graphConfig.input == null) {
cameraDevice.createCaptureSessionByOutputConfigurations(
outputs.all,
virtualSessionState,
threads.camera2Handler
)
} else {
+ val outputConfig = graphConfig.input.stream.outputs.single()
cameraDevice.createReprocessableCaptureSessionByConfigurations(
InputConfigData(
- graphConfig.inputStream.width,
- graphConfig.inputStream.height,
- graphConfig.inputStream.format
+ outputConfig.size.width,
+ outputConfig.size.height,
+ outputConfig.format.value
),
outputs.all,
virtualSessionState,
@@ -225,7 +229,7 @@
internal class AndroidPSessionFactory @Inject constructor(
private val threads: Threads,
private val graphConfig: CameraGraph.Config,
- private val streamMap: StreamMap
+ private val streamGraph: StreamGraphImpl
) : SessionFactory {
override fun create(
cameraDevice: CameraDeviceWrapper,
@@ -234,31 +238,34 @@
): Map<StreamId, OutputConfigurationWrapper> {
val operatingMode =
- when (graphConfig.operatingMode) {
- CameraGraph.OperatingMode.NORMAL -> 0
- CameraGraph.OperatingMode.HIGH_SPEED -> 1
+ when (graphConfig.sessionMode) {
+ CameraGraph.OperatingMode.NORMAL -> SessionConfigData.SESSION_TYPE_REGULAR
+ CameraGraph.OperatingMode.HIGH_SPEED -> SessionConfigData.SESSION_TYPE_HIGH_SPEED
}
val outputs = buildOutputConfigurations(
graphConfig,
- streamMap,
+ streamGraph,
surfaces
)
+ val input = graphConfig.input?.let {
+ val outputConfig = it.stream.outputs.single()
+ InputConfigData(
+ outputConfig.size.width,
+ outputConfig.size.height,
+ outputConfig.format.value
+ )
+ }
+
val sessionConfig = SessionConfigData(
operatingMode,
- graphConfig.inputStream?.let {
- InputConfigData(
- it.width,
- it.height,
- it.format
- )
- },
+ input,
outputs.all,
threads.camera2Executor,
virtualSessionState,
- graphConfig.template.value,
- graphConfig.defaultParameters
+ graphConfig.sessionTemplate.value,
+ graphConfig.sessionParameters
)
try {
@@ -276,46 +283,77 @@
@RequiresApi(Build.VERSION_CODES.N)
internal fun buildOutputConfigurations(
graphConfig: CameraGraph.Config,
- streamMap: StreamMap,
+ streamGraph: StreamGraphImpl,
surfaces: Map<StreamId, Surface>
): OutputConfigurations {
- // TODO: Add support for:
- // surfaceGroupId
- // surfaceSharing
- // multipleSurfaces?
-
- val outputs = arrayListOf<OutputConfigurationWrapper>()
+ val allOutputs = arrayListOf<OutputConfigurationWrapper>()
val deferredOutputs = mutableMapOf<StreamId, OutputConfigurationWrapper>()
- for (streamConfig in graphConfig.streams) {
- val streamId = streamMap.streamConfigMap[streamConfig]!!.id
- val physicalCameraId = if (streamConfig.camera != graphConfig.camera) {
- streamConfig.camera
- } else {
- null
+ for (outputConfig in streamGraph.outputConfigs) {
+ val outputSurfaces = outputConfig.streams.mapNotNull { surfaces[it.id] }
+
+ val externalConfig = outputConfig.externalOutputConfig
+ if (externalConfig != null) {
+ check(outputSurfaces.size == outputConfig.streams.size) {
+ val missingStreams = outputConfig.streams.filter { !surfaces.contains(it.id) }
+ "Surfaces are not yet available for $outputConfig!" +
+ " Missing surfaces for $missingStreams!"
+ }
+ allOutputs.add(
+ AndroidOutputConfiguration(
+ externalConfig,
+ surfaceSharing = false, // No way to read this value.
+ maxSharedSurfaceCount = 1, // Hardcoded
+ physicalCameraId = null, // No way to read this value.
+ )
+ )
+ continue
}
- val surface = surfaces[streamId]
+ if (outputConfig.deferrable && outputSurfaces.size != outputConfig.streams.size) {
+ val output = AndroidOutputConfiguration.create(
+ null,
+ size = outputConfig.size,
+ outputType = outputConfig.deferredOutputType!!,
+ surfaceSharing = outputConfig.surfaceSharing,
+ surfaceGroupId = outputConfig.groupNumber ?: SURFACE_GROUP_ID_NONE,
+ physicalCameraId = if (outputConfig.camera != graphConfig.camera) {
+ outputConfig.camera
+ } else {
+ null
+ }
+ )
+ allOutputs.add(output)
+ for (outputSurface in outputConfig.streamBuilder) {
+ deferredOutputs[outputSurface.id] = output
+ }
+ continue
+ }
- val config = AndroidOutputConfiguration.create(
- surface,
- streamType = streamConfig.type,
- size = streamConfig.size,
- physicalCameraId = physicalCameraId
+ // Default case: We have the surface(s)
+ check(outputSurfaces.size == outputConfig.streams.size) {
+ val missingStreams = outputConfig.streams.filter { !surfaces.contains(it.id) }
+ "Surfaces are not yet available for $outputConfig!" +
+ " Missing surfaces for $missingStreams!"
+ }
+ val output = AndroidOutputConfiguration.create(
+ outputSurfaces.first(),
+ size = outputConfig.size,
+ surfaceSharing = outputConfig.surfaceSharing,
+ surfaceGroupId = outputConfig.groupNumber ?: SURFACE_GROUP_ID_NONE,
+ physicalCameraId = if (outputConfig.camera != graphConfig.camera) {
+ outputConfig.camera
+ } else {
+ null
+ }
)
-
- outputs.add(config)
-
- if (surface == null) {
- deferredOutputs[streamId] = config
+ for (surface in outputSurfaces.drop(1)) {
+ output.addSurface(surface)
}
+ allOutputs.add(output)
}
- // TODO: Sort outputs by type to try and put the viewfinder output first in the list
- // This is important as some devices assume that the first surface is the viewfinder and
- // will treat it differently.
-
- return OutputConfigurations(outputs, deferredOutputs)
+ return OutputConfigurations(allOutputs, deferredOutputs)
}
internal data class OutputConfigurations(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StandardRequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StandardRequestProcessor.kt
new file mode 100644
index 0000000..66cca5f
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StandardRequestProcessor.kt
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.impl
+
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CaptureRequest
+import android.util.ArrayMap
+import android.view.Surface
+import androidx.annotation.GuardedBy
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.Metadata
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.RequestNumber
+import androidx.camera.camera2.pipe.RequestTemplate
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.wrapper.CameraCaptureSessionWrapper
+import androidx.camera.camera2.pipe.wrapper.ObjectUnavailableException
+import androidx.camera.camera2.pipe.writeParameters
+import kotlinx.atomicfu.atomic
+import java.util.Collections.singletonList
+import java.util.Collections.singletonMap
+import javax.inject.Inject
+
+internal interface RequestProcessorFactory {
+ fun create(
+ session: CameraCaptureSessionWrapper,
+ surfaceMap: Map<StreamId, Surface>
+ ): RequestProcessor
+}
+
+internal class StandardRequestProcessorFactory @Inject constructor(
+ private val threads: Threads,
+ private val graphConfig: CameraGraph.Config,
+ @ForCameraGraph private val graphListeners: ArrayList<Request.Listener>,
+ private val graphState3A: GraphState3A
+) : RequestProcessorFactory {
+ override fun create(
+ session: CameraCaptureSessionWrapper,
+ surfaceMap: Map<StreamId, Surface>
+ ): RequestProcessor =
+ StandardRequestProcessor(
+ session,
+ threads,
+ graphConfig,
+ surfaceMap,
+ graphListeners,
+ graphState3A
+ )
+}
+
+internal val requestProcessorDebugIds = atomic(0)
+internal val requestSequenceDebugIds = atomic(0L)
+internal val requestTags = atomic(0L)
+internal fun nextRequestTag(): RequestNumber = RequestNumber(requestTags.incrementAndGet())
+
+/**
+ * This class is designed to synchronously handle interactions with the Camera CaptureSession.
+ */
+internal class StandardRequestProcessor(
+ private val session: CameraCaptureSessionWrapper,
+ private val threads: Threads,
+ private val graphConfig: CameraGraph.Config,
+ private val surfaceMap: Map<StreamId, Surface>,
+ private val graphListeners: List<Request.Listener>,
+ private val graphState3A: GraphState3A
+) : RequestProcessor {
+
+ @GuardedBy("inFlightRequests")
+ private val inFlightRequests = mutableListOf<CaptureSequence>()
+ private val debugId = requestProcessorDebugIds.incrementAndGet()
+ private val closed = atomic(false)
+
+ override fun submit(
+ request: Request,
+ defaultParameters: Map<*, Any>,
+ requiredParameters: Map<*, Any>
+ ): Boolean {
+ return configureAndCapture(
+ singletonList(request),
+ defaultParameters,
+ requiredParameters,
+ requireStreams = false,
+ isRepeating = false
+ )
+ }
+
+ override fun submit(
+ requests: List<Request>,
+ defaultParameters: Map<*, Any>,
+ requiredParameters: Map<*, Any>
+ ): Boolean {
+ return configureAndCapture(
+ requests,
+ defaultParameters,
+ requiredParameters,
+ requireStreams = false,
+ isRepeating = false
+ )
+ }
+
+ override fun setRepeating(
+ request: Request,
+ defaultParameters: Map<*, Any>,
+ requiredParameters: Map<*, Any>
+ ): Boolean {
+ return configureAndCapture(
+ singletonList(request),
+ defaultParameters,
+ requiredParameters,
+ requireStreams = false,
+ isRepeating = true
+ )
+ }
+
+ override fun abortCaptures() {
+ val requestsToAbort = synchronized(inFlightRequests) {
+ val copy = inFlightRequests.toList()
+ inFlightRequests.clear()
+ copy
+ }
+ for (sequence in requestsToAbort) {
+ sequence.invokeOnAborted()
+ }
+ session.abortCaptures()
+ }
+
+ override fun stopRepeating() {
+ session.stopRepeating()
+ }
+
+ override fun close() {
+ closed.compareAndSet(expect = false, update = true)
+ }
+
+ private fun configureAndCapture(
+ requests: List<Request>,
+ defaultParameters: Map<*, Any>,
+ requiredParameters: Map<*, Any>,
+ requireStreams: Boolean,
+ isRepeating: Boolean
+ ): Boolean {
+ // Reject incoming requests if this instance has been stopped or closed.
+ if (closed.value) {
+ return false
+ }
+
+ val requestMap = ArrayMap<RequestNumber, RequestInfo>(requests.size)
+ val captureRequests = ArrayList<CaptureRequest>(requests.size)
+
+ val surfaceToStreamMap = ArrayMap<Surface, StreamId>()
+ val streamToSurfaceMap = ArrayMap<StreamId, Surface>()
+
+ for (request in requests) {
+ val requestTemplate = request.template ?: graphConfig.defaultTemplate
+
+ Log.debug { "Building CaptureRequest for $request" }
+
+ // Check to see if there is at least one valid surface for each stream.
+ var hasSurface = false
+ for (stream in request.streams) {
+ if (streamToSurfaceMap.contains(stream)) {
+ hasSurface = true
+ continue
+ }
+
+ val surface = surfaceMap[stream]
+ if (surface != null) {
+ Log.debug { " Binding $stream to $surface" }
+
+ // TODO(codelogic) There should be a more efficient way to do these lookups than
+ // having two maps.
+ surfaceToStreamMap[surface] = stream
+ streamToSurfaceMap[stream] = surface
+ hasSurface = true
+ } else if (requireStreams) {
+ Log.info { " Failed to bind surface to $stream" }
+ // If requireStreams is set we are required to map every stream to a valid
+ // Surface object for this request. If this condition is violated, then we
+ // return false because we cannot submit these request(s) until there is a valid
+ // StreamId -> Surface mapping for all streams.
+ return false
+ }
+ }
+
+ // If there are no surfaces on a particular request, camera2 will now allow us to
+ // submit it.
+ if (!hasSurface) {
+ return false
+ }
+
+ // Create the request builder. There is a risk this will throw an exception or return null
+ // if the CameraDevice has been closed or disconnected. If this fails, indicate that the
+ // request was not submitted.
+ val requestBuilder: CaptureRequest.Builder
+ try {
+ requestBuilder = session.device.createCaptureRequest(requestTemplate)
+ } catch (exception: ObjectUnavailableException) {
+ return false
+ }
+
+ // Apply the output surfaces to the requestBuilder
+ hasSurface = false
+ for (stream in request.streams) {
+ val surface = streamToSurfaceMap[stream]
+ if (surface != null) {
+ requestBuilder.addTarget(surface)
+ hasSurface = true
+ }
+ }
+
+ // Soundness check to make sure we add at least one surface. This should be guaranteed
+ // because we are supposed to exit early and return false if we cannot map at least one
+ // surface per request.
+ check(hasSurface)
+
+ // Apply default parameters to the request builder first.
+ requestBuilder.writeParameters(defaultParameters)
+
+ // Apply request parameters to the request builder.
+ requestBuilder.writeParameters(request.parameters)
+
+ // Apply the 3A parameters. This gives the users of camerapipe the ability to
+ // still override the 3A parameters for complicated use cases.
+ //
+ // TODO(sushilnath@): Implement one of the two options. (1) Apply the 3A parameters
+ // from internal 3A state machine at last and provide a flag in the Request object to
+ // specify when the clients want to explicitly override some of the 3A parameters
+ // directly. Add code to handle the flag. (2) Let clients override the 3A parameters
+ // freely and when that happens intercept those parameters from the request and keep the
+ // internal 3A state machine in sync.
+ graphState3A.writeTo(requestBuilder)
+
+ // Finally, write required parameters to the request builder. This will override any
+ // value that has ben previously set.
+ requestBuilder.writeParameters(requiredParameters)
+
+ // The tag must be set for every request. We use it to lookup listeners for the
+ // individual requests so that each request can specify individual listeners.
+ val requestTag = nextRequestTag()
+ requestBuilder.setTag(requestTag)
+
+ // Create the camera2 captureRequest and add it to our list of requests.
+ val captureRequest = requestBuilder.build()
+ captureRequests.add(captureRequest)
+
+ @Suppress("SyntheticAccessor")
+ requestMap[requestTag] = RequestInfo(
+ captureRequest,
+ defaultParameters,
+ requiredParameters,
+ streamToSurfaceMap,
+ requestTemplate,
+ isRepeating,
+ request,
+ requestTag
+ )
+ }
+
+ // Create the captureSequence listener
+ @Suppress("SyntheticAccessor")
+ val captureSequence = CaptureSequence(
+ graphListeners,
+ if (requests.size == 1) {
+ singletonMap(requestMap.keyAt(0), requestMap.valueAt(0))
+ } else {
+ requestMap
+ },
+ captureRequests,
+ surfaceToStreamMap,
+ inFlightRequests,
+ session.device.cameraId
+ )
+
+ // Non-repeating requests must always be aware of abort calls.
+ if (!isRepeating) {
+ synchronized(inFlightRequests) {
+ inFlightRequests.add(captureSequence)
+ }
+ }
+
+ var captured = false
+ return try {
+ Log.debug { "Submitting $captureSequence" }
+ capture(captureRequests, captureSequence, isRepeating)
+ captured = true
+ Log.debug { "Submitted $captureSequence" }
+ true
+ } catch (closedException: ObjectUnavailableException) {
+ false
+ } catch (accessException: CameraAccessException) {
+ false
+ } finally {
+ // If ANY unhandled exception occurs, don't throw, but make sure we remove it from the
+ // list of in-flight requests.
+ if (!captured && !isRepeating) {
+ captureSequence.invokeOnAborted()
+ }
+ }
+ }
+
+ private fun capture(
+ captureRequests: List<CaptureRequest>,
+ captureSequence: CaptureSequence,
+ isRepeating: Boolean
+ ) {
+ captureSequence.invokeOnRequestSequenceCreated()
+
+ // NOTE: This is a funny synchronization call. The purpose is to avoid a rare but possible
+ // situation where calling capture causes one of the callback methods to be invoked before
+ // sequenceNumber has been set on the callback. Both this call and the synchronized
+ // behavior on the CaptureSequence listener have been designed to minimize the number of
+ // synchronized calls.
+ synchronized(lock = captureSequence) {
+ // TODO: Update these calls to use executors on newer versions of the OS
+ val sequenceNumber: Int = if (captureRequests.size == 1) {
+ if (isRepeating) {
+ session.setRepeatingRequest(
+ captureRequests[0],
+ captureSequence,
+ threads.camera2Handler
+ )
+ } else {
+ session.capture(captureRequests[0], captureSequence, threads.camera2Handler)
+ }
+ } else {
+ if (isRepeating) {
+ session.setRepeatingBurst(
+ captureRequests,
+ captureSequence,
+ threads.camera2Handler
+ )
+ } else {
+ session.captureBurst(captureRequests, captureSequence, threads.camera2Handler)
+ }
+ }
+ captureSequence.sequenceNumber = sequenceNumber
+ }
+
+ // Invoke callbacks without holding a lock.
+ captureSequence.invokeOnRequestSequenceSubmitted()
+ }
+
+ override fun toString(): String {
+ return "RequestProcessor-$debugId"
+ }
+}
+
+/**
+ * This class packages together information about a request that was submitted to the camera.
+ */
+@Suppress("SyntheticAccessor") // Using an inline class generates a synthetic constructor
+internal class RequestInfo(
+ private val captureRequest: CaptureRequest,
+ private val defaultParameters: Map<*, Any>,
+ private val requiredParameters: Map<*, Any>,
+ override val streams: Map<StreamId, Surface>,
+ override val template: RequestTemplate,
+ override val repeating: Boolean,
+ override val request: Request,
+ override val requestNumber: RequestNumber
+) : RequestMetadata {
+ override fun <T> get(key: CaptureRequest.Key<T>): T? = captureRequest[key]
+ override fun <T> getOrDefault(key: CaptureRequest.Key<T>, default: T): T =
+ get(key) ?: default
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T> get(key: Metadata.Key<T>): T? =
+ (requiredParameters[key] ?: request.extras[key] ?: defaultParameters[key]) as T?
+
+ override fun <T> getOrDefault(key: Metadata.Key<T>, default: T): T = get(key) ?: default
+
+ override fun unwrap(): CaptureRequest = captureRequest
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StreamGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StreamGraphImpl.kt
new file mode 100644
index 0000000..cc8aabe
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StreamGraphImpl.kt
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.impl
+
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+import android.hardware.camera2.params.OutputConfiguration
+import android.os.Build
+import android.util.Size
+import android.view.Surface
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.CameraStream
+import androidx.camera.camera2.pipe.InputStream
+import androidx.camera.camera2.pipe.OutputId
+import androidx.camera.camera2.pipe.OutputStream
+import androidx.camera.camera2.pipe.OutputStream.Config.ExternalOutputConfig
+import androidx.camera.camera2.pipe.OutputStream.Config.LazyOutputConfig
+import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.StreamGraph
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.wrapper.Api24Compat
+import kotlinx.atomicfu.atomic
+import javax.inject.Inject
+
+private val streamIds = atomic(0)
+internal fun nextStreamId(): StreamId = StreamId(streamIds.incrementAndGet())
+
+private val outputIds = atomic(0)
+internal fun nextOutputId(): OutputId = OutputId(outputIds.incrementAndGet())
+
+private val configIds = atomic(0)
+internal fun nextConfigId(): CameraConfigId = CameraConfigId(configIds.incrementAndGet())
+
+private val groupIds = atomic(0)
+internal fun nextGroupId(): Int = groupIds.incrementAndGet()
+
+@Suppress("EXPERIMENTAL_FEATURE_WARNING")
+internal inline class CameraConfigId(val value: Int) {
+ override fun toString(): String = "OutputConfig-$value"
+}
+
+/**
+ * This object keeps track of which surfaces have been configured for each stream. In addition,
+ * it will keep track of which surfaces have changed or replaced so that the CaptureSession can be
+ * reconfigured if the configured surfaces change.
+ */
+@CameraGraphScope
+internal class StreamGraphImpl @Inject constructor(
+ cameraMetadata: CameraMetadata,
+ graphConfig: CameraGraph.Config
+) : StreamGraph {
+ private val surfaceMap: MutableMap<StreamId, Surface> = mutableMapOf()
+ private val _streamMap: Map<CameraStream.Config, CameraStream>
+
+ internal val outputConfigs: List<OutputConfig>
+
+ // TODO: Build InputStream(s)
+ override val input: InputStream? = null
+ override val streams: List<CameraStream>
+ override val outputs: List<OutputStream>
+
+ override fun get(config: CameraStream.Config): CameraStream? = _streamMap[config]
+
+ init {
+ val outputConfigListBuilder = mutableListOf<OutputConfig>()
+ val outputConfigMap = mutableMapOf<OutputStream.Config, OutputConfig>()
+
+ val streamListBuilder = mutableListOf<CameraStream>()
+ val streamMapBuilder = mutableMapOf<CameraStream.Config, CameraStream>()
+
+ val deferredOutputsAllowed = computeIfDeferredStreamsAreSupported(
+ cameraMetadata,
+ graphConfig
+ )
+
+ // Compute groupNumbers for buffer sharing.
+ val groupNumbers = mutableMapOf<CameraStream.Config, Int>()
+ for (group in graphConfig.streamSharingGroups) {
+ check(group.size > 1)
+ val surfaceGroupId = computeNextSurfaceGroupId(graphConfig)
+ for (config in group) {
+ check(!groupNumbers.containsKey(config))
+ groupNumbers[config] = surfaceGroupId
+ }
+ }
+
+ // Create outputConfigs. If outputs are shared there can be fewer entries in map than there
+ // are streams.
+ for (streamConfig in graphConfig.streams) {
+ for (output in streamConfig.outputs) {
+ if (outputConfigMap.containsKey(output)) {
+ continue
+ }
+
+ @SuppressWarnings("SyntheticAccessor")
+ val outputConfig = OutputConfig(
+ nextConfigId(),
+ output.size,
+ output.format,
+ output.camera ?: graphConfig.camera,
+ groupNumber = groupNumbers[streamConfig],
+ deferredOutputType = if (deferredOutputsAllowed) {
+ (output as? LazyOutputConfig)?.outputType
+ } else {
+ null
+ },
+ externalOutputConfig = (output as? ExternalOutputConfig)?.output
+ )
+ outputConfigMap[output] = outputConfig
+ outputConfigListBuilder.add(outputConfig)
+ }
+ }
+
+ // Build the streams
+ for (streamConfigIdx in graphConfig.streams.indices) {
+ val streamConfig = graphConfig.streams[streamConfigIdx]
+
+ val outputs = streamConfig.outputs.map {
+ val outputConfig = outputConfigMap[it]!!
+
+ @SuppressWarnings("SyntheticAccessor")
+ val outputStream = OutputStreamImpl(
+ nextOutputId(),
+ outputConfig.size,
+ outputConfig.format,
+ outputConfig.camera
+ )
+ outputStream
+ }
+
+ val stream = CameraStream(nextStreamId(), outputs)
+ streamMapBuilder[streamConfig] = stream
+ streamListBuilder.add(stream)
+ for (output in outputs) {
+ output.stream = stream
+ }
+ for (cameraOutputConfig in streamConfig.outputs) {
+ outputConfigMap[cameraOutputConfig]!!.streamBuilder.add(stream)
+ }
+ }
+
+ // TODO: Sort outputs by type to try and put the viewfinder output first in the list
+ // This is important as some devices assume that the first surface is the viewfinder and
+ // will treat it differently.
+
+ streams = streamListBuilder
+ _streamMap = streamMapBuilder
+ outputs = streams.flatMap { it.outputs }
+ outputConfigs = outputConfigListBuilder
+ }
+
+ private var _listener: SurfaceListener? = null
+ var listener: SurfaceListener?
+ get() = _listener
+ set(value) {
+ _listener = value
+ if (value != null) {
+ maybeUpdateSurfaces()
+ }
+ }
+
+ operator fun set(stream: StreamId, surface: Surface?) {
+ Log.info {
+ if (surface != null) {
+ "Configured $stream to use $surface"
+ } else {
+ "Removed surface for $stream"
+ }
+ }
+ if (surface == null) {
+ // TODO: Tell the graph processor that it should resubmit the repeating request or
+ // reconfigure the camera2 captureSession
+ surfaceMap.remove(stream)
+ } else {
+ surfaceMap[stream] = surface
+ }
+ maybeUpdateSurfaces()
+ }
+
+ private fun maybeUpdateSurfaces() {
+ val surfaceListener = _listener ?: return
+
+ // Rules:
+ // 1. In order to tell the captureSession that we have surfaces, we should wait until we
+ // have at least one valid surface.
+ // 2. All streams that are not deferrable, must have a valid surface.
+
+ val surfaces = mutableMapOf<StreamId, Surface>()
+ for (outputConfig in outputConfigs) {
+ for (stream in outputConfig.streamBuilder) {
+ val surface = surfaceMap[stream.id]
+ if (surface == null) {
+ if (!outputConfig.deferrable) {
+ return
+ }
+ } else {
+ surfaces[stream.id] = surface
+ }
+ }
+ }
+
+ if (surfaces.isEmpty()) {
+ return
+ }
+
+ surfaceListener.onSurfaceMapUpdated(surfaces)
+ }
+
+ @Suppress("SyntheticAccessor") // StreamId generates a synthetic constructor
+ class OutputConfig(
+ val id: CameraConfigId,
+ val size: Size,
+ val format: StreamFormat,
+ val camera: CameraId,
+ val groupNumber: Int?,
+ val externalOutputConfig: OutputConfiguration?,
+ val deferredOutputType: OutputStream.OutputType?,
+ ) {
+ internal val streamBuilder = mutableListOf<CameraStream>()
+ val streams: List<CameraStream>
+ get() = streamBuilder
+ val deferrable: Boolean
+ get() = deferredOutputType != null
+ val surfaceSharing = streamBuilder.size > 1
+ override fun toString(): String = id.toString()
+ }
+
+ @Suppress("SyntheticAccessor") // OutputId generates a synthetic constructor
+ private class OutputStreamImpl(
+ override val id: OutputId,
+ override val size: Size,
+ override val format: StreamFormat,
+ override val camera: CameraId,
+ ) : OutputStream {
+ override lateinit var stream: CameraStream
+ override fun toString(): String = id.toString()
+ }
+
+ interface SurfaceListener {
+ fun onSurfaceMapUpdated(surfaces: Map<StreamId, Surface>)
+ }
+
+ private fun computeNextSurfaceGroupId(graphConfig: CameraGraph.Config): Int {
+ // If there are any existing surfaceGroups, make sure the groups we define do not overlap
+ // with any existing values.
+ val existingGroupNumbers: List<Int> = readExistingGroupNumbers(graphConfig.streams)
+
+ // Loop until we produce a groupId that was not already used.
+ var number = nextGroupId()
+ while (existingGroupNumbers.contains(number)) {
+ number = nextGroupId()
+ }
+ return number
+ }
+
+ private fun readExistingGroupNumbers(outputs: List<CameraStream.Config>): List<Int> {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ outputs
+ .flatMap { it.outputs }
+ .filterIsInstance<ExternalOutputConfig>()
+ .fold(mutableListOf()) { values, config ->
+ val groupId = Api24Compat.getSurfaceGroupId(config.output)
+ if (!values.contains(groupId)) {
+ values.add(groupId)
+ }
+ values
+ }
+ } else {
+ emptyList()
+ }
+ }
+
+ private fun computeIfDeferredStreamsAreSupported(
+ cameraMetadata: CameraMetadata,
+ graphConfig: CameraGraph.Config
+ ): Boolean {
+ val hardwareLevel = cameraMetadata[INFO_SUPPORTED_HARDWARE_LEVEL]
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
+ graphConfig.sessionMode == CameraGraph.OperatingMode.NORMAL &&
+ hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY &&
+ hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED &&
+ (
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
+ hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL
+ )
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StreamMap.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StreamMap.kt
deleted file mode 100644
index 8f67b7a..0000000
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StreamMap.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.pipe.impl
-
-import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
-import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL
-import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
-import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
-import android.os.Build
-import android.util.Size
-import android.view.Surface
-import androidx.camera.camera2.pipe.CameraGraph
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraMetadata
-import androidx.camera.camera2.pipe.Stream
-import androidx.camera.camera2.pipe.StreamConfig
-import androidx.camera.camera2.pipe.StreamFormat
-import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.StreamType
-import kotlinx.atomicfu.atomic
-import javax.inject.Inject
-
-private val streamIds = atomic(0)
-internal fun nextStreamId(): StreamId = StreamId(streamIds.incrementAndGet())
-
-/**
- * This object keeps track of which surfaces have been configured for each stream. In addition,
- * it will keep track of which surfaces have changed or replaced so that the CaptureSession can be
- * reconfigured if the configured surfaces change.
- */
-@CameraGraphScope
-internal class StreamMap @Inject constructor(
- cameraMetadata: CameraMetadata,
- graphConfig: CameraGraph.Config
-) {
- private val surfaceMap: MutableMap<StreamId, Surface> = mutableMapOf()
- private val deferrableStreams: Set<StreamId>
-
- val streamConfigMap: Map<StreamConfig, Stream>
-
- init {
- val streamBuilder = mutableMapOf<StreamConfig, Stream>()
- val deferrableStreamBuilder = mutableSetOf<StreamId>()
-
- val hardwareLevel = cameraMetadata[INFO_SUPPORTED_HARDWARE_LEVEL]
- val deferredStreamsSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
- graphConfig.operatingMode == CameraGraph.OperatingMode.NORMAL &&
- hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY &&
- hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED &&
- (
- Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
- hardwareLevel != INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL
- )
-
- for (streamConfig in graphConfig.streams) {
- // Using an inline class generates a synthetic constructor
- @Suppress("SyntheticAccessor")
- val stream = StreamImpl(
- nextStreamId(),
- streamConfig.size,
- streamConfig.format,
- streamConfig.camera,
- streamConfig.type
- )
-
- streamBuilder[streamConfig] = stream
-
- if (deferredStreamsSupported &&
- streamConfig.deferrable &&
- (
- streamConfig.type == StreamType.SURFACE_TEXTURE ||
- streamConfig.type == StreamType.SURFACE_VIEW
- )
- ) {
- deferrableStreamBuilder.add(stream.id)
- }
- }
- deferrableStreams = deferrableStreamBuilder
- streamConfigMap = streamBuilder
- }
-
- private var _listener: SurfaceListener? = null
- var listener: SurfaceListener?
- get() = _listener
- set(value) {
- _listener = value
- if (value != null) {
- maybeUpdateSurfaces()
- }
- }
-
- operator fun set(stream: StreamId, surface: Surface?) {
- Log.info {
- if (surface != null) {
- "Configured $stream to use $surface"
- } else {
- "Removed surface for $stream"
- }
- }
- if (surface == null) {
- // TODO: Tell the graph processor that it should resubmit the repeating request or
- // reconfigure the camera2 captureSession
- surfaceMap.remove(stream)
- } else {
- surfaceMap[stream] = surface
- }
- maybeUpdateSurfaces()
- }
-
- private fun maybeUpdateSurfaces() {
- val surfaceListener = _listener ?: return
-
- // Rules:
- // 1. In order to tell the captureSession that we have surfaces, we should wait until we
- // have at least one valid surface.
- // 2. All streams that are not deferrable, must have a valid surface.
-
- val surfaces = mutableMapOf<StreamId, Surface>()
- for (stream in streamConfigMap) {
- val surface = surfaceMap[stream.value.id]
-
- if (surface == null) {
- if (!deferrableStreams.contains(stream.value.id)) {
- // Break early if no surface is defined, and the stream is not deferrable.
- return
- }
- } else {
- surfaces[stream.value.id] = surface
- }
- }
-
- if (surfaces.isEmpty()) {
- return
- }
-
- surfaceListener.setSurfaceMap(surfaces)
- }
-
- // Using an inline class generates a synthetic constructor
- @Suppress("SyntheticAccessor")
- data class StreamImpl(
- override val id: StreamId,
- override val size: Size,
- override val format: StreamFormat,
- override val camera: CameraId,
- override val type: StreamType
- ) : Stream {
- override fun toString(): String = id.toString()
- }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCamera.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCamera.kt
index 78c4b50..b25a0ec73 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCamera.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCamera.kt
@@ -22,7 +22,12 @@
import androidx.annotation.GuardedBy
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CameraMetadata
-import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.DurationNs
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.core.TimestampNs
+import androidx.camera.camera2.pipe.core.Timestamps
+import androidx.camera.camera2.pipe.core.Timestamps.formatMs
import androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice
import androidx.camera.camera2.pipe.wrapper.CameraDeviceWrapper
import androidx.camera.camera2.pipe.wrapper.closeWithTrace
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCameraManager.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCameraManager.kt
index 956758ee..662780a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCameraManager.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualCameraManager.kt
@@ -20,6 +20,9 @@
import android.hardware.camera2.CameraManager
import android.os.Build
import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.core.Timestamps
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualSessionState.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualSessionState.kt
index 43644b1..118d015 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualSessionState.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualSessionState.kt
@@ -17,10 +17,13 @@
package androidx.camera.camera2.pipe.impl
import android.view.Surface
-
import androidx.annotation.GuardedBy
import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.core.TimestampNs
+import androidx.camera.camera2.pipe.core.Timestamps
+import androidx.camera.camera2.pipe.core.Timestamps.formatMs
import androidx.camera.camera2.pipe.wrapper.CameraCaptureSessionWrapper
import androidx.camera.camera2.pipe.wrapper.CameraDeviceWrapper
import androidx.camera.camera2.pipe.wrapper.OutputConfigurationWrapper
@@ -29,10 +32,6 @@
import kotlinx.coroutines.launch
import java.util.Collections.synchronizedMap
-internal interface SurfaceListener {
- fun setSurfaceMap(surfaces: Map<StreamId, Surface>)
-}
-
internal val virtualSessionDebugIds = atomic(0)
/**
@@ -46,9 +45,9 @@
internal class VirtualSessionState(
private val graphProcessor: GraphProcessor,
private val sessionFactory: SessionFactory,
- private val requestProcessorFactory: RequestProcessor.Factory,
+ private val requestProcessorFactory: RequestProcessorFactory,
private val scope: CoroutineScope
-) : CameraCaptureSessionWrapper.StateCallback, SurfaceListener {
+) : CameraCaptureSessionWrapper.StateCallback, StreamGraphImpl.SurfaceListener {
private val debugId = virtualSessionDebugIds.incrementAndGet()
private val lock = Any()
@@ -93,7 +92,7 @@
@GuardedBy("lock")
private var _surfaceMap: Map<StreamId, Surface>? = null
- override fun setSurfaceMap(surfaces: Map<StreamId, Surface>) {
+ override fun onSurfaceMapUpdated(surfaces: Map<StreamId, Surface>) {
synchronized(lock) {
if (state == State.CLOSING || state == State.CLOSED) {
return@synchronized
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/ApiCompat.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/ApiCompat.kt
new file mode 100644
index 0000000..bfe6428
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/ApiCompat.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.wrapper
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
+import android.hardware.camera2.params.OutputConfiguration
+import android.os.Build
+import androidx.annotation.RequiresApi
+
+@RequiresApi(Build.VERSION_CODES.N)
+internal object Api24Compat {
+ @JvmStatic
+ fun getSurfaceGroupId(outputConfiguration: OutputConfiguration): Int {
+ return outputConfiguration.surfaceGroupId
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.P)
+internal object Api28Compat {
+ @JvmStatic
+ fun getAvailablePhysicalCameraRequestKeys(
+ cameraCharacteristics: CameraCharacteristics
+ ): List<CaptureRequest.Key<*>>? {
+ return cameraCharacteristics.availablePhysicalCameraRequestKeys
+ }
+
+ @JvmStatic
+ fun getAvailableSessionKeys(
+ cameraCharacteristics: CameraCharacteristics
+ ): List<CaptureRequest.Key<*>>? {
+ return cameraCharacteristics.availableSessionKeys
+ }
+
+ @JvmStatic
+ fun getPhysicalCaptureResults(
+ totalCaptureResult: TotalCaptureResult
+ ): Map<String, CaptureResult>? {
+ return totalCaptureResult.physicalCameraResults
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt
index 3e80b18..362db5f 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CameraDevice.kt
@@ -30,10 +30,10 @@
import androidx.camera.camera2.pipe.CameraMetadata
import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.UnsafeWrapper
-import androidx.camera.camera2.pipe.impl.Debug
-import androidx.camera.camera2.pipe.impl.Log
-import androidx.camera.camera2.pipe.impl.Timestamps
-import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
+import androidx.camera.camera2.pipe.core.Debug
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.core.Timestamps
+import androidx.camera.camera2.pipe.core.Timestamps.formatMs
import androidx.camera.camera2.pipe.writeParameter
import kotlin.jvm.Throws
@@ -248,7 +248,7 @@
// Iterate template parameters and CHECK BY NAME, as there have been cases where equality
// checks did not pass.
for ((key, value) in config.sessionParameters) {
- if (key is CaptureRequest.Key<*> && sessionKeyNames.contains(key.name)) {
+ if (sessionKeyNames.contains(key.name)) {
requestBuilder.writeParameter(key, value)
}
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CaptureSession.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CaptureSession.kt
index db12af07..f0daa8c 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CaptureSession.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/CaptureSession.kt
@@ -25,7 +25,7 @@
import android.view.Surface
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.UnsafeWrapper
-import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.core.Log
import kotlinx.atomicfu.atomic
import java.io.Closeable
import kotlin.jvm.Throws
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Configuration.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Configuration.kt
index 604ea45..0352257 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Configuration.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Configuration.kt
@@ -18,6 +18,7 @@
import android.annotation.SuppressLint
import android.graphics.SurfaceTexture
+import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.params.OutputConfiguration
import android.os.Build
import android.util.Size
@@ -25,11 +26,11 @@
import android.view.SurfaceHolder
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.StreamType
+import androidx.camera.camera2.pipe.OutputStream.OutputType
import androidx.camera.camera2.pipe.UnsafeWrapper
-import androidx.camera.camera2.pipe.impl.checkNOrHigher
-import androidx.camera.camera2.pipe.impl.checkOOrHigher
-import androidx.camera.camera2.pipe.impl.checkPOrHigher
+import androidx.camera.camera2.pipe.core.checkNOrHigher
+import androidx.camera.camera2.pipe.core.checkOOrHigher
+import androidx.camera.camera2.pipe.core.checkPOrHigher
import androidx.camera.camera2.pipe.wrapper.OutputConfigurationWrapper.Companion.SURFACE_GROUP_ID_NONE
import java.util.concurrent.Executor
@@ -46,7 +47,7 @@
val stateCallback: CameraCaptureSessionWrapper.StateCallback,
val sessionTemplateId: Int,
- val sessionParameters: Map<*, Any>
+ val sessionParameters: Map<CaptureRequest.Key<*>, Any>
) {
companion object {
/* NOTE: These must keep in sync with their SessionConfiguration values. */
@@ -132,7 +133,7 @@
*/
fun create(
surface: Surface?,
- streamType: StreamType = StreamType.SURFACE,
+ outputType: OutputType = OutputType.SURFACE,
size: Size? = null,
surfaceSharing: Boolean = false,
surfaceGroupId: Int = SURFACE_GROUP_ID_NONE,
@@ -142,7 +143,11 @@
// Create the OutputConfiguration using the groupId via the constructor (if set)
val configuration: OutputConfiguration
- if (surface != null) {
+ if (outputType == OutputType.SURFACE) {
+ check(surface != null) {
+ "OutputConfigurations defined with ${OutputType.SURFACE} must provide a valid" +
+ " surface!"
+ }
configuration = if (surfaceGroupId != SURFACE_GROUP_ID_NONE) {
OutputConfiguration(surfaceGroupId, surface)
} else {
@@ -159,17 +164,14 @@
check(size != null) {
"Size must defined when creating a deferred OutputConfiguration."
}
-
- configuration = OutputConfiguration(
- size,
- when (streamType) {
- StreamType.SURFACE_TEXTURE -> SurfaceTexture::class.java
- StreamType.SURFACE_VIEW -> SurfaceHolder::class.java
- StreamType.SURFACE -> throw IllegalArgumentException(
- "StreamType.Surface is not supported for deferred OutputConfigurations"
- )
- }
- )
+ val outputKlass = when (outputType) {
+ OutputType.SURFACE_TEXTURE -> SurfaceTexture::class.java
+ OutputType.SURFACE_VIEW -> SurfaceHolder::class.java
+ OutputType.SURFACE -> throw IllegalStateException(
+ "Unsupported OutputType: $outputType"
+ )
+ }
+ configuration = OutputConfiguration(size, outputKlass)
}
// Enable surface sharing, if set.
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Exceptions.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Exceptions.kt
index 60b5f60..f335a96 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Exceptions.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/Exceptions.kt
@@ -17,7 +17,7 @@
package androidx.camera.camera2.pipe.wrapper
import android.hardware.camera2.CameraAccessException
-import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.core.Log
import kotlin.jvm.Throws
/**
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt
index 116f43a..246802dd 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt
@@ -48,7 +48,7 @@
CameraGraph.Config(
camera = fakeCameraId,
streams = listOf(),
- template = RequestTemplate(0)
+ defaultTemplate = RequestTemplate(0)
)
)
assertThat(cameraGraph).isNotNull()
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt
index 338c160..5e02b30 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt
@@ -33,8 +33,8 @@
fun requestHasDefaults() {
val request = Request(listOf(StreamId(1)))
- assertThat(request.requestParameters).isEmpty()
- assertThat(request.extraRequestParameters).isEmpty()
+ assertThat(request.parameters).isEmpty()
+ assertThat(request.extras).isEmpty()
assertThat(request.template).isNull()
assertThat(request.listeners).isEmpty()
@@ -45,10 +45,10 @@
fun canReadCaptureParameters() {
val request = Request(
listOf(StreamId(1)),
- requestParameters = mapOf(
+ parameters = mapOf(
CaptureRequest.EDGE_MODE to CaptureRequest.EDGE_MODE_HIGH_QUALITY
),
- extraRequestParameters = mapOf(FakeMetadata.TEST_KEY to 42)
+ extras = mapOf(FakeMetadata.TEST_KEY to 42)
)
// Check with a valid test key
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt
index 3f935c0..5475134 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt
@@ -27,37 +27,50 @@
@RunWith(CameraPipeRobolectricTestRunner::class)
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
internal class StreamTest {
-
- private val streamConfig1 = StreamConfig(
+ private val streamConfig1 = CameraStream.Config.create(
size = Size(640, 480),
- format = StreamFormat.YUV_420_888,
- camera = CameraId("test"),
- type = StreamType.SURFACE
+ format = StreamFormat.YUV_420_888
)
- private val streamConfig2 = StreamConfig(
+ private val streamConfig2 = CameraStream.Config.create(
size = Size(640, 480),
- format = StreamFormat.YUV_420_888,
- camera = CameraId("test"),
- type = StreamType.SURFACE
+ format = StreamFormat.YUV_420_888
)
- private val streamConfig3 = StreamConfig(
+ private val streamConfig3 = CameraStream.Config.create(
size = Size(640, 480),
- format = StreamFormat.JPEG,
- camera = CameraId("test"),
- type = StreamType.SURFACE
+ format = StreamFormat.JPEG
)
@Test
+ fun differentStreamConfigsAreNotEqual() {
+ assertThat(streamConfig1).isNotEqualTo(streamConfig3)
+ assertThat(streamConfig2).isNotEqualTo(streamConfig3)
+ }
+
+ @Test
fun equivalentStreamConfigsAreNotEqual() {
assertThat(streamConfig1).isNotEqualTo(streamConfig2)
assertThat(streamConfig1).isNotSameInstanceAs(streamConfig2)
}
@Test
- fun differentStreamConfigsAreNotEqual() {
- assertThat(streamConfig1).isNotEqualTo(streamConfig3)
- assertThat(streamConfig2).isNotEqualTo(streamConfig3)
+ fun equivalentOutputsAreNotEqual() {
+ assertThat(streamConfig1.outputs.single()).isNotEqualTo(streamConfig2.outputs.single())
+ assertThat(streamConfig1.outputs.single())
+ .isNotSameInstanceAs(streamConfig2.outputs.single())
+ }
+
+ @Test
+ fun sharedOutputsAreShared() {
+ val outputConfig = OutputStream.Config.create(
+ size = Size(640, 480),
+ format = StreamFormat.YUV_420_888
+ )
+ val sharedConfig1 = CameraStream.Config.create(outputConfig)
+ val sharedConfig2 = CameraStream.Config.create(outputConfig)
+ assertThat(sharedConfig1).isNotEqualTo(sharedConfig2)
+ assertThat(sharedConfig1.outputs.single()).isEqualTo(sharedConfig2.outputs.single())
+ assertThat(sharedConfig1.outputs.single()).isSameInstanceAs(sharedConfig2.outputs.single())
}
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraGraphImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraGraphImplTest.kt
index e4d7239..c215ac1f 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraGraphImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraGraphImplTest.kt
@@ -21,7 +21,6 @@
import android.os.Build
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.Request
-import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
import androidx.camera.camera2.pipe.testing.FakeCameras
@@ -53,13 +52,12 @@
val config = CameraGraph.Config(
camera = fakeCameraId,
streams = listOf(),
- template = RequestTemplate(0)
)
impl = CameraGraphImpl(
config,
fakeMetadata,
fakeGraphProcessor,
- StreamMap(
+ StreamGraphImpl(
fakeMetadata,
config
),
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraPipeComponentTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraPipeComponentTest.kt
index 0195662..ea02fe7 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraPipeComponentTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraPipeComponentTest.kt
@@ -20,7 +20,6 @@
import android.os.Build
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.RequestTemplate
import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
import androidx.camera.camera2.pipe.testing.FakeCameras
import androidx.test.core.app.ApplicationProvider
@@ -58,7 +57,6 @@
val config = CameraGraph.Config(
camera = cameraId,
streams = listOf(),
- template = RequestTemplate(0)
)
val module = CameraGraphConfigModule(config)
val builder = component.cameraGraphComponentBuilder()
@@ -80,7 +78,6 @@
CameraGraph.Config(
camera = fakeCameraId,
streams = listOf(),
- template = RequestTemplate(0)
)
)
)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AForCaptureTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AForCaptureTest.kt
index c4b4465..61af5b8d 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AForCaptureTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AForCaptureTest.kt
@@ -109,10 +109,10 @@
// We now check if the correct sequence of requests were submitted by lock3AForCapture call.
// There should be a request to trigger AF and AE precapture metering.
val request1 = requestProcessor.nextEvent().request
- assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
CaptureRequest.CONTROL_AF_TRIGGER_START
)
- assertThat(request1.extraRequestParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
+ assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
.isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
}
@@ -186,10 +186,10 @@
// We now check if the correct sequence of requests were submitted by unlock3APostCapture
// call. There should be a request to cancel AF and AE precapture metering.
val request1 = requestProcessor.nextEvent().request
- assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
)
- assertThat(request1.extraRequestParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
+ assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER])
.isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL)
}
@@ -223,15 +223,15 @@
// We now check if the correct sequence of requests were submitted by unlock3APostCapture
// call. There should be a request to cancel AF and lock ae.
val request1 = requestProcessor.nextEvent().request
- assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
)
- assertThat(request1.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK])
+ assertThat(request1.requiredParameters[CaptureRequest.CONTROL_AE_LOCK])
.isEqualTo(true)
// Then another request to unlock ae.
val request2 = requestProcessor.nextEvent().request
- assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK])
+ assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK])
.isEqualTo(false)
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt
index 016a709..4374aee 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt
@@ -115,16 +115,16 @@
// We not check if the correct sequence of requests were submitted by lock3A call. The
// request should be a repeating request to lock AE.
val request1 = requestProcessor.nextEvent().request
- assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ assertThat(request1!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
true
)
// The second request should be a single request to lock AF.
val request2 = requestProcessor.nextEvent().request
- assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ assertThat(request2!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
CaptureRequest.CONTROL_AF_TRIGGER_START
)
- assertThat(request2.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ assertThat(request2.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
true
)
}
@@ -174,7 +174,7 @@
requestProcessor.nextEvent().request
// Once AE is converged, another repeatingrequest is sent to lock AE.
val request1 = requestProcessor.nextEvent().request
- assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ assertThat(request1!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
true
)
@@ -204,7 +204,7 @@
// A single request to lock AF must have been used as well.
val request2 = requestProcessor.nextEvent().request
- assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ assertThat(request2!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
CaptureRequest.CONTROL_AF_TRIGGER_START
)
}
@@ -250,7 +250,7 @@
// For a new AE scan we first send a request to unlock AE just in case it was
// previously or internally locked.
val request1 = requestProcessor.nextEvent().request
- assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ assertThat(request1!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
false
)
@@ -280,16 +280,16 @@
// There should be one more request to lock AE after new scan is done.
val request2 = requestProcessor.nextEvent().request
- assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
true
)
// And one request to lock AF.
val request3 = requestProcessor.nextEvent().request
- assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ assertThat(request3!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
CaptureRequest.CONTROL_AF_TRIGGER_START
)
- assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ assertThat(request3.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
true
)
}
@@ -360,16 +360,16 @@
requestProcessor.nextEvent()
// One request to lock AE
val request2 = requestProcessor.nextEvent().request
- assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
true
)
// And one request to lock AF.
val request3 = requestProcessor.nextEvent().request
- assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ assertThat(request3!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
CaptureRequest.CONTROL_AF_TRIGGER_START
)
- assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ assertThat(request3.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
true
)
}
@@ -438,7 +438,7 @@
// One request to cancel AF to start a new scan.
val request1 = requestProcessor.nextEvent().request
- assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ assertThat(request1!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
)
// There should be one request to monitor AF to finish it's scan.
@@ -446,16 +446,16 @@
// There should be one request to monitor lock AE.
val request2 = requestProcessor.nextEvent().request
- assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
true
)
// And one request to lock AF.
val request3 = requestProcessor.nextEvent().request
- assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ assertThat(request3!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
CaptureRequest.CONTROL_AF_TRIGGER_START
)
- assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ assertThat(request3.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
true
)
}
@@ -526,16 +526,16 @@
requestProcessor.nextEvent()
// One request to lock AE
val request2 = requestProcessor.nextEvent().request
- assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
true
)
// And one request to lock AF.
val request3 = requestProcessor.nextEvent().request
- assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ assertThat(request3!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
CaptureRequest.CONTROL_AF_TRIGGER_START
)
- assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ assertThat(request3.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
true
)
}
@@ -604,27 +604,27 @@
// One request to cancel AF to start a new scan.
val request1 = requestProcessor.nextEvent().request
- assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ assertThat(request1!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
)
// There should be one request to unlock AE and monitor the current AF scan to finish.
val request2 = requestProcessor.nextEvent().request
- assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
false
)
// There should be one request to monitor lock AE.
val request3 = requestProcessor.nextEvent().request
- assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ assertThat(request3!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
true
)
// And one request to lock AF.
val request4 = requestProcessor.nextEvent().request
- assertThat(request4!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ assertThat(request4!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
CaptureRequest.CONTROL_AF_TRIGGER_START
)
- assertThat(request4.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ assertThat(request4.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
true
)
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASetTorchTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASetTorchTest.kt
new file mode 100644
index 0000000..18e7c2c
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASetTorchTest.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.impl
+
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.os.Build
+import androidx.camera.camera2.pipe.AeMode
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestNumber
+import androidx.camera.camera2.pipe.Status3A
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.TorchState
+import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
+import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
+import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
+import androidx.camera.camera2.pipe.testing.FakeRequestProcessor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(CameraPipeRobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+internal class Controller3ASetTorchTest {
+ private val graphProcessor = FakeGraphProcessor()
+ private val graphState3A = GraphState3A()
+ private val requestProcessor = FakeRequestProcessor(graphState3A)
+ private val listener3A = Listener3A()
+ private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
+
+ @Test
+ fun testSetTorchOn() = runBlocking {
+ initGraphProcessor()
+
+ val result = controller3A.setTorch(TorchState.ON)
+ assertThat(graphState3A.aeMode!!.value).isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON)
+ assertThat(graphState3A.flashMode!!.value).isEqualTo(CaptureRequest.FLASH_MODE_TORCH)
+ assertThat(result.isCompleted).isFalse()
+
+ GlobalScope.launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
+ CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_TORCH
+ )
+ )
+ )
+ }
+ val result3A = result.await()
+ assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Status3A.OK)
+ }
+
+ @Test
+ fun testSetTorchOff() = runBlocking {
+ initGraphProcessor()
+
+ val result = controller3A.setTorch(TorchState.OFF)
+ assertThat(graphState3A.aeMode!!.value).isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON)
+ assertThat(graphState3A.flashMode!!.value).isEqualTo(CaptureRequest.FLASH_MODE_OFF)
+ assertThat(result.isCompleted).isFalse()
+
+ GlobalScope.launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_ON,
+ CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_OFF
+ )
+ )
+ )
+ }
+ val result3A = result.await()
+ assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Status3A.OK)
+ }
+
+ @Test
+ fun testSetTorchDoesNotChangeAeModeIfNotNeeded() = runBlocking {
+ initGraphProcessor()
+
+ graphState3A.update(aeMode = AeMode.OFF)
+
+ val result = controller3A.setTorch(TorchState.ON)
+ assertThat(graphState3A.aeMode!!.value).isEqualTo(CaptureRequest.CONTROL_AE_MODE_OFF)
+ assertThat(graphState3A.flashMode!!.value).isEqualTo(
+ CaptureRequest.FLASH_MODE_TORCH
+ )
+ assertThat(result.isCompleted).isFalse()
+
+ GlobalScope.launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AE_MODE to CaptureResult.CONTROL_AE_MODE_OFF,
+ CaptureResult.FLASH_MODE to CaptureResult.FLASH_MODE_TORCH
+ )
+ )
+ )
+ }
+ val result3A = result.await()
+ assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Status3A.OK)
+ }
+
+ private fun initGraphProcessor() {
+ graphProcessor.attach(requestProcessor)
+ graphProcessor.setRepeating(Request(streams = listOf(StreamId(1))))
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt
index 3c4f330..6309f96 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt
@@ -56,9 +56,7 @@
initGraphProcessor()
val result = controller3A.submit3A(afMode = AfMode.OFF)
- assertThat(graphState3A.readState()[CaptureRequest.CONTROL_AF_MODE]).isNotEqualTo(
- CaptureRequest.CONTROL_AE_MODE_OFF
- )
+ assertThat(graphState3A.afMode?.value).isNotEqualTo(CaptureRequest.CONTROL_AF_MODE_OFF)
assertThat(result.isCompleted).isFalse()
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt
index 53052d7..ed9455b 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt
@@ -85,7 +85,7 @@
// There should be one request to lock AE.
val request1 = requestProcessor.nextEvent().request
- Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK])
+ Truth.assertThat(request1!!.parameters[CaptureRequest.CONTROL_AE_LOCK])
.isEqualTo(false)
GlobalScope.launch {
@@ -146,7 +146,7 @@
// There should be one request to unlock AF.
val request1 = requestProcessor.nextEvent().request
- Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+ Truth.assertThat(request1!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER])
.isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
GlobalScope.launch {
@@ -209,7 +209,7 @@
// There should be one request to lock AWB.
val request1 = requestProcessor.nextEvent().request
- Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AWB_LOCK])
+ Truth.assertThat(request1!!.parameters[CaptureRequest.CONTROL_AWB_LOCK])
.isEqualTo(false)
GlobalScope.launch {
@@ -271,11 +271,11 @@
// There should be one request to unlock AF.
val request1 = requestProcessor.nextEvent().request
- Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+ Truth.assertThat(request1!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER])
.isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
// Then request to unlock AE.
val request2 = requestProcessor.nextEvent().request
- Truth.assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK])
+ Truth.assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK])
.isEqualTo(false)
GlobalScope.launch {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt
index 30a21b5..365e6fb 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt
@@ -57,9 +57,7 @@
initGraphProcessor()
val result = controller3A.update3A(afMode = AfMode.OFF)
- assertThat(graphState3A.readState()[CaptureRequest.CONTROL_AF_MODE]).isEqualTo(
- CaptureRequest.CONTROL_AE_MODE_OFF
- )
+ assertThat(graphState3A.afMode!!.value).isEqualTo(CaptureRequest.CONTROL_AF_MODE_OFF)
assertThat(result.isCompleted).isFalse()
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
index 6c3c06a..a3e5555 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
@@ -17,6 +17,8 @@
package androidx.camera.camera2.pipe.impl
import android.os.Build
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
@@ -45,6 +47,11 @@
private val requestListener2 = FakeRequestListener()
private val request2 = Request(listOf(StreamId(0)), listeners = listOf(requestListener2))
+ private val graphConfig = CameraGraph.Config(
+ camera = CameraId.fromCamera2Id("CameraId-Test"),
+ streams = listOf()
+ )
+
@Test
fun graphProcessorSubmitsRequests() {
// The graph processor uses 'launch' within the coroutine scope to invoke updates on the
@@ -53,6 +60,7 @@
runBlocking(Dispatchers.Default) {
val graphProcessor = GraphProcessorImpl(
FakeThreads.forTests,
+ graphConfig,
this,
arrayListOf(globalListener)
)
@@ -74,6 +82,7 @@
runBlocking(Dispatchers.Default) {
val graphProcessor = GraphProcessorImpl(
FakeThreads.forTests,
+ graphConfig,
this,
arrayListOf(globalListener)
)
@@ -100,6 +109,7 @@
runBlocking(Dispatchers.Default) {
val graphProcessor = GraphProcessorImpl(
FakeThreads.forTests,
+ graphConfig,
this,
arrayListOf(globalListener)
)
@@ -126,6 +136,7 @@
runBlocking(Dispatchers.Default) {
val graphProcessor = GraphProcessorImpl(
FakeThreads.forTests,
+ graphConfig,
this,
arrayListOf(globalListener)
)
@@ -144,6 +155,7 @@
runBlocking(Dispatchers.Default) {
val graphProcessor = GraphProcessorImpl(
FakeThreads.forTests,
+ graphConfig,
this,
arrayListOf(globalListener)
)
@@ -178,6 +190,7 @@
runBlocking(Dispatchers.Default) {
val graphProcessor = GraphProcessorImpl(
FakeThreads.forTests,
+ graphConfig,
this,
arrayListOf(globalListener)
)
@@ -220,6 +233,7 @@
runBlocking(Dispatchers.Default) {
val graphProcessor = GraphProcessorImpl(
FakeThreads.forTests,
+ graphConfig,
this,
arrayListOf(globalListener)
)
@@ -237,6 +251,7 @@
runBlocking(Dispatchers.Default) {
val graphProcessor = GraphProcessorImpl(
FakeThreads.forTests,
+ graphConfig,
this,
arrayListOf(globalListener)
)
@@ -258,6 +273,7 @@
runBlocking(Dispatchers.Default) {
val graphProcessor = GraphProcessorImpl(
FakeThreads.forTests,
+ graphConfig,
this,
arrayListOf(globalListener)
)
@@ -278,6 +294,7 @@
runBlocking(Dispatchers.Default) {
val graphProcessor = GraphProcessorImpl(
FakeThreads.forTests,
+ graphConfig,
this,
arrayListOf(globalListener)
)
@@ -297,6 +314,7 @@
runBlocking(Dispatchers.Default) {
val graphProcessor = GraphProcessorImpl(
FakeThreads.forTests,
+ graphConfig,
this,
arrayListOf(globalListener)
)
@@ -322,6 +340,7 @@
runBlocking(Dispatchers.Default) {
val graphProcessor = GraphProcessorImpl(
FakeThreads.forTests,
+ graphConfig,
this,
arrayListOf(globalListener)
)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt
index 26fcbcf..40fa0a3 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt
@@ -49,7 +49,7 @@
internal interface CameraSessionTestComponent {
fun graphConfig(): CameraGraph.Config
fun sessionFactory(): SessionFactory
- fun streamMap(): StreamMap
+ fun streamMap(): StreamGraphImpl
}
@RunWith(CameraPipeRobolectricTestRunner::class)
@@ -89,13 +89,14 @@
val sessionFactory = component.sessionFactory()
val streamMap = component.streamMap()
- val streamConfig = component.graphConfig().streams.first()
- val stream1 = streamMap.streamConfigMap[streamConfig]!!
+ val cameraStreamConfig = component.graphConfig().streams.first()
+ val stream1 = streamMap[cameraStreamConfig]!!
+ val stream1Output = stream1.outputs.first()
val surfaceTexture = SurfaceTexture(0)
surfaceTexture.setDefaultBufferSize(
- stream1.size.width,
- stream1.size.height
+ stream1Output.size.width,
+ stream1Output.size.height
)
val surface = Surface(surfaceTexture)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamGraphImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamGraphImplTest.kt
new file mode 100644
index 0000000..8391d35
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamGraphImplTest.kt
@@ -0,0 +1,390 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.impl
+
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
+import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+import android.os.Build
+import android.util.Size
+import android.view.Surface
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraStream
+import androidx.camera.camera2.pipe.OutputStream
+import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(CameraPipeRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+internal class StreamGraphImplTest {
+ private val fakeMetadata = FakeCameraMetadata(
+ mapOf(INFO_SUPPORTED_HARDWARE_LEVEL to INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
+ )
+
+ private val camera1 = CameraId("TestCamera-1")
+ private val camera2 = CameraId("TestCamera-2")
+
+ private val streamConfig1 = CameraStream.Config.create(
+ size = Size(100, 100),
+ format = StreamFormat.YUV_420_888
+ )
+ private val streamConfig2 = CameraStream.Config.create(
+ size = Size(123, 321),
+ format = StreamFormat.YUV_420_888,
+ camera = camera1
+ )
+ private val streamConfig3 = CameraStream.Config.create(
+ size = Size(200, 200),
+ format = StreamFormat.YUV_420_888,
+ camera = camera2,
+ outputType = OutputStream.OutputType.SURFACE_TEXTURE
+ )
+ private val sharedOutputConfig = OutputStream.Config.create(
+ size = Size(200, 200),
+ format = StreamFormat.YUV_420_888,
+ camera = camera1
+ )
+ private val sharedStreamConfig1 = CameraStream.Config.create(sharedOutputConfig)
+ private val sharedStreamConfig2 = CameraStream.Config.create(sharedOutputConfig)
+
+ private val graphConfig = CameraGraph.Config(
+ camera = camera1,
+ streams = listOf(
+ streamConfig1,
+ streamConfig2,
+ streamConfig3,
+ sharedStreamConfig1,
+ sharedStreamConfig2
+ ),
+ streamSharingGroups = listOf(listOf(streamConfig1, streamConfig2))
+ )
+
+ @Test
+ fun testPrecomputedTestData() {
+ val streamGraph = StreamGraphImpl(fakeMetadata, graphConfig)
+
+ assertThat(streamGraph.streams).hasSize(5)
+ assertThat(streamGraph.streams).hasSize(5)
+ assertThat(streamGraph.outputConfigs).hasSize(4)
+
+ val stream1 = streamGraph[streamConfig1]!!
+ val outputStream1 = stream1.outputs.single()
+ assertThat(outputStream1.format).isEqualTo(StreamFormat.YUV_420_888)
+ assertThat(outputStream1.size.width).isEqualTo(100)
+ assertThat(outputStream1.size.height).isEqualTo(100)
+
+ val stream2 = streamGraph[streamConfig2]!!
+ val outputStream2 = stream2.outputs.single()
+ assertThat(outputStream2.camera).isEqualTo(graphConfig.camera)
+ assertThat(outputStream2.format).isEqualTo(StreamFormat.YUV_420_888)
+ assertThat(outputStream2.size.width).isEqualTo(123)
+ assertThat(outputStream2.size.height).isEqualTo(321)
+ }
+
+ @Test
+ fun testStreamGraphPopulatesCameraId() {
+ val streamGraph = StreamGraphImpl(fakeMetadata, graphConfig)
+ val stream = streamGraph[streamConfig1]!!
+ assertThat(streamConfig1.outputs.single().camera).isNull()
+ assertThat(stream.outputs.single().camera).isEqualTo(graphConfig.camera)
+ }
+
+ @Test
+ fun testStreamWithMultipleOutputs() {
+
+ val streamConfig = CameraStream.Config.create(
+ listOf(
+ OutputStream.Config.create(
+ Size(800, 600),
+ StreamFormat.YUV_420_888
+ ),
+ OutputStream.Config.create(
+ Size(1600, 1200),
+ StreamFormat.YUV_420_888
+ ),
+ OutputStream.Config.create(
+ Size(800, 600),
+ StreamFormat.YUV_420_888
+ ),
+ )
+ )
+ val config = CameraGraph.Config(
+ camera = CameraId("TestCamera"),
+ streams = listOf(streamConfig),
+ )
+ val streamGraph = StreamGraphImpl(fakeMetadata, config)
+
+ assertThat(streamGraph.streams).hasSize(1)
+ assertThat(streamGraph.streams).hasSize(1)
+ assertThat(streamGraph.outputConfigs).hasSize(3)
+ }
+
+ @Test
+ fun testStreamMapConvertsConfigObjectsToStreamIds() {
+ val streamGraph = StreamGraphImpl(fakeMetadata, graphConfig)
+
+ assertThat(streamGraph[streamConfig1]).isNotNull()
+ assertThat(streamGraph[streamConfig2]).isNotNull()
+ assertThat(streamGraph[streamConfig3]).isNotNull()
+
+ val stream1 = streamGraph[streamConfig1]!!
+ val stream2 = streamGraph[streamConfig2]!!
+ val stream3 = streamGraph[streamConfig3]!!
+
+ assertThat(stream1).isEqualTo(streamGraph[streamConfig1])
+ assertThat(stream2).isEqualTo(streamGraph[streamConfig2])
+ assertThat(stream3).isEqualTo(streamGraph[streamConfig3])
+
+ assertThat(streamConfig1).isNotEqualTo(streamConfig2)
+ assertThat(streamConfig1).isNotEqualTo(streamConfig3)
+ assertThat(streamConfig2).isNotEqualTo(streamConfig3)
+ }
+
+ @Test
+ fun testStreamMapIdsAreNotEqualAcrossMultipleStreamMapInstances() {
+ val streamGraphA = StreamGraphImpl(fakeMetadata, graphConfig)
+ val streamGraphB = StreamGraphImpl(fakeMetadata, graphConfig)
+
+ val stream1A = streamGraphA[streamConfig1]!!
+ val stream1B = streamGraphB[streamConfig1]!!
+
+ assertThat(stream1A).isNotEqualTo(stream1B)
+ assertThat(stream1A.id).isNotEqualTo(stream1B.id)
+ }
+
+ @Test
+ fun testSharedStreamsHaveOneOutputConfig() {
+ val streamGraph = StreamGraphImpl(fakeMetadata, graphConfig)
+ val stream1 = streamGraph[sharedStreamConfig1]!!
+ val stream2 = streamGraph[sharedStreamConfig2]!!
+
+ val outputConfigForStream1 =
+ streamGraph.outputConfigs.filter { it.streams.contains(stream1) }
+ val outputConfigForStream2 =
+ streamGraph.outputConfigs.filter { it.streams.contains(stream2) }
+
+ assertThat(outputConfigForStream1).hasSize(1)
+ assertThat(outputConfigForStream2).hasSize(1)
+ assertThat(outputConfigForStream1.first()).isSameInstanceAs(outputConfigForStream2.first())
+ }
+
+ @Test
+ fun testSharedStreamsHaveDifferentOutputStreams() {
+ val streamGraph = StreamGraphImpl(fakeMetadata, graphConfig)
+ val stream1 = streamGraph[sharedStreamConfig1]!!
+ val stream2 = streamGraph[sharedStreamConfig2]!!
+
+ assertThat(stream1.outputs.first()).isNotEqualTo(stream2.outputs.first())
+ }
+
+ @Test
+ fun testGroupedStreamsHaveSameGroupNumber() {
+ val streamGraph = StreamGraphImpl(fakeMetadata, graphConfig)
+ val stream1 = streamGraph[streamConfig1]!!
+ val stream2 = streamGraph[streamConfig2]!!
+
+ val outputConfigForStream1 =
+ streamGraph.outputConfigs.filter { it.streams.contains(stream1) }
+ val outputConfigForStream2 =
+ streamGraph.outputConfigs.filter { it.streams.contains(stream2) }
+ assertThat(outputConfigForStream1).hasSize(1)
+ assertThat(outputConfigForStream2).hasSize(1)
+
+ val config1 = outputConfigForStream1.first()
+ val config2 = outputConfigForStream2.first()
+ assertThat(config1).isNotEqualTo(config2)
+
+ assertThat(config1.groupNumber).isGreaterThan(-1)
+ assertThat(config2.groupNumber).isGreaterThan(-1)
+ assertThat(config1.groupNumber).isEqualTo(config2.groupNumber)
+ }
+
+ @Test
+ fun outputSurfacesArePassedToListenerImmediately() {
+ val streamMap = StreamGraphImpl(fakeMetadata, graphConfig)
+ val stream1 = streamMap[streamConfig1]!!
+ val stream2 = streamMap[streamConfig2]!!
+ val stream3 = streamMap[streamConfig3]!!
+ val stream4 = streamMap[sharedStreamConfig1]!!
+ val stream5 = streamMap[sharedStreamConfig2]!!
+
+ val fakeSurface1 = Surface(SurfaceTexture(1))
+ val fakeSurface2 = Surface(SurfaceTexture(2))
+ val fakeSurface3 = Surface(SurfaceTexture(3))
+ val fakeSurface4 = Surface(SurfaceTexture(4))
+ val fakeSurface5 = Surface(SurfaceTexture(5))
+
+ streamMap[stream1.id] = fakeSurface1
+ streamMap[stream2.id] = fakeSurface2
+ streamMap[stream3.id] = fakeSurface3
+ streamMap[stream4.id] = fakeSurface4
+ streamMap[stream5.id] = fakeSurface5
+
+ val session = FakeSurfaceListener()
+
+ streamMap.listener = session
+
+ assertThat(session.surfaces).isNotNull()
+ assertThat(session.surfaces?.get(stream1.id)).isEqualTo(fakeSurface1)
+ assertThat(session.surfaces?.get(stream2.id)).isEqualTo(fakeSurface2)
+ assertThat(session.surfaces?.get(stream3.id)).isEqualTo(fakeSurface3)
+ }
+
+ @Test
+ fun outputSurfacesArePassedToListenerWhenAvailable() {
+ val streamMap = StreamGraphImpl(fakeMetadata, graphConfig)
+ val stream1 = streamMap[streamConfig1]!!
+ val stream2 = streamMap[streamConfig2]!!
+ val stream3 = streamMap[streamConfig3]!!
+ val stream4 = streamMap[sharedStreamConfig1]!!
+ val stream5 = streamMap[sharedStreamConfig2]!!
+
+ val fakeSurface1 = Surface(SurfaceTexture(1))
+ val fakeSurface2 = Surface(SurfaceTexture(2))
+ val fakeSurface3 = Surface(SurfaceTexture(3))
+ val fakeSurface4 = Surface(SurfaceTexture(4))
+ val fakeSurface5 = Surface(SurfaceTexture(5))
+
+ val session = FakeSurfaceListener()
+ streamMap.listener = session
+ assertThat(session.surfaces).isNull()
+
+ streamMap[stream1.id] = fakeSurface1
+ streamMap[stream2.id] = fakeSurface2
+ streamMap[stream3.id] = fakeSurface3
+ assertThat(session.surfaces).isNull()
+
+ streamMap[stream4.id] = fakeSurface4
+ streamMap[stream5.id] = fakeSurface5
+
+ assertThat(session.surfaces).isNotNull()
+ assertThat(session.surfaces?.get(stream1.id)).isEqualTo(fakeSurface1)
+ assertThat(session.surfaces?.get(stream2.id)).isEqualTo(fakeSurface2)
+ assertThat(session.surfaces?.get(stream3.id)).isEqualTo(fakeSurface3)
+ assertThat(session.surfaces?.get(stream4.id)).isEqualTo(fakeSurface4)
+ assertThat(session.surfaces?.get(stream5.id)).isEqualTo(fakeSurface5)
+ }
+
+ @Test
+ fun onlyFinalSurfacesAreSentToSession() {
+ val streamMap = StreamGraphImpl(fakeMetadata, graphConfig)
+ val stream1 = streamMap[streamConfig1]!!
+ val stream2 = streamMap[streamConfig2]!!
+ val stream3 = streamMap[streamConfig3]!!
+ val stream4 = streamMap[sharedStreamConfig1]!!
+ val stream5 = streamMap[sharedStreamConfig2]!!
+
+ val fakeSurface1A = Surface(SurfaceTexture(1))
+ val fakeSurface1B = Surface(SurfaceTexture(2))
+ val fakeSurface2 = Surface(SurfaceTexture(3))
+ val fakeSurface3 = Surface(SurfaceTexture(4))
+ val fakeSurface4 = Surface(SurfaceTexture(5))
+ val fakeSurface5 = Surface(SurfaceTexture(6))
+
+ val session = FakeSurfaceListener()
+ streamMap.listener = session
+ streamMap[stream1.id] = fakeSurface1A
+ streamMap[stream1.id] = fakeSurface1B
+ assertThat(session.surfaces).isNull()
+
+ streamMap[stream2.id] = fakeSurface2
+ streamMap[stream3.id] = fakeSurface3
+ streamMap[stream4.id] = fakeSurface4
+ streamMap[stream5.id] = fakeSurface5
+
+ assertThat(session.surfaces).isNotNull()
+ assertThat(session.surfaces?.get(stream1.id)).isEqualTo(fakeSurface1B)
+ assertThat(session.surfaces?.get(stream2.id)).isEqualTo(fakeSurface2)
+ assertThat(session.surfaces?.get(stream3.id)).isEqualTo(fakeSurface3)
+ assertThat(session.surfaces?.get(stream4.id)).isEqualTo(fakeSurface4)
+ assertThat(session.surfaces?.get(stream5.id)).isEqualTo(fakeSurface5)
+ }
+
+ @Test
+ fun settingListenerToNullDoesNotClearSurfaces() {
+ val streamMap = StreamGraphImpl(fakeMetadata, graphConfig)
+ val stream1 = streamMap[streamConfig1]!!
+ val stream2 = streamMap[streamConfig2]!!
+ val stream3 = streamMap[streamConfig3]!!
+
+ val fakeSurface1 = Surface(SurfaceTexture(1))
+ val fakeSurface2 = Surface(SurfaceTexture(2))
+ val fakeSurface3 = Surface(SurfaceTexture(3))
+
+ val session = FakeSurfaceListener()
+ streamMap.listener = session
+ streamMap[stream1.id] = fakeSurface1
+ streamMap.listener = null
+
+ streamMap[stream2.id] = fakeSurface2
+ streamMap[stream3.id] = fakeSurface3
+
+ assertThat(session.surfaces).isNull()
+ }
+
+ @Test
+ fun replacingSessionPassesSurfacesToNewSession() {
+ val streamMap = StreamGraphImpl(fakeMetadata, graphConfig)
+ val stream1 = streamMap[streamConfig1]!!
+ val stream2 = streamMap[streamConfig2]!!
+ val stream3 = streamMap[streamConfig3]!!
+ val stream4 = streamMap[sharedStreamConfig1]!!
+ val stream5 = streamMap[sharedStreamConfig2]!!
+
+ val fakeSurface1 = Surface(SurfaceTexture(1))
+ val fakeSurface2 = Surface(SurfaceTexture(2))
+ val fakeSurface3 = Surface(SurfaceTexture(3))
+ val fakeSurface4 = Surface(SurfaceTexture(4))
+ val fakeSurface5 = Surface(SurfaceTexture(5))
+
+ streamMap[stream1.id] = fakeSurface1
+ streamMap[stream2.id] = fakeSurface2
+ streamMap[stream3.id] = fakeSurface3
+ streamMap[stream4.id] = fakeSurface4
+ streamMap[stream5.id] = fakeSurface5
+
+ val listener1 = FakeSurfaceListener()
+ streamMap.listener = listener1
+
+ val listener2 = FakeSurfaceListener()
+ streamMap.listener = listener2
+
+ assertThat(listener2.surfaces).isNotNull()
+ assertThat(listener2.surfaces?.get(stream1.id)).isEqualTo(fakeSurface1)
+ assertThat(listener2.surfaces?.get(stream2.id)).isEqualTo(fakeSurface2)
+ assertThat(listener2.surfaces?.get(stream3.id)).isEqualTo(fakeSurface3)
+ assertThat(listener2.surfaces?.get(stream4.id)).isEqualTo(fakeSurface4)
+ assertThat(listener2.surfaces?.get(stream5.id)).isEqualTo(fakeSurface5)
+ }
+
+ class FakeSurfaceListener : StreamGraphImpl.SurfaceListener {
+ var surfaces: Map<StreamId, Surface>? = null
+
+ override fun onSurfaceMapUpdated(surfaces: Map<StreamId, Surface>) {
+ this.surfaces = surfaces
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamMapTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamMapTest.kt
deleted file mode 100644
index 68fa61b..0000000
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamMapTest.kt
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.pipe.impl
-
-import android.graphics.SurfaceTexture
-import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
-import android.hardware.camera2.CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
-import android.os.Build
-import android.util.Size
-import android.view.Surface
-import androidx.camera.camera2.pipe.CameraGraph
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.RequestTemplate
-import androidx.camera.camera2.pipe.StreamConfig
-import androidx.camera.camera2.pipe.StreamFormat
-import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.StreamType
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
-import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.annotation.Config
-import org.robolectric.annotation.internal.DoNotInstrument
-
-@RunWith(CameraPipeRobolectricTestRunner::class)
-@DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-internal class StreamMapTest {
- private val fakeMetadata = FakeCameraMetadata(
- mapOf(INFO_SUPPORTED_HARDWARE_LEVEL to INFO_SUPPORTED_HARDWARE_LEVEL_FULL)
- )
-
- private val camera1 = CameraId("TestCamera-1")
- private val camera2 = CameraId("TestCamera-2")
-
- private val streamConfig1 = StreamConfig(
- size = Size(100, 100),
- format = StreamFormat.YUV_420_888,
- camera = camera1,
- type = StreamType.SURFACE,
- deferrable = false
- )
-
- private val streamConfig2 = StreamConfig(
- size = Size(100, 100),
- format = StreamFormat.YUV_420_888,
- camera = camera1,
- type = StreamType.SURFACE,
- deferrable = false
- )
-
- private val streamConfig3 =
- StreamConfig(
- size = Size(200, 200),
- format = StreamFormat.YUV_420_888,
- camera = camera2,
- type = StreamType.SURFACE_TEXTURE,
- deferrable = true
- )
-
- private val graphConfig = CameraGraph.Config(
- camera = CameraId("0"),
- streams = listOf(
- streamConfig1,
- streamConfig2,
- streamConfig3
- ),
- template = RequestTemplate(0)
- )
-
- @Test
- fun testStreamMapConvertsConfigObjectsToStreamIds() {
- val streamMap = StreamMap(fakeMetadata, graphConfig)
-
- assertThat(streamMap.streamConfigMap[streamConfig1]).isNotNull()
- assertThat(streamMap.streamConfigMap[streamConfig2]).isNotNull()
- assertThat(streamMap.streamConfigMap[streamConfig3]).isNotNull()
-
- val stream1 = streamMap.streamConfigMap[streamConfig1]!!
- val stream2 = streamMap.streamConfigMap[streamConfig2]!!
- val stream3 = streamMap.streamConfigMap[streamConfig3]!!
-
- assertThat(stream1).isEqualTo(streamMap.streamConfigMap[streamConfig1])
- assertThat(stream2).isEqualTo(streamMap.streamConfigMap[streamConfig2])
- assertThat(stream3).isEqualTo(streamMap.streamConfigMap[streamConfig3])
-
- assertThat(stream1).isNotEqualTo(stream2)
- assertThat(stream1).isNotEqualTo(stream3)
- assertThat(stream2).isNotEqualTo(stream3)
- }
-
- @Test
- fun testStreamMapIdsAreNotEqualAcrossMultipleStreamMapInstances() {
- val streamMap1 = StreamMap(fakeMetadata, graphConfig)
- val streamMap2 = StreamMap(fakeMetadata, graphConfig)
-
- val stream1FromConfig1 = streamMap1.streamConfigMap[streamConfig1]
- val stream1FromConfig2 = streamMap2.streamConfigMap[streamConfig1]
-
- assertThat(stream1FromConfig1).isNotEqualTo(stream1FromConfig2)
- }
-
- @Test
- fun streamsFromSameConfigAreDifferent() {
- val stream1 = StreamMap.StreamImpl(
- StreamId(1),
- streamConfig1.size,
- streamConfig1.format,
- streamConfig1.camera,
- streamConfig1.type
- )
- val stream2 = StreamMap.StreamImpl(
- StreamId(2),
- streamConfig1.size,
- streamConfig1.format,
- streamConfig1.camera,
- streamConfig1.type
- )
-
- assertThat(stream1).isNotEqualTo(stream2)
- }
-
- @Test
- fun surfacesAreSetOnVirtualCaptureSession() {
- val streamMap = StreamMap(fakeMetadata, graphConfig)
- val stream1 = streamMap.streamConfigMap[streamConfig1]!!
- val stream2 = streamMap.streamConfigMap[streamConfig2]!!
- val stream3 = streamMap.streamConfigMap[streamConfig3]!!
-
- val fakeSurface1 = Surface(SurfaceTexture(1))
- val fakeSurface2 = Surface(SurfaceTexture(2))
- val fakeSurface3 = Surface(SurfaceTexture(3))
-
- streamMap[stream1.id] = fakeSurface1
- streamMap[stream2.id] = fakeSurface2
- streamMap[stream3.id] = fakeSurface3
-
- val session = FakeSurfaceListener()
-
- streamMap.listener = session
-
- assertThat(session.surfaces).isNotNull()
- assertThat(session.surfaces?.get(stream1.id)).isEqualTo(fakeSurface1)
- assertThat(session.surfaces?.get(stream2.id)).isEqualTo(fakeSurface2)
- assertThat(session.surfaces?.get(stream3.id)).isEqualTo(fakeSurface3)
- }
-
- @Test
- fun surfacesAreSetOnceAllSurfacesAreAvailable() {
- val streamMap = StreamMap(fakeMetadata, graphConfig)
- val stream1 = streamMap.streamConfigMap[streamConfig1]!!
- val stream2 = streamMap.streamConfigMap[streamConfig2]!!
- val stream3 = streamMap.streamConfigMap[streamConfig3]!!
-
- val fakeSurface1 = Surface(SurfaceTexture(1))
- val fakeSurface2 = Surface(SurfaceTexture(2))
- val fakeSurface3 = Surface(SurfaceTexture(3))
-
- val session = FakeSurfaceListener()
- streamMap.listener = session
- assertThat(session.surfaces).isNull()
-
- streamMap[stream1.id] = fakeSurface1
- assertThat(session.surfaces).isNull()
-
- streamMap[stream2.id] = fakeSurface2
- streamMap[stream3.id] = fakeSurface3
-
- assertThat(session.surfaces).isNotNull()
- assertThat(session.surfaces?.get(stream1.id)).isEqualTo(fakeSurface1)
- assertThat(session.surfaces?.get(stream2.id)).isEqualTo(fakeSurface2)
- assertThat(session.surfaces?.get(stream3.id)).isEqualTo(fakeSurface3)
- }
-
- @Test
- fun onlyFinalSurfacesAreSentToSession() {
- val streamMap = StreamMap(fakeMetadata, graphConfig)
- val stream1 = streamMap.streamConfigMap[streamConfig1]!!
- val stream2 = streamMap.streamConfigMap[streamConfig2]!!
- val stream3 = streamMap.streamConfigMap[streamConfig3]!!
-
- val fakeSurface1 = Surface(SurfaceTexture(1))
- val fakeSurface2 = Surface(SurfaceTexture(2))
- val fakeSurface3 = Surface(SurfaceTexture(3))
- val fakeSurface4 = Surface(SurfaceTexture(4))
-
- val session = FakeSurfaceListener()
- streamMap.listener = session
- streamMap[stream1.id] = fakeSurface1
- streamMap[stream1.id] = fakeSurface2
- assertThat(session.surfaces).isNull()
-
- streamMap[stream2.id] = fakeSurface3
- streamMap[stream3.id] = fakeSurface4
-
- assertThat(session.surfaces).isNotNull()
- assertThat(session.surfaces?.get(stream1.id)).isEqualTo(fakeSurface2)
- assertThat(session.surfaces?.get(stream2.id)).isEqualTo(fakeSurface3)
- assertThat(session.surfaces?.get(stream3.id)).isEqualTo(fakeSurface4)
- }
-
- @Test
- fun settingSessionToNullDoesNotSetSurfaces() {
- val streamMap = StreamMap(fakeMetadata, graphConfig)
- val stream1 = streamMap.streamConfigMap[streamConfig1]!!
- val stream2 = streamMap.streamConfigMap[streamConfig2]!!
- val stream3 = streamMap.streamConfigMap[streamConfig3]!!
-
- val fakeSurface1 = Surface(SurfaceTexture(1))
- val fakeSurface2 = Surface(SurfaceTexture(2))
- val fakeSurface3 = Surface(SurfaceTexture(3))
-
- val session = FakeSurfaceListener()
- streamMap.listener = session
- streamMap[stream1.id] = fakeSurface1
- streamMap.listener = null
-
- streamMap[stream2.id] = fakeSurface2
- streamMap[stream3.id] = fakeSurface3
-
- assertThat(session.surfaces).isNull()
- }
-
- @Test
- fun replacingSessionPassesSurfacesToNewSession() {
- val streamMap = StreamMap(fakeMetadata, graphConfig)
- val stream1 = streamMap.streamConfigMap[streamConfig1]!!
- val stream2 = streamMap.streamConfigMap[streamConfig2]!!
- val stream3 = streamMap.streamConfigMap[streamConfig3]!!
-
- val fakeSurface1 = Surface(SurfaceTexture(1))
- val fakeSurface2 = Surface(SurfaceTexture(2))
- val fakeSurface3 = Surface(SurfaceTexture(3))
-
- streamMap[stream1.id] = fakeSurface1
- streamMap[stream2.id] = fakeSurface2
- streamMap[stream3.id] = fakeSurface3
-
- val session1 = FakeSurfaceListener()
- streamMap.listener = session1
-
- val session2 = FakeSurfaceListener()
- streamMap.listener = session2
-
- assertThat(session2.surfaces).isNotNull()
- assertThat(session2.surfaces?.get(stream1.id)).isEqualTo(fakeSurface1)
- assertThat(session2.surfaces?.get(stream2.id)).isEqualTo(fakeSurface2)
- assertThat(session2.surfaces?.get(stream3.id)).isEqualTo(fakeSurface3)
- }
-
- class FakeSurfaceListener : SurfaceListener {
- var surfaces: Map<StreamId, Surface>? = null
-
- override fun setSurfaceMap(surfaces: Map<StreamId, Surface>) {
- this.surfaces = surfaces
- }
- }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/VirtualCameraTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/VirtualCameraTest.kt
index 0842c90..60f9c9b 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/VirtualCameraTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/VirtualCameraTest.kt
@@ -18,6 +18,7 @@
import android.os.Build
import android.os.Looper.getMainLooper
+import androidx.camera.camera2.pipe.core.Timestamps
import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
import androidx.camera.camera2.pipe.testing.FakeCameras
import com.google.common.truth.Truth.assertThat
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameras.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameras.kt
index 9260ebc..c11a475 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameras.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameras.kt
@@ -31,10 +31,8 @@
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CameraMetadata
import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.RequestTemplate
-import androidx.camera.camera2.pipe.StreamConfig
+import androidx.camera.camera2.pipe.CameraStream.Config
import androidx.camera.camera2.pipe.StreamFormat
-import androidx.camera.camera2.pipe.StreamType
import androidx.camera.camera2.pipe.impl.CameraGraphModules
import androidx.camera.camera2.pipe.impl.CameraMetadataImpl
import androidx.camera.camera2.pipe.impl.CameraPipeModules
@@ -195,18 +193,16 @@
@Provides
@Singleton
- fun provideFakeGraphConfig() = CameraGraph.Config(
- camera = fakeCamera.cameraId,
- streams = listOf(
- StreamConfig(
- Size(640, 480),
- StreamFormat.YUV_420_888,
- fakeCamera.cameraId,
- StreamType.SURFACE
- )
- ),
- template = RequestTemplate(0)
- )
+ fun provideFakeGraphConfig(): CameraGraph.Config {
+ val stream = Config.create(
+ Size(640, 480),
+ StreamFormat.YUV_420_888
+ )
+ return CameraGraph.Config(
+ camera = fakeCamera.cameraId,
+ streams = listOf(stream),
+ )
+ }
}
@Module(includes = [CameraGraphModules::class])
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
index 6757829..dde3ba50 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
@@ -16,7 +16,6 @@
package androidx.camera.camera2.pipe.testing
-import android.hardware.camera2.CaptureRequest
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.impl.GraphProcessor
import androidx.camera.camera2.pipe.impl.RequestProcessor
@@ -36,6 +35,7 @@
private val _requestQueue = mutableListOf<List<Request>>()
private var processor: RequestProcessor? = null
+ private val defaultParameters = mapOf<Any, Any>()
override fun setRepeating(request: Request) {
repeatingRequest = request
@@ -49,7 +49,7 @@
_requestQueue.add(requests)
}
- override suspend fun submit(parameters: Map<CaptureRequest.Key<*>, Any>): Boolean {
+ override suspend fun submit(parameters: Map<*, Any>): Boolean {
if (closed) {
return false
}
@@ -59,8 +59,8 @@
currProcessor == null || currRepeatingRequest == null -> false
else -> currProcessor.submit(
currRepeatingRequest,
- parameters,
- requireSurfacesForAllStreams = false
+ defaultParameters = defaultParameters,
+ requiredParameters = parameters
)
}
}
@@ -93,6 +93,6 @@
}
override fun invalidate() {
- processor!!.setRepeating(repeatingRequest!!, mapOf(), false)
+ processor!!.setRepeating(repeatingRequest!!, defaultParameters, mapOf<Any, Any>())
}
}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
index 35adddc..006a544 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
@@ -16,11 +16,11 @@
package androidx.camera.camera2.pipe.testing
-import android.hardware.camera2.CaptureRequest
import android.view.Surface
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.impl.GraphState3A
+import androidx.camera.camera2.pipe.impl.RequestProcessorFactory
import androidx.camera.camera2.pipe.impl.RequestProcessor
import androidx.camera.camera2.pipe.impl.TokenLock
import androidx.camera.camera2.pipe.impl.TokenLockImpl
@@ -32,7 +32,7 @@
* Fake implementation of a [RequestProcessor] for tests.
*/
internal class FakeRequestProcessor(private val graphState3A: GraphState3A) :
- RequestProcessor, RequestProcessor.Factory {
+ RequestProcessor, RequestProcessorFactory {
private val eventChannel = Channel<Event>(Channel.UNLIMITED)
val requestQueue: MutableList<FakeRequest> = mutableListOf()
@@ -54,7 +54,10 @@
data class FakeRequest(
val burst: List<Request>,
- val extraRequestParameters: Map<CaptureRequest.Key<*>, Any> = emptyMap(),
+ val defaultParameters: Map<*, Any>,
+ val internalParameters: Map<*, Any>,
+ val requiredParameters: Map<*, Any>,
+ val parameters: Map<*, Any>,
val requireStreams: Boolean = false
)
@@ -69,11 +72,11 @@
override fun submit(
request: Request,
- extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
- requireSurfacesForAllStreams: Boolean
+ defaultParameters: Map<*, Any>,
+ requiredParameters: Map<*, Any>
): Boolean {
val fakeRequest =
- createFakeRequest(listOf(request), extraRequestParameters, requireSurfacesForAllStreams)
+ createFakeRequest(listOf(request), defaultParameters, requiredParameters)
if (rejectRequests || closeInvoked) {
check(eventChannel.offer(Event(request = fakeRequest, rejected = true)))
@@ -88,11 +91,11 @@
override fun submit(
requests: List<Request>,
- extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
- requireSurfacesForAllStreams: Boolean
+ defaultParameters: Map<*, Any>,
+ requiredParameters: Map<*, Any>
): Boolean {
val fakeRequest =
- createFakeRequest(requests, extraRequestParameters, requireSurfacesForAllStreams)
+ createFakeRequest(requests, defaultParameters, requiredParameters)
if (rejectRequests || closeInvoked) {
check(eventChannel.offer(Event(request = fakeRequest, rejected = true)))
return false
@@ -106,11 +109,11 @@
override fun setRepeating(
request: Request,
- extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
- requireSurfacesForAllStreams: Boolean
+ defaultParameters: Map<*, Any>,
+ requiredParameters: Map<*, Any>
): Boolean {
val fakeRequest =
- createFakeRequest(listOf(request), extraRequestParameters, requireSurfacesForAllStreams)
+ createFakeRequest(listOf(request), defaultParameters, requiredParameters)
if (rejectRequests || closeInvoked) {
check(eventChannel.offer(Event(request = fakeRequest, rejected = true)))
return false
@@ -145,13 +148,29 @@
private fun createFakeRequest(
burst: List<Request>,
- extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
- requireStreams: Boolean
+ defaultParameters: Map<*, Any>,
+ requiredParameters: Map<*, Any>
): FakeRequest {
- val parameterMap = mutableMapOf<CaptureRequest.Key<*>, Any>()
- parameterMap.putAll(graphState3A.readState())
- parameterMap.putAll(extraRequestParameters)
- return FakeRequest(burst, parameterMap, requireStreams)
+ val internalParameters = graphState3A.readState()
+ val parameterMap = mutableMapOf<Any, Any>()
+ for ((k, v) in defaultParameters) {
+ if (k != null) {
+ parameterMap[k] = v
+ }
+ }
+ parameterMap.putAll(internalParameters)
+ for ((k, v) in requiredParameters) {
+ if (k != null) {
+ parameterMap[k] = v
+ }
+ }
+ return FakeRequest(
+ burst,
+ defaultParameters,
+ internalParameters,
+ requiredParameters,
+ parameterMap
+ )
}
}
diff --git a/camera/camera-video/build.gradle b/camera/camera-video/build.gradle
index 36ff47c..0463fe0 100644
--- a/camera/camera-video/build.gradle
+++ b/camera/camera-video/build.gradle
@@ -45,6 +45,7 @@
exclude group: "androidx.camera", module: "camera-core"
}
+ androidTestImplementation project(path: ':camera:camera-camera2')
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
androidTestImplementation(ANDROIDX_TEST_CORE)
androidTestImplementation(ANDROIDX_TEST_RUNNER)
diff --git a/camera/camera-video/src/androidTest/AndroidManifest.xml b/camera/camera-video/src/androidTest/AndroidManifest.xml
index 9f27ec7..7f7f316 100644
--- a/camera/camera-video/src/androidTest/AndroidManifest.xml
+++ b/camera/camera-video/src/androidTest/AndroidManifest.xml
@@ -17,4 +17,5 @@
package="androidx.camera.video.test">
<uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="android.permission.CAMERA" />
</manifest>
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt
new file mode 100644
index 0000000..d925e3e
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.encoder
+
+import android.content.Context
+import android.graphics.SurfaceTexture
+import android.media.CamcorderProfile
+import android.media.MediaCodecInfo
+import android.media.MediaFormat
+import android.media.MediaRecorder
+import android.os.Build
+import android.util.Size
+import android.view.Surface
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.Preview
+import androidx.camera.core.Preview.SurfaceProvider
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.SurfaceTextureProvider
+import androidx.camera.testing.SurfaceTextureProvider.SurfaceTextureCallback
+import androidx.core.content.ContextCompat
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import org.mockito.invocation.InvocationOnMock
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class VideoEncoderTest {
+
+ @get: Rule
+ var cameraRule: TestRule = CameraUtil.grantCameraPermissionAndPreTest()
+
+ private var camera: CameraUseCaseAdapter? = null
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val context: Context = ApplicationProvider.getApplicationContext()
+ private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+
+ private lateinit var videoEncoderConfig: VideoEncoderConfig
+ private lateinit var videoEncoder: EncoderImpl
+ private lateinit var videoEncoderCallback: EncoderCallback
+ private lateinit var previewForVideoEncoder: Preview
+ private lateinit var preview: Preview
+ private lateinit var mainExecutor: Executor
+ private lateinit var encoderExecutor: Executor
+
+ @Before
+ fun setUp() {
+ assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+ // Same issue happened in new video encoder in pre-submit test. Bypass this test on
+ // CuttleFish API 29.
+ // TODO(b/168175357): Fix VideoCaptureTest problems on CuttleFish API 29
+ assumeFalse(
+ "Cuttlefish has MediaCodec dequeueInput/Output buffer fails issue. Unable to test.",
+ Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
+ )
+
+ val cameraXConfig: CameraXConfig = Camera2Config.defaultConfig()
+ CameraX.initialize(context, cameraXConfig).get()
+
+ mainExecutor = ContextCompat.getMainExecutor(context)
+ encoderExecutor = CameraXExecutors.ioExecutor()
+
+ previewForVideoEncoder = Preview.Builder().build()
+ // Binding one more preview use case to create a surface texture, this is for testing on
+ // Pixel API 26, it needs a surface texture at least.
+ preview = Preview.Builder().build()
+
+ camera = CameraUtil.createCameraAndAttachUseCase(
+ context,
+ cameraSelector,
+ previewForVideoEncoder,
+ preview
+ )
+
+ initVideoEncoder()
+
+ instrumentation.runOnMainSync {
+ preview.setSurfaceProvider(
+ getSurfaceProvider()
+ )
+ }
+ }
+
+ @After
+ fun tearDown() {
+ // Since the mVideoEncoder is late initialized, check the status before end test.
+ if (this::videoEncoder.isInitialized) {
+ videoEncoder.release()
+ }
+
+ camera?.apply {
+ instrumentation.runOnMainSync {
+ removeUseCases(setOf(previewForVideoEncoder, preview))
+ }
+ }
+
+ // Ensure all cameras are released for the next test
+ CameraX.shutdown()[10, TimeUnit.SECONDS]
+ }
+
+ @Test
+ fun canRestartVideoEncoder() {
+ for (i in 0..3) {
+ clearInvocations(videoEncoderCallback)
+
+ videoEncoder.start()
+ val inOrder = inOrder(videoEncoderCallback)
+ inOrder.verify(videoEncoderCallback, timeout(5000L)).onEncodeStart()
+ inOrder.verify(videoEncoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+
+ videoEncoder.stop()
+
+ inOrder.verify(videoEncoderCallback, timeout(5000L)).onEncodeStop()
+ }
+ }
+
+ @Test
+ fun canPauseResumeVideoEncoder() {
+ videoEncoder.start()
+
+ verify(videoEncoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+
+ videoEncoder.pause()
+
+ // Since there is no exact event to know the encoder is paused, wait for a while until no
+ // callback.
+ verify(videoEncoderCallback, noInvocation(3000L, 10000L)).onEncodedData(any())
+
+ clearInvocations(videoEncoderCallback)
+
+ videoEncoder.start()
+
+ verify(videoEncoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+ }
+
+ @Test
+ fun canPauseStopStartVideoEncoder() {
+ videoEncoder.start()
+
+ verify(videoEncoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+
+ videoEncoder.pause()
+
+ // Since there is no exact event to know the encoder is paused, wait for a while until no
+ // callback.
+ verify(videoEncoderCallback, noInvocation(3000L, 10000L)).onEncodedData(any())
+
+ videoEncoder.stop()
+
+ verify(videoEncoderCallback, timeout(5000L)).onEncodeStop()
+
+ clearInvocations(videoEncoderCallback)
+
+ videoEncoder.start()
+
+ verify(videoEncoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
+ }
+
+ private fun initVideoEncoder() {
+ val cameraId: Int = (camera?.cameraInfo as CameraInfoInternal).cameraId.toInt()
+
+ val profile: CamcorderProfile = when {
+ CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P) -> {
+ CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P)
+ }
+ CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P) -> {
+ CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P)
+ }
+ else -> {
+ CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW)
+ }
+ }
+
+ videoEncoderConfig = VideoEncoderConfig.builder()
+ .setBitrate(profile.videoBitRate)
+ .setColorFormat(MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
+ .setFrameRate(profile.videoFrameRate)
+ .setIFrameInterval(1)
+ .setMimeType(getMimeTypeString(profile.videoCodec))
+ .setResolution(Size(profile.videoFrameWidth, profile.videoFrameHeight))
+ .build()
+
+ // init video encoder
+ videoEncoderCallback = mock(EncoderCallback::class.java)
+ doAnswer { args: InvocationOnMock ->
+ val encodedData: EncodedData = args.getArgument(0)
+ encodedData.close()
+ null
+ }.`when`(videoEncoderCallback).onEncodedData(any())
+
+ videoEncoder = EncoderImpl(
+ encoderExecutor,
+ videoEncoderConfig
+ )
+
+ videoEncoder.setEncoderCallback(videoEncoderCallback, CameraXExecutors.directExecutor())
+
+ (videoEncoder.input as Encoder.SurfaceInput).setOnSurfaceUpdateListener(
+ mainExecutor,
+ { surface: Surface ->
+ previewForVideoEncoder.setSurfaceProvider { request: SurfaceRequest ->
+ request.provideSurface(
+ surface,
+ CameraXExecutors.directExecutor(),
+ {
+ surface.release()
+ }
+ )
+ }
+ }
+ )
+ }
+
+ private fun getSurfaceProvider(): SurfaceProvider? {
+ return SurfaceTextureProvider.createSurfaceTextureProvider(object : SurfaceTextureCallback {
+ override fun onSurfaceTextureReady(surfaceTexture: SurfaceTexture, resolution: Size) {
+ // No-op
+ }
+
+ override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
+ surfaceTexture.release()
+ }
+ })
+ }
+
+ private fun getMimeTypeString(encoder: Int): String {
+ return when (encoder) {
+ MediaRecorder.VideoEncoder.H263 -> MediaFormat.MIMETYPE_VIDEO_H263
+ MediaRecorder.VideoEncoder.H264 -> MediaFormat.MIMETYPE_VIDEO_AVC
+ MediaRecorder.VideoEncoder.HEVC -> MediaFormat.MIMETYPE_VIDEO_HEVC
+ MediaRecorder.VideoEncoder.MPEG_4_SP -> MediaFormat.MIMETYPE_VIDEO_MPEG4
+ MediaRecorder.VideoEncoder.VP8 -> MediaFormat.MIMETYPE_VIDEO_VP8
+ MediaRecorder.VideoEncoder.DEFAULT -> MediaFormat.MIMETYPE_VIDEO_AVC
+ else -> MediaFormat.MIMETYPE_VIDEO_AVC
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java
new file mode 100644
index 0000000..8ea2b97
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirks.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.compat.quirk;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.impl.Quirk;
+import androidx.camera.core.impl.Quirks;
+
+/**
+ * Provider of video capture related quirks, which are used for device or API level specific
+ * workarounds.
+ * <p>Video related quirks that include depending on API level
+ * ({@link android.os.Build.VERSION#SDK_INT}) or specific devices.
+ * <p>Video specific quirks are lazily loaded, i.e. They are loaded the first time they're needed.
+ */
+public class DeviceQuirks {
+ @NonNull
+ private static final Quirks QUIRKS;
+
+ static {
+ QUIRKS = new Quirks(DeviceQuirksLoader.loadQuirks());
+ }
+
+ private DeviceQuirks() {
+ }
+
+ /** Returns all video specific quirks loaded on the current device. */
+ @NonNull
+ public static Quirks getAll() {
+ return QUIRKS;
+ }
+
+ /**
+ * Retrieves a specific video {@link Quirk} instance given its type.
+ *
+ * @param quirkClass The type of video quirk to retrieve.
+ * @return A video {@link Quirk} instance of the provided type, or {@code null} if it isn't
+ * found.
+ */
+ @Nullable
+ public static <T extends Quirk> T get(@NonNull final Class<T> quirkClass) {
+ return QUIRKS.get(quirkClass);
+ }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
new file mode 100644
index 0000000..a394bc6
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.compat.quirk;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Loads all video specific quirks required for the current device.
+ */
+public class DeviceQuirksLoader {
+
+ private DeviceQuirksLoader() {
+ }
+
+ /**
+ * Goes through all defined video related quirks, and returns those that should be loaded
+ * on the current device.
+ */
+ @NonNull
+ static List<Quirk> loadQuirks() {
+ final List<Quirk> quirks = new ArrayList<>();
+
+ // Load all video specific quirks
+ if (ExcludeKeyFrameRateInFindEncoderQuirk.load()) {
+ quirks.add(new ExcludeKeyFrameRateInFindEncoderQuirk());
+ }
+
+ return quirks;
+ }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ExcludeKeyFrameRateInFindEncoderQuirk.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ExcludeKeyFrameRateInFindEncoderQuirk.java
new file mode 100644
index 0000000..ae4c469
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ExcludeKeyFrameRateInFindEncoderQuirk.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.compat.quirk;
+
+import android.media.MediaFormat;
+import android.os.Build;
+
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * Quirk requiring that the frame rate is not set on the MediaFormat during codec selection.
+ * <p>For API 21, before using
+ * {@link android.media.MediaCodecList#findEncoderForFormat(MediaFormat)}, it needs to reset
+ * frame rate config to null. But in the MediaCode.configure() phase on API 21, the MediaFormat
+ * should include frame rate value.
+ *
+ * @see <a href="https://developer.android
+ * .com/reference/android/media/MediaCodec#creation">MediaCodec's creation</a>
+ */
+public class ExcludeKeyFrameRateInFindEncoderQuirk implements Quirk {
+
+ static boolean load() {
+ return Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP;
+ }
+}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTesting.kt b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/package-info.java
similarity index 65%
rename from compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTesting.kt
rename to camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/package-info.java
index d5ffe26..e0f4cbe 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTesting.kt
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/package-info.java
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-package androidx.compose.ui.test
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.camera.video.internal.compat.quirk;
-@RequiresOptIn("This testing API is experimental and is likely to be changed or removed entirely")
-annotation class ExperimentalTesting
-
-@RequiresOptIn(
- "This is internal API for Compose modules that may change frequently and without warning."
-)
-annotation class InternalTestingApi
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
index acd7535..4d39192 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
@@ -28,6 +28,7 @@
import android.annotation.SuppressLint;
import android.media.MediaCodec;
+import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.os.Build;
import android.os.Bundle;
@@ -42,6 +43,7 @@
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.camera.video.internal.workaround.EncoderFinder;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
import androidx.core.util.Preconditions;
@@ -150,6 +152,7 @@
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
InternalState mState;
+ final EncoderFinder mEncoderFinder = new EncoderFinder();
/**
* Creates the encoder with a {@link EncoderConfig}
*
@@ -188,14 +191,8 @@
throw new InvalidConfigException("Unknown encoder config type");
}
- try {
- mMediaCodec = MediaCodec.createEncoderByType(encoderConfig.getMimeType());
- } catch (IOException e) {
- throw new InvalidConfigException(
- "Unsupported mime type: " + encoderConfig.getMimeType(), e);
- }
-
mMediaFormat = encoderConfig.toMediaFormat();
+ mMediaCodec = selectMediaCodecEncoder(mMediaFormat);
try {
reset();
@@ -423,7 +420,7 @@
@ExecutedBy("mEncoderExecutor")
private void updatePauseToMediaCodec(boolean paused) {
Bundle bundle = new Bundle();
- bundle.putBoolean(MediaCodec.PARAMETER_KEY_SUSPEND, paused);
+ bundle.putInt(MediaCodec.PARAMETER_KEY_SUSPEND, paused ? 1 : 0);
mMediaCodec.setParameters(bundle);
}
@@ -563,6 +560,26 @@
}
}
+ @NonNull
+ private MediaCodec selectMediaCodecEncoder(@NonNull MediaFormat mediaFormat)
+ throws InvalidConfigException {
+ MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
+ String encoderName;
+
+ encoderName = mEncoderFinder.findEncoderForFormat(mediaFormat, mediaCodecList);
+
+ MediaCodec codec;
+
+ try {
+ codec = MediaCodec.createByCodecName(encoderName);
+ } catch (IOException | NullPointerException | IllegalArgumentException e) {
+ throw new InvalidConfigException("Encoder cannot created: " + encoderName, e);
+ }
+ Logger.i(TAG, "Selected encoder: " + codec.getName());
+
+ return codec;
+ }
+
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ExecutedBy("mEncoderExecutor")
@NonNull
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/EncoderFinder.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/EncoderFinder.java
new file mode 100644
index 0000000..e510147
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/EncoderFinder.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.workaround;
+
+import android.media.MediaCodecList;
+import android.media.MediaFormat;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.video.internal.compat.quirk.ExcludeKeyFrameRateInFindEncoderQuirk;
+
+/**
+ * Workaround to fix the selection of video encoder by MediaFormat on API 21.
+ *
+ * @see ExcludeKeyFrameRateInFindEncoderQuirk
+ */
+public class EncoderFinder {
+ private final boolean mShouldRemoveKeyFrameRate;
+
+ public EncoderFinder() {
+ final ExcludeKeyFrameRateInFindEncoderQuirk quirk =
+ DeviceQuirks.get(ExcludeKeyFrameRateInFindEncoderQuirk.class);
+
+ mShouldRemoveKeyFrameRate = (quirk != null);
+ }
+
+ /**
+ * Selects an encoder by a given MediaFormat.
+ *
+ * <p>There is one particular case when get a video encoder on API 21.
+ */
+ @Nullable
+ public String findEncoderForFormat(@NonNull MediaFormat mediaFormat,
+ @NonNull MediaCodecList mediaCodecList) {
+ // If the frame rate value is assigned, keep it and restore it later.
+ String encoderName;
+
+ if (mShouldRemoveKeyFrameRate && mediaFormat.containsKey(MediaFormat.KEY_FRAME_RATE)) {
+ int tempFrameRate = mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE);
+ // Reset frame rate value in API 21.
+ mediaFormat.setString(MediaFormat.KEY_FRAME_RATE, null);
+ encoderName = mediaCodecList.findEncoderForFormat(mediaFormat);
+ // Restore the frame rate value.
+ mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, tempFrameRate);
+ } else {
+ encoderName = mediaCodecList.findEncoderForFormat(mediaFormat);
+ }
+
+ return encoderName;
+ }
+
+}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTesting.kt b/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/package-info.java
similarity index 65%
copy from compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTesting.kt
copy to camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/package-info.java
index d5ffe26..29928ac 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTesting.kt
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/package-info.java
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-package androidx.compose.ui.test
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.camera.video.internal.workaround;
-@RequiresOptIn("This testing API is experimental and is likely to be changed or removed entirely")
-annotation class ExperimentalTesting
-
-@RequiresOptIn(
- "This is internal API for Compose modules that may change frequently and without warning."
-)
-annotation class InternalTestingApi
+import androidx.annotation.RestrictTo;
diff --git a/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt b/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt
index 9d18861..c5bb347 100644
--- a/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt
+++ b/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt
@@ -17,9 +17,7 @@
package androidx.camera.integration.camera2.pipe
import android.graphics.ImageFormat
-import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
-import android.hardware.camera2.CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
import android.media.ImageReader
import android.os.Handler
import android.util.Log
@@ -31,9 +29,10 @@
import androidx.camera.camera2.pipe.CameraPipe
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.RequestTemplate
-import androidx.camera.camera2.pipe.StreamConfig
+import androidx.camera.camera2.pipe.CameraStream.Config
+import androidx.camera.camera2.pipe.OutputStream
import androidx.camera.camera2.pipe.StreamFormat
-import androidx.camera.camera2.pipe.StreamType
+import androidx.camera.camera2.pipe.core.Debug
import kotlin.math.absoluteValue
private const val defaultWidth = 960
@@ -42,9 +41,9 @@
private const val defaultAspectRatio = defaultWidth.toDouble() / defaultHeight.toDouble()
class SimpleCamera(
- private val cameraId: CameraId,
- private val cameraMetadata: CameraMetadata,
+ private val cameraConfig: CameraGraph.Config,
private val cameraGraph: CameraGraph,
+ private val cameraMetadata: CameraMetadata,
private val imageReader: ImageReader
) {
companion object {
@@ -77,18 +76,15 @@
Log.i("CXCP-App", "Selected $yuvSize as the YUV output size")
- val yuvStreamConfig = StreamConfig(
- yuvSize,
- StreamFormat.YUV_420_888,
- cameraId,
- StreamType.SURFACE
- )
-
- val viewfinderStreamConfig = StreamConfig(
+ val viewfinderStreamConfig = Config.create(
yuvSize,
StreamFormat.UNKNOWN,
- cameraId,
- StreamType.SURFACE_VIEW
+ outputType = OutputStream.OutputType.SURFACE_VIEW
+ )
+
+ val yuvStreamConfig = Config.create(
+ yuvSize,
+ StreamFormat.YUV_420_888
)
val config = CameraGraph.Config(
@@ -97,15 +93,17 @@
viewfinderStreamConfig,
yuvStreamConfig
),
- listeners = listeners,
- template = RequestTemplate(CameraDevice.TEMPLATE_PREVIEW)
+ defaultListeners = listeners,
+ defaultTemplate = RequestTemplate(CameraDevice.TEMPLATE_PREVIEW)
)
val cameraGraph = cameraPipe.create(config)
val viewfinderStream = cameraGraph.streams[viewfinderStreamConfig]!!
+ val viewfinderOutput = viewfinderStream.outputs.single()
+
viewfinder.configure(
- viewfinderStream.size,
+ viewfinderOutput.size,
object : Viewfinder.SurfaceListener {
override fun onSurfaceChanged(surface: Surface?, size: Size?) {
Log.i("CXCP-App", "Viewfinder surface changed to $surface at $size")
@@ -114,6 +112,16 @@
}
)
val yuvStream = cameraGraph.streams[yuvStreamConfig]!!
+ val yuvOutput = yuvStream.outputs.single()
+
+ val imageReader = ImageReader.newInstance(
+ yuvOutput.size.width,
+ yuvOutput.size.height,
+ yuvOutput.format.value,
+ 10
+ )
+ cameraGraph.setSurface(yuvStream.id, imageReader.surface)
+
cameraGraph.acquireSessionOrNull()!!.use {
it.setRepeating(
Request(
@@ -122,14 +130,12 @@
)
}
- val imageReader = ImageReader.newInstance(
- yuvSize.width,
- yuvSize.height,
- ImageFormat.YUV_420_888,
- 10
+ return SimpleCamera(
+ config,
+ cameraGraph,
+ cameraMetadata,
+ imageReader
)
- cameraGraph.setSurface(yuvStream.id, imageReader.surface)
- return SimpleCamera(cameraId, cameraMetadata, cameraGraph, imageReader)
}
private fun Size.aspectRatio(): Double {
@@ -177,36 +183,6 @@
imageReader.close()
}
- fun cameraInfoString(): String {
- val lensFacing = when (cameraMetadata[CameraCharacteristics.LENS_FACING]) {
- CameraCharacteristics.LENS_FACING_FRONT -> "Front"
- CameraCharacteristics.LENS_FACING_BACK -> "Back"
- CameraCharacteristics.LENS_FACING_EXTERNAL -> "External"
- else -> "Unknown"
- }
-
- val capabilities = cameraMetadata[CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES]
- val cameraType = if (capabilities != null &&
- capabilities.contains(REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)
- ) {
- "Logical"
- } else {
- "Physical"
- }
-
- return StringBuilder().apply {
- append("$cameraGraph (Camera ${cameraId.value})\n")
- append(" Facing: $lensFacing ($cameraType)\n")
- append("Streams:")
- for (stream in cameraGraph.streams) {
- append("\n ")
- append(stream.value.id.toString().padEnd(12, ' '))
- append(stream.value.size.toString().padEnd(12, ' '))
- append(stream.value.format.name.padEnd(16, ' '))
- append(stream.value.type.toString().padEnd(16, ' '))
- }
-
- // TODO: Add static configuration info.
- }.toString()
- }
+ fun cameraInfoString(): String =
+ Debug.formatCameraGraphProperties(cameraMetadata, cameraConfig, cameraGraph)
}
diff --git a/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/SingleValueAnimationTest.kt b/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/SingleValueAnimationTest.kt
index e3cd31a..419c6e8 100644
--- a/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/SingleValueAnimationTest.kt
+++ b/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/SingleValueAnimationTest.kt
@@ -29,7 +29,7 @@
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.Bounds
import androidx.compose.ui.unit.DpOffset
@@ -44,7 +44,7 @@
@RunWith(AndroidJUnit4::class)
@MediumTest
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class SingleValueAnimationTest {
@get:Rule
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonotonicFrameAnimationClock.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonotonicFrameAnimationClock.kt
index 9402702..ffe6bb5 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonotonicFrameAnimationClock.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/MonotonicFrameAnimationClock.kt
@@ -20,6 +20,8 @@
import androidx.compose.runtime.dispatch.MonotonicFrameClock
import androidx.compose.runtime.dispatch.withFrameMillis
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -59,7 +61,10 @@
// until unsubscribe for that observer is called.
@Suppress("DEPRECATION_ERROR")
synchronized(observers) {
- observers[observer] = scope.launch {
+ // Start the coroutine undispatched to make sure it is awaiting
+ // the next frame before the frame clock sends that next frame
+ @OptIn(ExperimentalCoroutinesApi::class)
+ observers[observer] = scope.launch(start = CoroutineStart.UNDISPATCHED) {
val clock = coroutineContext[MonotonicFrameClock] ?: DefaultMonotonicFrameClock
// ManualAnimationClock might send the current time when a subscriber subscribes.
// ManualFrameClock doesn't, because there's no concept of subscription. Fix this
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
index f294292..a808fc3 100644
--- a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
+++ b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
@@ -30,7 +30,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.AmbientDensity
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@@ -47,7 +47,7 @@
@RunWith(AndroidJUnit4::class)
@LargeTest
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class AnimatedVisibilityTest {
@get:Rule
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt
index 8968251..4ba104e 100644
--- a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt
+++ b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimationModifierTest.kt
@@ -31,7 +31,7 @@
import androidx.compose.ui.platform.AmbientDensity
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
@@ -52,7 +52,7 @@
@RunWith(AndroidJUnit4::class)
@LargeTest
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class AnimationModifierTest {
@get:Rule
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/CrossfadeTest.kt b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/CrossfadeTest.kt
index a2a81cb..16e9d50 100644
--- a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/CrossfadeTest.kt
+++ b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/CrossfadeTest.kt
@@ -23,7 +23,7 @@
import androidx.compose.runtime.onDispose
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -35,7 +35,7 @@
@RunWith(AndroidJUnit4::class)
@MediumTest
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class CrossfadeTest {
@get:Rule
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
index db9c668..734012b 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
@@ -96,9 +96,29 @@
TextBox(text = AppState.wndTitle.value)
}
Row {
- Button(color = Color(232, 182, 109), size = IntSize(16, 16))
+ Button(
+ color = Color(210, 210, 210),
+ size = IntSize(16, 16),
+ onClick = {
+ AppManager.focusedWindow?.makeFullscreen()
+ }
+ )
+ Spacer(modifier = Modifier.width(30.dp))
+ Button(
+ color = Color(232, 182, 109),
+ size = IntSize(16, 16),
+ onClick = {
+ AppManager.focusedWindow?.minimize()
+ }
+ )
Spacer(modifier = Modifier.width(3.dp))
- Button(color = Color(150, 232, 150), size = IntSize(16, 16))
+ Button(
+ color = Color(150, 232, 150),
+ size = IntSize(16, 16),
+ onClick = {
+ AppManager.focusedWindow?.maximize()
+ }
+ )
Spacer(modifier = Modifier.width(3.dp))
Button(
onClick = { AppManager.exit() },
@@ -130,6 +150,9 @@
AppManager.focusedWindow?.close()
}
)
+ onDispose {
+ println("Dispose composition")
+ }
}
},
color = Color(26, 198, 188)
@@ -160,15 +183,14 @@
.fillMaxWidth()
) {
ContextMenu()
- Spacer(modifier = Modifier.height(30.dp))
- Spacer(modifier = Modifier.height(60.dp))
+ Spacer(modifier = Modifier.height(100.dp))
Row {
Checkbox(
checked = AppState.undecorated.value,
onCheckedChange = {
AppState.undecorated.value = !AppState.undecorated.value
},
- modifier = Modifier.height(30.dp).padding(start = 20.dp)
+ modifier = Modifier.height(35.dp).padding(start = 20.dp)
)
Spacer(modifier = Modifier.width(5.dp))
TextBox(text = "- undecorated")
@@ -320,7 +342,7 @@
text: String = "",
onClick: () -> Unit = {},
color: Color = Color(10, 162, 232),
- size: IntSize = IntSize(150, 30)
+ size: IntSize = IntSize(200, 35)
) {
val buttonHover = remember { mutableStateOf(false) }
Button(
@@ -371,7 +393,8 @@
Surface(
modifier = Modifier
- .padding(start = 4.dp, top = 2.dp),
+ .padding(start = 4.dp, top = 2.dp)
+ .clickable(onClick = { showMenu.value = true }),
color = Color(255, 255, 255, 40),
shape = RoundedCornerShape(4.dp)
) {
@@ -380,9 +403,8 @@
TextBox(
text = "Selected: ${items[selectedIndex.value]}",
modifier = Modifier
- .height(26.dp)
+ .height(35.dp)
.padding(start = 4.dp, end = 4.dp)
- .clickable(onClick = { showMenu.value = true })
)
},
expanded = showMenu.value,
@@ -405,7 +427,7 @@
@Composable
fun RadioButton(text: String, state: MutableState<Boolean>) {
Box(
- modifier = Modifier.height(30.dp),
+ modifier = Modifier.height(35.dp),
contentAlignment = Alignment.Center
) {
RadioButton(
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/Main.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/Main.kt
index 4269d43..3fc4f935 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/Main.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/Main.kt
@@ -71,6 +71,7 @@
),
Menu(
"About",
+ MenuItems.IsFullscreen,
MenuItems.About,
MenuItems.Update
)
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/MenuItems.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/MenuItems.kt
index 0d41ab0..e43c1d2 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/MenuItems.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/MenuItems.kt
@@ -60,4 +60,12 @@
},
shortcut = KeyStroke(Key.U)
)
+
+ val IsFullscreen = MenuItem(
+ name = "Is fullscreen mode",
+ onClick = {
+ println("Fullscreen mode: ${AppManager.focusedWindow?.isFullscreen}")
+ },
+ shortcut = KeyStroke(Key.F)
+ )
}
\ No newline at end of file
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
index bfb6073..d3826cd 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
@@ -36,6 +36,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.onDispose
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -70,6 +71,9 @@
// setting the content
composePanel.setContent {
ComposeContent()
+ onDispose {
+ println("Dispose composition")
+ }
}
val window = JFrame()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
index b163cd1..8e0ee4ea 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
@@ -42,7 +42,7 @@
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.center
import androidx.compose.ui.test.down
import androidx.compose.ui.test.junit4.createComposeRule
@@ -90,7 +90,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_horizontalScroll() = runBlockingWithManualClock { clock ->
var total = 0f
val controller = ScrollableController(
@@ -146,7 +146,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_verticalScroll() = runBlockingWithManualClock { clock ->
var total = 0f
val controller = ScrollableController(
@@ -202,7 +202,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_startStop_notify() = runBlockingWithManualClock(true) { clock ->
var startTrigger = 0f
var stopTrigger = 0f
@@ -248,7 +248,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_disabledWontCallLambda() = runBlockingWithManualClock(true) { clock ->
val enabled = mutableStateOf(true)
var total = 0f
@@ -294,7 +294,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_velocityProxy() = runBlockingWithManualClock { clock ->
var velocityTriggered = 0f
var total = 0f
@@ -342,7 +342,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_startWithoutSlop_ifFlinging() = runBlockingWithManualClock { clock ->
var total = 0f
val controller = ScrollableController(
@@ -386,7 +386,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_cancel_callsDragStop() = runBlocking {
var total by mutableStateOf(0f)
var dragStopped = 0f
@@ -425,7 +425,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_snappingScrolling() = runBlocking {
var total = 0f
val controller = ScrollableController(
@@ -450,7 +450,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_explicitDisposal() = runBlockingWithManualClock { clock ->
val disposed = mutableStateOf(false)
var total = 0f
@@ -491,7 +491,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_nestedDrag() = runBlockingWithManualClock { clock ->
var innerDrag = 0f
var outerDrag = 0f
@@ -560,7 +560,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_nestedFling() = runBlockingWithManualClock { clock ->
var innerDrag = 0f
var outerDrag = 0f
@@ -631,7 +631,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_nestedScrollAbove_respectsPreConsumption() =
runBlockingWithManualClock { clock ->
var value = 0f
@@ -699,7 +699,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_nestedScrollAbove_proxiesPostCycles() =
runBlockingWithManualClock { clock ->
var value = 0f
@@ -776,7 +776,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_nestedScrollBelow_listensDispatches() =
runBlockingWithManualClock { clock ->
var value = 0f
@@ -848,7 +848,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_interactionState() = runBlocking {
val interactionState = InteractionState()
var total = 0f
@@ -894,7 +894,7 @@
}
@Test
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun scrollable_interactionState_resetWhenDisposed() = runBlocking {
val interactionState = InteractionState()
var emitScrollableBox by mutableStateOf(true)
@@ -985,7 +985,7 @@
}
}
- @ExperimentalTesting
+ @ExperimentalTestApi
private suspend fun advanceClockWhileAwaitersExist(clock: ManualFrameClock) {
rule.awaitIdle()
yield()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ZoomableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ZoomableTest.kt
index d658268..509facc 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ZoomableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ZoomableTest.kt
@@ -27,7 +27,7 @@
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.center
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
@@ -51,7 +51,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class ZoomableTest {
@get:Rule
val rule = createComposeRule()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
index f5b69ad..4bfd688 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldCursorTest.kt
@@ -33,7 +33,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.junit4.createComposeRule
@@ -54,7 +54,7 @@
import java.util.concurrent.TimeUnit
@LargeTest
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class TextFieldCursorTest {
@get:Rule
diff --git a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
index c2f7983..010d2cb 100644
--- a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
+++ b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
@@ -38,7 +38,7 @@
import androidx.compose.ui.platform.DesktopPlatform
import androidx.compose.ui.platform.DesktopPlatformAmbient
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
import androidx.compose.ui.test.down
import androidx.compose.ui.test.junit4.ComposeTestRule
@@ -61,13 +61,13 @@
import org.junit.Test
@Suppress("WrapUnaryOperator")
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class ScrollbarTest {
@get:Rule
val rule = createComposeRule()
// don't inline, surface controls canvas life time
- private val surface = Surface.makeRasterN32Premul(100, 100)
+ private val surface = Surface.makeRasterN32Premul(100, 100)!!
private val canvas = surface.canvas
@Test
@@ -468,4 +468,4 @@
DesktopPlatformAmbient provides DesktopPlatform.MacOS,
content = content
)
-}
\ No newline at end of file
+}
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt
index 7d08b8b..061ca60 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt
@@ -33,7 +33,7 @@
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
import androidx.compose.ui.test.junit4.DisableTransitionsTestRule
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
@@ -128,7 +128,7 @@
val benchmarkRule = BenchmarkRule()
override fun apply(base: Statement, description: Description?): Statement {
- @OptIn(InternalTestingApi::class)
+ @OptIn(InternalTestApi::class)
return RuleChain
.outerRule(DisableTransitionsTestRule())
.around(benchmarkRule)
diff --git a/compose/integration-tests/demos/src/androidTest/java/androidx/compose/integration/demos/test/DemoTest.kt b/compose/integration-tests/demos/src/androidTest/java/androidx/compose/integration/demos/test/DemoTest.kt
index e0f954c..f7fd132 100644
--- a/compose/integration-tests/demos/src/androidTest/java/androidx/compose/integration/demos/test/DemoTest.kt
+++ b/compose/integration-tests/demos/src/androidTest/java/androidx/compose/integration/demos/test/DemoTest.kt
@@ -26,7 +26,7 @@
import androidx.compose.integration.demos.common.DemoCategory
import androidx.compose.integration.demos.common.allDemos
import androidx.compose.integration.demos.common.allLaunchableDemos
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.hasClickAction
@@ -55,7 +55,7 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class DemoTest {
@get:Rule
val rule = createAndroidComposeRule<DemoActivity>()
diff --git a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/CheckboxesInRowsTest.kt b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/CheckboxesInRowsTest.kt
index ef71158..2d4b960 100644
--- a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/CheckboxesInRowsTest.kt
+++ b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/CheckboxesInRowsTest.kt
@@ -20,7 +20,7 @@
import androidx.compose.testutils.assertMeasureSizeIsPositive
import androidx.compose.testutils.assertNoPendingChanges
import androidx.compose.testutils.forGivenTestCase
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.filters.MediumTest
import androidx.ui.integration.test.material.CheckboxesInRowsTestCase
@@ -34,7 +34,7 @@
*/
@MediumTest
@RunWith(Parameterized::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class CheckboxesInRowsTest(private val numberOfCheckboxes: Int) {
companion object {
diff --git a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ObservableThemeTest.kt b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ObservableThemeTest.kt
index f07c9e6..3fbead4 100644
--- a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ObservableThemeTest.kt
+++ b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ObservableThemeTest.kt
@@ -33,7 +33,7 @@
import androidx.compose.testutils.doFramesUntilNoChangesPending
import androidx.compose.testutils.forGivenTestCase
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -49,7 +49,7 @@
*/
@MediumTest
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class ObservableThemeTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
diff --git a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnSharedModelTest.kt b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnSharedModelTest.kt
index 8de27ae..9c3e556 100644
--- a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnSharedModelTest.kt
+++ b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnSharedModelTest.kt
@@ -20,7 +20,7 @@
import androidx.compose.testutils.assertMeasureSizeIsPositive
import androidx.compose.testutils.assertNoPendingChanges
import androidx.compose.testutils.forGivenTestCase
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.filters.MediumTest
import androidx.ui.integration.test.foundation.RectsInColumnSharedModelTestCase
@@ -34,7 +34,7 @@
*/
@MediumTest
@RunWith(Parameterized::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class RectsInColumnSharedModelTest(private val numberOfRectangles: Int) {
companion object {
diff --git a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnTest.kt b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnTest.kt
index d77f08c..f6f4944 100644
--- a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnTest.kt
+++ b/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/RectsInColumnTest.kt
@@ -20,7 +20,7 @@
import androidx.compose.testutils.assertMeasureSizeIsPositive
import androidx.compose.testutils.assertNoPendingChanges
import androidx.compose.testutils.forGivenTestCase
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.filters.MediumTest
import androidx.ui.integration.test.foundation.RectsInColumnTestCase
@@ -34,7 +34,7 @@
*/
@MediumTest
@RunWith(Parameterized::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class RectsInColumnTest(private val numberOfRectangles: Int) {
companion object {
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt
index 4d647fa..737fb0f 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationScreenshotTest.kt
@@ -29,7 +29,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
@@ -44,7 +44,7 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterialApi::class, ExperimentalTesting::class)
+@OptIn(ExperimentalMaterialApi::class, ExperimentalTestApi::class)
class BottomNavigationScreenshotTest {
@get:Rule
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt
index 1f907e6..86bc79e 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt
@@ -21,7 +21,7 @@
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.center
import androidx.compose.ui.test.down
@@ -42,7 +42,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class ButtonScreenshotTest {
@get:Rule
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
index 23b19e1..616c84a 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
@@ -25,7 +25,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.state.ToggleableState
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.center
import androidx.compose.ui.test.down
@@ -46,7 +46,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class CheckboxScreenshotTest {
@get:Rule
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
index 2bd7a2d..b4c3dc2 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
@@ -46,7 +46,7 @@
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.createComposeRule
@@ -68,7 +68,7 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterialApi::class, ExperimentalTesting::class, ExperimentalRippleApi::class)
+@OptIn(ExperimentalMaterialApi::class, ExperimentalTestApi::class, ExperimentalRippleApi::class)
class MaterialRippleThemeTest {
@get:Rule
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
index d048f77..9c678e3 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
@@ -28,7 +28,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.AmbientDensity
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.hasAnyDescendant
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.isPopup
@@ -51,7 +51,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class MenuTest {
@get:Rule
val rule = createComposeRule()
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
index 5514d9d..c59785d 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
@@ -19,7 +19,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.AccessibilityRangeInfo
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertRangeInfoEquals
@@ -36,7 +36,7 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class ProgressIndicatorTest {
private val ExpectedLinearWidth = 240.dp
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
index 747b65f..4ad4c06 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
@@ -25,7 +25,7 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.center
import androidx.compose.ui.test.down
@@ -46,7 +46,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class RadioButtonScreenshotTest {
@get:Rule
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
index 9b05fd8..e4c06ed 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
@@ -29,7 +29,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.AmbientLayoutDirection
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.center
import androidx.compose.ui.test.down
@@ -52,7 +52,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class SwitchScreenshotTest {
@get:Rule
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
index 694bbc6..844ac05 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
@@ -27,7 +27,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
@@ -42,7 +42,7 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterialApi::class, ExperimentalTesting::class)
+@OptIn(ExperimentalMaterialApi::class, ExperimentalTestApi::class)
class TabScreenshotTest {
@get:Rule
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
index b35ec1e..37e62f8 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
@@ -47,7 +47,7 @@
import androidx.compose.ui.node.Ref
import androidx.compose.ui.platform.AmbientTextInputService
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.click
import androidx.compose.ui.test.junit4.createComposeRule
@@ -81,7 +81,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class OutlinedTextFieldTest {
private val ExpectedMinimumTextFieldHeight = 56.dp
private val ExpectedPadding = 16.dp
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
index d35d2bc9..5158ad4 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
@@ -57,7 +57,7 @@
import androidx.compose.ui.platform.AmbientTextInputService
import androidx.compose.ui.platform.AmbientView
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.click
@@ -95,7 +95,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class TextFieldTest {
private val ExpectedMinimumTextFieldHeight = 56.dp
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/ComposeBenchmarkRule.kt b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/ComposeBenchmarkRule.kt
index 26c1511..dda8985 100644
--- a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/ComposeBenchmarkRule.kt
+++ b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/ComposeBenchmarkRule.kt
@@ -23,8 +23,8 @@
import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.benchmark.android.AndroidTestCase
import androidx.compose.testutils.createAndroidComposeBenchmarkRunner
+import androidx.compose.ui.test.InternalTestApi
import androidx.compose.ui.test.junit4.DisableTransitionsTestRule
-import androidx.compose.ui.test.InternalTestingApi
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.Description
@@ -44,7 +44,7 @@
val benchmarkRule = BenchmarkRule()
override fun apply(base: Statement, description: Description?): Statement {
- @OptIn(InternalTestingApi::class)
+ @OptIn(InternalTestApi::class)
return RuleChain
.outerRule(DisableTransitionsTestRule(!enableTransitions))
.around(benchmarkRule)
diff --git a/compose/ui/ui-test-junit4/api/current.txt b/compose/ui/ui-test-junit4/api/current.txt
index f4d018a..a6f3662 100644
--- a/compose/ui/ui-test-junit4/api/current.txt
+++ b/compose/ui/ui-test-junit4/api/current.txt
@@ -2,13 +2,13 @@
package androidx.compose.ui.test.junit4 {
public final class AndroidAnimationClockTestRuleKt {
- method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
}
public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
- method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method @androidx.compose.ui.test.ExperimentalTestApi public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public R getActivityRule();
method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
method public androidx.compose.ui.unit.Density getDensity();
@@ -41,7 +41,7 @@
public final class AndroidSynchronizationKt {
}
- @androidx.compose.ui.test.ExperimentalTesting public interface AnimationClockTestRule extends org.junit.rules.TestRule {
+ @androidx.compose.ui.test.ExperimentalTestApi public interface AnimationClockTestRule extends org.junit.rules.TestRule {
method public default void advanceClock(long milliseconds);
method public androidx.compose.ui.test.TestAnimationClock getClock();
method public default boolean isPaused();
@@ -52,7 +52,7 @@
}
public interface ComposeTestRule extends org.junit.rules.TestRule androidx.compose.ui.test.SemanticsNodeInteractionsProvider {
- method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method @androidx.compose.ui.test.ExperimentalTestApi public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
method public androidx.compose.ui.unit.Density getDensity();
method public long getDisplaySize-YbymL2g();
@@ -67,7 +67,7 @@
property public abstract long displaySize;
}
- @androidx.compose.ui.test.InternalTestingApi public final class DisableTransitionsTestRule implements org.junit.rules.TestRule {
+ @androidx.compose.ui.test.InternalTestApi public final class DisableTransitionsTestRule implements org.junit.rules.TestRule {
ctor public DisableTransitionsTestRule(boolean disableTransitions);
ctor public DisableTransitionsTestRule();
method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
@@ -91,9 +91,9 @@
public final class ComposeIdlingResourceKt {
method @Deprecated public static void registerComposeWithEspresso();
- method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
method @Deprecated public static void unregisterComposeFromEspresso();
- method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
}
public final class ComposeNotIdleException extends java.lang.Throwable {
diff --git a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
index f4d018a..a6f3662 100644
--- a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
@@ -2,13 +2,13 @@
package androidx.compose.ui.test.junit4 {
public final class AndroidAnimationClockTestRuleKt {
- method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
}
public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
- method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method @androidx.compose.ui.test.ExperimentalTestApi public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public R getActivityRule();
method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
method public androidx.compose.ui.unit.Density getDensity();
@@ -41,7 +41,7 @@
public final class AndroidSynchronizationKt {
}
- @androidx.compose.ui.test.ExperimentalTesting public interface AnimationClockTestRule extends org.junit.rules.TestRule {
+ @androidx.compose.ui.test.ExperimentalTestApi public interface AnimationClockTestRule extends org.junit.rules.TestRule {
method public default void advanceClock(long milliseconds);
method public androidx.compose.ui.test.TestAnimationClock getClock();
method public default boolean isPaused();
@@ -52,7 +52,7 @@
}
public interface ComposeTestRule extends org.junit.rules.TestRule androidx.compose.ui.test.SemanticsNodeInteractionsProvider {
- method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method @androidx.compose.ui.test.ExperimentalTestApi public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
method public androidx.compose.ui.unit.Density getDensity();
method public long getDisplaySize-YbymL2g();
@@ -67,7 +67,7 @@
property public abstract long displaySize;
}
- @androidx.compose.ui.test.InternalTestingApi public final class DisableTransitionsTestRule implements org.junit.rules.TestRule {
+ @androidx.compose.ui.test.InternalTestApi public final class DisableTransitionsTestRule implements org.junit.rules.TestRule {
ctor public DisableTransitionsTestRule(boolean disableTransitions);
ctor public DisableTransitionsTestRule();
method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
@@ -91,9 +91,9 @@
public final class ComposeIdlingResourceKt {
method @Deprecated public static void registerComposeWithEspresso();
- method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
method @Deprecated public static void unregisterComposeFromEspresso();
- method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
}
public final class ComposeNotIdleException extends java.lang.Throwable {
diff --git a/compose/ui/ui-test-junit4/api/restricted_current.txt b/compose/ui/ui-test-junit4/api/restricted_current.txt
index f4d018a..a6f3662 100644
--- a/compose/ui/ui-test-junit4/api/restricted_current.txt
+++ b/compose/ui/ui-test-junit4/api/restricted_current.txt
@@ -2,13 +2,13 @@
package androidx.compose.ui.test.junit4 {
public final class AndroidAnimationClockTestRuleKt {
- method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
}
public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
- method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method @androidx.compose.ui.test.ExperimentalTestApi public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public R getActivityRule();
method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
method public androidx.compose.ui.unit.Density getDensity();
@@ -41,7 +41,7 @@
public final class AndroidSynchronizationKt {
}
- @androidx.compose.ui.test.ExperimentalTesting public interface AnimationClockTestRule extends org.junit.rules.TestRule {
+ @androidx.compose.ui.test.ExperimentalTestApi public interface AnimationClockTestRule extends org.junit.rules.TestRule {
method public default void advanceClock(long milliseconds);
method public androidx.compose.ui.test.TestAnimationClock getClock();
method public default boolean isPaused();
@@ -52,7 +52,7 @@
}
public interface ComposeTestRule extends org.junit.rules.TestRule androidx.compose.ui.test.SemanticsNodeInteractionsProvider {
- method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method @androidx.compose.ui.test.ExperimentalTestApi public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
method public androidx.compose.ui.unit.Density getDensity();
method public long getDisplaySize-YbymL2g();
@@ -67,7 +67,7 @@
property public abstract long displaySize;
}
- @androidx.compose.ui.test.InternalTestingApi public final class DisableTransitionsTestRule implements org.junit.rules.TestRule {
+ @androidx.compose.ui.test.InternalTestApi public final class DisableTransitionsTestRule implements org.junit.rules.TestRule {
ctor public DisableTransitionsTestRule(boolean disableTransitions);
ctor public DisableTransitionsTestRule();
method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
@@ -91,9 +91,9 @@
public final class ComposeIdlingResourceKt {
method @Deprecated public static void registerComposeWithEspresso();
- method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
method @Deprecated public static void unregisterComposeFromEspresso();
- method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+ method @Deprecated @androidx.compose.ui.test.ExperimentalTestApi public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
}
public final class ComposeNotIdleException extends java.lang.Throwable {
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt
index cde1ff1..7f6c642 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt
@@ -17,7 +17,7 @@
package androidx.compose.ui.test.junit4
import androidx.compose.ui.test.IdlingResource
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -40,7 +40,7 @@
private var onIdleCalled = false
@OptIn(ExperimentalCoroutinesApi::class)
private val scope = TestCoroutineScope()
- @OptIn(InternalTestingApi::class)
+ @OptIn(InternalTestApi::class)
private val registry = IdlingResourceRegistry(scope).apply {
setOnIdleCallback { onIdleCalled = true }
}
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRuleTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRuleTest.kt
index ec13577..0607fb5 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRuleTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRuleTest.kt
@@ -35,7 +35,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.test.espresso.Espresso.onIdle
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
@@ -45,7 +45,7 @@
import org.junit.Test
@LargeTest
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class MonotonicFrameClockTestRuleTest {
companion object {
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
index 886546d..d2a6f5a 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
@@ -38,7 +38,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.test.espresso.Espresso.onIdle
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
@@ -48,7 +48,7 @@
import org.junit.Test
@LargeTest
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class TestAnimationClockTest {
companion object {
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitingForOnCommitCallbackTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitingForOnCommitCallbackTest.kt
index 644b2b5..65b5d60 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitingForOnCommitCallbackTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/WaitingForOnCommitCallbackTest.kt
@@ -20,7 +20,7 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.onCommit
import androidx.compose.runtime.setValue
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
@@ -125,7 +125,7 @@
fun cascadingOnCommits_suspendedWait_mainDispatcher() =
cascadingOnCommits_suspendedWait(Dispatchers.Main)
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun cascadingOnCommits_suspendedWait(context: CoroutineContext) = runBlocking(context) {
// Collect unique values (markers) at each step during the process and
// at the end verify that they were collected in the right order
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt
index c417e42..69d290d 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt
@@ -18,7 +18,7 @@
import androidx.compose.animation.core.InternalAnimationApi
import androidx.compose.animation.core.rootAnimationClockFactory
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.TestAnimationClock
import androidx.compose.ui.test.junit4.android.AndroidTestAnimationClock
import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
@@ -31,7 +31,7 @@
* the ambient animation clock provided at the root of the composition tree with a
* [TestAnimationClock].
*/
-@ExperimentalTesting
+@ExperimentalTestApi
internal class AndroidAnimationClockTestRule(
private val composeIdlingResource: ComposeIdlingResource
) : AnimationClockTestRule {
@@ -70,7 +70,7 @@
level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("composeTestRule.clockTestRule")
)
-@ExperimentalTesting
+@ExperimentalTestApi
@Suppress("DocumentExceptions")
actual fun createAnimationClockRule(): AnimationClockTestRule =
throw UnsupportedOperationException()
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
index 572d8c2..b2e9557 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
@@ -22,9 +22,9 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Recomposer
import androidx.compose.ui.platform.setContent
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.IdlingResource
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
@@ -85,7 +85,7 @@
fun <A : ComponentActivity> createAndroidComposeRule(
activityClass: Class<A>
): AndroidComposeTestRule<ActivityScenarioRule<A>, A> =
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
createAndroidComposeRule(
activityClass = activityClass,
driveClockByMonotonicFrameClock = false
@@ -97,14 +97,14 @@
* experimental and _will_ be removed in the future. See the other overloads of
* [createAndroidComposeRule] for the recommended way of creating a [ComposeTestRule].
*/
-@ExperimentalTesting
+@ExperimentalTestApi
internal fun createAndroidComposeRule(
driveClockByMonotonicFrameClock: Boolean
): AndroidComposeTestRule<ActivityScenarioRule<ComponentActivity>, ComponentActivity> {
return createAndroidComposeRule(ComponentActivity::class.java, driveClockByMonotonicFrameClock)
}
-@ExperimentalTesting
+@ExperimentalTestApi
private fun <A : ComponentActivity> createAndroidComposeRule(
activityClass: Class<A>,
driveClockByMonotonicFrameClock: Boolean
@@ -125,16 +125,16 @@
* @param activityProvider To resolve the activity from the given test rule. Must be a blocking
* function.
*/
-@OptIn(InternalTestingApi::class)
+@OptIn(InternalTestApi::class)
class AndroidComposeTestRule<R : TestRule, A : ComponentActivity>
-@ExperimentalTesting
+@ExperimentalTestApi
internal constructor(
val activityRule: R,
private val activityProvider: (R) -> A,
driveClockByMonotonicFrameClock: Boolean
) : ComposeTestRule {
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
constructor(
activityRule: R,
activityProvider: (R) -> A
@@ -148,7 +148,7 @@
registerIdlingResource(it)
}
- @ExperimentalTesting
+ @ExperimentalTestApi
override val clockTestRule: AnimationClockTestRule =
if (!driveClockByMonotonicFrameClock) {
AndroidAnimationClockTestRule(composeIdlingResource)
@@ -183,7 +183,7 @@
override fun apply(base: Statement, description: Description): Statement {
@Suppress("NAME_SHADOWING")
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
return RuleChain
.outerRule { base, _ -> composeIdlingResource.getStatementFor(base) }
.around { base, _ -> idlingResourceRegistry.getStatementFor(base) }
@@ -228,7 +228,7 @@
composeIdlingResource.waitForIdle()
}
- @ExperimentalTesting
+ @ExperimentalTestApi
override suspend fun awaitIdle() {
composeIdlingResource.awaitIdle()
}
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
index ded27d1..0c4c387 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
@@ -20,13 +20,13 @@
import androidx.compose.ui.node.Owner
import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.semantics.SemanticsNode
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
import androidx.compose.ui.test.TestOwner
import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
import androidx.compose.ui.text.input.EditOperation
import androidx.compose.ui.text.input.ImeAction
-@OptIn(InternalTestingApi::class)
+@OptIn(InternalTestApi::class)
internal class AndroidTestOwner(
private val composeIdlingResource: ComposeIdlingResource
) : TestOwner {
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/DisableTransitionsTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/DisableTransitionsTestRule.kt
index 13dc642..9048acfc 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/DisableTransitionsTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/DisableTransitionsTestRule.kt
@@ -19,7 +19,7 @@
import androidx.compose.animation.core.InternalAnimationApi
import androidx.compose.animation.transition
import androidx.compose.animation.transitionsEnabled
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
@@ -29,7 +29,7 @@
* can be turned into a no-op by setting [disableTransitions] to `false`, allowing you to put it
* into a rule chain without branching logic.
*/
-@InternalTestingApi
+@InternalTestApi
class DisableTransitionsTestRule(private val disableTransitions: Boolean = false) : TestRule {
override fun apply(base: Statement, description: Description?): Statement {
@@ -58,5 +58,5 @@
)
)
@Suppress("unused")
-@OptIn(InternalTestingApi::class)
+@OptIn(InternalTestApi::class)
typealias DisableTransitions = DisableTransitionsTestRule
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt
index 8aa2f09..3aae3b8 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt
@@ -20,14 +20,14 @@
import androidx.compose.animation.core.InternalAnimationApi
import androidx.compose.animation.core.MonotonicFrameAnimationClock
import androidx.compose.animation.core.rootAnimationClockFactory
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.TestAnimationClock
import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
import kotlinx.coroutines.CoroutineScope
import org.junit.runner.Description
import org.junit.runners.model.Statement
-@ExperimentalTesting
+@ExperimentalTestApi
internal class MonotonicFrameClockTestRule(
private val composeIdlingResource: ComposeIdlingResource
) : AnimationClockTestRule {
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
index 0de743a..b0af5ca 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
@@ -19,7 +19,7 @@
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.compose.ui.platform.ViewRootForTest
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import kotlinx.coroutines.suspendCancellableCoroutine
import org.junit.runners.model.Statement
import java.util.Collections
@@ -203,7 +203,7 @@
}
}
-@ExperimentalTesting
+@ExperimentalTestApi
@OptIn(ExperimentalTime::class)
internal suspend fun AndroidOwnerRegistry.awaitAndroidOwners() {
ensureAndroidOwnerRegistryIsSetUp()
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidTestAnimationClock.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidTestAnimationClock.kt
index be6439b..db46102 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidTestAnimationClock.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidTestAnimationClock.kt
@@ -19,7 +19,7 @@
import android.view.Choreographer
import androidx.compose.animation.core.AnimationClockObserver
import androidx.compose.animation.core.ManualAnimationClock
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.TestAnimationClock
import androidx.compose.ui.test.junit4.runOnUiThread
@@ -36,7 +36,7 @@
* @see advanceClock
* @see resumeClock
*/
-@ExperimentalTesting
+@ExperimentalTestApi
internal class AndroidTestAnimationClock : TestAnimationClock {
/**
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
index df32ab2..c2def80 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
@@ -21,7 +21,7 @@
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.node.Owner
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.IdlingResource
import androidx.compose.ui.test.TestAnimationClock
import androidx.compose.ui.test.junit4.createAndroidComposeRule
@@ -72,7 +72,7 @@
level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("composeIdlingResource.registerTestClock(clock)")
)
-@ExperimentalTesting
+@ExperimentalTestApi
@Suppress("UNUSED_PARAMETER", "DocumentExceptions")
fun registerTestClock(clock: TestAnimationClock): Unit = throw UnsupportedOperationException(
"Global (un)registration of TestAnimationClocks is no longer supported. Register clocks " +
@@ -88,7 +88,7 @@
level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("composeIdlingResource.unregisterTestClock(clock)")
)
-@ExperimentalTesting
+@ExperimentalTestApi
@Suppress("UNUSED_PARAMETER", "DocumentExceptions")
fun unregisterTestClock(clock: TestAnimationClock): Unit = throw UnsupportedOperationException(
"Global (un)registration of TestAnimationClocks is no longer supported. Register clocks " +
@@ -107,7 +107,7 @@
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val androidOwnerRegistry = AndroidOwnerRegistry()
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
private val clocks = mutableSetOf<TestAnimationClock>()
private var hadAnimationClocksIdle = true
@@ -142,21 +142,21 @@
hadNoPendingDraw*/
}
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun registerTestClock(clock: TestAnimationClock) {
synchronized(clocks) {
clocks.add(clock)
}
}
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
fun unregisterTestClock(clock: TestAnimationClock) {
synchronized(clocks) {
clocks.remove(clock)
}
}
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
private fun areAllClocksIdle(): Boolean {
return synchronized(clocks) {
clocks.all { it.isIdle }
@@ -221,7 +221,7 @@
// waitForAndroidOwners() suggests that we are now guaranteed one.
}
- @ExperimentalTesting
+ @ExperimentalTestApi
suspend fun awaitIdle() {
// TODO(b/169038516): when we can query AndroidOwners for measure or layout, remove
// runEspressoOnIdle() and replace it with a suspend fun that loops while the
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt
index 20c2fb6..86422f4 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt
@@ -18,12 +18,12 @@
import androidx.compose.animation.core.AnimationClockObserver
import androidx.compose.animation.core.InternalAnimationApi
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.TestAnimationClock
import org.junit.runner.Description
import org.junit.runners.model.Statement
-@ExperimentalTesting
+@ExperimentalTestApi
internal class DesktopTestAnimationClock : TestAnimationClock {
override val isIdle: Boolean
get() = TODO("Not yet implemented")
@@ -52,7 +52,7 @@
}
}
-@ExperimentalTesting
+@ExperimentalTestApi
internal class DesktopAnimationClockTestRule : AnimationClockTestRule {
override val clock: TestAnimationClock get() = DesktopTestAnimationClock()
@@ -95,7 +95,7 @@
level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("composeTestRule.clockTestRule")
)
-@ExperimentalTesting
+@ExperimentalTestApi
@Suppress("DocumentExceptions")
actual fun createAnimationClockRule(): AnimationClockTestRule =
- throw UnsupportedOperationException()
\ No newline at end of file
+ throw UnsupportedOperationException()
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
index f66b098..706f304 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
@@ -25,9 +25,9 @@
import androidx.compose.ui.platform.DesktopOwners
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.semantics.SemanticsNode
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.IdlingResource
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
@@ -49,7 +49,7 @@
actual fun createComposeRule(): ComposeTestRule = DesktopComposeTestRule()
-@OptIn(InternalTestingApi::class)
+@OptIn(InternalTestApi::class)
class DesktopComposeTestRule : ComposeTestRule {
companion object {
@@ -59,7 +59,7 @@
var owners: DesktopOwners? = null
private var owner: DesktopOwner? = null
- @ExperimentalTesting
+ @ExperimentalTestApi
override val clockTestRule: AnimationClockTestRule = DesktopAnimationClockTestRule()
override val density: Density
@@ -104,7 +104,7 @@
}
}
- @ExperimentalTesting
+ @ExperimentalTestApi
override suspend fun awaitIdle() {
while (!isIdle()) {
runExecutionQueue()
@@ -159,7 +159,7 @@
}
private fun performSetContent(composable: @Composable() () -> Unit) {
- val surface = Surface.makeRasterN32Premul(displaySize.width, displaySize.height)
+ val surface = Surface.makeRasterN32Premul(displaySize.width, displaySize.height)!!
val canvas = surface.canvas
val owners = DesktopOwners(invalidate = {}).also {
owners = it
@@ -203,4 +203,4 @@
return rule.owners!!.list
}
}
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt
index 3fbde5c..4a549fe 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt
@@ -16,11 +16,11 @@
package androidx.compose.ui.test.junit4
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.TestAnimationClock
import org.junit.rules.TestRule
-@ExperimentalTesting
+@ExperimentalTestApi
interface AnimationClockTestRule : TestRule {
/**
* The ambient animation clock that is provided at the root of the composition tree.
@@ -54,5 +54,5 @@
level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("composeTestRule.clockTestRule")
)
-@ExperimentalTesting
+@ExperimentalTestApi
expect fun createAnimationClockRule(): AnimationClockTestRule
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt
index 1190b77..af3bf47 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt
@@ -17,7 +17,7 @@
package androidx.compose.ui.test.junit4
import androidx.compose.runtime.Composable
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.IdlingResource
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.unit.Density
@@ -46,7 +46,7 @@
/**
* A test rule that allows you to control the animation clock
*/
- @OptIn(ExperimentalTesting::class)
+ @OptIn(ExperimentalTestApi::class)
val clockTestRule: AnimationClockTestRule
/**
@@ -79,7 +79,7 @@
* Suspends until compose is idle. Compose is idle if there are no pending compositions, no
* pending changes that could lead to another composition, and no pending draw calls.
*/
- @ExperimentalTesting
+ @ExperimentalTestApi
suspend fun awaitIdle()
/**
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistry.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistry.kt
index 6ffd62e..677a512 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistry.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistry.kt
@@ -18,7 +18,7 @@
import androidx.annotation.VisibleForTesting
import androidx.compose.ui.test.IdlingResource
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -29,12 +29,12 @@
internal class IdlingResourceRegistry
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-@InternalTestingApi
+@InternalTestApi
internal constructor(
private val pollScopeOverride: CoroutineScope?
) : IdlingResource {
// Publicly facing constructor, that doesn't override the poll scope
- @OptIn(InternalTestingApi::class)
+ @OptIn(InternalTestApi::class)
constructor() : this(null)
private val lock = Any()
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index d88b6ad..26826d2 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -24,8 +24,8 @@
}
public final class AnimationClocksKt {
- method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext, androidx.compose.runtime.dispatch.MonotonicFrameClock clock);
- method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext);
+ method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext, androidx.compose.runtime.dispatch.MonotonicFrameClock clock);
+ method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext);
}
public final class AssertionsKt {
@@ -71,13 +71,16 @@
}
public final class CoroutineBuildersKt {
- method @androidx.compose.ui.test.ExperimentalTesting public static <R> void runBlockingWithManualClock(optional boolean compatibleWithManualAnimationClock, kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.animation.core.ManualFrameClock,? super kotlin.coroutines.Continuation<? super R>,?> block);
+ method @androidx.compose.ui.test.ExperimentalTestApi public static <R> void runBlockingWithManualClock(optional boolean compatibleWithManualAnimationClock, kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.animation.core.ManualFrameClock,? super kotlin.coroutines.Continuation<? super R>,?> block);
}
public final class ErrorMessagesKt {
}
- @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTesting {
+ @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTestApi {
+ }
+
+ @Deprecated @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTesting {
}
public final class FiltersKt {
@@ -185,7 +188,10 @@
property public abstract boolean isIdleNow;
}
- @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
+ @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestApi {
+ }
+
+ @Deprecated @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
}
public final class KeyInputHelpersKt {
@@ -270,7 +276,7 @@
public final class SemanticsSelectorKt {
}
- @androidx.compose.ui.test.ExperimentalTesting public interface TestAnimationClock extends androidx.compose.animation.core.AnimationClockObservable {
+ @androidx.compose.ui.test.ExperimentalTestApi public interface TestAnimationClock extends androidx.compose.animation.core.AnimationClockObservable {
method public void advanceClock(long milliseconds);
method public boolean isIdle();
method public boolean isPaused();
@@ -295,7 +301,7 @@
method public static long getFrameDelayMillis(androidx.compose.ui.test.TestMonotonicFrameClock);
}
- @androidx.compose.ui.test.InternalTestingApi public interface TestOwner {
+ @androidx.compose.ui.test.InternalTestApi public interface TestOwner {
method public java.util.Set<androidx.compose.ui.node.Owner> getOwners();
method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
method public void sendImeAction(androidx.compose.ui.semantics.SemanticsNode node, androidx.compose.ui.text.input.ImeAction actionSpecified);
@@ -303,10 +309,10 @@
}
public final class TestOwnerKt {
- method @androidx.compose.ui.test.InternalTestingApi public static androidx.compose.ui.test.TestContext createTestContext(androidx.compose.ui.test.TestOwner owner);
+ method @androidx.compose.ui.test.InternalTestApi public static androidx.compose.ui.test.TestContext createTestContext(androidx.compose.ui.test.TestOwner owner);
}
- @androidx.compose.ui.test.ExperimentalTesting public final class TestUiDispatcher {
+ @androidx.compose.ui.test.ExperimentalTestApi public final class TestUiDispatcher {
method @Deprecated public kotlin.coroutines.CoroutineContext getMain();
property @Deprecated public final kotlin.coroutines.CoroutineContext Main;
field public static final androidx.compose.ui.test.TestUiDispatcher INSTANCE;
diff --git a/compose/ui/ui-test/api/public_plus_experimental_current.txt b/compose/ui/ui-test/api/public_plus_experimental_current.txt
index d88b6ad..26826d2 100644
--- a/compose/ui/ui-test/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test/api/public_plus_experimental_current.txt
@@ -24,8 +24,8 @@
}
public final class AnimationClocksKt {
- method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext, androidx.compose.runtime.dispatch.MonotonicFrameClock clock);
- method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext);
+ method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext, androidx.compose.runtime.dispatch.MonotonicFrameClock clock);
+ method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext);
}
public final class AssertionsKt {
@@ -71,13 +71,16 @@
}
public final class CoroutineBuildersKt {
- method @androidx.compose.ui.test.ExperimentalTesting public static <R> void runBlockingWithManualClock(optional boolean compatibleWithManualAnimationClock, kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.animation.core.ManualFrameClock,? super kotlin.coroutines.Continuation<? super R>,?> block);
+ method @androidx.compose.ui.test.ExperimentalTestApi public static <R> void runBlockingWithManualClock(optional boolean compatibleWithManualAnimationClock, kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.animation.core.ManualFrameClock,? super kotlin.coroutines.Continuation<? super R>,?> block);
}
public final class ErrorMessagesKt {
}
- @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTesting {
+ @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTestApi {
+ }
+
+ @Deprecated @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTesting {
}
public final class FiltersKt {
@@ -185,7 +188,10 @@
property public abstract boolean isIdleNow;
}
- @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
+ @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestApi {
+ }
+
+ @Deprecated @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
}
public final class KeyInputHelpersKt {
@@ -270,7 +276,7 @@
public final class SemanticsSelectorKt {
}
- @androidx.compose.ui.test.ExperimentalTesting public interface TestAnimationClock extends androidx.compose.animation.core.AnimationClockObservable {
+ @androidx.compose.ui.test.ExperimentalTestApi public interface TestAnimationClock extends androidx.compose.animation.core.AnimationClockObservable {
method public void advanceClock(long milliseconds);
method public boolean isIdle();
method public boolean isPaused();
@@ -295,7 +301,7 @@
method public static long getFrameDelayMillis(androidx.compose.ui.test.TestMonotonicFrameClock);
}
- @androidx.compose.ui.test.InternalTestingApi public interface TestOwner {
+ @androidx.compose.ui.test.InternalTestApi public interface TestOwner {
method public java.util.Set<androidx.compose.ui.node.Owner> getOwners();
method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
method public void sendImeAction(androidx.compose.ui.semantics.SemanticsNode node, androidx.compose.ui.text.input.ImeAction actionSpecified);
@@ -303,10 +309,10 @@
}
public final class TestOwnerKt {
- method @androidx.compose.ui.test.InternalTestingApi public static androidx.compose.ui.test.TestContext createTestContext(androidx.compose.ui.test.TestOwner owner);
+ method @androidx.compose.ui.test.InternalTestApi public static androidx.compose.ui.test.TestContext createTestContext(androidx.compose.ui.test.TestOwner owner);
}
- @androidx.compose.ui.test.ExperimentalTesting public final class TestUiDispatcher {
+ @androidx.compose.ui.test.ExperimentalTestApi public final class TestUiDispatcher {
method @Deprecated public kotlin.coroutines.CoroutineContext getMain();
property @Deprecated public final kotlin.coroutines.CoroutineContext Main;
field public static final androidx.compose.ui.test.TestUiDispatcher INSTANCE;
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index d88b6ad..26826d2 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -24,8 +24,8 @@
}
public final class AnimationClocksKt {
- method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext, androidx.compose.runtime.dispatch.MonotonicFrameClock clock);
- method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext);
+ method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext, androidx.compose.runtime.dispatch.MonotonicFrameClock clock);
+ method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext);
}
public final class AssertionsKt {
@@ -71,13 +71,16 @@
}
public final class CoroutineBuildersKt {
- method @androidx.compose.ui.test.ExperimentalTesting public static <R> void runBlockingWithManualClock(optional boolean compatibleWithManualAnimationClock, kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.animation.core.ManualFrameClock,? super kotlin.coroutines.Continuation<? super R>,?> block);
+ method @androidx.compose.ui.test.ExperimentalTestApi public static <R> void runBlockingWithManualClock(optional boolean compatibleWithManualAnimationClock, kotlin.jvm.functions.Function3<? super kotlinx.coroutines.CoroutineScope,? super androidx.compose.animation.core.ManualFrameClock,? super kotlin.coroutines.Continuation<? super R>,?> block);
}
public final class ErrorMessagesKt {
}
- @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTesting {
+ @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTestApi {
+ }
+
+ @Deprecated @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTesting {
}
public final class FiltersKt {
@@ -185,7 +188,10 @@
property public abstract boolean isIdleNow;
}
- @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
+ @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestApi {
+ }
+
+ @Deprecated @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
}
public final class KeyInputHelpersKt {
@@ -270,7 +276,7 @@
public final class SemanticsSelectorKt {
}
- @androidx.compose.ui.test.ExperimentalTesting public interface TestAnimationClock extends androidx.compose.animation.core.AnimationClockObservable {
+ @androidx.compose.ui.test.ExperimentalTestApi public interface TestAnimationClock extends androidx.compose.animation.core.AnimationClockObservable {
method public void advanceClock(long milliseconds);
method public boolean isIdle();
method public boolean isPaused();
@@ -295,7 +301,7 @@
method public static long getFrameDelayMillis(androidx.compose.ui.test.TestMonotonicFrameClock);
}
- @androidx.compose.ui.test.InternalTestingApi public interface TestOwner {
+ @androidx.compose.ui.test.InternalTestApi public interface TestOwner {
method public java.util.Set<androidx.compose.ui.node.Owner> getOwners();
method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
method public void sendImeAction(androidx.compose.ui.semantics.SemanticsNode node, androidx.compose.ui.text.input.ImeAction actionSpecified);
@@ -303,10 +309,10 @@
}
public final class TestOwnerKt {
- method @androidx.compose.ui.test.InternalTestingApi public static androidx.compose.ui.test.TestContext createTestContext(androidx.compose.ui.test.TestOwner owner);
+ method @androidx.compose.ui.test.InternalTestApi public static androidx.compose.ui.test.TestContext createTestContext(androidx.compose.ui.test.TestOwner owner);
}
- @androidx.compose.ui.test.ExperimentalTesting public final class TestUiDispatcher {
+ @androidx.compose.ui.test.ExperimentalTestApi public final class TestUiDispatcher {
method @Deprecated public kotlin.coroutines.CoroutineContext getMain();
property @Deprecated public final kotlin.coroutines.CoroutineContext Main;
field public static final androidx.compose.ui.test.TestUiDispatcher INSTANCE;
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/inputdispatcher/InputDispatcherTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/inputdispatcher/InputDispatcherTest.kt
index 7cb1c84..fd5a3dc 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/inputdispatcher/InputDispatcherTest.kt
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/inputdispatcher/InputDispatcherTest.kt
@@ -19,7 +19,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.test.AndroidInputDispatcher
import androidx.compose.ui.test.InputDispatcher
-import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.InternalTestApi
import androidx.compose.ui.test.createTestContext
import androidx.compose.ui.test.util.InputDispatcherTestRule
import androidx.compose.ui.test.util.MotionEventRecorder
@@ -29,7 +29,7 @@
import org.junit.Rule
import org.junit.rules.TestRule
-@OptIn(InternalTestingApi::class)
+@OptIn(InternalTestApi::class)
open class InputDispatcherTest(eventPeriodOverride: Long? = null) {
@get:Rule
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/CoroutineBuilders.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/CoroutineBuilders.kt
index 4626cb2..63c8a6cf 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/CoroutineBuilders.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/CoroutineBuilders.kt
@@ -72,7 +72,7 @@
* first frame immediately upon subscription. Avoid reliance on this if possible. `false` by
* default.
*/
-@ExperimentalTesting
+@ExperimentalTestApi
fun <R> runBlockingWithManualClock(
compatibleWithManualAnimationClock: Boolean = false,
block: suspend CoroutineScope.(clock: ManualFrameClock) -> R
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
index c680bb8..109ea4a 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
@@ -98,7 +98,7 @@
0f
}
- @OptIn(InternalTestingApi::class)
+ @OptIn(InternalTestApi::class)
testContext.testOwner.runOnUiThread {
scrollableNode.config[SemanticsActions.ScrollBy].action(dx, dy)
}
@@ -182,7 +182,7 @@
}
@Suppress("DEPRECATION")
- @OptIn(InternalTestingApi::class)
+ @OptIn(InternalTestApi::class)
testContext.testOwner.runOnUiThread {
invocation(node.config[key].action)
}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/AnimationClocks.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/AnimationClocks.kt
index d2b0062..d883a9e2 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/AnimationClocks.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/AnimationClocks.kt
@@ -32,7 +32,7 @@
* Use [pauseClock] to switch from automatic ticking to manual ticking, [resumeClock] to switch
* from manual to automatic with; and manually tick the clock with [advanceClock].
*/
-@ExperimentalTesting
+@ExperimentalTestApi
interface TestAnimationClock : AnimationClockObservable {
/**
* Whether the clock is idle or not. An idle clock is one that is not driving animations,
@@ -73,7 +73,7 @@
*
* @see MonotonicFrameAnimationClock
*/
-@ExperimentalTesting
+@ExperimentalTestApi
fun monotonicFrameAnimationClockOf(
coroutineContext: CoroutineContext,
clock: MonotonicFrameClock
@@ -88,7 +88,7 @@
*
* @see MonotonicFrameAnimationClock
*/
-@ExperimentalTesting
+@ExperimentalTestApi
fun monotonicFrameAnimationClockOf(
coroutineContext: CoroutineContext
): MonotonicFrameAnimationClock =
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTestApi.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTestApi.kt
new file mode 100644
index 0000000..c4a2c1c
--- /dev/null
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTestApi.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test
+
+@Deprecated(
+ "Renamed to ExperimentalTestApi for consistency with other experimental module APIs",
+ replaceWith = ReplaceWith("ExperimentalTestApi", "androidx.compose.ui.test.ExperimentalTestApi")
+)
+@RequiresOptIn("This testing API is experimental and is likely to be changed or removed entirely")
+annotation class ExperimentalTesting
+
+@RequiresOptIn("This testing API is experimental and is likely to be changed or removed entirely")
+annotation class ExperimentalTestApi
+
+@Deprecated(
+ "Renamed to InternalTestApi for consistency with other internal module APIs",
+ replaceWith = ReplaceWith("InternalTestApi", "androidx.compose.ui.test.InternalTestApi")
+)
+@RequiresOptIn(
+ "This is internal API for Compose modules that may change frequently and without warning."
+)
+annotation class InternalTestingApi
+
+@RequiresOptIn(
+ "This is internal API for Compose modules that may change frequently and without warning."
+)
+annotation class InternalTestApi
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
index 86871e9..0af8010 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
@@ -27,6 +27,6 @@
val semanticsNode = fetchSemanticsNode("Failed to send key Event (${keyEvent.key})")
val owner = semanticsNode.owner
requireNotNull(owner) { "Failed to find owner" }
- @OptIn(InternalTestingApi::class)
+ @OptIn(InternalTestApi::class)
return testContext.testOwner.runOnUiThread { owner.sendKeyEvent(keyEvent) }
}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
index 9624c69..8c17354 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
@@ -27,7 +27,7 @@
*
* This is typically implemented by entities like test rule.
*/
-@InternalTestingApi
+@InternalTestApi
interface TestOwner {
/**
@@ -67,17 +67,17 @@
* Can crash in case it hits time out. This is not supposed to be handled as it
* surfaces only in incorrect tests.
*/
-@OptIn(InternalTestingApi::class)
+@OptIn(InternalTestApi::class)
internal fun TestOwner.getAllSemanticsNodes(useUnmergedTree: Boolean): List<SemanticsNode> {
return getOwners().flatMap { it.semanticsOwner.getAllSemanticsNodes(useUnmergedTree) }
}
-@InternalTestingApi
+@InternalTestApi
fun createTestContext(owner: TestOwner): TestContext {
return TestContext(owner)
}
-@OptIn(InternalTestingApi::class)
+@OptIn(InternalTestApi::class)
class TestContext internal constructor(internal val testOwner: TestOwner) {
/**
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestUiDispatcher.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestUiDispatcher.kt
index 2c0c3cd..e6a9d34 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestUiDispatcher.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestUiDispatcher.kt
@@ -19,7 +19,7 @@
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext
-@ExperimentalTesting
+@ExperimentalTestApi
object TestUiDispatcher {
/**
* The dispatcher to use if you need to dispatch coroutines on the main thread in tests.
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TextActions.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TextActions.kt
index e12bc35..65062b3 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TextActions.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TextActions.kt
@@ -110,7 +110,7 @@
)
}
- @OptIn(InternalTestingApi::class)
+ @OptIn(InternalTestApi::class)
testContext.testOwner.sendImeAction(node, actionSpecified)
}
@@ -119,6 +119,6 @@
val node = fetchSemanticsNode(errorOnFail)
assert(hasSetTextAction()) { errorOnFail }
- @OptIn(InternalTestingApi::class)
+ @OptIn(InternalTestApi::class)
testContext.testOwner.sendTextInputCommand(node, command)
}
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt
index 3c47455..1f05602 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt
+++ b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt
@@ -32,7 +32,7 @@
val density: Density = Density(1f, 1f),
var desktopPlatform: DesktopPlatform = DesktopPlatform.Linux
) {
- val surface = Surface.makeRasterN32Premul(width, height)
+ val surface = Surface.makeRasterN32Premul(width, height)!!
val canvas = surface.canvas
val owners = DesktopOwners(invalidate = {})
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt
index cd34bb7..2f06ec2 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt
@@ -19,6 +19,7 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.window.MenuBar
import java.awt.image.BufferedImage
+import javax.swing.JFrame
/**
* AppFrame is an abstract class that represents a window.
@@ -121,6 +122,54 @@
abstract fun removeMenuBar()
/**
+ * Switches the window to fullscreen mode if the window is resizable. If the window is in
+ * fullscreen mode [minimize] and [maximize] methods are ignored.
+ */
+ abstract fun makeFullscreen()
+
+ /**
+ * Returns true if the window is in fullscreen state, false otherwise.
+ */
+ abstract val isFullscreen: Boolean
+ get
+
+ /**
+ * Minimizes the window to the taskbar.
+ */
+ abstract fun minimize()
+
+ /**
+ * Returns true if the window is minimized, false otherwise.
+ */
+ val isMinimized: Boolean
+ get() = window.extendedState == JFrame.ICONIFIED
+
+ /**
+ * Maximizes the window to fill all available screen space.
+ */
+ abstract fun maximize()
+
+ /**
+ * Returns true if the window is maximized, false otherwise.
+ */
+ val isMaximized: Boolean
+ get() = window.extendedState == JFrame.MAXIMIZED_BOTH
+
+ /**
+ * Restores the previous state and size of the window after
+ * maximizing/minimizing/fullscreen mode.
+ */
+ abstract fun restore()
+
+ /**
+ * Sets the ability to resize the window. True - the window can be resized,
+ * false - the window cannot be resized. If the window is in fullscreen mode
+ * setter of this property is ignored. If this property is true the [makeFullscreen()]
+ * method is ignored.
+ */
+ abstract var resizable: Boolean
+
+ /**
* Sets the new position of the window on the screen.
*
* @param x the new x-coordinate of the window.
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
index a4a323b..36b80cd 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
@@ -32,6 +32,7 @@
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import java.awt.image.BufferedImage
+import javax.swing.JFrame
import javax.swing.JMenuBar
import javax.swing.SwingUtilities
import javax.swing.WindowConstants
@@ -52,6 +53,8 @@
* @param menuBar Window menu bar. The menu bar can be displayed inside a window (Windows,
* Linux) or at the top of the screen (Mac OS).
* @param undecorated Removes the native window border if set to true. The default value is false.
+ * @param resizable Makes the window resizable if is set to true and unresizable if is set to
+ * false. The default value is true.
* @param events Allows to describe events of the window.
* Supported events: onOpen, onClose, onMinimize, onMaximize, onRestore, onFocusGet, onFocusLost,
* onResize, onRelocate.
@@ -65,6 +68,7 @@
icon: BufferedImage? = null,
menuBar: MenuBar? = null,
undecorated: Boolean = false,
+ resizable: Boolean = true,
events: WindowEvents = WindowEvents(),
onDismissRequest: (() -> Unit)? = null,
content: @Composable () -> Unit = emptyContent()
@@ -77,6 +81,7 @@
icon = icon,
menuBar = menuBar,
undecorated = undecorated,
+ resizable = resizable,
events = events,
onDismissRequest = onDismissRequest
).show {
@@ -121,7 +126,10 @@
})
addWindowFocusListener(object : WindowAdapter() {
override fun windowGainedFocus(event: WindowEvent) {
- window.setJMenuBar(parent.menuBar?.menuBar)
+ // Dialogs should not receive a common application menu bar
+ if (invoker == null) {
+ window.setJMenuBar(parent.menuBar?.menuBar)
+ }
events.invokeOnFocusGet()
}
override fun windowLostFocus(event: WindowEvent) {
@@ -158,6 +166,7 @@
icon: BufferedImage? = null,
menuBar: MenuBar? = null,
undecorated: Boolean = false,
+ resizable: Boolean = true,
events: WindowEvents = WindowEvents(),
onDismissRequest: (() -> Unit)? = null
) : this(
@@ -168,6 +177,7 @@
icon = icon,
menuBar = menuBar,
undecorated = undecorated,
+ resizable = resizable,
events = events,
onDismissRequest = onDismissRequest
) {
@@ -188,6 +198,8 @@
* @param menuBar Window menu bar. The menu bar can be displayed inside a window (Windows,
* Linux) or at the top of the screen (Mac OS).
* @param undecorated Removes the native window border if set to true. The default value is false.
+ * @param resizable Makes the window resizable if is set to true and unresizable if is set to
+ * false. The default value is true.
* @param events Allows to describe events of the window.
* Supported events: onOpen, onClose, onMinimize, onMaximize, onRestore, onFocusGet, onFocusLost,
* onResize, onRelocate.
@@ -201,6 +213,7 @@
icon: BufferedImage? = null,
menuBar: MenuBar? = null,
undecorated: Boolean = false,
+ resizable: Boolean = true,
events: WindowEvents = WindowEvents(),
onDismissRequest: (() -> Unit)? = null
) {
@@ -209,6 +222,7 @@
setTitle(title)
setIcon(icon)
setSize(size.width, size.height)
+ this.resizable = resizable
if (centered) {
setWindowCentered()
} else {
@@ -282,6 +296,72 @@
}
/**
+ * Returns true if the window is in fullscreen mode, false otherwise.
+ */
+ override val isFullscreen: Boolean
+ get() = window.layer.wrapped.fullscreen
+
+ /**
+ * Switches the window to fullscreen mode if the window is resizable. If the window is in
+ * fullscreen mode [minimize] and [maximize] methods are ignored.
+ */
+ override fun makeFullscreen() {
+ if (!isFullscreen && resizable) {
+ window.layer.wrapped.fullscreen = true
+ }
+ }
+
+ /**
+ * Minimizes the window to the taskbar. If the window is in fullscreen mode this method
+ * is ignored.
+ */
+ override fun minimize() {
+ if (!isFullscreen) {
+ window.setExtendedState(JFrame.ICONIFIED)
+ }
+ }
+
+ /**
+ * Maximizes the window to fill all available screen space. If the window is in fullscreen mode
+ * this method is ignored.
+ */
+ override fun maximize() {
+ if (!isFullscreen) {
+ window.setExtendedState(JFrame.MAXIMIZED_BOTH)
+ }
+ }
+
+ /**
+ * Restores the previous state and size of the window after
+ * maximizing/minimizing/fullscreen mode.
+ */
+ override fun restore() {
+ if (isFullscreen) {
+ window.layer.wrapped.fullscreen = false
+ }
+ window.setExtendedState(JFrame.NORMAL)
+ }
+
+ private var _resizable: Boolean = true
+
+ /**
+ * Sets the ability to resize the window. True - the window can be resized,
+ * false - the window cannot be resized. If the window is in fullscreen mode
+ * setter of this property is ignored. If this property is true the [makeFullscreen()]
+ * method is ignored.
+ */
+ override var resizable: Boolean
+ get() {
+ return window.isResizable()
+ }
+ set(value) {
+ if (!isFullscreen) {
+ _resizable = value
+ window.setResizable(value)
+ }
+ }
+
+ /**
* Sets the new size of the window.
*
* @param width the new width of the window.
@@ -384,11 +464,11 @@
window.apply {
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
setFocusableWindowState(true)
- setResizable(true)
setEnabled(true)
toFront()
requestFocus()
}
+ resizable = _resizable
disconnectPair()
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt
index 66110a8..062c2a9 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt
@@ -50,6 +50,7 @@
internal class ComposeLayer {
+ private var composition: Composition? = null
private val events = AWTDebounceEventQueue()
var owners: DesktopOwners? = null
@@ -275,6 +276,7 @@
}
fun dispose() {
+ composition?.dispose()
events.cancel()
check(!isDisposed)
frameDispatcher.cancel()
@@ -298,7 +300,7 @@
invalidate: () -> Unit = this::needRedrawLayer,
parentComposition: CompositionReference? = null,
content: @Composable () -> Unit
- ): Composition {
+ ) {
check(owners == null) {
"Cannot setContent twice."
}
@@ -306,7 +308,7 @@
val desktopOwner = DesktopOwner(desktopOwners, density)
owners = desktopOwners
- val composition = desktopOwner.setContent(parent = parentComposition, content = content)
+ composition = desktopOwner.setContent(parent = parentComposition, content = content)
onDensityChanged(desktopOwner::density::set)
@@ -314,7 +316,5 @@
is AppFrame -> parent.onDispose = desktopOwner::dispose
is ComposePanel -> parent.onDispose = desktopOwner::dispose
}
-
- return composition
}
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt
index 6915ec41..57d1f10 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt
@@ -114,6 +114,8 @@
}
override fun paint(g: Graphics?) {
+ super.paint(g)
+ layer?.reinit()
needRedrawLayer()
}
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt
index 4b5c6b7..677caf8 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt
@@ -16,7 +16,6 @@
package androidx.compose.desktop
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Composition
import androidx.compose.runtime.CompositionReference
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
@@ -47,14 +46,12 @@
* scheduling of composition updates.
* If null then default root composition will be used.
* @param content Composable content of the ComposeWindow.
- *
- * @return Composition of the content.
*/
fun setContent(
parentComposition: CompositionReference? = null,
content: @Composable () -> Unit
- ): Composition {
- return layer.setContent(
+ ) {
+ layer.setContent(
parent = parent,
invalidate = this::needRedrawLayer,
parentComposition = parentComposition,
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopDialog.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopDialog.kt
index 9b5cc81..f9a249d 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopDialog.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopDialog.kt
@@ -43,6 +43,8 @@
* @param menuBar Window menu bar. The menu bar can be displayed inside a window (Windows,
* Linux) or at the top of the screen (Mac OS).
* @param undecorated Removes the native window border if set to true. The default value is false.
+ * @param resizable Makes the window resizable if is set to true and unresizable if is set to
+ * false. The default value is true.
* @param events Allows to describe events of the window.
* Supported events: onOpen, onClose, onMinimize, onMaximize, onRestore, onFocusGet, onFocusLost,
* onResize, onRelocate.
@@ -56,6 +58,7 @@
val icon: BufferedImage? = null,
val menuBar: MenuBar? = null,
val undecorated: Boolean = false,
+ val resizable: Boolean = true,
val events: WindowEvents = WindowEvents()
) : DialogProperties
@@ -86,6 +89,7 @@
icon = desktopProperties.icon,
menuBar = desktopProperties.menuBar,
undecorated = desktopProperties.undecorated,
+ resizable = desktopProperties.resizable,
events = desktopProperties.events,
onDismissRequest = onDismissRequest
)
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
index 6c3c4b2..9f966fa 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
@@ -55,7 +55,7 @@
nanoTime = { currentTimeMillis * 1_000_000 }
)
- val surface: Surface = Surface.makeRasterN32Premul(width, height)
+ val surface: Surface = Surface.makeRasterN32Premul(width, height)!!
val canvas: Canvas = surface.canvas
val owners = DesktopOwners(
invalidate = frameDispatcher::scheduleFrame
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt
index 307fc91..b0c68fe 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DesktopPopupTest.kt
@@ -23,14 +23,14 @@
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticAmbientOf
import androidx.compose.ui.platform.AmbientDensity
-import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.Density
import com.google.common.truth.Truth
import org.junit.Rule
import org.junit.Test
-@OptIn(ExperimentalTesting::class)
+@OptIn(ExperimentalTestApi::class)
class DesktopPopupTest {
@get:Rule
val rule = createComposeRule()
@@ -92,4 +92,4 @@
rule.waitForIdle()
Truth.assertThat(densityInsidePopup).isEqualTo(3f)
}
-}
\ No newline at end of file
+}
diff --git a/datastore/datastore-core/api/current.txt b/datastore/datastore-core/api/current.txt
index 6d4f585..d0bb6a8 100644
--- a/datastore/datastore-core/api/current.txt
+++ b/datastore/datastore-core/api/current.txt
@@ -5,10 +5,6 @@
ctor public CorruptionException(String message, Throwable? cause);
}
- public interface CorruptionHandler<T> {
- method public suspend Object? handleCorruption(androidx.datastore.core.CorruptionException ex, kotlin.coroutines.Continuation<? super T> p);
- }
-
public interface DataMigration<T> {
method public suspend Object? cleanUp(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public suspend Object? migrate(T? currentData, kotlin.coroutines.Continuation<? super T> p);
@@ -40,7 +36,7 @@
package androidx.datastore.core.handlers {
- public final class ReplaceFileCorruptionHandler<T> implements androidx.datastore.core.CorruptionHandler<T> {
+ public final class ReplaceFileCorruptionHandler<T> {
ctor public ReplaceFileCorruptionHandler(kotlin.jvm.functions.Function1<? super androidx.datastore.core.CorruptionException,? extends T> produceNewData);
method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public suspend Object? handleCorruption(androidx.datastore.core.CorruptionException ex, kotlin.coroutines.Continuation<? super T> p) throws java.io.IOException;
}
diff --git a/datastore/datastore-core/api/public_plus_experimental_current.txt b/datastore/datastore-core/api/public_plus_experimental_current.txt
index 6d4f585..d0bb6a8 100644
--- a/datastore/datastore-core/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-core/api/public_plus_experimental_current.txt
@@ -5,10 +5,6 @@
ctor public CorruptionException(String message, Throwable? cause);
}
- public interface CorruptionHandler<T> {
- method public suspend Object? handleCorruption(androidx.datastore.core.CorruptionException ex, kotlin.coroutines.Continuation<? super T> p);
- }
-
public interface DataMigration<T> {
method public suspend Object? cleanUp(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public suspend Object? migrate(T? currentData, kotlin.coroutines.Continuation<? super T> p);
@@ -40,7 +36,7 @@
package androidx.datastore.core.handlers {
- public final class ReplaceFileCorruptionHandler<T> implements androidx.datastore.core.CorruptionHandler<T> {
+ public final class ReplaceFileCorruptionHandler<T> {
ctor public ReplaceFileCorruptionHandler(kotlin.jvm.functions.Function1<? super androidx.datastore.core.CorruptionException,? extends T> produceNewData);
method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public suspend Object? handleCorruption(androidx.datastore.core.CorruptionException ex, kotlin.coroutines.Continuation<? super T> p) throws java.io.IOException;
}
diff --git a/datastore/datastore-core/api/restricted_current.txt b/datastore/datastore-core/api/restricted_current.txt
index 6d4f585..d0bb6a8 100644
--- a/datastore/datastore-core/api/restricted_current.txt
+++ b/datastore/datastore-core/api/restricted_current.txt
@@ -5,10 +5,6 @@
ctor public CorruptionException(String message, Throwable? cause);
}
- public interface CorruptionHandler<T> {
- method public suspend Object? handleCorruption(androidx.datastore.core.CorruptionException ex, kotlin.coroutines.Continuation<? super T> p);
- }
-
public interface DataMigration<T> {
method public suspend Object? cleanUp(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public suspend Object? migrate(T? currentData, kotlin.coroutines.Continuation<? super T> p);
@@ -40,7 +36,7 @@
package androidx.datastore.core.handlers {
- public final class ReplaceFileCorruptionHandler<T> implements androidx.datastore.core.CorruptionHandler<T> {
+ public final class ReplaceFileCorruptionHandler<T> {
ctor public ReplaceFileCorruptionHandler(kotlin.jvm.functions.Function1<? super androidx.datastore.core.CorruptionException,? extends T> produceNewData);
method @kotlin.jvm.Throws(exceptionClasses=IOException::class) public suspend Object? handleCorruption(androidx.datastore.core.CorruptionException ex, kotlin.coroutines.Continuation<? super T> p) throws java.io.IOException;
}
diff --git a/datastore/datastore-core/src/main/java/androidx/datastore/core/CorruptionHandler.kt b/datastore/datastore-core/src/main/java/androidx/datastore/core/CorruptionHandler.kt
index 5a04a5d..0ce09b6 100644
--- a/datastore/datastore-core/src/main/java/androidx/datastore/core/CorruptionHandler.kt
+++ b/datastore/datastore-core/src/main/java/androidx/datastore/core/CorruptionHandler.kt
@@ -20,7 +20,7 @@
* CorruptionHandlers allow recovery from corruption that prevents reading data from the file (as
* indicated by a CorruptionException).
*/
-public interface CorruptionHandler<T> {
+internal interface CorruptionHandler<T> {
/**
* This function will be called by DataStore when it encounters corruption. If the
* implementation of this function throws an exception, it will be propagated to the original
diff --git a/datastore/datastore-preferences-rxjava2/api/current.txt b/datastore/datastore-preferences-rxjava2/api/current.txt
new file mode 100644
index 0000000..8235e24
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/api/current.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.datastore.preferences.rxjava2 {
+
+ public final class RxPreferenceDataStoreBuilder {
+ ctor public RxPreferenceDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile);
+ ctor public RxPreferenceDataStoreBuilder(android.content.Context context, String name);
+ method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder addDataMigration(androidx.datastore.core.DataMigration<androidx.datastore.preferences.core.Preferences> dataMigration);
+ method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder addRxDataMigration(androidx.datastore.rxjava2.RxDataMigration<androidx.datastore.preferences.core.Preferences> rxDataMigration);
+ method public androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences> build();
+ method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.core.Preferences> corruptionHandler);
+ method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder setIoScheduler(io.reactivex.Scheduler ioScheduler);
+ }
+
+}
+
diff --git a/datastore/datastore-preferences-rxjava2/api/public_plus_experimental_current.txt b/datastore/datastore-preferences-rxjava2/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..8235e24
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/api/public_plus_experimental_current.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.datastore.preferences.rxjava2 {
+
+ public final class RxPreferenceDataStoreBuilder {
+ ctor public RxPreferenceDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile);
+ ctor public RxPreferenceDataStoreBuilder(android.content.Context context, String name);
+ method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder addDataMigration(androidx.datastore.core.DataMigration<androidx.datastore.preferences.core.Preferences> dataMigration);
+ method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder addRxDataMigration(androidx.datastore.rxjava2.RxDataMigration<androidx.datastore.preferences.core.Preferences> rxDataMigration);
+ method public androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences> build();
+ method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.core.Preferences> corruptionHandler);
+ method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder setIoScheduler(io.reactivex.Scheduler ioScheduler);
+ }
+
+}
+
diff --git a/datastore/datastore-preferences-rxjava2/api/res-current.txt b/datastore/datastore-preferences-rxjava2/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/api/res-current.txt
diff --git a/datastore/datastore-preferences-rxjava2/api/restricted_current.txt b/datastore/datastore-preferences-rxjava2/api/restricted_current.txt
new file mode 100644
index 0000000..8235e24
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/api/restricted_current.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.datastore.preferences.rxjava2 {
+
+ public final class RxPreferenceDataStoreBuilder {
+ ctor public RxPreferenceDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile);
+ ctor public RxPreferenceDataStoreBuilder(android.content.Context context, String name);
+ method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder addDataMigration(androidx.datastore.core.DataMigration<androidx.datastore.preferences.core.Preferences> dataMigration);
+ method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder addRxDataMigration(androidx.datastore.rxjava2.RxDataMigration<androidx.datastore.preferences.core.Preferences> rxDataMigration);
+ method public androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences> build();
+ method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.core.Preferences> corruptionHandler);
+ method public androidx.datastore.preferences.rxjava2.RxPreferenceDataStoreBuilder setIoScheduler(io.reactivex.Scheduler ioScheduler);
+ }
+
+}
+
diff --git a/datastore/datastore-preferences-rxjava2/build.gradle b/datastore/datastore-preferences-rxjava2/build.gradle
new file mode 100644
index 0000000..9782a40
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/build.gradle
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.AndroidXExtension
+import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+android {
+ sourceSets {
+ test.java.srcDirs += 'src/test-common/java'
+ androidTest.java.srcDirs += 'src/test-common/java'
+ }
+}
+
+dependencies {
+ api(KOTLIN_STDLIB)
+ api(KOTLIN_COROUTINES_CORE)
+ api("androidx.annotation:annotation:1.1.0")
+ api(RX_JAVA)
+
+ api(project(":datastore:datastore"))
+ api(project(":datastore:datastore-rxjava2"))
+ api(project(":datastore:datastore-preferences"))
+
+ implementation(KOTLIN_COROUTINES_RX2)
+
+ testImplementation(JUNIT)
+ testImplementation(KOTLIN_COROUTINES_TEST)
+ testImplementation(TRUTH)
+ testImplementation(project(":internal-testutils-truth"))
+
+ androidTestImplementation(project(":datastore:datastore-core"))
+ androidTestImplementation(project(":datastore:datastore"))
+ androidTestImplementation(JUNIT)
+ androidTestImplementation(project(":internal-testutils-truth"))
+ androidTestImplementation(ANDROIDX_TEST_RUNNER)
+ androidTestImplementation(ANDROIDX_TEST_CORE)
+}
+
+androidx {
+ name = "Android DataStore Core RxJava2 Wrappers"
+ publish = Publish.SNAPSHOT_AND_RELEASE
+ mavenGroup = LibraryGroups.DATASTORE
+ inceptionYear = "2020"
+ description = "Android DataStore Core - contains wrappers for using DataStore using RxJava2"
+ legacyDisableKotlinStrictApiMode = true
+}
diff --git a/datastore/datastore-preferences-rxjava2/src/androidTest/java/androidx/datastore/preferences/rxjava2/RxPreferencesDataStoreBuilderTest.java b/datastore/datastore-preferences-rxjava2/src/androidTest/java/androidx/datastore/preferences/rxjava2/RxPreferencesDataStoreBuilderTest.java
new file mode 100644
index 0000000..ba1ae8f
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/src/androidTest/java/androidx/datastore/preferences/rxjava2/RxPreferencesDataStoreBuilderTest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.preferences.rxjava2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.datastore.core.DataStore;
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler;
+import androidx.datastore.preferences.core.MutablePreferences;
+import androidx.datastore.preferences.core.Preferences;
+import androidx.datastore.preferences.core.PreferencesFactory;
+import androidx.datastore.preferences.core.PreferencesKeys;
+import androidx.datastore.rxjava2.RxDataMigration;
+import androidx.datastore.rxjava2.RxDataStore;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.FileOutputStream;
+
+import io.reactivex.Completable;
+import io.reactivex.Single;
+
+public class RxPreferencesDataStoreBuilderTest {
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private static final Preferences.Key<Integer> INTEGER_KEY =
+ PreferencesKeys.intKey("int_key");
+
+ private static Single<Preferences> incrementInteger(Preferences preferencesIn) {
+ MutablePreferences prefs = preferencesIn.toMutablePreferences();
+ Integer currentInt = prefs.get(INTEGER_KEY);
+ prefs.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1);
+ return Single.just(prefs);
+ }
+
+ @Test
+ public void testConstructWithProduceFile() throws Exception {
+ File file = tempFolder.newFile("temp.preferences_pb");
+
+ DataStore<Preferences> dataStore =
+ new RxPreferenceDataStoreBuilder(() -> file).build();
+
+ Single<Preferences> incrementInt = RxDataStore.updateDataAsync(dataStore,
+ RxPreferencesDataStoreBuilderTest::incrementInteger);
+ assertThat(incrementInt.blockingGet().get(INTEGER_KEY)).isEqualTo(1);
+
+ // Construct it again and confirm that the data is still there:
+ dataStore = new RxPreferenceDataStoreBuilder(() -> file).build();
+
+ assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY))
+ .isEqualTo(1);
+ }
+
+
+ @Test
+ public void testConstructWithContextAndName() throws Exception {
+
+ Context context = ApplicationProvider.getApplicationContext();
+ String name = "my_data_store";
+
+ File prefsFile = new File(context.getFilesDir().getPath()
+ + "/datastore/" + name + ".preferences_pb");
+ if (prefsFile.exists()) {
+ prefsFile.delete();
+ }
+
+ DataStore<Preferences> dataStore =
+ new RxPreferenceDataStoreBuilder(context, name).build();
+
+ Single<Preferences> set1 = RxDataStore.updateDataAsync(dataStore,
+ RxPreferencesDataStoreBuilderTest::incrementInteger);
+ assertThat(set1.blockingGet().get(INTEGER_KEY)).isEqualTo(1);
+
+ // Construct it again and confirm that the data is still there:
+ dataStore = new RxPreferenceDataStoreBuilder(context, name).build();
+ assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY)).isEqualTo(1);
+
+ // Construct it again with the expected file path and confirm that the data is there:
+ dataStore =
+ new RxPreferenceDataStoreBuilder(
+ () ->
+ new File(context.getFilesDir().getPath()
+ + "/datastore/" + name + ".preferences_pb")
+ ).build();
+
+ assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY)).isEqualTo(1);
+ }
+
+ @Test
+ public void testMigrationsAreInstalledAndRun() throws Exception {
+ RxDataMigration<Preferences> plusOneMigration = new RxDataMigration<Preferences>() {
+ @NonNull
+ @Override
+ public Single<Boolean> shouldMigrate(@NonNull Preferences currentData) {
+ return Single.just(true);
+ }
+
+ @NonNull
+ @Override
+ public Single<Preferences> migrate(@NonNull Preferences currentData) {
+ return incrementInteger(currentData);
+ }
+
+ @NonNull
+ @Override
+ public Completable cleanUp() {
+ return Completable.complete();
+ }
+ };
+
+ DataStore<Preferences> dataStore =
+ new RxPreferenceDataStoreBuilder(() ->
+ tempFolder.newFile("temp.preferences_pb"))
+ .addRxDataMigration(plusOneMigration)
+ .build();
+
+ assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY))
+ .isEqualTo(1);
+ }
+
+
+ @Test
+ public void testCorruptionHandlerIsUser() throws Exception {
+
+ File file = tempFolder.newFile("temp.preferences_pb");
+
+ try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
+ fileOutputStream.write(0); // will cause corruption exception
+ }
+
+ ReplaceFileCorruptionHandler<Preferences> replaceFileCorruptionHandler =
+ new ReplaceFileCorruptionHandler<Preferences>(exception -> {
+ MutablePreferences mutablePreferences =
+ PreferencesFactory.createMutable();
+ mutablePreferences.set(INTEGER_KEY, 99);
+ return (Preferences) mutablePreferences;
+ });
+
+
+ DataStore<Preferences> dataStore =
+ new RxPreferenceDataStoreBuilder(() -> file)
+ .setCorruptionHandler(replaceFileCorruptionHandler)
+ .build();
+
+ assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY))
+ .isEqualTo(99);
+ }
+}
diff --git a/datastore/datastore-preferences-rxjava2/src/main/AndroidManifest.xml b/datastore/datastore-preferences-rxjava2/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a3c8250
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.datastore.preferences.rxjava2">
+
+</manifest>
diff --git a/datastore/datastore-preferences-rxjava2/src/main/java/androidx/datastore/preferences/rxjava2/RxPreferenceDataStoreBuilder.kt b/datastore/datastore-preferences-rxjava2/src/main/java/androidx/datastore/preferences/rxjava2/RxPreferenceDataStoreBuilder.kt
new file mode 100644
index 0000000..2075576
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava2/src/main/java/androidx/datastore/preferences/rxjava2/RxPreferenceDataStoreBuilder.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.preferences.rxjava2
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.datastore.core.DataMigration
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.createDataStore
+import androidx.datastore.rxjava2.RxDataMigration
+import io.reactivex.Scheduler
+import io.reactivex.schedulers.Schedulers
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.rx2.asCoroutineDispatcher
+import kotlinx.coroutines.rx2.await
+import java.io.File
+import java.util.concurrent.Callable
+
+/**
+ * RxSharedPreferencesMigrationBuilder class for a DataStore that works on a single process.
+ */
+@SuppressLint("TopLevelBuilder")
+public class RxPreferenceDataStoreBuilder {
+
+ // Either produceFile or context & name must be set, but not both.
+ private var produceFile: Callable<File>? = null
+
+ private var context: Context? = null
+ private var name: String? = null
+
+ // Optional
+ private var ioScheduler: Scheduler = Schedulers.io()
+ private var corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null
+ private val dataMigrations: MutableList<DataMigration<Preferences>> = mutableListOf()
+
+ /**
+ * Create a RxPreferenceDataStoreBuilder with the callable which returns the File that
+ * DataStore acts on. The user is responsible for ensuring that there is never more than one
+ * DataStore acting on a file at a time.
+ *
+ * @param produceFile Function which returns the file that the new DataStore will act on. The
+ * function must return the same path every time. No two instances of DataStore should act on
+ * the same file at the same time.
+ */
+ public constructor(produceFile: Callable<File>) {
+ this.produceFile = produceFile
+ }
+
+ /**
+ * Create a RxPreferenceDataStoreBuilder with the Context and name from which to derive the
+ * DataStore file. The file is generated by See [Context.createDataStore] for more info. The
+ * user is responsible for ensuring that there is never more than one DataStore acting on a
+ * file at a time.
+ *
+ * @param context the context from which we retrieve files directory.
+ * @param name the filename relative to Context.filesDir that DataStore acts on. The File is
+ * obtained by calling File(this.filesDir, "datastore/$name.preferences_pb"). No two instances
+ * of DataStore should act on the same file at the same time.
+ */
+ public constructor(context: Context, name: String) {
+ this.context = context
+ this.name = name
+ }
+
+ /**
+ * Set the Scheduler on which to perform IO and transform operations. This is converted into
+ * a CoroutineDispatcher before being added to DataStore.
+ *
+ * This parameter is optional and defaults to Schedulers.io().
+ *
+ * @param ioScheduler the scheduler on which IO and transform operations are run
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun setIoScheduler(ioScheduler: Scheduler): RxPreferenceDataStoreBuilder =
+ apply { this.ioScheduler = ioScheduler }
+
+ /**
+ * Sets the corruption handler to install into the DataStore.
+ *
+ * This parameter is optional and defaults to no corruption handler.
+ *
+ * @param corruptionHandler the ReplaceFileCorruptionHandler to install
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun setCorruptionHandler(corruptionHandler: ReplaceFileCorruptionHandler<Preferences>):
+ RxPreferenceDataStoreBuilder = apply { this.corruptionHandler = corruptionHandler }
+
+ /**
+ * Add an RxDataMigration to the DataStore. Migrations are run in the order they are added.
+ *
+ * @param rxDataMigration the migration to add.
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun addRxDataMigration(rxDataMigration: RxDataMigration<Preferences>):
+ RxPreferenceDataStoreBuilder = apply {
+ this.dataMigrations.add(DataMigrationFromRxDataMigration(rxDataMigration))
+ }
+
+ /**
+ * Add a DataMigration to the Datastore. Migrations are run in the order they are added.
+ *
+ * @param dataMigration the migration to add
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun addDataMigration(dataMigration: DataMigration<Preferences>):
+ RxPreferenceDataStoreBuilder = apply {
+ this.dataMigrations.add(dataMigration)
+ }
+
+ /**
+ * Build the DataStore.
+ *
+ * @throws IllegalStateException if serializer is not set or if neither produceFile not
+ * context and name are set.
+ * @return the DataStore with the provided parameters
+ */
+ public fun build(): DataStore<Preferences> {
+ val scope = CoroutineScope(ioScheduler.asCoroutineDispatcher())
+
+ val produceFile: Callable<File>? = this.produceFile
+ val context: Context? = this.context
+ val name: String? = this.name
+
+ return if (produceFile != null) {
+ PreferenceDataStoreFactory.create(
+ produceFile = { produceFile.call() },
+ scope = CoroutineScope(
+ ioScheduler.asCoroutineDispatcher()
+ ),
+ corruptionHandler = corruptionHandler,
+ migrations = dataMigrations
+ )
+ } else if (context != null && name != null) {
+ return context.createDataStore(
+ name = name,
+ scope = scope,
+ corruptionHandler = corruptionHandler,
+ migrations = dataMigrations
+ )
+ } else {
+ error("Either produceFile or context and name must be set. This should never happen.")
+ }
+ }
+}
+
+internal class DataMigrationFromRxDataMigration<T>(private val migration: RxDataMigration<T>) :
+ DataMigration<T> {
+ override suspend fun shouldMigrate(currentData: T): Boolean {
+ return migration.shouldMigrate(currentData).await()
+ }
+
+ override suspend fun migrate(currentData: T): T {
+ return migration.migrate(currentData).await()
+ }
+
+ override suspend fun cleanUp() {
+ migration.cleanUp().await()
+ }
+}
diff --git a/datastore/datastore-preferences-rxjava3/api/current.txt b/datastore/datastore-preferences-rxjava3/api/current.txt
new file mode 100644
index 0000000..88fe233
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/api/current.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.datastore.preferences.rxjava3 {
+
+ public final class RxPreferenceDataStoreBuilder {
+ ctor public RxPreferenceDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile);
+ ctor public RxPreferenceDataStoreBuilder(android.content.Context context, String name);
+ method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder addDataMigration(androidx.datastore.core.DataMigration<androidx.datastore.preferences.core.Preferences> dataMigration);
+ method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder addRxDataMigration(androidx.datastore.rxjava3.RxDataMigration<androidx.datastore.preferences.core.Preferences> rxDataMigration);
+ method public androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences> build();
+ method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.core.Preferences> corruptionHandler);
+ method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder setIoScheduler(io.reactivex.rxjava3.core.Scheduler ioScheduler);
+ }
+
+}
+
diff --git a/datastore/datastore-preferences-rxjava3/api/public_plus_experimental_current.txt b/datastore/datastore-preferences-rxjava3/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..88fe233
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/api/public_plus_experimental_current.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.datastore.preferences.rxjava3 {
+
+ public final class RxPreferenceDataStoreBuilder {
+ ctor public RxPreferenceDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile);
+ ctor public RxPreferenceDataStoreBuilder(android.content.Context context, String name);
+ method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder addDataMigration(androidx.datastore.core.DataMigration<androidx.datastore.preferences.core.Preferences> dataMigration);
+ method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder addRxDataMigration(androidx.datastore.rxjava3.RxDataMigration<androidx.datastore.preferences.core.Preferences> rxDataMigration);
+ method public androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences> build();
+ method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.core.Preferences> corruptionHandler);
+ method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder setIoScheduler(io.reactivex.rxjava3.core.Scheduler ioScheduler);
+ }
+
+}
+
diff --git a/datastore/datastore-preferences-rxjava3/api/res-current.txt b/datastore/datastore-preferences-rxjava3/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/api/res-current.txt
diff --git a/datastore/datastore-preferences-rxjava3/api/restricted_current.txt b/datastore/datastore-preferences-rxjava3/api/restricted_current.txt
new file mode 100644
index 0000000..88fe233
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/api/restricted_current.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.datastore.preferences.rxjava3 {
+
+ public final class RxPreferenceDataStoreBuilder {
+ ctor public RxPreferenceDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile);
+ ctor public RxPreferenceDataStoreBuilder(android.content.Context context, String name);
+ method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder addDataMigration(androidx.datastore.core.DataMigration<androidx.datastore.preferences.core.Preferences> dataMigration);
+ method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder addRxDataMigration(androidx.datastore.rxjava3.RxDataMigration<androidx.datastore.preferences.core.Preferences> rxDataMigration);
+ method public androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences> build();
+ method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<androidx.datastore.preferences.core.Preferences> corruptionHandler);
+ method public androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder setIoScheduler(io.reactivex.rxjava3.core.Scheduler ioScheduler);
+ }
+
+}
+
diff --git a/datastore/datastore-preferences-rxjava3/build.gradle b/datastore/datastore-preferences-rxjava3/build.gradle
new file mode 100644
index 0000000..44af2c9
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/build.gradle
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.AndroidXExtension
+import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+android {
+ sourceSets {
+ test.java.srcDirs += 'src/test-common/java'
+ androidTest.java.srcDirs += 'src/test-common/java'
+ }
+}
+
+dependencies {
+ api(KOTLIN_STDLIB)
+ api(KOTLIN_COROUTINES_CORE)
+ api("androidx.annotation:annotation:1.1.0")
+ api(RX_JAVA3)
+
+ api(project(":datastore:datastore"))
+ api(project(":datastore:datastore-rxjava3"))
+ api(project(":datastore:datastore-preferences"))
+
+ implementation(KOTLIN_COROUTINES_RX3)
+
+ testImplementation(JUNIT)
+ testImplementation(KOTLIN_COROUTINES_TEST)
+ testImplementation(TRUTH)
+ testImplementation(project(":internal-testutils-truth"))
+
+ androidTestImplementation(project(":datastore:datastore-core"))
+ androidTestImplementation(project(":datastore:datastore"))
+ androidTestImplementation(JUNIT)
+ androidTestImplementation(project(":internal-testutils-truth"))
+ androidTestImplementation(ANDROIDX_TEST_RUNNER)
+ androidTestImplementation(ANDROIDX_TEST_CORE)
+}
+
+androidx {
+ name = "Android DataStore Core RxJava2 Wrappers"
+ publish = Publish.SNAPSHOT_AND_RELEASE
+ mavenGroup = LibraryGroups.DATASTORE
+ inceptionYear = "2020"
+ description = "Android DataStore Core - contains wrappers for using DataStore using RxJava2"
+ legacyDisableKotlinStrictApiMode = true
+}
diff --git a/datastore/datastore-preferences-rxjava3/src/androidTest/java/androidx/datastore/preferences/rxjava3/RxPreferencesDataStoreBuilderTest.java b/datastore/datastore-preferences-rxjava3/src/androidTest/java/androidx/datastore/preferences/rxjava3/RxPreferencesDataStoreBuilderTest.java
new file mode 100644
index 0000000..f72a779
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/src/androidTest/java/androidx/datastore/preferences/rxjava3/RxPreferencesDataStoreBuilderTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.preferences.rxjava3;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.datastore.core.DataStore;
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler;
+import androidx.datastore.preferences.core.MutablePreferences;
+import androidx.datastore.preferences.core.Preferences;
+import androidx.datastore.preferences.core.PreferencesFactory;
+import androidx.datastore.preferences.core.PreferencesKeys;
+import androidx.datastore.rxjava3.RxDataMigration;
+import androidx.datastore.rxjava3.RxDataStore;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.FileOutputStream;
+
+import io.reactivex.rxjava3.core.Completable;
+import io.reactivex.rxjava3.core.Single;
+
+
+public class RxPreferencesDataStoreBuilderTest {
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private static final Preferences.Key<Integer> INTEGER_KEY =
+ PreferencesKeys.intKey("int_key");
+
+ private static Single<Preferences> incrementInteger(Preferences preferencesIn) {
+ MutablePreferences prefs = preferencesIn.toMutablePreferences();
+ Integer currentInt = prefs.get(INTEGER_KEY);
+ prefs.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1);
+ return Single.just(prefs);
+ }
+
+ @Test
+ public void testConstructWithProduceFile() throws Exception {
+ File file = tempFolder.newFile("temp.preferences_pb");
+
+ DataStore<Preferences> dataStore =
+ new RxPreferenceDataStoreBuilder(() -> file).build();
+
+ Single<Preferences> incrementInt = RxDataStore.updateDataAsync(dataStore,
+ RxPreferencesDataStoreBuilderTest::incrementInteger);
+ assertThat(incrementInt.blockingGet().get(INTEGER_KEY)).isEqualTo(1);
+
+ // Construct it again and confirm that the data is still there:
+ dataStore = new RxPreferenceDataStoreBuilder(() -> file).build();
+
+ assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY))
+ .isEqualTo(1);
+ }
+
+
+ @Test
+ public void testConstructWithContextAndName() throws Exception {
+
+ Context context = ApplicationProvider.getApplicationContext();
+ String name = "my_data_store";
+
+ File prefsFile = new File(context.getFilesDir().getPath()
+ + "/datastore/" + name + ".preferences_pb");
+ if (prefsFile.exists()) {
+ prefsFile.delete();
+ }
+
+ DataStore<Preferences> dataStore =
+ new RxPreferenceDataStoreBuilder(context, name).build();
+
+ Single<Preferences> set1 = RxDataStore.updateDataAsync(dataStore,
+ RxPreferencesDataStoreBuilderTest::incrementInteger);
+ assertThat(set1.blockingGet().get(INTEGER_KEY)).isEqualTo(1);
+
+ // Construct it again and confirm that the data is still there:
+ dataStore = new RxPreferenceDataStoreBuilder(context, name).build();
+ assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY)).isEqualTo(1);
+
+ // Construct it again with the expected file path and confirm that the data is there:
+ dataStore =
+ new RxPreferenceDataStoreBuilder(
+ () ->
+ new File(context.getFilesDir().getPath()
+ + "/datastore/" + name + ".preferences_pb")
+ ).build();
+
+ assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY)).isEqualTo(1);
+ }
+
+ @Test
+ public void testMigrationsAreInstalledAndRun() throws Exception {
+ RxDataMigration<Preferences> plusOneMigration = new RxDataMigration<Preferences>() {
+ @NonNull
+ @Override
+ public Single<Boolean> shouldMigrate(@NonNull Preferences currentData) {
+ return Single.just(true);
+ }
+
+ @NonNull
+ @Override
+ public Single<Preferences> migrate(@NonNull Preferences currentData) {
+ return incrementInteger(currentData);
+ }
+
+ @NonNull
+ @Override
+ public Completable cleanUp() {
+ return Completable.complete();
+ }
+ };
+
+ DataStore<Preferences> dataStore =
+ new RxPreferenceDataStoreBuilder(() ->
+ tempFolder.newFile("temp.preferences_pb"))
+ .addRxDataMigration(plusOneMigration)
+ .build();
+
+ assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY))
+ .isEqualTo(1);
+ }
+
+
+ @Test
+ public void testCorruptionHandlerIsUser() throws Exception {
+
+ File file = tempFolder.newFile("temp.preferences_pb");
+
+ try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
+ fileOutputStream.write(0); // will cause corruption exception
+ }
+
+ ReplaceFileCorruptionHandler<Preferences> replaceFileCorruptionHandler =
+ new ReplaceFileCorruptionHandler<Preferences>(exception -> {
+ MutablePreferences mutablePreferences =
+ PreferencesFactory.createMutable();
+ mutablePreferences.set(INTEGER_KEY, 99);
+ return (Preferences) mutablePreferences;
+ });
+
+
+ DataStore<Preferences> dataStore =
+ new RxPreferenceDataStoreBuilder(() -> file)
+ .setCorruptionHandler(replaceFileCorruptionHandler)
+ .build();
+
+ assertThat(RxDataStore.data(dataStore).blockingFirst().get(INTEGER_KEY))
+ .isEqualTo(99);
+ }
+}
diff --git a/datastore/datastore-preferences-rxjava3/src/main/AndroidManifest.xml b/datastore/datastore-preferences-rxjava3/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..13f23ea
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.datastore.preferences.rxjava3">
+
+</manifest>
diff --git a/datastore/datastore-preferences-rxjava3/src/main/java/androidx/datastore/preferences/rxjava3/RxPreferenceDataStoreBuilder.kt b/datastore/datastore-preferences-rxjava3/src/main/java/androidx/datastore/preferences/rxjava3/RxPreferenceDataStoreBuilder.kt
new file mode 100644
index 0000000..527cfb3
--- /dev/null
+++ b/datastore/datastore-preferences-rxjava3/src/main/java/androidx/datastore/preferences/rxjava3/RxPreferenceDataStoreBuilder.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.preferences.rxjava3
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.datastore.core.DataMigration
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.createDataStore
+import androidx.datastore.rxjava3.RxDataMigration
+import io.reactivex.rxjava3.core.Scheduler
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.rx3.asCoroutineDispatcher
+import kotlinx.coroutines.rx3.await
+import java.io.File
+import java.util.concurrent.Callable
+
+/**
+ * RxSharedPreferencesMigrationBuilder class for a DataStore that works on a single process.
+ */
+@SuppressLint("TopLevelBuilder")
+public class RxPreferenceDataStoreBuilder {
+
+ // Either produceFile or context & name must be set, but not both.
+ private var produceFile: Callable<File>? = null
+
+ private var context: Context? = null
+ private var name: String? = null
+
+ // Optional
+ private var ioScheduler: Scheduler = Schedulers.io()
+ private var corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null
+ private val dataMigrations: MutableList<DataMigration<Preferences>> = mutableListOf()
+
+ /**
+ * Create a RxPreferenceDataStoreBuilder with the callable which returns the File that
+ * DataStore acts on. The user is responsible for ensuring that there is never more than one
+ * DataStore acting on a file at a time.
+ *
+ * @param produceFile Function which returns the file that the new DataStore will act on. The
+ * function must return the same path every time. No two instances of DataStore should act on
+ * the same file at the same time.
+ */
+ public constructor(produceFile: Callable<File>) {
+ this.produceFile = produceFile
+ }
+
+ /**
+ * Create a RxPreferenceDataStoreBuilder with the Context and name from which to derive the
+ * DataStore file. The file is generated by See [Context.createDataStore] for more info. The
+ * user is responsible for ensuring that there is never more than one DataStore acting on a
+ * file at a time.
+ *
+ * @param context the context from which we retrieve files directory.
+ * @param name the filename relative to Context.filesDir that DataStore acts on. The File is
+ * obtained by calling File(this.filesDir, "datastore/$name.preferences_pb"). No two instances
+ * of DataStore should act on the same file at the same time.
+ */
+ public constructor(context: Context, name: String) {
+ this.context = context
+ this.name = name
+ }
+
+ /**
+ * Set the Scheduler on which to perform IO and transform operations. This is converted into
+ * a CoroutineDispatcher before being added to DataStore.
+ *
+ * This parameter is optional and defaults to Schedulers.io().
+ *
+ * @param ioScheduler the scheduler on which IO and transform operations are run
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun setIoScheduler(ioScheduler: Scheduler): RxPreferenceDataStoreBuilder =
+ apply { this.ioScheduler = ioScheduler }
+
+ /**
+ * Sets the corruption handler to install into the DataStore.
+ *
+ * This parameter is optional and defaults to no corruption handler.
+ *
+ * @param corruptionHandler
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun setCorruptionHandler(corruptionHandler: ReplaceFileCorruptionHandler<Preferences>):
+ RxPreferenceDataStoreBuilder = apply { this.corruptionHandler = corruptionHandler }
+
+ /**
+ * Add an RxDataMigration to the DataStore. Migrations are run in the order they are added.
+ *
+ * @param rxDataMigration the migration to add.
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun addRxDataMigration(rxDataMigration: RxDataMigration<Preferences>):
+ RxPreferenceDataStoreBuilder = apply {
+ this.dataMigrations.add(DataMigrationFromRxDataMigration(rxDataMigration))
+ }
+
+ /**
+ * Add a DataMigration to the Datastore. Migrations are run in the order they are added.
+ *
+ * @param dataMigration the migration to add
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun addDataMigration(dataMigration: DataMigration<Preferences>):
+ RxPreferenceDataStoreBuilder = apply {
+ this.dataMigrations.add(dataMigration)
+ }
+
+ /**
+ * Build the DataStore.
+ *
+ * @throws IllegalStateException if serializer is not set or if neither produceFile not
+ * context and name are set.
+ * @return the DataStore with the provided parameters
+ */
+ public fun build(): DataStore<Preferences> {
+ val scope = CoroutineScope(ioScheduler.asCoroutineDispatcher())
+
+ val produceFile: Callable<File>? = this.produceFile
+ val context: Context? = this.context
+ val name: String? = this.name
+
+ return if (produceFile != null) {
+ PreferenceDataStoreFactory.create(
+ produceFile = { produceFile.call() },
+ scope = CoroutineScope(
+ ioScheduler.asCoroutineDispatcher()
+ ),
+ corruptionHandler = corruptionHandler,
+ migrations = dataMigrations
+ )
+ } else if (context != null && name != null) {
+ return context.createDataStore(
+ name = name,
+ scope = scope,
+ corruptionHandler = corruptionHandler,
+ migrations = dataMigrations
+ )
+ } else {
+ error("Either produceFile or context and name must be set. This should never happen.")
+ }
+ }
+}
+
+internal class DataMigrationFromRxDataMigration<T>(private val migration: RxDataMigration<T>) :
+ DataMigration<T> {
+ override suspend fun shouldMigrate(currentData: T): Boolean {
+ return migration.shouldMigrate(currentData).await()
+ }
+
+ override suspend fun migrate(currentData: T): T {
+ return migration.migrate(currentData).await()
+ }
+
+ override suspend fun cleanUp() {
+ migration.cleanUp().await()
+ }
+}
diff --git a/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/AssertThrows.kt b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/AssertThrows.kt
deleted file mode 100644
index cc1f3493..0000000
--- a/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/AssertThrows.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.datastore.rxjava2
-
-@Suppress("UNCHECKED_CAST")
-internal fun <T : Throwable?> assertThrows(
- expectedType: Class<T>,
- runnable: Runnable
-): T {
- try {
- runnable.run()
- } catch (t: Throwable) {
- if (!expectedType.isInstance(t)) {
- throw RuntimeException(t)
- }
- return t as T
- }
- throw AssertionError(
- String.format(
- "Expected %s wasn't thrown",
- expectedType.simpleName
- )
- )
-}
diff --git a/datastore/datastore-rxjava3/api/current.txt b/datastore/datastore-rxjava3/api/current.txt
new file mode 100644
index 0000000..bdba98a
--- /dev/null
+++ b/datastore/datastore-rxjava3/api/current.txt
@@ -0,0 +1,38 @@
+// Signature format: 4.0
+package androidx.datastore.rxjava3 {
+
+ public interface RxDataMigration<T> {
+ method public io.reactivex.rxjava3.core.Completable cleanUp();
+ method public io.reactivex.rxjava3.core.Single<T!> migrate(T?);
+ method public io.reactivex.rxjava3.core.Single<java.lang.Boolean!> shouldMigrate(T?);
+ }
+
+ public final class RxDataStore {
+ method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Flowable<T> data(androidx.datastore.core.DataStore<T>);
+ method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Single<T> updateDataAsync(androidx.datastore.core.DataStore<T>, io.reactivex.rxjava3.functions.Function<T,io.reactivex.rxjava3.core.Single<T>> transform);
+ }
+
+ public final class RxDataStoreBuilder<T> {
+ ctor public RxDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile, androidx.datastore.core.Serializer<T> serializer);
+ ctor public RxDataStoreBuilder(android.content.Context context, String fileName, androidx.datastore.core.Serializer<T> serializer);
+ method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> addDataMigration(androidx.datastore.core.DataMigration<T> dataMigration);
+ method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> addRxDataMigration(androidx.datastore.rxjava3.RxDataMigration<T> rxDataMigration);
+ method public androidx.datastore.core.DataStore<T> build();
+ method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T> corruptionHandler);
+ method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> setIoScheduler(io.reactivex.rxjava3.core.Scheduler ioScheduler);
+ }
+
+ public interface RxSharedPreferencesMigration<T> {
+ method public io.reactivex.rxjava3.core.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
+ method public default io.reactivex.rxjava3.core.Single<java.lang.Boolean> shouldMigrate(T? currentData);
+ }
+
+ public final class RxSharedPreferencesMigrationBuilder<T> {
+ ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava3.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
+ method public androidx.datastore.core.DataMigration<T> build();
+ method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
+ method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
+ }
+
+}
+
diff --git a/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt b/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..bdba98a
--- /dev/null
+++ b/datastore/datastore-rxjava3/api/public_plus_experimental_current.txt
@@ -0,0 +1,38 @@
+// Signature format: 4.0
+package androidx.datastore.rxjava3 {
+
+ public interface RxDataMigration<T> {
+ method public io.reactivex.rxjava3.core.Completable cleanUp();
+ method public io.reactivex.rxjava3.core.Single<T!> migrate(T?);
+ method public io.reactivex.rxjava3.core.Single<java.lang.Boolean!> shouldMigrate(T?);
+ }
+
+ public final class RxDataStore {
+ method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Flowable<T> data(androidx.datastore.core.DataStore<T>);
+ method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Single<T> updateDataAsync(androidx.datastore.core.DataStore<T>, io.reactivex.rxjava3.functions.Function<T,io.reactivex.rxjava3.core.Single<T>> transform);
+ }
+
+ public final class RxDataStoreBuilder<T> {
+ ctor public RxDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile, androidx.datastore.core.Serializer<T> serializer);
+ ctor public RxDataStoreBuilder(android.content.Context context, String fileName, androidx.datastore.core.Serializer<T> serializer);
+ method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> addDataMigration(androidx.datastore.core.DataMigration<T> dataMigration);
+ method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> addRxDataMigration(androidx.datastore.rxjava3.RxDataMigration<T> rxDataMigration);
+ method public androidx.datastore.core.DataStore<T> build();
+ method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T> corruptionHandler);
+ method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> setIoScheduler(io.reactivex.rxjava3.core.Scheduler ioScheduler);
+ }
+
+ public interface RxSharedPreferencesMigration<T> {
+ method public io.reactivex.rxjava3.core.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
+ method public default io.reactivex.rxjava3.core.Single<java.lang.Boolean> shouldMigrate(T? currentData);
+ }
+
+ public final class RxSharedPreferencesMigrationBuilder<T> {
+ ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava3.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
+ method public androidx.datastore.core.DataMigration<T> build();
+ method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
+ method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
+ }
+
+}
+
diff --git a/datastore/datastore-rxjava3/api/res-current.txt b/datastore/datastore-rxjava3/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/datastore/datastore-rxjava3/api/res-current.txt
diff --git a/datastore/datastore-rxjava3/api/restricted_current.txt b/datastore/datastore-rxjava3/api/restricted_current.txt
new file mode 100644
index 0000000..bdba98a
--- /dev/null
+++ b/datastore/datastore-rxjava3/api/restricted_current.txt
@@ -0,0 +1,38 @@
+// Signature format: 4.0
+package androidx.datastore.rxjava3 {
+
+ public interface RxDataMigration<T> {
+ method public io.reactivex.rxjava3.core.Completable cleanUp();
+ method public io.reactivex.rxjava3.core.Single<T!> migrate(T?);
+ method public io.reactivex.rxjava3.core.Single<java.lang.Boolean!> shouldMigrate(T?);
+ }
+
+ public final class RxDataStore {
+ method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Flowable<T> data(androidx.datastore.core.DataStore<T>);
+ method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.rxjava3.core.Single<T> updateDataAsync(androidx.datastore.core.DataStore<T>, io.reactivex.rxjava3.functions.Function<T,io.reactivex.rxjava3.core.Single<T>> transform);
+ }
+
+ public final class RxDataStoreBuilder<T> {
+ ctor public RxDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile, androidx.datastore.core.Serializer<T> serializer);
+ ctor public RxDataStoreBuilder(android.content.Context context, String fileName, androidx.datastore.core.Serializer<T> serializer);
+ method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> addDataMigration(androidx.datastore.core.DataMigration<T> dataMigration);
+ method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> addRxDataMigration(androidx.datastore.rxjava3.RxDataMigration<T> rxDataMigration);
+ method public androidx.datastore.core.DataStore<T> build();
+ method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T> corruptionHandler);
+ method public androidx.datastore.rxjava3.RxDataStoreBuilder<T> setIoScheduler(io.reactivex.rxjava3.core.Scheduler ioScheduler);
+ }
+
+ public interface RxSharedPreferencesMigration<T> {
+ method public io.reactivex.rxjava3.core.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
+ method public default io.reactivex.rxjava3.core.Single<java.lang.Boolean> shouldMigrate(T? currentData);
+ }
+
+ public final class RxSharedPreferencesMigrationBuilder<T> {
+ ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava3.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
+ method public androidx.datastore.core.DataMigration<T> build();
+ method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
+ method public androidx.datastore.rxjava3.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
+ }
+
+}
+
diff --git a/datastore/datastore-rxjava3/build.gradle b/datastore/datastore-rxjava3/build.gradle
new file mode 100644
index 0000000..9f8e453
--- /dev/null
+++ b/datastore/datastore-rxjava3/build.gradle
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.AndroidXExtension
+import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+android {
+ sourceSets {
+ test.java.srcDirs += 'src/test-common/java'
+ androidTest.java.srcDirs += 'src/test-common/java'
+ }
+}
+
+dependencies {
+ api(KOTLIN_STDLIB)
+ api(KOTLIN_COROUTINES_CORE)
+ api("androidx.annotation:annotation:1.1.0")
+ api(RX_JAVA3)
+
+ api(project(":datastore:datastore"))
+
+ implementation(KOTLIN_COROUTINES_RX3)
+
+ testImplementation(JUNIT)
+ testImplementation(KOTLIN_COROUTINES_TEST)
+ testImplementation(TRUTH)
+ testImplementation(project(":internal-testutils-truth"))
+
+ androidTestImplementation(JUNIT)
+ androidTestImplementation(project(":internal-testutils-truth"))
+ androidTestImplementation(ANDROIDX_TEST_RUNNER)
+ androidTestImplementation(ANDROIDX_TEST_CORE)
+}
+
+androidx {
+ name = "Android DataStore Core RxJava2 Wrappers"
+ publish = Publish.SNAPSHOT_AND_RELEASE
+ mavenGroup = LibraryGroups.DATASTORE
+ inceptionYear = "2020"
+ description = "Android DataStore Core - contains wrappers for using DataStore using RxJava2"
+ legacyDisableKotlinStrictApiMode = true
+}
diff --git a/datastore/datastore-rxjava3/src/androidTest/AndroidManifest.xml b/datastore/datastore-rxjava3/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..3369992
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.datastore.rxjava3">
+
+</manifest>
diff --git a/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxDataStoreBuilderTest.java b/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxDataStoreBuilderTest.java
new file mode 100644
index 0000000..0564f77
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxDataStoreBuilderTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.datastore.rxjava3;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.datastore.core.DataStore;
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import io.reactivex.rxjava3.core.Completable;
+import io.reactivex.rxjava3.core.Scheduler;
+import io.reactivex.rxjava3.core.Single;
+import io.reactivex.rxjava3.schedulers.Schedulers;
+
+public class RxDataStoreBuilderTest {
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private static Single<Byte> incrementByte(Byte byteIn) {
+ return Single.just(++byteIn);
+ }
+
+ @Test
+ public void testConstructWithProduceFile() throws Exception {
+ File file = tempFolder.newFile();
+ DataStore<Byte> dataStore =
+ new RxDataStoreBuilder<Byte>(() -> file, new TestingSerializer())
+ .build();
+ Single<Byte> incrementByte = RxDataStore.updateDataAsync(dataStore,
+ RxDataStoreBuilderTest::incrementByte);
+ assertThat(incrementByte.blockingGet()).isEqualTo(1);
+ // Construct it again and confirm that the data is still there:
+ dataStore =
+ new RxDataStoreBuilder<Byte>(() -> file, new TestingSerializer())
+ .build();
+ assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
+ }
+
+ @Test
+ public void testConstructWithContextAndName() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ String name = "my_data_store";
+ DataStore<Byte> dataStore =
+ new RxDataStoreBuilder<Byte>(context, name, new TestingSerializer())
+ .build();
+ Single<Byte> set1 = RxDataStore.updateDataAsync(dataStore, input -> Single.just((byte) 1));
+ assertThat(set1.blockingGet()).isEqualTo(1);
+ // Construct it again and confirm that the data is still there:
+ dataStore =
+ new RxDataStoreBuilder<Byte>(context, name, new TestingSerializer())
+ .build();
+ assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
+ // Construct it again with the expected file path and confirm that the data is there:
+ dataStore =
+ new RxDataStoreBuilder<Byte>(() -> new File(context.getFilesDir().getPath()
+ + "/datastore/" + name), new TestingSerializer()
+ )
+ .build();
+ assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
+ }
+
+ @Test
+ public void testMigrationsAreInstalledAndRun() throws Exception {
+ RxDataMigration<Byte> plusOneMigration = new RxDataMigration<Byte>() {
+ @NonNull
+ @Override
+ public Single<Boolean> shouldMigrate(@NonNull Byte currentData) {
+ return Single.just(true);
+ }
+
+ @NonNull
+ @Override
+ public Single<Byte> migrate(@NonNull Byte currentData) {
+ return incrementByte(currentData);
+ }
+
+ @NonNull
+ @Override
+ public Completable cleanUp() {
+ return Completable.complete();
+ }
+ };
+
+ DataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(
+ () -> tempFolder.newFile(), new TestingSerializer())
+ .addRxDataMigration(plusOneMigration)
+ .build();
+
+ assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
+ }
+
+ @Test
+ public void testSpecifiedSchedulerIsUser() throws Exception {
+ Scheduler singleThreadedScheduler =
+ Schedulers.from(Executors.newSingleThreadExecutor(new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(r, "TestingThread");
+ }
+ }));
+
+
+ DataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(() -> tempFolder.newFile(),
+ new TestingSerializer())
+ .setIoScheduler(singleThreadedScheduler)
+ .build();
+ Single<Byte> update = RxDataStore.updateDataAsync(dataStore, input -> {
+ Thread currentThread = Thread.currentThread();
+ assertThat(currentThread.getName()).isEqualTo("TestingThread");
+ return Single.just(input);
+ });
+ assertThat(update.blockingGet()).isEqualTo((byte) 0);
+ Single<Byte> subsequentUpdate = RxDataStore.updateDataAsync(dataStore, input -> {
+ Thread currentThread = Thread.currentThread();
+ assertThat(currentThread.getName()).isEqualTo("TestingThread");
+ return Single.just(input);
+ });
+ assertThat(subsequentUpdate.blockingGet()).isEqualTo((byte) 0);
+ }
+
+ @Test
+ public void testCorruptionHandlerIsUser() {
+ TestingSerializer testingSerializer = new TestingSerializer();
+ testingSerializer.setFailReadWithCorruptionException(true);
+ ReplaceFileCorruptionHandler<Byte> replaceFileCorruptionHandler =
+ new ReplaceFileCorruptionHandler<Byte>(exception -> (byte) 99);
+
+
+ DataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(
+ () -> tempFolder.newFile(),
+ testingSerializer)
+ .setCorruptionHandler(replaceFileCorruptionHandler)
+ .build();
+ assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(99);
+ }
+}
diff --git a/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxSharedPreferencesMigrationTest.java b/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxSharedPreferencesMigrationTest.java
new file mode 100644
index 0000000..b66d6ef
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/androidTest/java/androidx/datastore/rxjava3/RxSharedPreferencesMigrationTest.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.rxjava3;
+
+import static androidx.testutils.AssertionsKt.assertThrows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.datastore.core.DataMigration;
+import androidx.datastore.core.DataStore;
+import androidx.datastore.migrations.SharedPreferencesView;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.truth.Truth;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+
+import io.reactivex.rxjava3.core.Single;
+
+public class RxSharedPreferencesMigrationTest {
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private final String mSharedPrefsName = "shared_prefs_name";
+
+
+ private Context mContext;
+ private SharedPreferences mSharedPrefs;
+ private File mDatastoreFile;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = ApplicationProvider.getApplicationContext();
+ mSharedPrefs = mContext.getSharedPreferences(mSharedPrefsName, Context.MODE_PRIVATE);
+ mDatastoreFile = temporaryFolder.newFile("test_file.preferences_pb");
+
+ assertThat(mSharedPrefs.edit().clear().commit()).isTrue();
+ }
+
+ @Test
+ public void testShouldMigrateSkipsMigration() {
+ RxSharedPreferencesMigration<Byte> skippedMigration =
+ new RxSharedPreferencesMigration<Byte>() {
+ @NotNull
+ @Override
+ public Single<Boolean> shouldMigrate(Byte currentData) {
+ return Single.just(false);
+ }
+
+ @NotNull
+ @Override
+ public Single<Byte> migrate(
+ @NotNull SharedPreferencesView sharedPreferencesView,
+ Byte currentData) {
+ return Single.error(
+ new IllegalStateException("We shouldn't reach this point!"));
+ }
+ };
+
+
+ DataMigration<Byte> spMigration =
+ getSpMigrationBuilder(skippedMigration).build();
+
+ DataStore<Byte> dataStoreWithMigrations = getDataStoreWithMigration(spMigration);
+
+ Truth.assertThat(RxDataStore.data(dataStoreWithMigrations).blockingFirst()).isEqualTo(0);
+ }
+
+ @Test
+ public void testSharedPrefsViewContainsSpecifiedKeys() {
+ String includedKey = "key1";
+ int includedVal = 99;
+ String notMigratedKey = "key2";
+
+ assertThat(mSharedPrefs.edit().putInt(includedKey, includedVal).putInt(notMigratedKey,
+ 123).commit()).isTrue();
+
+ DataMigration<Byte> dataMigration =
+ getSpMigrationBuilder(
+ new DefaultMigration() {
+ @NotNull
+ @Override
+ public Single<Byte> migrate(
+ @NotNull SharedPreferencesView sharedPreferencesView,
+ Byte currentData) {
+ assertThat(sharedPreferencesView.contains(includedKey)).isTrue();
+ assertThat(sharedPreferencesView.getAll().size()).isEqualTo(1);
+ assertThrows(IllegalStateException.class,
+ () -> sharedPreferencesView.getInt(notMigratedKey, -1));
+
+ return Single.just((byte) 50);
+ }
+ }
+ ).setKeysToMigrate(includedKey).build();
+
+ DataStore<Byte> byteStore = getDataStoreWithMigration(dataMigration);
+
+ assertThat(RxDataStore.data(byteStore).blockingFirst()).isEqualTo(50);
+
+ assertThat(mSharedPrefs.contains(includedKey)).isFalse();
+ assertThat(mSharedPrefs.contains(notMigratedKey)).isTrue();
+ }
+
+
+ @Test
+ public void testSharedPrefsViewWithAllKeysSpecified() {
+ String includedKey = "key1";
+ String includedKey2 = "key2";
+ int value = 99;
+
+ assertThat(mSharedPrefs.edit().putInt(includedKey, value).putInt(includedKey2,
+ value).commit()).isTrue();
+
+ DataMigration<Byte> dataMigration =
+ getSpMigrationBuilder(
+ new DefaultMigration() {
+ @NotNull
+ @Override
+ public Single<Byte> migrate(
+ @NotNull SharedPreferencesView sharedPreferencesView,
+ Byte currentData) {
+ assertThat(sharedPreferencesView.contains(includedKey)).isTrue();
+ assertThat(sharedPreferencesView.contains(includedKey2)).isTrue();
+ assertThat(sharedPreferencesView.getAll().size()).isEqualTo(2);
+
+ return Single.just((byte) 50);
+ }
+ }
+ ).build();
+
+ DataStore<Byte> byteStore = getDataStoreWithMigration(dataMigration);
+
+ assertThat(RxDataStore.data(byteStore).blockingFirst()).isEqualTo(50);
+
+ assertThat(mSharedPrefs.contains(includedKey)).isFalse();
+ assertThat(mSharedPrefs.contains(includedKey2)).isFalse();
+ }
+
+ @Test
+ public void testDeletesEmptySharedPreferences() {
+ String key = "key";
+ String value = "value";
+ assertThat(mSharedPrefs.edit().putString(key, value).commit()).isTrue();
+
+ DataMigration<Byte> dataMigration =
+ getSpMigrationBuilder(new DefaultMigration()).setDeleteEmptyPreferences(
+ true).build();
+ DataStore<Byte> byteStore = getDataStoreWithMigration(dataMigration);
+ assertThat(RxDataStore.data(byteStore).blockingFirst()).isEqualTo(0);
+
+ // Check that the shared preferences files are deleted
+ File prefsDir = new File(mContext.getApplicationInfo().dataDir, "shared_prefs");
+ File prefsFile = new File(prefsDir, mSharedPrefsName + ".xml");
+ File backupPrefsFile = new File(prefsFile.getPath() + ".bak");
+ assertThat(prefsFile.exists()).isFalse();
+ assertThat(backupPrefsFile.exists()).isFalse();
+ }
+
+ private RxSharedPreferencesMigrationBuilder<Byte> getSpMigrationBuilder(
+ RxSharedPreferencesMigration<Byte> rxSharedPreferencesMigration) {
+ return new RxSharedPreferencesMigrationBuilder<Byte>(mContext, mSharedPrefsName,
+ rxSharedPreferencesMigration);
+ }
+
+ private DataStore<Byte> getDataStoreWithMigration(DataMigration<Byte> dataMigration) {
+ return new RxDataStoreBuilder<Byte>(() -> mDatastoreFile, new TestingSerializer())
+ .addDataMigration(dataMigration).build();
+ }
+
+
+ private static class DefaultMigration implements RxSharedPreferencesMigration<Byte> {
+
+ @NotNull
+ @Override
+ public Single<Boolean> shouldMigrate(Byte currentData) {
+ return Single.just(true);
+ }
+
+ @NotNull
+ @Override
+ public Single<Byte> migrate(@NotNull SharedPreferencesView sharedPreferencesView,
+ Byte currentData) {
+ return Single.just(currentData);
+ }
+ }
+}
diff --git a/datastore/datastore-rxjava3/src/main/AndroidManifest.xml b/datastore/datastore-rxjava3/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3369992
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.datastore.rxjava3">
+
+</manifest>
diff --git a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataMigration.java b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataMigration.java
new file mode 100644
index 0000000..ade2acc
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataMigration.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.rxjava3;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import io.reactivex.rxjava3.core.Completable;
+import io.reactivex.rxjava3.core.Single;
+
+/**
+ * Interface for migrations to DataStore. Methods on this migration ([shouldMigrate], [migrate]
+ * and [cleanUp]) may be called multiple times, so their implementations must be idempotent.
+ * These methods may be called multiple times if DataStore encounters issues when writing the
+ * newly migrated data to disk or if any migration installed in the same DataStore throws an
+ * Exception.
+ *
+ * If you're migrating from SharedPreferences see [SharedPreferencesMigration].
+ *
+ * @param <T> the exception type
+ */
+public interface RxDataMigration<T> {
+
+ /**
+ * Return whether this migration needs to be performed. If this returns false, no migration or
+ * cleanup will occur. Apps should do the cheapest possible check to determine if this migration
+ * should run, since this will be called every time the DataStore is initialized. This method
+ * may be run multiple times when any failure is encountered.
+ *
+ * Note that this will always be called before each call to [migrate].
+ *
+ * @param currentData the current data (which might already populated from previous runs of this
+ * or other migrations). Only Nullable if the type used with DataStore is
+ * Nullable.
+ */
+ @NonNull
+ Single<Boolean> shouldMigrate(@Nullable T currentData);
+
+ /**
+ * Perform the migration. Implementations should be idempotent since this may be called
+ * multiple times. If migrate fails, DataStore will not commit any data to disk, cleanUp will
+ * not be called, and the exception will be propagated back to the DataStore call that
+ * triggered the migration. Future calls to DataStore will result in DataMigrations being
+ * attempted again. This method may be run multiple times when any failure is encountered.
+ *
+ * Note that this will always be called before a call to [cleanUp].
+ *
+ * @param currentData the current data (it might be populated from other migrations or from
+ * manual changes before this migration was added to the app). Only
+ * Nullable if the type used with DataStore is Nullable.
+ * @return The migrated data.
+ */
+ @NonNull
+ Single<T> migrate(@Nullable T currentData);
+
+ /**
+ * Clean up any old state/data that was migrated into the DataStore. This will not be called
+ * if the migration fails. If cleanUp throws an exception, the exception will be propagated
+ * back to the DataStore call that triggered the migration and future calls to DataStore will
+ * result in DataMigrations being attempted again. This method may be run multiple times when
+ * any failure is encountered.
+ */
+ @NonNull
+ Completable cleanUp();
+}
diff --git a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataStore.kt b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataStore.kt
new file mode 100644
index 0000000..467e039
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataStore.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:JvmName("RxDataStore")
+
+package androidx.datastore.rxjava3
+
+import androidx.datastore.core.DataStore
+import io.reactivex.rxjava3.core.Flowable
+import io.reactivex.rxjava3.core.Single
+import io.reactivex.rxjava3.functions.Function
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.rx3.asFlowable
+import kotlinx.coroutines.rx3.asSingle
+import kotlinx.coroutines.rx3.await
+
+/**
+ * Gets a reactivex.Flowable of the data from DataStore. See [DataStore.data] for more information.
+ *
+ * Provides efficient, cached (when possible) access to the latest durably persisted state.
+ * The flow will always either emit a value or throw an exception encountered when attempting
+ * to read from disk. If an exception is encountered, collecting again will attempt to read the
+ * data again.
+ *
+ * Do not layer a cache on top of this API: it will be be impossible to guarantee consistency.
+ * Instead, use data.first() to access a single snapshot.
+ *
+ * The Flowable will complete with an IOException when an exception is encountered when reading
+ * data.
+ *
+ * @return a flow representing the current state of the data
+ */
+@ExperimentalCoroutinesApi
+public fun <T : Any> DataStore<T>.data(): Flowable<T> {
+ return this.data.asFlowable()
+}
+
+/**
+ * See [DataStore.updateData]
+ *
+ * Updates the data transactionally in an atomic read-modify-write operation. All operations
+ * are serialized, and the transform itself is a async so it can perform heavy work
+ * such as RPCs.
+ *
+ * The Single completes when the data has been persisted durably to disk (after which
+ * [data] will reflect the update). If the transform or write to disk fails, the
+ * transaction is aborted and the returned Single is completed with the error.
+ *
+ * The transform will be run on the scheduler that DataStore was constructed with.
+ *
+ * @return the snapshot returned by the transform
+ * @throws Exception when thrown by the transform function
+ */
+@ExperimentalCoroutinesApi
+public fun <T : Any> DataStore<T>.updateDataAsync(transform: Function<T, Single<T>>): Single<T> {
+ return CoroutineScope(Dispatchers.Unconfined).async {
+ this@updateDataAsync.updateData {
+ transform.apply(it).await()
+ }
+ }.asSingle(Dispatchers.Unconfined)
+}
diff --git a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataStoreBuilder.kt b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataStoreBuilder.kt
new file mode 100644
index 0000000..139bbca
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxDataStoreBuilder.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.rxjava3
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.datastore.core.DataMigration
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.core.Serializer
+import androidx.datastore.createDataStore
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
+import io.reactivex.rxjava3.core.Scheduler
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.rx3.asCoroutineDispatcher
+import kotlinx.coroutines.rx3.await
+import java.io.File
+import java.util.concurrent.Callable
+
+/**
+ * RxSharedPreferencesMigrationBuilder class for a DataStore that works on a single process.
+ */
+@SuppressLint("TopLevelBuilder")
+public class RxDataStoreBuilder<T> {
+
+ /**
+ * Create a RxDataStoreBuilder with the callable which returns the File that DataStore acts on.
+ * The user is responsible for ensuring that there is never more than one DataStore acting on
+ * a file at a time.
+ *
+ * @param produceFile Function which returns the file that the new DataStore will act on. The
+ * function must return the same path every time. No two instances of DataStore should act on
+ * the same file at the same time.
+ * @param serializer the serializer for the type that this DataStore acts on.
+ */
+ public constructor(produceFile: Callable<File>, serializer: Serializer<T>) {
+ this.produceFile = produceFile
+ this.serializer = serializer
+ }
+
+ /**
+ * Create a RxDataStoreBuilder with the Context and name from which to derive the DataStore
+ * file. The file is generated by See [Context.createDataStore] for more info. The user is
+ * responsible for ensuring that there is never more than one DataStore acting on a file at a
+ * time.
+ *
+ * @param context the context from which we retrieve files directory.
+ * @param fileName the filename relative to Context.filesDir that DataStore acts on. The File is
+ * obtained by calling File(context.filesDir, fileName). No two instances of DataStore should
+ * act on the same file at the same time.
+ * @param serializer the serializer for the type that this DataStore acts on.
+ */
+ public constructor(context: Context, fileName: String, serializer: Serializer<T>) {
+ this.context = context
+ this.name = fileName
+ this.serializer = serializer
+ }
+
+ // Either produceFile or context & name must be set, but not both. This is enforced by the
+ // two constructors.
+ private var produceFile: Callable<File>? = null
+
+ private var context: Context? = null
+ private var name: String? = null
+
+ // Required. This is enforced by the constructors.
+ private var serializer: Serializer<T>? = null
+
+ // Optional
+ private var ioScheduler: Scheduler = Schedulers.io()
+ private var corruptionHandler: ReplaceFileCorruptionHandler<T>? = null
+ private val dataMigrations: MutableList<DataMigration<T>> = mutableListOf()
+
+ /**
+ * Set the Scheduler on which to perform IO and transform operations. This is converted into
+ * a CoroutineDispatcher before being added to DataStore.
+ *
+ * This parameter is optional and defaults to Schedulers.io().
+ *
+ * @param ioScheduler the scheduler on which IO and transform operations are run
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun setIoScheduler(ioScheduler: Scheduler): RxDataStoreBuilder<T> =
+ apply { this.ioScheduler = ioScheduler }
+
+ /**
+ * Sets the corruption handler to install into the DataStore.
+ *
+ * This parameter is optional and defaults to no corruption handler.
+ *
+ * @param corruptionHandler
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun setCorruptionHandler(corruptionHandler: ReplaceFileCorruptionHandler<T>):
+ RxDataStoreBuilder<T> = apply { this.corruptionHandler = corruptionHandler }
+
+ /**
+ * Add an RxDataMigration to the DataStore. Migrations are run in the order they are added.
+ *
+ * @param rxDataMigration the migration to add.
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun addRxDataMigration(rxDataMigration: RxDataMigration<T>): RxDataStoreBuilder<T> =
+ apply {
+ this.dataMigrations.add(DataMigrationFromRxDataMigration(rxDataMigration))
+ }
+
+ /**
+ * Add a DataMigration to the Datastore. Migrations are run in the order they are added.
+ *
+ * @param dataMigration the migration to add
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun addDataMigration(dataMigration: DataMigration<T>): RxDataStoreBuilder<T> = apply {
+ this.dataMigrations.add(dataMigration)
+ }
+
+ /**
+ * Build the DataStore.
+ *
+ * @return the DataStore with the provided parameters
+ */
+ public fun build(): DataStore<T> {
+ val scope = CoroutineScope(ioScheduler.asCoroutineDispatcher())
+
+ return if (produceFile != null) {
+ DataStoreFactory.create(
+ produceFile = { produceFile!!.call() },
+ serializer = serializer!!,
+ scope = CoroutineScope(
+ ioScheduler.asCoroutineDispatcher()
+ ),
+ corruptionHandler = corruptionHandler,
+ migrations = dataMigrations
+ )
+ } else if (context != null && name != null) {
+ return context!!.createDataStore(
+ fileName = name!!,
+ serializer = serializer!!,
+ scope = scope,
+ corruptionHandler = corruptionHandler,
+ migrations = dataMigrations
+ )
+ } else {
+ error(
+ "Either produceFile or context and name must be set. This should never happen."
+ )
+ }
+ }
+}
+
+internal class DataMigrationFromRxDataMigration<T>(private val migration: RxDataMigration<T>) :
+ DataMigration<T> {
+ override suspend fun shouldMigrate(currentData: T): Boolean {
+ return migration.shouldMigrate(currentData).await()
+ }
+
+ override suspend fun migrate(currentData: T): T {
+ return migration.migrate(currentData).await()
+ }
+
+ override suspend fun cleanUp() {
+ migration.cleanUp().await()
+ }
+}
diff --git a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
new file mode 100644
index 0000000..e5f71c6
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.rxjava3
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.datastore.core.DataMigration
+import androidx.datastore.migrations.SharedPreferencesMigration
+import androidx.datastore.migrations.SharedPreferencesView
+import io.reactivex.rxjava3.core.Single
+import kotlinx.coroutines.rx3.await
+
+/**
+ * Client implemented migration interface.
+ **/
+public interface RxSharedPreferencesMigration<T> {
+ /**
+ * Whether or not the migration should be run. This can be used to skip a read from the
+ * SharedPreferences.
+ *
+ * @param currentData the most recently persisted data
+ * @return a Single indicating whether or not the migration should be run.
+ */
+ public fun shouldMigrate(currentData: T): Single<Boolean> {
+ return Single.just(true)
+ }
+
+ /**
+ * Maps SharedPreferences into T. Implementations should be idempotent
+ * since this may be called multiple times. See [DataMigration.migrate] for more
+ * information. The method accepts a SharedPreferencesView which is the view of the
+ * SharedPreferences to migrate from (limited to [keysToMigrate] and a T which represent
+ * the current data. The function must return the migrated data.
+ *
+ * @param sharedPreferencesView the current state of the SharedPreferences
+ * @param currentData the most recently persisted data
+ * @return a Single of the updated data
+ */
+ public fun migrate(sharedPreferencesView: SharedPreferencesView, currentData: T): Single<T>
+}
+
+/**
+ * RxSharedPreferencesMigrationBuilder for the RxSharedPreferencesMigration.
+ */
+@SuppressLint("TopLevelBuilder")
+public class RxSharedPreferencesMigrationBuilder<T>
+/**
+ * Construct a RxSharedPreferencesMigrationBuilder.
+ *
+ * @param context the Context used for getting the SharedPreferences.
+ * @param sharedPreferencesName the name of the SharedPreference from which to migrate.
+ * @param rxSharedPreferencesMigration the user implemented migration for this SharedPreference.
+ */
+constructor(
+ private val context: Context,
+ private val sharedPreferencesName: String,
+ private val rxSharedPreferencesMigration: RxSharedPreferencesMigration<T>
+) {
+
+ /** Optional */
+ private var deleteEmptyPreference: Boolean = true
+ private var keysToMigrate: Set<String>? = null
+
+ /**
+ * Set the list of keys to migrate. The keys will be mapped to datastore.Preferences with
+ * their same values. If the key is already present in the new Preferences, the key
+ * will not be migrated again. If the key is not present in the SharedPreferences it
+ * will not be migrated.
+ *
+ * This method is optional and if keysToMigrate is not set, all keys will be migrated from the
+ * existing SharedPreferences.
+ *
+ * @param keys the keys to migrate
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun setKeysToMigrate(vararg keys: String):
+ RxSharedPreferencesMigrationBuilder<T> = apply {
+ keysToMigrate = setOf(*keys)
+ }
+
+ /**
+ * If enabled and the SharedPreferences are empty (i.e. no remaining
+ * keys) after this migration runs, the leftover SharedPreferences file is deleted. Note that
+ * this cleanup runs only if the migration itself runs, i.e., if the keys were never in
+ * SharedPreferences to begin with then the (potentially) empty SharedPreferences
+ * won't be cleaned up by this option. This functionality is best effort - if there
+ * is an issue deleting the SharedPreferences file it will be silently ignored.
+ *
+ * This method is optional and defaults to true.
+ *
+ * @param deleteEmptyPreferences whether or not to delete the empty shared preferences file
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun setDeleteEmptyPreferences(deleteEmptyPreferences: Boolean):
+ RxSharedPreferencesMigrationBuilder<T> = apply {
+ this.deleteEmptyPreference = deleteEmptyPreferences
+ }
+
+ public fun build(): DataMigration<T> {
+ return SharedPreferencesMigration(
+ context = context,
+ sharedPreferencesName = sharedPreferencesName,
+ migrate = { spView, curData ->
+ rxSharedPreferencesMigration.migrate(spView, curData).await()
+ },
+ keysToMigrate = keysToMigrate,
+ deleteEmptyPreferences = deleteEmptyPreference,
+ shouldRunMigration = { curData ->
+ rxSharedPreferencesMigration.shouldMigrate(curData).await()
+ }
+ )
+ }
+}
diff --git a/datastore/datastore-rxjava3/src/test-common/java/androidx/datastore/rxjava3/TestingSerializer.kt b/datastore/datastore-rxjava3/src/test-common/java/androidx/datastore/rxjava3/TestingSerializer.kt
new file mode 100644
index 0000000..f1e02bb
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/test-common/java/androidx/datastore/rxjava3/TestingSerializer.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.rxjava3
+
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.Serializer
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+
+class TestingSerializer(
+ @Volatile var failReadWithCorruptionException: Boolean = false,
+ @Volatile var failingRead: Boolean = false,
+ @Volatile var failingWrite: Boolean = false
+) : Serializer<Byte> {
+ override fun readFrom(input: InputStream): Byte {
+ if (failReadWithCorruptionException) {
+ throw CorruptionException(
+ "CorruptionException",
+ IOException()
+ )
+ }
+
+ if (failingRead) {
+ throw IOException("I was asked to fail on reads")
+ }
+
+ val read = input.read()
+ if (read == -1) {
+ return 0
+ }
+ return read.toByte()
+ }
+
+ override fun writeTo(t: Byte, output: OutputStream) {
+ if (failingWrite) {
+ throw IOException("I was asked to fail on writes")
+ }
+ output.write(t.toInt())
+ }
+
+ override val defaultValue: Byte = 0
+}
\ No newline at end of file
diff --git a/datastore/datastore-rxjava3/src/test/java/androidx/datastore/rxjava3/RxDataStoreTest.java b/datastore/datastore-rxjava3/src/test/java/androidx/datastore/rxjava3/RxDataStoreTest.java
new file mode 100644
index 0000000..83c05a8
--- /dev/null
+++ b/datastore/datastore-rxjava3/src/test/java/androidx/datastore/rxjava3/RxDataStoreTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.rxjava3;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.datastore.core.DataStore;
+import androidx.datastore.core.DataStoreFactory;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+import io.reactivex.rxjava3.core.Single;
+import io.reactivex.rxjava3.subscribers.TestSubscriber;
+import kotlinx.coroutines.CoroutineScopeKt;
+import kotlinx.coroutines.Dispatchers;
+
+public class RxDataStoreTest {
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private static Single<Byte> incrementByte(Byte byteIn) {
+ return Single.just(++byteIn);
+ }
+
+ @Test
+ public void testGetSingleValue() throws Exception {
+ File newFile = tempFolder.newFile();
+
+ DataStore<Byte> byteStore = DataStoreFactory.INSTANCE.create(
+ new TestingSerializer(),
+ null,
+ new ArrayList<>(),
+ CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()),
+ () -> newFile);
+
+ Byte firstByte = RxDataStore.data(byteStore).blockingFirst();
+ assertThat(firstByte).isEqualTo(0);
+
+ Single<Byte> incrementByte = RxDataStore.updateDataAsync(byteStore,
+ RxDataStoreTest::incrementByte);
+
+ assertThat(incrementByte.blockingGet()).isEqualTo(1);
+
+ firstByte = RxDataStore.data(byteStore).blockingFirst();
+ assertThat(firstByte).isEqualTo(1);
+ }
+
+ @Test
+ public void testTake3() throws Exception {
+ File newFile = tempFolder.newFile();
+
+ DataStore<Byte> byteStore = DataStoreFactory.INSTANCE.create(
+ new TestingSerializer(),
+ null,
+ new ArrayList<>(),
+ CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()),
+ () -> newFile);
+
+ TestSubscriber<Byte> testSubscriber = RxDataStore.data(byteStore).test();
+
+ RxDataStore.updateDataAsync(byteStore, RxDataStoreTest::incrementByte);
+ RxDataStore.updateDataAsync(byteStore, RxDataStoreTest::incrementByte);
+
+ testSubscriber.awaitCount(3).assertValues((byte) 0, (byte) 1, (byte) 2);
+ }
+
+
+ @Test
+ public void testReadFailure() throws Exception {
+ File newFile = tempFolder.newFile();
+ TestingSerializer testingSerializer = new TestingSerializer();
+
+ DataStore<Byte> byteStore = DataStoreFactory.INSTANCE.create(
+ testingSerializer,
+ null,
+ new ArrayList<>(),
+ CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()),
+ () -> newFile);
+
+ testingSerializer.setFailingRead(true);
+
+ TestSubscriber<Byte> testSubscriber = RxDataStore.data(byteStore).test();
+
+ assertThat(testSubscriber.await(5, TimeUnit.SECONDS)).isTrue();
+
+ testSubscriber.assertError(IOException.class);
+
+ testingSerializer.setFailingRead(false);
+
+ testSubscriber = RxDataStore.data(byteStore).test();
+ testSubscriber.awaitCount(1).assertValues((byte) 0);
+ }
+
+ @Test
+ public void testWriteFailure() throws Exception {
+ File newFile = tempFolder.newFile();
+ TestingSerializer testingSerializer = new TestingSerializer();
+
+ DataStore<Byte> byteStore = DataStoreFactory.INSTANCE.create(
+ testingSerializer,
+ null,
+ new ArrayList<>(),
+ CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()),
+ () -> newFile);
+
+ TestSubscriber<Byte> testSubscriber = RxDataStore.data(byteStore).test();
+
+ testingSerializer.setFailingWrite(true);
+ Single<Byte> incrementByte = RxDataStore.updateDataAsync(byteStore,
+ RxDataStoreTest::incrementByte);
+
+ incrementByte.cache().test().await().assertError(IOException.class);
+
+ testSubscriber.awaitCount(1).assertNoErrors().assertValues((byte) 0);
+ testingSerializer.setFailingWrite(false);
+
+ Single<Byte> incrementByte2 = RxDataStore.updateDataAsync(byteStore,
+ RxDataStoreTest::incrementByte);
+ assertThat(incrementByte2.blockingGet()).isEqualTo((byte) 1);
+
+ testSubscriber.awaitCount(2).assertValues((byte) 0, (byte) 1);
+ }
+}
diff --git a/lifecycle/.idea/codeStyles/Project.xml b/lifecycle/.idea/codeStyles/Project.xml
new file mode 120000
index 0000000..b52b28c
--- /dev/null
+++ b/lifecycle/.idea/codeStyles/Project.xml
@@ -0,0 +1 @@
+../../../.idea/codeStyles/Project.xml
\ No newline at end of file
diff --git a/lifecycle/.idea/codeStyles/codeStyleConfig.xml b/lifecycle/.idea/codeStyles/codeStyleConfig.xml
new file mode 120000
index 0000000..19c4848
--- /dev/null
+++ b/lifecycle/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1 @@
+../../../.idea/codeStyles/codeStyleConfig.xml
\ No newline at end of file
diff --git a/lifecycle/.idea/copyright/AndroidCopyright.xml b/lifecycle/.idea/copyright/AndroidCopyright.xml
new file mode 120000
index 0000000..afbbd04
--- /dev/null
+++ b/lifecycle/.idea/copyright/AndroidCopyright.xml
@@ -0,0 +1 @@
+../../../.idea/copyright/AndroidCopyright.xml
\ No newline at end of file
diff --git a/lifecycle/.idea/copyright/profiles_settings.xml b/lifecycle/.idea/copyright/profiles_settings.xml
new file mode 120000
index 0000000..5996ccd
--- /dev/null
+++ b/lifecycle/.idea/copyright/profiles_settings.xml
@@ -0,0 +1 @@
+../../../.idea/copyright/profiles_settings.xml
\ No newline at end of file
diff --git a/lifecycle/.idea/inspectionProfiles/Project_Default.xml b/lifecycle/.idea/inspectionProfiles/Project_Default.xml
new file mode 120000
index 0000000..a7481f4
--- /dev/null
+++ b/lifecycle/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1 @@
+../../../.idea/inspectionProfiles/Project_Default.xml
\ No newline at end of file
diff --git a/lifecycle/.idea/scopes/Ignore_API_Files.xml b/lifecycle/.idea/scopes/Ignore_API_Files.xml
new file mode 120000
index 0000000..3361ee1
--- /dev/null
+++ b/lifecycle/.idea/scopes/Ignore_API_Files.xml
@@ -0,0 +1 @@
+../../../.idea/scopes/Ignore_API_Files.xml
\ No newline at end of file
diff --git a/lifecycle/.idea/scopes/buildSrc.xml b/lifecycle/.idea/scopes/buildSrc.xml
new file mode 120000
index 0000000..25b7d3b
--- /dev/null
+++ b/lifecycle/.idea/scopes/buildSrc.xml
@@ -0,0 +1 @@
+../../../.idea/scopes/buildSrc.xml
\ No newline at end of file
diff --git a/lifecycle/gradle b/lifecycle/gradle
new file mode 120000
index 0000000..1c936b3
--- /dev/null
+++ b/lifecycle/gradle
@@ -0,0 +1 @@
+../playground-common/gradle
\ No newline at end of file
diff --git a/lifecycle/gradle.properties b/lifecycle/gradle.properties
new file mode 120000
index 0000000..d952fb0
--- /dev/null
+++ b/lifecycle/gradle.properties
@@ -0,0 +1 @@
+../playground-common/androidx-shared.properties
\ No newline at end of file
diff --git a/lifecycle/gradlew b/lifecycle/gradlew
new file mode 120000
index 0000000..05b75179
--- /dev/null
+++ b/lifecycle/gradlew
@@ -0,0 +1 @@
+../playground-common/gradlew
\ No newline at end of file
diff --git a/lifecycle/gradlew.bat b/lifecycle/gradlew.bat
new file mode 120000
index 0000000..b20877e
--- /dev/null
+++ b/lifecycle/gradlew.bat
@@ -0,0 +1 @@
+../playground-common/gradlew.bat
\ No newline at end of file
diff --git a/lifecycle/integration-tests/kotlintestapp/build.gradle b/lifecycle/integration-tests/kotlintestapp/build.gradle
index 432adec..e786607 100644
--- a/lifecycle/integration-tests/kotlintestapp/build.gradle
+++ b/lifecycle/integration-tests/kotlintestapp/build.gradle
@@ -28,7 +28,7 @@
dependencies {
implementation(project(":lifecycle:lifecycle-runtime-ktx"))
- implementation(project(":activity:activity")) {
+ implementation(projectOrArtifact(":activity:activity")) {
exclude group: 'androidx.lifecycle'
}
implementation(project(":lifecycle:lifecycle-viewmodel-savedstate")) {
diff --git a/lifecycle/integration-tests/testapp/build.gradle b/lifecycle/integration-tests/testapp/build.gradle
index 975f3ec..d231187 100644
--- a/lifecycle/integration-tests/testapp/build.gradle
+++ b/lifecycle/integration-tests/testapp/build.gradle
@@ -24,9 +24,12 @@
dependencies {
implementation(KOTLIN_STDLIB)
- implementation(project(":fragment:fragment"))
+ implementation(projectOrArtifact(":fragment:fragment")) {
+ exclude group: "androidx.lifecycle", module: "lifecycle-runtime"
+ }
implementation(project(":lifecycle:lifecycle-process"))
implementation(project(":lifecycle:lifecycle-common"))
+ implementation(project(":lifecycle:lifecycle-runtime"))
annotationProcessor(project(":lifecycle:lifecycle-compiler"))
androidTestAnnotationProcessor(project(":lifecycle:lifecycle-compiler"))
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
index 7969a7c..4a710c9 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
@@ -38,7 +38,11 @@
api(project(":lifecycle:lifecycle-livedata-core"))
api(project(":lifecycle:lifecycle-viewmodel"))
- androidTestImplementation project(":fragment:fragment"), {
+ androidTestImplementation project(":lifecycle:lifecycle-runtime")
+ androidTestImplementation project(":lifecycle:lifecycle-livedata-core")
+ androidTestImplementation projectOrArtifact(":fragment:fragment"), {
+ exclude group: 'androidx.lifecycle', module: 'lifecycle-runtime'
+ exclude group: 'androidx.lifecycle', module: 'lifecycle-livedata-core'
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-savedstate'
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel'
}
diff --git a/lifecycle/settings.gradle b/lifecycle/settings.gradle
new file mode 100644
index 0000000..a451397
--- /dev/null
+++ b/lifecycle/settings.gradle
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// see ../playground-common/README.md for details on how this works
+rootProject.name = "navigation-playground"
+apply from: "../playground-common/playground-include-settings.gradle"
+setupPlayground(this, "..")
+selectProjectsFromAndroidX({ name ->
+ if (name.startsWith(":lifecycle")) return true
+ if (name == ":annotation:annotation") return true
+ if (name == ":internal-testutils-runtime") return true
+ if (name == ":internal-testutils-truth") return true
+ return false
+})
+
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
index e7de321..b167251 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
@@ -463,12 +463,9 @@
checkCallingThread();
// Choose the fallback route if it's not already selected.
- // Otherwise, select the default route.
RouteInfo fallbackRoute = sGlobal.chooseFallbackRoute();
if (sGlobal.getSelectedRoute() != fallbackRoute) {
sGlobal.selectRoute(fallbackRoute, reason);
- } else {
- sGlobal.selectRoute(sGlobal.getDefaultRoute(), reason);
}
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/AutoClosingRoomOpenHelperTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/AutoClosingRoomOpenHelperTest.java
index 8554527..3033b44 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/AutoClosingRoomOpenHelperTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/AutoClosingRoomOpenHelperTest.java
@@ -63,16 +63,16 @@
@Before
public void createDb() throws TimeoutException, InterruptedException {
Context context = ApplicationProvider.getApplicationContext();
+ context.deleteDatabase("testDb");
mDb = Room.databaseBuilder(context, TestDatabase.class, "testDb")
.setAutoCloseTimeout(10, TimeUnit.MILLISECONDS)
.addCallback(mCallback).build();
mUserDao = mDb.getUserDao();
- drain();
}
@After
public void cleanUp() throws Exception {
- mDb.clearAllTables();
+ drain();
mDb.close();
}
@@ -124,11 +124,9 @@
@Test
@MediumTest
public void slowCursorClosing_keepsDbAlive() throws Exception {
- assertFalse(mCallback.mOpened);
User user = TestUtil.createUser(1);
user.setName("bob");
mUserDao.insert(user);
- assertTrue(mCallback.mOpened);
mUserDao.load(1);
Cursor cursor = mDb.query("select * from user", null);
@@ -194,20 +192,22 @@
@MediumTest
public void testCanExecSqlInCallback() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
-
- mDb = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+ context.deleteDatabase("testDb2");
+ TestDatabase db = Room.databaseBuilder(context, TestDatabase.class, "testDb2")
.setAutoCloseTimeout(10, TimeUnit.MILLISECONDS)
.addCallback(new ExecSqlInCallback())
.build();
- mDb.getUserDao().insert(TestUtil.createUser(1));
+ db.getUserDao().insert(TestUtil.createUser(1));
+
+ db.close();
}
@Test
public void testManuallyRoomDatabaseClose() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
// Create a new db since the other one is cleared in the @After
- TestDatabase testDatabase = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+ TestDatabase testDatabase = Room.databaseBuilder(context, TestDatabase.class, "testDb2")
.setAutoCloseTimeout(10, TimeUnit.MILLISECONDS)
.addCallback(new ExecSqlInCallback())
.build();
@@ -222,7 +222,7 @@
assertFalse(testDatabase.isOpen());
assertFalse(testDatabase.isOpen());
- TestDatabase testDatabase2 = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+ TestDatabase testDatabase2 = Room.databaseBuilder(context, TestDatabase.class, "testDb2")
.setAutoCloseTimeout(10, TimeUnit.MILLISECONDS)
.addCallback(new ExecSqlInCallback())
.build();
@@ -235,7 +235,7 @@
public void testManuallyOpenHelperClose() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
// Create a new db since the other one is cleared in the @After
- TestDatabase testDatabase = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+ TestDatabase testDatabase = Room.databaseBuilder(context, TestDatabase.class, "testDb2")
.setAutoCloseTimeout(10, TimeUnit.MILLISECONDS)
.addCallback(new ExecSqlInCallback())
.build();
@@ -248,7 +248,7 @@
}).hasMessageThat().contains("closed");
assertFalse(testDatabase.isOpen());
- TestDatabase testDatabase2 = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+ TestDatabase testDatabase2 = Room.databaseBuilder(context, TestDatabase.class, "testDb2")
.setAutoCloseTimeout(10, TimeUnit.MILLISECONDS)
.addCallback(new ExecSqlInCallback())
.build();
@@ -294,8 +294,8 @@
public void invalidationObserver_canRequeryDb() throws TimeoutException, InterruptedException {
Context context = ApplicationProvider.getApplicationContext();
- context.deleteDatabase("testDb");
- mDb = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+ context.deleteDatabase("testDb2");
+ TestDatabase db = Room.databaseBuilder(context, TestDatabase.class, "testDb2")
// create contention for callback
.setAutoCloseTimeout(0, TimeUnit.MILLISECONDS)
.addCallback(mCallback).build();
@@ -303,20 +303,21 @@
AtomicInteger userCount = new AtomicInteger(0);
UserTableObserver userTableObserver = new UserTableObserver(
- () -> userCount.set(mUserDao.count()));
+ () -> userCount.set(db.getUserDao().count()));
- mDb.getInvalidationTracker().addObserver(userTableObserver);
+ db.getInvalidationTracker().addObserver(userTableObserver);
- mDb.getUserDao().insert(TestUtil.createUser(1));
- mDb.getUserDao().insert(TestUtil.createUser(2));
- mDb.getUserDao().insert(TestUtil.createUser(3));
- mDb.getUserDao().insert(TestUtil.createUser(4));
- mDb.getUserDao().insert(TestUtil.createUser(5));
- mDb.getUserDao().insert(TestUtil.createUser(6));
- mDb.getUserDao().insert(TestUtil.createUser(7));
+ db.getUserDao().insert(TestUtil.createUser(1));
+ db.getUserDao().insert(TestUtil.createUser(2));
+ db.getUserDao().insert(TestUtil.createUser(3));
+ db.getUserDao().insert(TestUtil.createUser(4));
+ db.getUserDao().insert(TestUtil.createUser(5));
+ db.getUserDao().insert(TestUtil.createUser(6));
+ db.getUserDao().insert(TestUtil.createUser(7));
drain();
assertEquals(7, userCount.get());
+ db.close();
}
@Test
@@ -325,8 +326,8 @@
InterruptedException {
Context context = ApplicationProvider.getApplicationContext();
- context.deleteDatabase("testDb");
- mDb = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+ context.deleteDatabase("testDb2");
+ TestDatabase db = Room.databaseBuilder(context, TestDatabase.class, "testDb2")
// create contention for callback
.setAutoCloseTimeout(0, TimeUnit.MILLISECONDS)
.addCallback(mCallback).build();
@@ -336,21 +337,22 @@
UserTableObserver userTableObserver =
new UserTableObserver(invalidationCount::getAndIncrement);
- mDb.getInvalidationTracker().addObserver(userTableObserver);
+ db.getInvalidationTracker().addObserver(userTableObserver);
- mDb.getUserDao().insert(TestUtil.createUser(1));
+ db.getUserDao().insert(TestUtil.createUser(1));
drain();
assertEquals(1, invalidationCount.get());
Thread.sleep(100); // Let db auto close
- mDb.getInvalidationTracker().notifyObserversByTableNames("user");
+ db.getInvalidationTracker().notifyObserversByTableNames("user");
drain();
assertEquals(2, invalidationCount.get());
+ db.close();
}
private void drain() throws TimeoutException, InterruptedException {
diff --git a/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt b/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
index bee72dd..1a6147b 100644
--- a/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
+++ b/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
@@ -23,7 +23,7 @@
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
+import androidx.test.filters.MediumTest
import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
import org.junit.After
@@ -35,7 +35,7 @@
import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
-@SmallTest
+@MediumTest
public class AutoCloserTest {
@get:Rule
diff --git a/settings.gradle b/settings.gradle
index 07e33f3..72c87d0 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -285,8 +285,13 @@
[BuildType.MAIN])
includeProject(":datastore:datastore-preferences-core:datastore-preferences-proto",
"datastore/datastore-preferences-core/datastore-preferences-proto", [BuildType.MAIN])
+includeProject(":datastore:datastore-preferences-rxjava2",
+ "datastore/datastore-preferences-rxjava2", [BuildType.MAIN])
+includeProject(":datastore:datastore-preferences-rxjava3",
+ "datastore/datastore-preferences-rxjava3", [BuildType.MAIN])
includeProject(":datastore:datastore-proto", "datastore/datastore-proto", [BuildType.MAIN])
includeProject(":datastore:datastore-rxjava2", "datastore/datastore-rxjava2", [BuildType.MAIN])
+includeProject(":datastore:datastore-rxjava3", "datastore/datastore-rxjava3", [BuildType.MAIN])
includeProject(":datastore:datastore-sampleapp", "datastore/datastore-sampleapp", [BuildType.MAIN])
includeProject(":documentfile:documentfile", "documentfile/documentfile", [BuildType.MAIN])
includeProject(":drawerlayout:drawerlayout", "drawerlayout/drawerlayout", [BuildType.MAIN])
diff --git a/wear/wear-watchface-client/api/current.txt b/wear/wear-watchface-client/api/current.txt
index c744f24..1cdcbe1 100644
--- a/wear/wear-watchface-client/api/current.txt
+++ b/wear/wear-watchface-client/api/current.txt
@@ -57,7 +57,7 @@
method public long getPreviewReferenceTimeMillis();
method public void performAmbientTick();
method public void sendTouchEvent(int xPosition, int yPosition, int tapType);
- method public void setSystemState(androidx.wear.watchface.data.SystemState systemState);
+ method public void setSystemState(androidx.wear.watchface.client.SystemState systemState);
method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
property public abstract String instanceId;
diff --git a/wear/wear-watchface-client/api/public_plus_experimental_current.txt b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
index 3e7223f..ea7f3e7 100644
--- a/wear/wear-watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
@@ -57,7 +57,7 @@
method public long getPreviewReferenceTimeMillis();
method public void performAmbientTick();
method public void sendTouchEvent(int xPosition, int yPosition, int tapType);
- method public void setSystemState(androidx.wear.watchface.data.SystemState systemState);
+ method public void setSystemState(androidx.wear.watchface.client.SystemState systemState);
method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
property public abstract String instanceId;
diff --git a/wear/wear-watchface-client/api/restricted_current.txt b/wear/wear-watchface-client/api/restricted_current.txt
index 7961108..0ef5468 100644
--- a/wear/wear-watchface-client/api/restricted_current.txt
+++ b/wear/wear-watchface-client/api/restricted_current.txt
@@ -57,7 +57,7 @@
method public long getPreviewReferenceTimeMillis();
method public void performAmbientTick();
method public void sendTouchEvent(int xPosition, int yPosition, @androidx.wear.watchface.client.TapType int tapType);
- method public void setSystemState(androidx.wear.watchface.data.SystemState systemState);
+ method public void setSystemState(androidx.wear.watchface.client.SystemState systemState);
method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
property public abstract String instanceId;
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
index 5d41c43..f8b63aa 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
@@ -32,7 +32,6 @@
import androidx.wear.watchface.control.IInteractiveWatchFaceSysUI
import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
-import androidx.wear.watchface.data.SystemState
import androidx.wear.watchface.style.UserStyle
import java.util.Objects
@@ -219,7 +218,7 @@
override fun setSystemState(systemState: SystemState) {
iInteractiveWatchFaceSysUI.setSystemState(
- SystemState(
+ androidx.wear.watchface.data.SystemState(
systemState.inAmbientMode,
systemState.interruptionFilter
)
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
index ad56258..9df8117 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
@@ -108,6 +108,8 @@
private val styleListeners = HashSet<UserStyleListener>()
+ private val idToStyleSetting = schema.userStyleSettings.associateBy { it.id }
+
/**
* The current [UserStyle]. Assigning to this property triggers immediate [UserStyleListener]
* callbacks if if any options have changed.
@@ -128,11 +130,12 @@
field.selectedOptions as HashMap<UserStyleSetting, UserStyleSetting.Option>
for ((setting, option) in style.selectedOptions) {
// Ignore an unrecognized setting.
- val styleSetting = field.selectedOptions[setting] ?: continue
+ val localSetting = idToStyleSetting[setting.id] ?: continue
+ val styleSetting = field.selectedOptions[localSetting] ?: continue
if (styleSetting.id != option.id) {
changed = true
}
- hashmap[setting] = option
+ hashmap[localSetting] = option
}
if (!changed) {
diff --git a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleRepositoryTest.kt b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleRepositoryTest.kt
index e8cc5b6..c4c338f 100644
--- a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleRepositoryTest.kt
+++ b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleRepositoryTest.kt
@@ -122,7 +122,6 @@
Mockito.verify(mockListener3).onUserStyleChanged(userStyleRepository.userStyle)
}
- @Test
fun assigning_userStyle() {
val newStyle = UserStyle(
hashMapOf(
@@ -140,6 +139,40 @@
}
@Test
+ fun assign_userStyle_with_distinctButMatchingRefs() {
+ val colorStyleSetting2 = ListUserStyleSetting(
+ "color_style_setting",
+ "Colors",
+ "Watchface colorization", /* icon = */
+ null,
+ colorStyleList,
+ listOf(Layer.BASE_LAYER)
+ )
+ val watchHandStyleSetting2 = ListUserStyleSetting(
+ "hand_style_setting",
+ "Hand Style",
+ "Hand visual look", /* icon = */
+ null,
+ watchHandStyleList,
+ listOf(Layer.TOP_LAYER)
+ )
+
+ val newStyle = UserStyle(
+ hashMapOf(
+ colorStyleSetting2 to greenStyleOption,
+ watchHandStyleSetting2 to gothicStyleOption
+ )
+ )
+
+ userStyleRepository.userStyle = newStyle
+
+ assertThat(userStyleRepository.userStyle.selectedOptions[colorStyleSetting])
+ .isEqualTo(greenStyleOption)
+ assertThat(userStyleRepository.userStyle.selectedOptions[watchHandStyleSetting])
+ .isEqualTo(gothicStyleOption)
+ }
+
+ @Test
fun defaultValues() {
val watchHandLengthOption =
userStyleRepository.userStyle.selectedOptions[watchHandLengthStyleSetting]!! as
diff --git a/wear/wear-watchface/api/current.txt b/wear/wear-watchface/api/current.txt
index 37fb061..b3a90c4 100644
--- a/wear/wear-watchface/api/current.txt
+++ b/wear/wear-watchface/api/current.txt
@@ -59,6 +59,7 @@
public static final class Complication.Builder {
method public androidx.wear.watchface.Complication build();
method public androidx.wear.watchface.Complication.Builder setDefaultProviderType(androidx.wear.complications.data.ComplicationType defaultProviderType);
+ method public androidx.wear.watchface.Complication.Builder setEnabled(boolean enabled);
}
public static final class Complication.Companion {
@@ -242,13 +243,14 @@
}
public final class WatchState {
- ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
+ ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis, boolean isHeadless);
method public long getAnalogPreviewReferenceTimeMillis();
method public long getDigitalPreviewReferenceTimeMillis();
method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
method public boolean hasBurnInProtection();
method public boolean hasLowBitAmbient();
method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient();
+ method public boolean isHeadless();
method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible();
property public final long analogPreviewReferenceTimeMillis;
property public final long digitalPreviewReferenceTimeMillis;
@@ -256,6 +258,7 @@
property public final boolean hasLowBitAmbient;
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter;
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient;
+ property public final boolean isHeadless;
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible;
}
diff --git a/wear/wear-watchface/api/public_plus_experimental_current.txt b/wear/wear-watchface/api/public_plus_experimental_current.txt
index 37fb061..b3a90c4 100644
--- a/wear/wear-watchface/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface/api/public_plus_experimental_current.txt
@@ -59,6 +59,7 @@
public static final class Complication.Builder {
method public androidx.wear.watchface.Complication build();
method public androidx.wear.watchface.Complication.Builder setDefaultProviderType(androidx.wear.complications.data.ComplicationType defaultProviderType);
+ method public androidx.wear.watchface.Complication.Builder setEnabled(boolean enabled);
}
public static final class Complication.Companion {
@@ -242,13 +243,14 @@
}
public final class WatchState {
- ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
+ ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis, boolean isHeadless);
method public long getAnalogPreviewReferenceTimeMillis();
method public long getDigitalPreviewReferenceTimeMillis();
method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
method public boolean hasBurnInProtection();
method public boolean hasLowBitAmbient();
method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient();
+ method public boolean isHeadless();
method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible();
property public final long analogPreviewReferenceTimeMillis;
property public final long digitalPreviewReferenceTimeMillis;
@@ -256,6 +258,7 @@
property public final boolean hasLowBitAmbient;
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter;
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient;
+ property public final boolean isHeadless;
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible;
}
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index a4ad2dc..cc848dc 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -59,6 +59,7 @@
public static final class Complication.Builder {
method public androidx.wear.watchface.Complication build();
method public androidx.wear.watchface.Complication.Builder setDefaultProviderType(androidx.wear.complications.data.ComplicationType defaultProviderType);
+ method public androidx.wear.watchface.Complication.Builder setEnabled(boolean enabled);
}
public static final class Complication.Companion {
@@ -133,11 +134,13 @@
method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Integer> getInterruptionFilter();
method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isAmbient();
method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging();
+ method public boolean isHeadless();
method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isVisible();
method public void setAnalogPreviewReferenceTimeMillis(long p);
method public void setDigitalPreviewReferenceTimeMillis(long p);
method public void setHasBurnInProtection(boolean p);
method public void setHasLowBitAmbient(boolean p);
+ method public void setHeadless(boolean p);
method public void setInterruptionFilter(androidx.wear.watchface.MutableObservableWatchData<java.lang.Integer> p);
property public final long analogPreviewReferenceTimeMillis;
property public final long digitalPreviewReferenceTimeMillis;
@@ -146,6 +149,7 @@
property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Integer> interruptionFilter;
property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isAmbient;
property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging;
+ property public final boolean isHeadless;
property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isVisible;
}
@@ -287,13 +291,14 @@
}
public final class WatchState {
- ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
+ ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis, boolean isHeadless);
method public long getAnalogPreviewReferenceTimeMillis();
method public long getDigitalPreviewReferenceTimeMillis();
method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
method public boolean hasBurnInProtection();
method public boolean hasLowBitAmbient();
method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient();
+ method public boolean isHeadless();
method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible();
property public final long analogPreviewReferenceTimeMillis;
property public final long digitalPreviewReferenceTimeMillis;
@@ -301,6 +306,7 @@
property public final boolean hasLowBitAmbient;
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter;
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient;
+ property public final boolean isHeadless;
property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible;
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
index 2074339..ab40b74 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
@@ -244,7 +244,12 @@
canvasComplication: CanvasComplication,
supportedTypes: List<ComplicationType>,
defaultProviderPolicy: DefaultComplicationProviderPolicy,
- defaultProviderType: ComplicationType
+ defaultProviderType: ComplicationType,
+ /**
+ * The initial state of the complication. Note complications can be enabled / disabled by
+ * [UserStyleSetting.ComplicationsUserStyleSetting].
+ */
+ initiallyEnabled: Boolean
) {
public companion object {
internal val unitSquare = RectF(0f, 0f, 1f, 1f)
@@ -346,6 +351,7 @@
private val complicationBounds: ComplicationBounds
) {
private var defaultProviderType = ComplicationType.NOT_CONFIGURED
+ private var initiallyEnabled = true
/**
* Sets the initial [ComplicationType] to use with the initial complication provider.
@@ -359,6 +365,11 @@
return this
}
+ public fun setEnabled(enabled: Boolean): Builder {
+ this.initiallyEnabled = enabled
+ return this
+ }
+
/** Constructs the [Complication]. */
public fun build(): Complication = Complication(
id,
@@ -367,7 +378,8 @@
renderer,
supportedTypes,
defaultProviderPolicy,
- defaultProviderType
+ defaultProviderType,
+ initiallyEnabled
)
}
@@ -412,7 +424,7 @@
internal var enabledDirty = true
/** Whether or not the complication should be drawn and accept taps. */
- public var enabled: Boolean = true
+ public var enabled: Boolean = initiallyEnabled
@JvmName("isEnabled")
@UiThread
get
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index bd3728c..97a6926 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -490,39 +490,41 @@
}
)
- WatchFaceConfigActivity.registerWatchFace(
- componentName,
- object : WatchFaceConfigDelegate {
- override fun getUserStyleSchema() = userStyleRepository.schema.toWireFormat()
+ if (!watchState.isHeadless) {
+ WatchFaceConfigActivity.registerWatchFace(
+ componentName,
+ object : WatchFaceConfigDelegate {
+ override fun getUserStyleSchema() = userStyleRepository.schema.toWireFormat()
- override fun getUserStyle() = userStyleRepository.userStyle.toWireFormat()
+ override fun getUserStyle() = userStyleRepository.userStyle.toWireFormat()
- override fun setUserStyle(userStyle: UserStyleWireFormat) {
- userStyleRepository.userStyle =
- UserStyle(userStyle, userStyleRepository.schema)
+ override fun setUserStyle(userStyle: UserStyleWireFormat) {
+ userStyleRepository.userStyle =
+ UserStyle(userStyle, userStyleRepository.schema)
+ }
+
+ override fun getBackgroundComplicationId() =
+ complicationsManager.getBackgroundComplication()?.id
+
+ override fun getComplicationsMap() = complicationsManager.complications
+
+ override fun getCalendar() = calendar
+
+ override fun getComplicationIdAt(tapX: Int, tapY: Int) =
+ complicationsManager.getComplicationAt(tapX, tapY)?.id
+
+ override fun brieflyHighlightComplicationId(complicationId: Int) {
+ complicationsManager.bringAttentionToComplication(complicationId)
+ }
+
+ override fun takeScreenshot(
+ drawRect: Rect,
+ calendar: Calendar,
+ renderParameters: RenderParametersWireFormat
+ ) = renderer.takeScreenshot(calendar, RenderParameters(renderParameters))
}
-
- override fun getBackgroundComplicationId() =
- complicationsManager.getBackgroundComplication()?.id
-
- override fun getComplicationsMap() = complicationsManager.complications
-
- override fun getCalendar() = calendar
-
- override fun getComplicationIdAt(tapX: Int, tapY: Int) =
- complicationsManager.getComplicationAt(tapX, tapY)?.id
-
- override fun brieflyHighlightComplicationId(complicationId: Int) {
- complicationsManager.bringAttentionToComplication(complicationId)
- }
-
- override fun takeScreenshot(
- drawRect: Rect,
- calendar: Calendar,
- renderParameters: RenderParametersWireFormat
- ) = renderer.takeScreenshot(calendar, RenderParameters(renderParameters))
- }
- )
+ )
+ }
watchState.isAmbient.addObserver(ambientObserver)
watchState.interruptionFilter.addObserver(interruptionFilterObserver)
@@ -549,7 +551,9 @@
watchState.isAmbient.removeObserver(ambientObserver)
watchState.interruptionFilter.removeObserver(interruptionFilterObserver)
watchState.isVisible.removeObserver(visibilityObserver)
- WatchFaceConfigActivity.unregisterWatchFace(componentName)
+ if (!watchState.isHeadless) {
+ WatchFaceConfigActivity.unregisterWatchFace(componentName)
+ }
unregisterReceivers()
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index c1334b1..3ed9356 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -801,6 +801,7 @@
}
}
+ mutableWatchState.isHeadless = true
val watchState = mutableWatchState.asWatchState()
watchFaceImpl = WatchFaceImpl(
createWatchFace(fakeSurfaceHolder, watchState),
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
index 26df645..8b664c3 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
@@ -68,7 +68,10 @@
public val analogPreviewReferenceTimeMillis: Long,
/** UTC reference time for previews of digital watch faces in milliseconds since the epoch. */
- public val digitalPreviewReferenceTimeMillis: Long
+ public val digitalPreviewReferenceTimeMillis: Long,
+
+ /** Whether or not this is a headless watchface. */
+ public val isHeadless: Boolean
)
/** @hide */
@@ -83,6 +86,7 @@
public var hasBurnInProtection: Boolean = false
public var analogPreviewReferenceTimeMillis: Long = 0
public var digitalPreviewReferenceTimeMillis: Long = 0
+ public var isHeadless: Boolean = false
public fun asWatchState(): WatchState = WatchState(
interruptionFilter = interruptionFilter,
@@ -92,6 +96,7 @@
hasLowBitAmbient = hasLowBitAmbient,
hasBurnInProtection = hasBurnInProtection,
analogPreviewReferenceTimeMillis = analogPreviewReferenceTimeMillis,
- digitalPreviewReferenceTimeMillis = digitalPreviewReferenceTimeMillis
+ digitalPreviewReferenceTimeMillis = digitalPreviewReferenceTimeMillis,
+ isHeadless = isHeadless
)
}
\ No newline at end of file