Merge "AndroidX Webkit: demo the per-webview-enable API" into androidx-master-dev
diff --git a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java
index f9695e0..7c45fa9 100644
--- a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java
+++ b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java
@@ -23,14 +23,15 @@
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.RootMatchers.isPlatformPopup;
import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
-import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import android.app.Instrumentation;
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.os.SystemClock;
@@ -50,6 +51,8 @@
import androidx.test.espresso.action.Swipe;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.testutils.PollingCheck;
import org.hamcrest.Matcher;
import org.junit.After;
@@ -63,6 +66,7 @@
public class AppCompatSpinnerTest
extends AppCompatBaseViewTest<AppCompatSpinnerActivity, AppCompatSpinner> {
private static final String EARTH = "Earth";
+ private Instrumentation mInstrumentation;
public AppCompatSpinnerTest() {
super(AppCompatSpinnerActivity.class);
@@ -77,6 +81,8 @@
@Override
public void setUp() {
super.setUp();
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+
if (mActivity.getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
SystemClock.sleep(250);
@@ -105,6 +111,9 @@
// Click the spinner to show its popup content
onView(withId(spinnerId)).perform(click());
+ // Wait until the popup is showing
+ waitUntilPopupIsShown(spinner);
+
// The internal implementation details of the AppCompatSpinner's popup content depends
// on the platform version itself (in android.widget.PopupWindow) as well as on when the
// popup theme is being applied first (in XML or at runtime). Instead of trying to catch
@@ -124,6 +133,9 @@
// Click an entry in the popup to dismiss it
onView(withText(itemText)).perform(click());
+
+ // Wait until the popup is gone
+ waitUntilPopupIsHidden(spinner);
}
@LargeTest
@@ -172,6 +184,8 @@
assertThat(popup, instanceOf(AppCompatSpinner.DialogPopup.class));
onView(withId(R.id.spinner_dialog_popup)).perform(click());
+ // Wait until the popup is showing
+ waitUntilPopupIsShown(spinner);
final AppCompatSpinner.DialogPopup dialogPopup = (AppCompatSpinner.DialogPopup) popup;
assertThat(dialogPopup.mPopup, instanceOf(AlertDialog.class));
@@ -180,28 +194,47 @@
@LargeTest
@Test
public void testChangeOrientationDialogPopupPersists() {
- verifyChangeOrientationPopupPersists(R.id.spinner_dialog_popup);
+ verifyChangeOrientationPopupPersists(R.id.spinner_dialog_popup, true);
}
@LargeTest
@Test
public void testChangeOrientationDropdownPopupPersists() {
- verifyChangeOrientationPopupPersists(R.id.spinner_dropdown_popup);
+ verifyChangeOrientationPopupPersists(R.id.spinner_dropdown_popup, false);
}
- private void verifyChangeOrientationPopupPersists(@IdRes int spinnerId) {
+ private void verifyChangeOrientationPopupPersists(@IdRes int spinnerId, boolean isDialog) {
onView(withId(spinnerId)).perform(click());
+ // Wait until the popup is showing
+ waitUntilPopupIsShown((AppCompatSpinner) mActivity.findViewById(spinnerId));
+
+ // Use ActivityMonitor so that we can get the Activity instance after it has been
+ // recreated when the rotation request completes
+ Instrumentation.ActivityMonitor monitor =
+ new Instrumentation.ActivityMonitor(mActivity.getClass().getName(), null, false);
+ mInstrumentation.addMonitor(monitor);
mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
- onView(withText(EARTH)).check(matches(isDisplayed()));
+ SystemClock.sleep(250);
+ mInstrumentation.waitForIdleSync();
+
+ mActivity = (AppCompatSpinnerActivity) mInstrumentation.waitForMonitor(monitor);
+
+ // Now we can get the new (post-rotation) instance of our spinner
+ AppCompatSpinner newSpinner = mActivity.findViewById(spinnerId);
+ // And check that it's showing the popup
+ assertTrue(newSpinner.getInternalPopup().isShowing());
}
@LargeTest
@Test
public void testSlowScroll() {
- onView(withId(R.id.spinner_dropdown_popup_with_scroll)).perform(click());
-
final AppCompatSpinner spinner = mContainer
.findViewById(R.id.spinner_dropdown_popup_with_scroll);
+ onView(withId(R.id.spinner_dropdown_popup_with_scroll)).perform(click());
+
+ // Wait until the popup is showing
+ waitUntilPopupIsShown(spinner);
+
String secondItem = (String) spinner.getAdapter().getItem(1);
onView(isAssignableFrom(DropDownListView.class)).perform(slowScrollPopup());
@@ -211,8 +244,7 @@
// because we scroll twice with one element height each,
// the second item should not be visible
- onView(withText(secondItem))
- .check(doesNotExist());
+ onView(withText(secondItem)).check(doesNotExist());
}
private ViewAction slowScrollPopup() {
@@ -264,4 +296,22 @@
// so we add a little bit more to be safe
return child.getHeight() * 2;
}
+
+ private void waitUntilPopupIsShown(final AppCompatSpinner spinner) {
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return spinner.getInternalPopup().isShowing();
+ }
+ });
+ }
+
+ private void waitUntilPopupIsHidden(final AppCompatSpinner spinner) {
+ PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return !spinner.getInternalPopup().isShowing();
+ }
+ });
+ }
}
diff --git a/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml b/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml
index 38228a9..534048e 100644
--- a/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml
+++ b/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml
@@ -25,13 +25,20 @@
android:layout_height="wrap_content"
android:orientation="vertical">
+ <View
+ android:id="@+id/for_focus"
+ android:layout_width="match_parent"
+ android:layout_height="4dp"
+ android:focusable="true"/>
+
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/view_tinted_no_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/planets_array"
app:backgroundTint="@color/color_state_list_lilac"
- app:backgroundTintMode="src_in" />
+ app:backgroundTintMode="src_in"
+ android:focusable="false" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/view_tinted_background"
@@ -40,20 +47,23 @@
android:background="@drawable/test_drawable"
android:entries="@array/planets_array"
app:backgroundTint="@color/color_state_list_lilac"
- app:backgroundTintMode="src_in" />
+ app:backgroundTintMode="src_in"
+ android:focusable="false" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/view_untinted_no_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:entries="@array/planets_array" />
+ android:entries="@array/planets_array"
+ android:focusable="false" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/view_untinted_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/planets_array"
- android:background="@drawable/test_background_green" />
+ android:background="@drawable/test_background_green"
+ android:focusable="false" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/view_magenta_themed_popup"
@@ -62,7 +72,8 @@
android:entries="@array/planets_array"
app:backgroundTint="@color/color_state_list_lilac"
app:backgroundTintMode="src_in"
- app:popupTheme="@style/MagentaSpinnerPopupTheme" />
+ app:popupTheme="@style/MagentaSpinnerPopupTheme"
+ android:focusable="false" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/view_unthemed_popup"
@@ -70,7 +81,8 @@
android:layout_height="wrap_content"
android:entries="@array/planets_array"
app:backgroundTint="@color/color_state_list_lilac"
- app:backgroundTintMode="src_in" />
+ app:backgroundTintMode="src_in"
+ android:focusable="false" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/view_ocean_themed_popup"
@@ -78,28 +90,32 @@
android:layout_height="wrap_content"
android:entries="@array/planets_array"
android:spinnerMode="dropdown"
- app:popupTheme="@style/OceanSpinnerPopupTheme" />
+ app:popupTheme="@style/OceanSpinnerPopupTheme"
+ android:focusable="false" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/spinner_dialog_popup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/planets_array"
- android:spinnerMode="dialog" />
+ android:spinnerMode="dialog"
+ android:focusable="false" />
<androidx.appcompat.widget.AppCompatSpinner
- android:id="@+id/spinner_dropdown_popup"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:entries="@array/planets_array"
- android:spinnerMode="dropdown" />
+ android:id="@+id/spinner_dropdown_popup"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:entries="@array/planets_array"
+ android:focusable="false"
+ android:spinnerMode="dropdown" />
<androidx.appcompat.widget.AppCompatSpinner
- android:id="@+id/spinner_dropdown_popup_with_scroll"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:entries="@array/numbers_array"
- android:spinnerMode="dropdown" />
+ android:id="@+id/spinner_dropdown_popup_with_scroll"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:entries="@array/numbers_array"
+ android:focusable="false"
+ android:spinnerMode="dropdown" />
</LinearLayout>
</ScrollView>
diff --git a/benchmark/src/androidTest/AndroidManifest.xml b/benchmark/src/androidTest/AndroidManifest.xml
index 522a8f4..d3e8cf0 100644
--- a/benchmark/src/androidTest/AndroidManifest.xml
+++ b/benchmark/src/androidTest/AndroidManifest.xml
@@ -24,5 +24,7 @@
<!-- Important: disable debuggable for accurate performance results -->
<application
android:debuggable="false"
- tools:replace="android:debuggable" />
+ tools:replace="android:debuggable">
+ <activity android:name="android.app.Activity"/>
+ </application>
</manifest>
diff --git a/benchmark/src/androidTest/java/androidx/benchmark/ActivityScenarioBenchmark.kt b/benchmark/src/androidTest/java/androidx/benchmark/ActivityScenarioBenchmark.kt
new file mode 100644
index 0000000..5f21bd5
--- /dev/null
+++ b/benchmark/src/androidTest/java/androidx/benchmark/ActivityScenarioBenchmark.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark
+
+import android.app.Activity
+import androidx.test.core.app.ActivityScenario
+import androidx.test.filters.LargeTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@LargeTest
+@RunWith(JUnit4::class)
+class ActivityScenarioBenchmark {
+ @get:Rule
+ val benchmarkRule = BenchmarkRule()
+
+ private lateinit var activityRule: ActivityScenario<Activity>
+
+ @Before
+ fun setup() {
+ activityRule = ActivityScenario.launch(Activity::class.java)
+ }
+
+ @Test
+ fun activityScenario() {
+ activityRule.onActivity {
+ var i = 0
+ benchmarkRule.measureRepeated {
+ i++
+ }
+ }
+ }
+}
diff --git a/benchmark/src/androidTest/java/androidx/benchmark/ActivityTestRuleBenchmark.kt b/benchmark/src/androidTest/java/androidx/benchmark/ActivityTestRuleBenchmark.kt
new file mode 100644
index 0000000..e1a3547
--- /dev/null
+++ b/benchmark/src/androidTest/java/androidx/benchmark/ActivityTestRuleBenchmark.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark
+
+import android.app.Activity
+import androidx.test.filters.LargeTest
+import androidx.test.rule.ActivityTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@LargeTest
+@RunWith(JUnit4::class)
+class ActivityTestRuleBenchmark {
+ @get:Rule
+ val benchmarkRule = BenchmarkRule()
+
+ @get:Rule
+ val activityRule = ActivityTestRule(Activity::class.java)
+
+ @Test
+ fun activityTestRule() {
+ activityRule.runOnUiThread {
+ var i = 0
+ benchmarkRule.measureRepeated {
+ i++
+ }
+ }
+ }
+}
diff --git a/benchmark/src/main/java/androidx/benchmark/Clocks.kt b/benchmark/src/main/java/androidx/benchmark/Clocks.kt
index 41ad0c4..fa41332 100644
--- a/benchmark/src/main/java/androidx/benchmark/Clocks.kt
+++ b/benchmark/src/main/java/androidx/benchmark/Clocks.kt
@@ -18,6 +18,7 @@
import android.util.Log
import java.io.File
+import java.io.IOException
internal object Clocks {
private const val TAG = "Benchmark"
@@ -98,11 +99,17 @@
}
/**
- * Read the text of a file as a String, null if file doesn't exist.
+ * Read the text of a file as a String, null if file doesn't exist or can't be read.
*/
private fun readFileTextOrNull(path: String): String? {
- File(path).run {
- return if (exists()) readText().trim() else null
+ try {
+ File(path).run {
+ return if (exists()) {
+ readText().trim()
+ } else null
+ }
+ } catch (e: IOException) {
+ return null
}
}
}
diff --git a/benchmark/src/main/java/androidx/benchmark/IsolationActivity.kt b/benchmark/src/main/java/androidx/benchmark/IsolationActivity.kt
index 7a96db6..eaf50e8 100644
--- a/benchmark/src/main/java/androidx/benchmark/IsolationActivity.kt
+++ b/benchmark/src/main/java/androidx/benchmark/IsolationActivity.kt
@@ -41,6 +41,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
class IsolationActivity : android.app.Activity() {
var resumed = false
+ private var destroyed = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -50,7 +51,7 @@
overridePendingTransition(0, 0)
val old = singleton.getAndSet(this)
- if (old != null) {
+ if (old != null && !old.destroyed && !old.isFinishing) {
throw IllegalStateException("Only one IsolationActivity should exist")
}
@@ -71,6 +72,11 @@
resumed = false
}
+ override fun onDestroy() {
+ super.onDestroy()
+ destroyed = true
+ }
+
/** finish is ignored! we defer until [actuallyFinish] is called. */
override fun finish() {
}
diff --git a/build.gradle b/build.gradle
index 6d5d017..a583e16 100644
--- a/build.gradle
+++ b/build.gradle
@@ -48,6 +48,3 @@
apply plugin: AndroidXPlugin
-// AndroidX needed before jetify since it accesses the createArchive task name directly.
-apply from: 'buildSrc/jetify.gradle'
-
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt
index e336047..67eb40e 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt
@@ -61,6 +61,7 @@
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.extra
+import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.getPlugin
import org.gradle.kotlin.dsl.withType
import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
@@ -201,15 +202,22 @@
}
val createLibraryBuildInfoFilesTask =
tasks.register(CREATE_LIBRARY_BUILD_INFO_FILES_TASK)
- val buildOnServerTask = tasks.create(BUILD_ON_SERVER_TASK)
+
+ extra.set("versionChecker", GMavenVersionChecker(logger))
+ val createArchiveTask = Release.getGlobalFullZipTask(this)
+
+ val buildOnServerTask = tasks.create(BUILD_ON_SERVER_TASK, BuildOnServer::class.java)
buildOnServerTask.dependsOn(createLibraryBuildInfoFilesTask)
+ val partiallyDejetifyArchiveTask = partiallyDejetifyArchiveTask(
+ createArchiveTask.get().archiveFile)
+ buildOnServerTask.dependsOn(partiallyDejetifyArchiveTask)
+
val projectModules = ConcurrentHashMap<String, String>()
extra.set("projects", projectModules)
tasks.all { task ->
if (task.name.startsWith(Release.DIFF_TASK_PREFIX) ||
"distDocs" == task.name ||
- "partiallyDejetifyArchive" == task.name ||
CheckExternalDependencyLicensesTask.TASK_NAME == task.name) {
buildOnServerTask.dependsOn(task)
}
@@ -236,6 +244,15 @@
}
}
+ project(":jetifier-standalone").afterEvaluate { standAloneProject ->
+ partiallyDejetifyArchiveTask.configure {
+ it.dependsOn(standAloneProject.tasks.named("installDist"))
+ }
+ createArchiveTask.configure {
+ it.dependsOn(standAloneProject.tasks.named("dist"))
+ }
+ }
+
val createCoverageJarTask = Jacoco.createCoverageJarTask(this)
buildOnServerTask.dependsOn(createCoverageJarTask)
@@ -243,8 +260,6 @@
it.dependsOn(createCoverageJarTask)
}
- extra.set("versionChecker", GMavenVersionChecker(logger))
- Release.createGlobalArchiveTask(this)
val allDocsTask = DiffAndDocs.configureDiffAndDocs(this, projectDir,
DacOptions("androidx", "ANDROIDX_DATA"),
listOf(RELEASE_RULE))
diff --git a/buildSrc/src/main/kotlin/androidx/build/BuildOnServer.kt b/buildSrc/src/main/kotlin/androidx/build/BuildOnServer.kt
new file mode 100644
index 0000000..f5697d1
--- /dev/null
+++ b/buildSrc/src/main/kotlin/androidx/build/BuildOnServer.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+import java.io.FileNotFoundException
+
+/**
+ * Task for building all of Androidx libraries and documentation
+ *
+ * AndroidXPlugin configuration add dependencies to BuildOnServer for all of the tasks that
+ * produce artifacts that we want to build on server builds
+ * When BuildOnServer executes, it double-checks that all expected artifacts were built
+ */
+open class BuildOnServer : DefaultTask() {
+
+ init {
+ group = "Build"
+ description = "Builds all of the Androidx libraries and documentation"
+ }
+
+ @InputFiles
+ fun getRequiredFiles(): List<File> {
+ val distributionDirectory = project.getDistributionDirectory()
+ val buildId = getBuildId()
+ return listOf(
+ "android-support-public-docs-$buildId.zip",
+ "android-support-tipOfTree-docs-$buildId.zip",
+ "dokkaTipOfTreeDocs-$buildId.zip",
+ "dokkaPublicDocs-$buildId.zip",
+ "gmaven-diff-all-$buildId.zip",
+ "jetifier-standalone.zip",
+ "top-of-tree-m2repository-all-$buildId.zip",
+ "top-of-tree-m2repository-partially-dejetified-$buildId.zip"
+ ).map { fileName -> File(distributionDirectory, fileName) }
+ }
+
+ @TaskAction
+ fun checkAllBuildOutputs() {
+
+ val missingFiles = mutableListOf<String>()
+ getRequiredFiles().forEach { file ->
+ if (!file.exists()) {
+ missingFiles.add(file.name)
+ }
+ }
+
+ if (missingFiles.isNotEmpty()) {
+ val missingFileString = missingFiles.reduce { acc, s -> "$acc, $s" }
+ throw FileNotFoundException("buildOnServer required output missing: $missingFileString")
+ }
+ }
+}
\ No newline at end of file
diff --git a/buildSrc/jetify.gradle b/buildSrc/src/main/kotlin/androidx/build/Jetify.kt
similarity index 61%
rename from buildSrc/jetify.gradle
rename to buildSrc/src/main/kotlin/androidx/build/Jetify.kt
index 9efebca..1dcbc05 100644
--- a/buildSrc/jetify.gradle
+++ b/buildSrc/src/main/kotlin/androidx/build/Jetify.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018 The Android Open Source Project
+ * Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,16 @@
* limitations under the License.
*/
-import androidx.build.BuildServerConfigurationKt
-import androidx.build.Release
-def standaloneProject = project(":jetifier-standalone")
-def jetifierBin = file("${standaloneProject.buildDir}/install/jetifier-standalone/bin/jetifier-standalone")
+package androidx.build
-def archivesToDejetify = [
+import org.gradle.api.Project
+import org.gradle.api.file.RegularFile
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.Exec
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.api.tasks.bundling.Zip
+
+val archivesToDejetify = listOf(
"m2repository/androidx/activity/**",
"m2repository/androidx/annotation/**",
"m2repository/androidx/appcompat/**",
@@ -80,41 +84,41 @@
"m2repository/androidx/wear/**",
"m2repository/androidx/media2/**",
"m2repository/androidx/concurrent/**",
- "m2repository/androidx/sharetarget/**"
-]
+ "m2repository/androidx/sharetarget/**")
-task stripArchiveForPartialDejetification(type: Zip) {
- dependsOn tasks[Release.FULL_ARCHIVE_TASK_NAME]
- from zipTree(project.tasks['createArchive'].archivePath)
- destinationDir rootProject.buildDir
- archiveName "stripped_archive_partial.zip"
- include archivesToDejetify
- doLast {
- if (archivePath.exists()) {
- project.logger.info("stripArchiveForPartialDejetification sees that " + archivePath + " exists")
- } else {
- throw new Exception("stripArchiveForPartialDejetification sees that " + archivePath + " does not exist!?")
- }
+fun Project.partiallyDejetifyArchiveTask(archiveFile: Provider<RegularFile>): TaskProvider<Exec> {
+ val standaloneProject = project(":jetifier-standalone")
+ val stripTask = stripArchiveForPartialDejetificationTask(archiveFile)
+
+ return tasks.register("partiallyDejetifyArchive", Exec::class.java) {
+ val outputFileName = "${getDistributionDirectory().absolutePath}/" +
+ "top-of-tree-m2repository-partially-dejetified-${getBuildId()}.zip"
+ val jetifierBin = "${standaloneProject.buildDir}/install/jetifier-standalone/bin/" +
+ "jetifier-standalone"
+ val migrationConfig = "${standaloneProject.projectDir.getParentFile()}/migration.config"
+
+ it.dependsOn(stripTask)
+ it.inputs.file(stripTask.get().archiveFile)
+ it.outputs.file(outputFileName)
+
+ it.commandLine = listOf(
+ jetifierBin,
+ "-i", "${it.inputs.files.singleFile}",
+ "-o", "${it.outputs.files.singleFile}",
+ "-c", migrationConfig,
+ "--log", "warning",
+ "--reversed",
+ "--rebuildTopOfTree")
}
}
-task partiallyDejetifyArchive(type: Exec) {
- description "Produces a zip of partially dejetified artifacts by running Dejetifier against refactored" +
- " artifacts, for temporary migration purposes."
-
- dependsOn ':jetifier-standalone:installDist'
- dependsOn project.tasks['stripArchiveForPartialDejetification']
- inputs.file project.tasks['stripArchiveForPartialDejetification'].archivePath
-
- outputs.file "${BuildServerConfigurationKt.getDistributionDirectory(rootProject).absolutePath}/top-of-tree-m2repository-partially-dejetified-${BuildServerConfigurationKt.getBuildId()}.zip"
-
- commandLine (
- "${jetifierBin}",
- "-i", "${inputs.files.singleFile}",
- "-o", "${outputs.files.singleFile}",
- "-c", "${standaloneProject.projectDir.getParentFile()}/migration.config",
- "--log", "warning",
- "--reversed",
- "--rebuildTopOfTree"
- )
+fun Project.stripArchiveForPartialDejetificationTask(archiveFile: Provider<RegularFile>):
+ TaskProvider<Zip> {
+ return tasks.register("stripArchiveForPartialDejetification", Zip::class.java) {
+ it.dependsOn(rootProject.tasks.named(Release.FULL_ARCHIVE_TASK_NAME))
+ it.from(zipTree(archiveFile))
+ it.destinationDirectory.set(rootProject.buildDir)
+ it.archiveFileName.set("stripped_archive_partial.zip")
+ it.include(archivesToDejetify)
+ }
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt b/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
index 273b8b2..3c294d2 100644
--- a/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
@@ -25,24 +25,32 @@
import androidx.build.Strategy.TipOfTree
val RELEASE_RULE = docsRules("public", false) {
- prebuilts(LibraryGroups.ACTIVITY, "1.0.0-alpha07")
- prebuilts(LibraryGroups.ANNOTATION, "1.1.0-beta01")
- prebuilts(LibraryGroups.APPCOMPAT, "1.1.0-alpha04")
- prebuilts(LibraryGroups.ARCH_CORE, "2.1.0-alpha02")
+ prebuilts(LibraryGroups.ACTIVITY, "1.0.0-alpha08")
+ prebuilts(LibraryGroups.ANNOTATION, "1.1.0-rc01")
+ prebuilts(LibraryGroups.APPCOMPAT, "1.1.0-alpha05")
+ prebuilts(LibraryGroups.ARCH_CORE, "2.1.0-beta01")
prebuilts(LibraryGroups.ASYNCLAYOUTINFLATER, "1.0.0")
+ ignore(LibraryGroups.BENCHMARK.group, "benchmark-gradle-plugin")
+ prebuilts(LibraryGroups.BENCHMARK, "1.0.0-alpha01")
prebuilts(LibraryGroups.BIOMETRIC, "biometric", "1.0.0-alpha04")
prebuilts(LibraryGroups.BROWSER, "1.0.0")
+ ignore(LibraryGroups.CAMERA.group, "camera-view")
+ ignore(LibraryGroups.CAMERA.group, "camera-testing")
+ ignore(LibraryGroups.CAMERA.group, "camera-extensions")
+ ignore(LibraryGroups.CAMERA.group, "camera-extensions-stub")
+ ignore(LibraryGroups.CAMERA.group, "camera-testlib-extensions")
+ prebuilts(LibraryGroups.CAMERA, "1.0.0-alpha01")
ignore(LibraryGroups.CAR.group, "car-moderator")
prebuilts(LibraryGroups.CAR, "car-cluster", "1.0.0-alpha5")
prebuilts(LibraryGroups.CAR, "car", "1.0.0-alpha7")
.addStubs("car/stubs/android.car.jar")
prebuilts(LibraryGroups.CARDVIEW, "1.0.0")
- prebuilts(LibraryGroups.COLLECTION, "1.1.0-beta01")
- prebuilts(LibraryGroups.CONCURRENT, "1.0.0-alpha03")
+ prebuilts(LibraryGroups.COLLECTION, "1.1.0-rc01")
+ prebuilts(LibraryGroups.CONCURRENT, "1.0.0-beta01")
prebuilts(LibraryGroups.CONTENTPAGER, "1.0.0")
prebuilts(LibraryGroups.COORDINATORLAYOUT, "1.1.0-alpha01")
- prebuilts(LibraryGroups.CORE, "core", "1.1.0-alpha05")
- prebuilts(LibraryGroups.CORE, "core-ktx", "1.1.0-alpha05")
+ prebuilts(LibraryGroups.CORE, "core", "1.1.0-beta01")
+ prebuilts(LibraryGroups.CORE, "core-ktx", "1.1.0-beta01")
prebuilts(LibraryGroups.CURSORADAPTER, "1.0.0")
prebuilts(LibraryGroups.CUSTOMVIEW, "1.0.0")
prebuilts(LibraryGroups.DOCUMENTFILE, "1.0.0")
@@ -52,11 +60,11 @@
prebuilts(LibraryGroups.EMOJI, "1.0.0")
prebuilts(LibraryGroups.ENTERPRISE, "1.0.0-alpha01")
prebuilts(LibraryGroups.EXIFINTERFACE, "1.1.0-alpha01")
- prebuilts(LibraryGroups.FRAGMENT, "1.1.0-alpha07")
+ prebuilts(LibraryGroups.FRAGMENT, "1.1.0-alpha08")
prebuilts(LibraryGroups.GRIDLAYOUT, "1.0.0")
prebuilts(LibraryGroups.HEIFWRITER, "1.0.0")
prebuilts(LibraryGroups.INTERPOLATOR, "1.0.0")
- prebuilts(LibraryGroups.LEANBACK, "1.1.0-alpha01")
+ prebuilts(LibraryGroups.LEANBACK, "1.1.0-alpha02")
prebuilts(LibraryGroups.LEGACY, "1.0.0")
ignore(LibraryGroups.LIFECYCLE.group, "lifecycle-savedstate-core")
ignore(LibraryGroups.LIFECYCLE.group, "lifecycle-savedstate-fragment")
@@ -67,57 +75,54 @@
ignore(LibraryGroups.LIFECYCLE.group, "lifecycle-runtime-ktx")
ignore(LibraryGroups.LIFECYCLE.group, "lifecycle-runtime-ktx-lint")
prebuilts(LibraryGroups.LIFECYCLE, "lifecycle-viewmodel-savedstate", "1.0.0-alpha01")
- prebuilts(LibraryGroups.LIFECYCLE, "2.1.0-alpha04")
+ prebuilts(LibraryGroups.LIFECYCLE, "2.2.0-alpha01")
prebuilts(LibraryGroups.LOADER, "1.1.0-beta01")
prebuilts(LibraryGroups.LOCALBROADCASTMANAGER, "1.1.0-alpha01")
- prebuilts(LibraryGroups.MEDIA, "media", "1.1.0-alpha04")
+ prebuilts(LibraryGroups.MEDIA, "media", "1.1.0-beta01")
// TODO: Rename media-widget to media2-widget after 1.0.0-alpha06
prebuilts(LibraryGroups.MEDIA, "media-widget", "1.0.0-alpha06")
ignore(LibraryGroups.MEDIA2.group, "media2-widget")
ignore(LibraryGroups.MEDIA2.group, "media2-exoplayer")
- // TODO: Reenable to use media2-{common,player,session} prebuilts (b/130839413)
- ignore(LibraryGroups.MEDIA2.group, "media2-common")
- ignore(LibraryGroups.MEDIA2.group, "media2-player")
- ignore(LibraryGroups.MEDIA2.group, "media2-session")
- prebuilts(LibraryGroups.MEDIA2, "1.0.0-alpha04")
- prebuilts(LibraryGroups.MEDIAROUTER, "1.1.0-alpha03")
+ prebuilts(LibraryGroups.MEDIA2, "1.0.0-beta01")
+ prebuilts(LibraryGroups.MEDIAROUTER, "1.1.0-beta01")
ignore(LibraryGroups.NAVIGATION.group, "navigation-testing")
- prebuilts(LibraryGroups.NAVIGATION, "2.1.0-alpha02")
+ prebuilts(LibraryGroups.NAVIGATION, "2.1.0-alpha03")
prebuilts(LibraryGroups.PAGING, "2.1.0")
prebuilts(LibraryGroups.PALETTE, "1.0.0")
prebuilts(LibraryGroups.PERCENTLAYOUT, "1.0.0")
prebuilts(LibraryGroups.PERSISTENCE, "2.0.0")
- prebuilts(LibraryGroups.PREFERENCE, "preference-ktx", "1.1.0-alpha04")
- prebuilts(LibraryGroups.PREFERENCE, "1.1.0-alpha04")
+ prebuilts(LibraryGroups.PREFERENCE, "preference-ktx", "1.1.0-alpha05")
+ prebuilts(LibraryGroups.PREFERENCE, "1.1.0-alpha05")
prebuilts(LibraryGroups.PRINT, "1.0.0")
prebuilts(LibraryGroups.RECOMMENDATION, "1.0.0")
- prebuilts(LibraryGroups.RECYCLERVIEW, "recyclerview", "1.1.0-alpha04")
- prebuilts(LibraryGroups.RECYCLERVIEW, "recyclerview-selection", "1.1.0-alpha01")
- prebuilts(LibraryGroups.REMOTECALLBACK, "1.0.0-alpha01")
+ prebuilts(LibraryGroups.RECYCLERVIEW, "recyclerview", "1.1.0-alpha05")
+ prebuilts(LibraryGroups.RECYCLERVIEW, "recyclerview-selection", "1.1.0-alpha05")
+ prebuilts(LibraryGroups.REMOTECALLBACK, "1.0.0-alpha02")
ignore(LibraryGroups.ROOM.group, "room-common-java8")
- prebuilts(LibraryGroups.ROOM, "2.1.0-alpha07")
- prebuilts(LibraryGroups.SAVEDSTATE, "1.0.0-alpha02")
+ prebuilts(LibraryGroups.ROOM, "2.1.0-beta01")
+ prebuilts(LibraryGroups.SAVEDSTATE, "1.0.0-beta01")
+ prebuilts(LibraryGroups.SECURITY, "1.0.0-alpha01")
prebuilts(LibraryGroups.SHARETARGET, "1.0.0-alpha01")
- prebuilts(LibraryGroups.SLICE, "slice-builders", "1.0.0")
- prebuilts(LibraryGroups.SLICE, "slice-builders-ktx", "1.0.0-alpha6")
- prebuilts(LibraryGroups.SLICE, "slice-core", "1.0.0")
+ prebuilts(LibraryGroups.SLICE, "slice-builders", "1.1.0-alpha01")
+ prebuilts(LibraryGroups.SLICE, "slice-builders-ktx", "1.0.0-alpha07")
+ prebuilts(LibraryGroups.SLICE, "slice-core", "1.1.0-alpha01")
// TODO: land prebuilts
// prebuilts(LibraryGroups.SLICE.group, "slice-test", "1.0.0")
ignore(LibraryGroups.SLICE.group, "slice-test")
- prebuilts(LibraryGroups.SLICE, "slice-view", "1.0.0")
+ prebuilts(LibraryGroups.SLICE, "slice-view", "1.1.0-alpha01")
prebuilts(LibraryGroups.SLIDINGPANELAYOUT, "1.0.0")
prebuilts(LibraryGroups.SWIPEREFRESHLAYOUT, "1.1.0-alpha01")
prebuilts(LibraryGroups.TEXTCLASSIFIER, "1.0.0-alpha02")
- prebuilts(LibraryGroups.TRANSITION, "1.1.0-beta01")
+ prebuilts(LibraryGroups.TRANSITION, "1.1.0-rc01")
prebuilts(LibraryGroups.TVPROVIDER, "1.0.0")
- prebuilts(LibraryGroups.VECTORDRAWABLE, "1.1.0-alpha01")
- prebuilts(LibraryGroups.VECTORDRAWABLE, "vectordrawable-animated", "1.1.0-alpha01")
- prebuilts(LibraryGroups.VERSIONEDPARCELABLE, "1.1.0-alpha02")
+ prebuilts(LibraryGroups.VECTORDRAWABLE, "1.1.0-beta01")
+ prebuilts(LibraryGroups.VECTORDRAWABLE, "vectordrawable-animated", "1.1.0-beta01")
+ prebuilts(LibraryGroups.VERSIONEDPARCELABLE, "1.1.0-beta01")
prebuilts(LibraryGroups.VIEWPAGER, "1.0.0")
- prebuilts(LibraryGroups.VIEWPAGER2, "1.0.0-alpha03")
+ prebuilts(LibraryGroups.VIEWPAGER2, "1.0.0-alpha04")
prebuilts(LibraryGroups.WEAR, "1.0.0")
.addStubs("wear/wear_stubs/com.google.android.wearable-stubs.jar")
- prebuilts(LibraryGroups.WEBKIT, "1.0.0")
+ prebuilts(LibraryGroups.WEBKIT, "1.1.0-alpha01")
ignore(LibraryGroups.WORKMANAGER.group, "work-gcm")
prebuilts(LibraryGroups.WORKMANAGER, "2.1.0-alpha01")
default(Ignore)
diff --git a/buildSrc/src/main/kotlin/androidx/build/Release.kt b/buildSrc/src/main/kotlin/androidx/build/Release.kt
index 46640be..a00c08f 100644
--- a/buildSrc/src/main/kotlin/androidx/build/Release.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/Release.kt
@@ -273,7 +273,7 @@
/**
* Creates and returns the task that includes all projects regardless of their release status.
*/
- private fun getGlobalFullZipTask(project: Project): TaskProvider<GMavenZipTask> {
+ fun getGlobalFullZipTask(project: Project): TaskProvider<GMavenZipTask> {
return project.rootProject.maybeRegister(
name = FULL_ARCHIVE_TASK_NAME,
onConfigure = {
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index 51d04f6..a60a099 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -17,7 +17,7 @@
package androidx.build.dependencies
const val ANDROIDX_TEST_CORE = "androidx.test:core:1.1.0"
-const val ANDROIDX_TEST_EXT_JUNIT = "androidx.test.ext:junit:1.0.0"
+const val ANDROIDX_TEST_EXT_JUNIT = "androidx.test.ext:junit:1.1.0"
const val ANDROIDX_TEST_EXT_KTX = "androidx.test.ext:junit-ktx:1.1.0"
const val ANDROIDX_TEST_RULES = "androidx.test:rules:1.1.0"
const val ANDROIDX_TEST_RUNNER = "androidx.test:runner:1.1.1"
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisTest.java
index b408d9b..ad346eb 100644
--- a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisTest.java
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisTest.java
@@ -42,7 +42,7 @@
import androidx.camera.testing.CameraUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.MediumTest;
+import androidx.test.filters.LargeTest;
import org.junit.After;
import org.junit.Before;
@@ -57,7 +57,7 @@
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
-@MediumTest
+@LargeTest
@RunWith(AndroidJUnit4.class)
public final class ImageAnalysisTest {
// Use most supported resolution for different supported hardware level devices,
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageCaptureTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageCaptureTest.java
index a7b81cc..819b3b6 100644
--- a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageCaptureTest.java
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageCaptureTest.java
@@ -480,7 +480,8 @@
}
}
- @Test
+ // Skipping test due to b/132108192. Add back once test has been fixed.
+ // @Test
public void camera2InteropCaptureSessionCallbacks() throws InterruptedException {
ImageCaptureConfig.Builder configBuilder =
new ImageCaptureConfig.Builder().setCallbackHandler(mHandler);
@@ -506,6 +507,8 @@
requestCaptor.capture(),
any(TotalCaptureResult.class));
CaptureRequest captureRequest = requestCaptor.getValue(); // Obtains the last value.
+ // TODO This method removed temporary due to the side effect of aosp/943904. It's needed
+ // keep tracking.
assertThat(captureRequest.get(CaptureRequest.CONTROL_CAPTURE_INTENT))
.isEqualTo(CaptureRequest.CONTROL_CAPTURE_INTENT_STILL_CAPTURE);
}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java
index 25b1feb..d81d3cf 100644
--- a/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java
@@ -49,7 +49,7 @@
import androidx.lifecycle.Observer;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.MediumTest;
+import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import org.junit.After;
@@ -65,7 +65,7 @@
* Contains tests for {@link androidx.camera.core.CameraX} which varies use case combinations to
* run.
*/
-@MediumTest
+@LargeTest
@RunWith(AndroidJUnit4.class)
public final class UseCaseCombinationTest {
private static final LensFacing DEFAULT_LENS_FACING = LensFacing.BACK;
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.java
index bd27bd6..c15d2cf 100644
--- a/camera/camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.java
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.java
@@ -37,7 +37,7 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import org.junit.Before;
@@ -58,7 +58,7 @@
* <p>TODO(b/112325215): The VideoCapture will be more thoroughly tested via integration
* tests
*/
-@SmallTest
+@LargeTest
@RunWith(AndroidJUnit4.class)
public final class VideoCaptureTest {
// Use most supported resolution for different supported hardware level devices,
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2CaptureCallbacksTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2CaptureCallbacksTest.java
index 8912678..ec743bc 100644
--- a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2CaptureCallbacksTest.java
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2CaptureCallbacksTest.java
@@ -28,13 +28,13 @@
import android.view.Surface;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.LargeTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
-@SmallTest
+@LargeTest
@RunWith(AndroidJUnit4.class)
public final class Camera2CaptureCallbacksTest {
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2ImplCameraRepositoryTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2ImplCameraRepositoryTest.java
index 48fd51a..781755a 100644
--- a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2ImplCameraRepositoryTest.java
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2ImplCameraRepositoryTest.java
@@ -34,7 +34,7 @@
import androidx.camera.testing.fakes.FakeUseCaseConfig;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import org.junit.After;
@@ -49,7 +49,7 @@
* Contains tests for {@link androidx.camera.core.CameraRepository} which require an actual
* implementation to run.
*/
-@SmallTest
+@LargeTest
@RunWith(AndroidJUnit4.class)
public final class Camera2ImplCameraRepositoryTest {
private CameraRepository mCameraRepository;
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2ImplCameraXTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2ImplCameraXTest.java
index d43876a..76e748c 100644
--- a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2ImplCameraXTest.java
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2ImplCameraXTest.java
@@ -47,7 +47,7 @@
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.FlakyTest;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import org.junit.After;
@@ -64,7 +64,7 @@
* run.
*/
@FlakyTest
-@SmallTest
+@LargeTest
@RunWith(AndroidJUnit4.class)
public final class Camera2ImplCameraXTest {
private static final LensFacing DEFAULT_LENS_FACING = LensFacing.BACK;
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2InitializerTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2InitializerTest.java
index e3c65e7..15d8636 100644
--- a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2InitializerTest.java
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/Camera2InitializerTest.java
@@ -24,7 +24,7 @@
import androidx.camera.testing.fakes.FakeActivity;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import org.junit.Before;
@@ -35,7 +35,7 @@
/**
* Unit tests for {@link Camera2Initializer}.
*/
-@SmallTest
+@LargeTest
@RunWith(AndroidJUnit4.class)
public final class Camera2InitializerTest {
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/CameraTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/CameraTest.java
index cfd89ab..0e03783 100644
--- a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/CameraTest.java
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/CameraTest.java
@@ -40,7 +40,7 @@
import androidx.camera.testing.fakes.FakeUseCaseConfig;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.LargeTest;
import org.junit.After;
import org.junit.Before;
@@ -53,7 +53,7 @@
import java.util.HashMap;
import java.util.Map;
-@SmallTest
+@LargeTest
@RunWith(AndroidJUnit4.class)
public final class CameraTest {
private static final LensFacing DEFAULT_LENS_FACING = LensFacing.BACK;
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/CaptureCallbackContainerTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/CaptureCallbackContainerTest.java
index 458addf..48966e9 100644
--- a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/CaptureCallbackContainerTest.java
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/CaptureCallbackContainerTest.java
@@ -21,13 +21,13 @@
import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.LargeTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
-@SmallTest
+@LargeTest
@RunWith(AndroidJUnit4.class)
public final class CaptureCallbackContainerTest {
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/CaptureSessionTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/CaptureSessionTest.java
index 6f5ca39..cf1c821 100644
--- a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/CaptureSessionTest.java
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/CaptureSessionTest.java
@@ -44,7 +44,7 @@
import androidx.camera.core.SessionConfig;
import androidx.camera.testing.CameraUtil;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.LargeTest;
import org.junit.After;
import org.junit.Before;
@@ -62,7 +62,7 @@
* android.hardware.camera2.CameraDevice} can be opened since it is used to open a {@link
* android.hardware.camera2.CaptureRequest}.
*/
-@SmallTest
+@LargeTest
@RunWith(AndroidJUnit4.class)
public final class CaptureSessionTest {
private CaptureSessionTestParameters mTestParameters0;
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatDeviceTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatDeviceTest.java
new file mode 100644
index 0000000..e544f71
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatDeviceTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.impl.compat.params;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.SurfaceTexture;
+import android.view.Surface;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests some of the methods of OutputConfigurationCompat on device.
+ *
+ * <p>These need to run on device since they rely on native implementation details of the
+ * {@link Surface} class.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class OutputConfigurationCompatDeviceTest {
+
+ private static final int DEFAULT_WIDTH = 1024;
+ private static final int DEFAULT_HEIGHT = 768;
+
+ private static final int SECONDARY_WIDTH = 640;
+ private static final int SECONDARY_HEIGHT = 480;
+
+ // Same surface
+ private OutputConfigurationCompat mOutputConfigCompat0;
+ private OutputConfigurationCompat mOutputConfigCompat1;
+
+ // Different surface, same SurfaceTexture
+ private OutputConfigurationCompat mOutputConfigCompat2;
+
+ // Different Surface and SurfaceTexture
+ private OutputConfigurationCompat mOutputConfigCompat3;
+
+ @Before
+ public void setUp() {
+ SurfaceTexture surfaceTexture0 = new SurfaceTexture(0);
+ surfaceTexture0.setDefaultBufferSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
+ Surface surface0 = new Surface(surfaceTexture0);
+ Surface surface1 = new Surface(surfaceTexture0);
+
+ SurfaceTexture surfaceTexture1 = new SurfaceTexture(0);
+ surfaceTexture0.setDefaultBufferSize(SECONDARY_WIDTH, SECONDARY_HEIGHT);
+ Surface surface2 = new Surface(surfaceTexture1);
+
+ mOutputConfigCompat0 = new OutputConfigurationCompat(surface0);
+ mOutputConfigCompat1 = new OutputConfigurationCompat(surface0);
+ mOutputConfigCompat2 = new OutputConfigurationCompat(surface1);
+ mOutputConfigCompat3 = new OutputConfigurationCompat(surface2);
+ }
+
+ @Test
+ public void hashCode_producesExpectedResults() {
+ assertThat(mOutputConfigCompat0.hashCode()).isEqualTo(mOutputConfigCompat1.hashCode());
+ assertThat(mOutputConfigCompat0.hashCode()).isNotEqualTo(mOutputConfigCompat2.hashCode());
+ assertThat(mOutputConfigCompat0.hashCode()).isNotEqualTo(mOutputConfigCompat3.hashCode());
+ assertThat(mOutputConfigCompat2.hashCode()).isNotEqualTo(mOutputConfigCompat3.hashCode());
+ }
+
+ @Test
+ public void equals_producesExpectedResults() {
+ assertThat(mOutputConfigCompat0).isEqualTo(mOutputConfigCompat1);
+ assertThat(mOutputConfigCompat0).isNotEqualTo(mOutputConfigCompat2);
+ assertThat(mOutputConfigCompat0).isNotEqualTo(mOutputConfigCompat3);
+ assertThat(mOutputConfigCompat2).isNotEqualTo(mOutputConfigCompat3);
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/CameraDeviceCompat.java b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/CameraDeviceCompat.java
new file mode 100644
index 0000000..d6bc064
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/CameraDeviceCompat.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.impl.compat;
+
+import android.annotation.TargetApi;
+import android.hardware.camera2.CameraDevice;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * Helper for accessing features in {@link CameraDevice} in a backwards compatible fashion.
+ *
+ * @hide Will be unhidden once some methods are implemented
+ */
+@RestrictTo(Scope.LIBRARY)
+@TargetApi(21)
+public final class CameraDeviceCompat {
+
+ /**
+ * Standard camera operation mode.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY)
+ public static final int SESSION_OPERATION_MODE_NORMAL =
+ 0; // ICameraDeviceUser.NORMAL_MODE;
+
+ /**
+ * Constrained high-speed operation mode.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY)
+ public static final int SESSION_OPERATION_MODE_CONSTRAINED_HIGH_SPEED =
+ 1; // ICameraDeviceUser.CONSTRAINED_HIGH_SPEED_MODE;
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/package-info.java b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/package-info.java
new file mode 100644
index 0000000..f6409d6
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+package androidx.camera.camera2.impl.compat;
+
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompat.java b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompat.java
new file mode 100644
index 0000000..a76687d
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompat.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.impl.compat.params;
+
+import android.annotation.TargetApi;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.params.OutputConfiguration;
+import android.os.Build;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.util.List;
+
+/**
+ * Helper for accessing features in OutputConfiguration in a backwards compatible fashion.
+ */
+@TargetApi(21)
+public final class OutputConfigurationCompat {
+
+ /**
+ * Invalid surface group ID.
+ *
+ * <p>An OutputConfiguration with this value indicates that the included surface
+ * doesn't belong to any surface group.</p>
+ */
+ public static final int SURFACE_GROUP_ID_NONE = -1;
+
+ private final OutputConfigurationCompatImpl mImpl;
+
+ public OutputConfigurationCompat(@NonNull Surface surface) {
+ if (Build.VERSION.SDK_INT >= 28) {
+ mImpl = new OutputConfigurationCompatApi28Impl(surface);
+ } else if (Build.VERSION.SDK_INT >= 26) {
+ mImpl = new OutputConfigurationCompatApi26Impl(surface);
+ } else if (Build.VERSION.SDK_INT >= 24) {
+ mImpl = new OutputConfigurationCompatApi24Impl(surface);
+ } else {
+ mImpl = new OutputConfigurationCompatBaseImpl(surface);
+ }
+ }
+
+
+ /**
+ * Create a new {@link OutputConfiguration} instance, with desired Surface size and Surface
+ * source class.
+ *
+ * <p>
+ * This constructor takes an argument for desired Surface size and the Surface source class
+ * without providing the actual output Surface. This is used to setup an output configuration
+ * with a deferred Surface. The application can use this output configuration to create a
+ * session.
+ * </p>
+ * <p>
+ * However, the actual output Surface must be set via {@link #addSurface} and the deferred
+ * Surface configuration must be finalized via {@link
+ * CameraCaptureSession#finalizeOutputConfigurations} before submitting a request with this
+ * Surface target. The deferred Surface can only be obtained either from {@link
+ * android.view.SurfaceView} by calling {@link android.view.SurfaceHolder#getSurface}, or from
+ * {@link android.graphics.SurfaceTexture} via
+ * {@link android.view.Surface#Surface(android.graphics.SurfaceTexture)}).
+ * </p>
+ *
+ * @param surfaceSize Size for the deferred surface.
+ * @param klass a non-{@code null} {@link Class} object reference that indicates the
+ * source of
+ * this surface. Only {@link android.view.SurfaceHolder SurfaceHolder
+ * .class} and
+ * {@link android.graphics.SurfaceTexture SurfaceTexture.class} are
+ * supported.
+ * @throws IllegalArgumentException if the Surface source class is not supported, or Surface
+ * size is zero.
+ */
+ @RequiresApi(26)
+ public <T> OutputConfigurationCompat(@NonNull Size surfaceSize, @NonNull Class<T> klass) {
+ OutputConfiguration deferredConfig = new OutputConfiguration(surfaceSize, klass);
+ if (Build.VERSION.SDK_INT >= 28) {
+ mImpl = new OutputConfigurationCompatApi28Impl(deferredConfig);
+ } else {
+ mImpl = new OutputConfigurationCompatApi26Impl(deferredConfig);
+ }
+ }
+
+ private OutputConfigurationCompat(@NonNull OutputConfigurationCompatImpl impl) {
+ mImpl = impl;
+ }
+
+ /**
+ * Creates an instance from a framework android.hardware.camera2.params.OutputConfiguration
+ * object.
+ *
+ * <p>This method always returns {@code null} on API <= 23.</p>
+ *
+ * @param outputConfiguration an android.hardware.camera2.params.OutputConfiguration object, or
+ * {@code null} if none.
+ * @return an equivalent {@link OutputConfigurationCompat} object, or {@code null} if not
+ * supported.
+ */
+ @Nullable
+ public static OutputConfigurationCompat wrap(@Nullable Object outputConfiguration) {
+ if (outputConfiguration == null) {
+ return null;
+ }
+
+ OutputConfigurationCompatImpl outputConfigurationCompatImpl = null;
+ if (Build.VERSION.SDK_INT >= 28) {
+ outputConfigurationCompatImpl = new OutputConfigurationCompatApi28Impl(
+ outputConfiguration);
+ } else if (Build.VERSION.SDK_INT >= 26) {
+ outputConfigurationCompatImpl = new OutputConfigurationCompatApi26Impl(
+ outputConfiguration);
+ } else if (Build.VERSION.SDK_INT >= 24) {
+ outputConfigurationCompatImpl = new OutputConfigurationCompatApi24Impl(
+ outputConfiguration);
+ }
+
+ if (outputConfigurationCompatImpl == null) {
+ return null;
+ }
+
+ return new OutputConfigurationCompat(outputConfigurationCompatImpl);
+ }
+
+ /**
+ * Enable multiple surfaces sharing the same OutputConfiguration.
+ *
+ * <p>For advanced use cases, a camera application may require more streams than the combination
+ * guaranteed by {@code CameraDevice.createCaptureSession}. In this case, more than one
+ * compatible surface can be attached to an OutputConfiguration so that they map to one
+ * camera stream, and the outputs share memory buffers when possible. Due to buffer sharing
+ * clients should be careful when adding surface outputs that modify their input data. If such
+ * case exists, camera clients should have an additional mechanism to synchronize read and write
+ * access between individual consumers.</p>
+ *
+ * <p>Two surfaces are compatible in the below cases:</p>
+ *
+ * <li> Surfaces with the same size, format, dataSpace, and Surface source class. In this case,
+ * {@code CameraDevice.createCaptureSessionByOutputConfigurations} is guaranteed to succeed.
+ *
+ * <li> Surfaces with the same size, format, and dataSpace, but different Surface source classes
+ * that are generally not compatible. However, on some devices, the underlying camera device is
+ * able to use the same buffer layout for both surfaces. The only way to discover if this is the
+ * case is to create a capture session with that output configuration. For example, if the
+ * camera device uses the same private buffer format between a SurfaceView/SurfaceTexture and a
+ * MediaRecorder/MediaCodec, {@code CameraDevice.createCaptureSessionByOutputConfigurations}
+ * will succeed. Otherwise, it fails with {@link
+ * CameraCaptureSession.StateCallback#onConfigureFailed}.
+ * </ol>
+ *
+ * <p>To enable surface sharing, this function must be called before {@code
+ * CameraDevice.createCaptureSessionByOutputConfigurations} or {@code
+ * CameraDevice.createReprocessableCaptureSessionByConfigurations}. Calling this function after
+ * {@code CameraDevice.createCaptureSessionByOutputConfigurations} has no effect.</p>
+ *
+ * <p>Up to {@link #getMaxSharedSurfaceCount} surfaces can be shared for an OutputConfiguration.
+ * The supported surfaces for sharing must be of type SurfaceTexture, SurfaceView,
+ * MediaRecorder, MediaCodec, or implementation defined ImageReader.</p>
+ */
+ public void enableSurfaceSharing() {
+ mImpl.enableSurfaceSharing();
+ }
+
+ /**
+ * Set the id of the physical camera for this OutputConfiguration.
+ *
+ * <p>This method is a no-op on API <= 27, as these APIs do not support logical cameras.
+ *
+ * <p>In the case one logical camera is made up of multiple physical cameras, it could be
+ * desirable for the camera application to request streams from individual physical cameras.
+ * This call achieves it by mapping the OutputConfiguration to the physical camera id.</p>
+ *
+ * <p>The valid physical camera ids can be queried by {@code
+ * CameraCharacteristics.getPhysicalCameraIds} on API >= 28.
+ * </p>
+ *
+ * <p>Passing in a null physicalCameraId means that the OutputConfiguration is for a logical
+ * stream.</p>
+ *
+ * <p>This function must be called before {@code
+ * CameraDevice.createCaptureSessionByOutputConfigurations} or {@code
+ * CameraDevice.createReprocessableCaptureSessionByConfigurations}. Calling this function
+ * after {@code CameraDevice.createCaptureSessionByOutputConfigurations} or {@code
+ * CameraDevice.createReprocessableCaptureSessionByConfigurations} has no effect.</p>
+ *
+ * <p>The surface belonging to a physical camera OutputConfiguration must not be used as input
+ * or output of a reprocessing request. </p>
+ */
+ public void setPhysicalCameraId(@Nullable String physicalCameraId) {
+ mImpl.setPhysicalCameraId(physicalCameraId);
+ }
+
+ /**
+ * Add a surface to this OutputConfiguration.
+ *
+ * <p> This method will always throw on API <= 25, as these API levels do not support surface
+ * sharing. Users should always check {@link #getMaxSharedSurfaceCount} before attempting to
+ * add a surface.
+ *
+ * <p> This function can be called before or after {@code
+ * CameraDevice#createCaptureSessionByOutputConfigurations}. If it's called after,
+ * the application must finalize the capture session with
+ * {@code CameraCaptureSession.finalizeOutputConfigurations}. It is possible to call this method
+ * after the output configurations have been finalized only in cases of enabled surface sharing
+ * see {@link #enableSurfaceSharing}. The modified output configuration must be updated with
+ * {@code CameraCaptureSession.updateOutputConfiguration}.</p>
+ *
+ * <p> If the OutputConfiguration was constructed with a deferred surface by {@link
+ * OutputConfigurationCompat#OutputConfigurationCompat(Size, Class)}, the added surface must
+ * be obtained
+ * from {@link android.view.SurfaceView} by calling
+ * {@link android.view.SurfaceHolder#getSurface},
+ * or from {@link android.graphics.SurfaceTexture} via
+ * {@link android.view.Surface#Surface(android.graphics.SurfaceTexture)}).</p>
+ *
+ * <p> If the OutputConfiguration was constructed by other constructors, the added
+ * surface must be compatible with the existing surface. See {@link #enableSurfaceSharing} for
+ * details of compatible surfaces.</p>
+ *
+ * <p> If the OutputConfiguration already contains a Surface, {@link #enableSurfaceSharing} must
+ * be called before calling this function to add a new Surface.</p>
+ *
+ * @param surface The surface to be added.
+ * @throws IllegalArgumentException if the Surface is invalid, the Surface's
+ * dataspace/format doesn't match, or adding the Surface
+ * would exceed number of
+ * shared surfaces supported.
+ * @throws IllegalStateException if the Surface was already added to this
+ * OutputConfiguration,
+ * or if the OutputConfiguration is not shared and it
+ * already has a surface associated
+ * with it.
+ */
+ public void addSurface(@NonNull Surface surface) {
+ mImpl.addSurface(surface);
+ }
+
+ /**
+ * Remove a surface from this OutputConfiguration.
+ *
+ * <p> Surfaces added via calls to {@link #addSurface} can also be removed from the
+ * OutputConfiguration. The only notable exception is the surface associated with
+ * the OutputConfigration see {@link #getSurface} which was passed as part of the constructor
+ * or was added first in the deferred case
+ * {@link OutputConfigurationCompat#OutputConfigurationCompat(Size, Class)}.</p>
+ *
+ * @param surface The surface to be removed.
+ * @throws IllegalArgumentException If the surface is associated with this OutputConfiguration
+ * (see {@link #getSurface}) or the surface didn't get added
+ * with {@link #addSurface}.
+ */
+ public void removeSurface(@NonNull Surface surface) {
+ mImpl.removeSurface(surface);
+ }
+
+ /**
+ * Get the maximum supported shared {@link Surface} count.
+ *
+ * @return the maximum number of surfaces that can be added per each OutputConfiguration.
+ * @see #enableSurfaceSharing
+ */
+ public int getMaxSharedSurfaceCount() {
+ return mImpl.getMaxSharedSurfaceCount();
+ }
+
+ /**
+ * Get the {@link Surface} associated with this {@link OutputConfigurationCompat}.
+ *
+ * If more than one surface is associated with this {@link OutputConfigurationCompat}, return
+ * the
+ * first one as specified in the constructor or {@link OutputConfigurationCompat#addSurface}.
+ */
+ @Nullable
+ public Surface getSurface() {
+ return mImpl.getSurface();
+ }
+
+ /**
+ * Get the immutable list of surfaces associated with this {@link OutputConfigurationCompat}.
+ *
+ * @return the list of surfaces associated with this {@link OutputConfigurationCompat} as
+ * specified in
+ * the constructor and {@link OutputConfigurationCompat#addSurface}. The list should not be
+ * modified.
+ */
+ @NonNull
+ public List<Surface> getSurfaces() {
+ return mImpl.getSurfaces();
+ }
+
+ /**
+ * Get the surface group ID associated with this {@link OutputConfigurationCompat}.
+ *
+ * @return the surface group ID associated with this {@link OutputConfigurationCompat}.
+ * The default value is {@value #SURFACE_GROUP_ID_NONE}.
+ */
+ public int getSurfaceGroupId() {
+ return mImpl.getSurfaceGroupId();
+ }
+
+ /**
+ * Check if this {@link OutputConfigurationCompat} is equal to another
+ * {@link OutputConfigurationCompat}.
+ *
+ * <p>Two output configurations are only equal if and only if the underlying surfaces, surface
+ * properties (width, height, format, dataspace) when the output configurations are created,
+ * and all other configuration parameters are equal. </p>
+ *
+ * @return {@code true} if the objects were equal, {@code false} otherwise
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof OutputConfigurationCompat)) {
+ return false;
+ }
+
+ return mImpl.equals(((OutputConfigurationCompat) obj).mImpl);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return mImpl.hashCode();
+ }
+
+ /**
+ * Gets the underlying framework android.hardware.camera2.params.OutputConfiguration object.
+ *
+ * <p>This method always returns {@code null} on API <= 23.</p>
+ *
+ * @return an equivalent android.hardware.camera2.params.OutputConfiguration object, or {@code
+ * null} if not supported.
+ */
+ @Nullable
+ public Object unwrap() {
+ return mImpl.getOutputConfiguration();
+ }
+
+ interface OutputConfigurationCompatImpl {
+ void enableSurfaceSharing();
+
+ void setPhysicalCameraId(@Nullable String physicalCameraId);
+
+ void addSurface(@NonNull Surface surface);
+
+ void removeSurface(@NonNull Surface surface);
+
+ int getMaxSharedSurfaceCount();
+
+ @Nullable
+ Surface getSurface();
+
+ List<Surface> getSurfaces();
+
+ int getSurfaceGroupId();
+
+ @Nullable
+ Object getOutputConfiguration();
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatApi24Impl.java b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatApi24Impl.java
new file mode 100644
index 0000000..550ae75
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatApi24Impl.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.impl.compat.params;
+
+import android.hardware.camera2.params.OutputConfiguration;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.util.Preconditions;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Implementation of the OutputConfiguration compat methods for API 24 and above.
+ */
+@RequiresApi(24)
+class OutputConfigurationCompatApi24Impl extends OutputConfigurationCompatBaseImpl{
+
+ OutputConfigurationCompatApi24Impl(@NonNull Surface surface) {
+ super(new OutputConfiguration(surface));
+ }
+
+ OutputConfigurationCompatApi24Impl(@NonNull Object outputConfiguration) {
+ super(outputConfiguration);
+ }
+
+ @Override
+ @Nullable
+ public Surface getSurface() {
+ OutputConfiguration outputConfig = (OutputConfiguration) mObject;
+ return outputConfig.getSurface();
+ }
+
+ @Override
+ @NonNull
+ public List<Surface> getSurfaces() {
+ return Collections.singletonList(getSurface());
+ }
+
+ @Override
+ public int getSurfaceGroupId() {
+ OutputConfiguration outputConfig = (OutputConfiguration) mObject;
+ return outputConfig.getSurfaceGroupId();
+ }
+
+ @Nullable
+ @Override
+ public Object getOutputConfiguration() {
+ Preconditions.checkArgument(mObject instanceof OutputConfiguration);
+ return mObject;
+ }
+}
+
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatApi26Impl.java b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatApi26Impl.java
new file mode 100644
index 0000000..0e13a83
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatApi26Impl.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.impl.compat.params;
+
+import android.hardware.camera2.params.OutputConfiguration;
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.lang.reflect.Field;
+import java.util.List;
+
+/**
+ * Implementation of the OutputConfiguration compat methods for API 26 and above.
+ */
+@RequiresApi(26)
+class OutputConfigurationCompatApi26Impl extends OutputConfigurationCompatApi24Impl {
+
+ private static final String MAX_SHARED_SURFACES_COUNT_FIELD = "MAX_SURFACES_COUNT";
+ private static final String SURFACES_FIELD = "mSurfaces";
+
+ OutputConfigurationCompatApi26Impl(@NonNull Surface surface) {
+ super(new OutputConfiguration(surface));
+ }
+
+ OutputConfigurationCompatApi26Impl(@NonNull Object outputConfiguration) {
+ super(outputConfiguration);
+ }
+
+ // The following methods use reflection to call into the framework code, These methods are
+ // only between API 26 and API 28, and are not guaranteed to work on API levels greater than 27.
+ //=========================================================================================
+
+ private static int getMaxSharedSurfaceCountApi26()
+ throws NoSuchFieldException, IllegalAccessException {
+ Field maxSurfacesCountField = OutputConfiguration.class.getDeclaredField(
+ MAX_SHARED_SURFACES_COUNT_FIELD);
+ maxSurfacesCountField.setAccessible(true);
+ return maxSurfacesCountField.getInt(null);
+ }
+
+ private static List<Surface> getMutableSurfaceListApi26(OutputConfiguration outputConfiguration)
+ throws NoSuchFieldException, IllegalAccessException {
+ Field surfacesField = OutputConfiguration.class.getDeclaredField(SURFACES_FIELD);
+ surfacesField.setAccessible(true);
+ return (List<Surface>) surfacesField.get(outputConfiguration);
+ }
+
+ //=========================================================================================
+
+ /**
+ * Enable multiple surfaces sharing the same OutputConfiguration
+ */
+ @Override
+ public void enableSurfaceSharing() {
+ OutputConfiguration outputConfig = (OutputConfiguration) mObject;
+ outputConfig.enableSurfaceSharing();
+ }
+
+ /**
+ * Add a surface to this OutputConfiguration.
+ */
+ @Override
+ public void addSurface(@NonNull Surface surface) {
+ OutputConfiguration outputConfig = (OutputConfiguration) mObject;
+ outputConfig.addSurface(surface);
+ }
+
+ /**
+ * Remove a surface from this OutputConfiguration.
+ */
+ @Override
+ public void removeSurface(@NonNull Surface surface) {
+ if (getSurface() == surface) {
+ throw new IllegalArgumentException(
+ "Cannot remove surface associated with this output configuration");
+ }
+
+ try {
+ List<Surface> surfaces = getMutableSurfaceListApi26((OutputConfiguration) mObject);
+ if (!surfaces.remove(surface)) {
+ throw new IllegalArgumentException(
+ "Surface is not part of this output configuration");
+ }
+ } catch (IllegalAccessException | NoSuchFieldException e) {
+ Log.e(TAG, "Unable to remove surface from this output configuration.", e);
+ }
+
+ }
+
+ /**
+ * Get the maximum supported shared {@link Surface} count.
+ */
+ @Override
+ public int getMaxSharedSurfaceCount() {
+ try {
+ return getMaxSharedSurfaceCountApi26();
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ Log.e(TAG, "Unable to retrieve max shared surface count.", e);
+ return super.getMaxSharedSurfaceCount();
+ }
+ }
+
+ /**
+ * Get the immutable list of surfaces associated with this {@link OutputConfigurationCompat}.
+ */
+ @Override
+ @NonNull
+ public List<Surface> getSurfaces() {
+ OutputConfiguration outputConfig = (OutputConfiguration) mObject;
+ return outputConfig.getSurfaces();
+ }
+}
+
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatApi28Impl.java b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatApi28Impl.java
new file mode 100644
index 0000000..f83ebb7
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatApi28Impl.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.impl.compat.params;
+
+import android.hardware.camera2.params.OutputConfiguration;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+/**
+ * Implementation of the OutputConfiguration compat methods for API 28 and above.
+ */
+@RequiresApi(28)
+class OutputConfigurationCompatApi28Impl extends OutputConfigurationCompatApi26Impl {
+
+ OutputConfigurationCompatApi28Impl(@NonNull Surface surface) {
+ super(new OutputConfiguration(surface));
+ }
+
+ OutputConfigurationCompatApi28Impl(@NonNull Object outputConfiguration) {
+ super(outputConfiguration);
+ }
+
+ /**
+ * Remove a surface from this OutputConfiguration.
+ */
+ @Override
+ public void removeSurface(@NonNull Surface surface) {
+ OutputConfiguration outputConfig = (OutputConfiguration) mObject;
+ outputConfig.removeSurface(surface);
+ }
+
+ /**
+ * Get the maximum supported shared {@link Surface} count.
+ */
+ @Override
+ public int getMaxSharedSurfaceCount() {
+ OutputConfiguration outputConfig = (OutputConfiguration) mObject;
+ return outputConfig.getMaxSharedSurfaceCount();
+ }
+
+ /**
+ * Set the id of the physical camera for this OutputConfiguration.
+ */
+ @Override
+ public void setPhysicalCameraId(@Nullable String physicalCameraId) {
+ OutputConfiguration outputConfiguration = (OutputConfiguration) mObject;
+ outputConfiguration.setPhysicalCameraId(physicalCameraId);
+ }
+}
+
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatBaseImpl.java b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatBaseImpl.java
new file mode 100644
index 0000000..79c9d99
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatBaseImpl.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.impl.compat.params;
+
+import android.graphics.ImageFormat;
+import android.util.Log;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.util.Preconditions;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Implementation of the OutputConfiguration compat methods for API 21 and above.
+ */
+@RequiresApi(21) // Needed for LegacyCameraDevice reflection
+class OutputConfigurationCompatBaseImpl implements
+ OutputConfigurationCompat.OutputConfigurationCompatImpl {
+ static final String TAG = "OutputConfigCompat";
+
+ final Object mObject;
+
+ OutputConfigurationCompatBaseImpl(@NonNull Surface surface) {
+ mObject = new OutputConfigurationParamsApi21(surface);
+ }
+
+ /**
+ * Sets the underlying implementation object.
+ */
+ OutputConfigurationCompatBaseImpl(@NonNull Object outputConfiguration) {
+ mObject = outputConfiguration;
+ }
+
+ /**
+ * Enable multiple surfaces sharing the same OutputConfiguration
+ *
+ * <p>This is always a no-op on API <= 25.
+ */
+ @Override
+ public void enableSurfaceSharing() {
+ // No-op. Surface sharing not possible on less than API 26.
+ Log.w(TAG, "enableSurfaceSharing: surface sharing not supported on current API level");
+ }
+
+ /**
+ * Set the id of the physical camera for this OutputConfiguration.
+ *
+ * <p>This value is unused by this implementation. Added in API 28.
+ */
+ @Override
+ public void setPhysicalCameraId(@Nullable String physicalCameraId) {
+ // No-op. Physical camera ID is not supported on API < 28.
+ Log.w(TAG, "setPhysicalCameraId: physical camera id not supported on current API level");
+ }
+
+ /**
+ * Add a surface to this OutputConfiguration.
+ *
+ * <p>Since surface sharing is not supported in on API <= 25, this will always throw.
+ */
+ @Override
+ public void addSurface(@NonNull Surface surface) {
+ Preconditions.checkNotNull(surface, "Surface must not be null");
+ if (getSurface() == surface) {
+ throw new IllegalStateException("Surface is already added!");
+ }
+
+ // Surface sharing not possible on API < 26
+ throw new IllegalStateException("Cannot have 2 surfaces for a non-sharing configuration");
+ }
+
+ /**
+ * Remove a surface from this OutputConfiguration.
+ *
+ * <p>removeSurface is not supported in on API <= 25, this will always throw.
+ */
+ @Override
+ public void removeSurface(@NonNull Surface surface) {
+ if (getSurface() == surface) {
+ throw new IllegalArgumentException(
+ "Cannot remove surface associated with this output configuration");
+ }
+
+ // Only a single surface is allowed in this implementation.
+ throw new IllegalArgumentException("Surface is not part of this output configuration");
+ }
+
+ /**
+ * Get the maximum supported shared {@link Surface} count.
+ *
+ * <p>Since surface sharing is not supported in on API <= 25, always returns 1.
+ */
+ @Override
+ public int getMaxSharedSurfaceCount() {
+ return OutputConfigurationParamsApi21.MAX_SURFACES_COUNT;
+ }
+
+ /**
+ * Get the {@link Surface} associated with this {@link OutputConfigurationCompat}.
+ */
+ @Override
+ @Nullable
+ public Surface getSurface() {
+ List<Surface> surfaces = ((OutputConfigurationParamsApi21) mObject).mSurfaces;
+ if (surfaces.size() == 0) {
+ return null;
+ }
+
+ return surfaces.get(0);
+ }
+
+ /**
+ * Get the immutable list of surfaces associated with this {@link OutputConfigurationCompat}.
+ */
+ @Override
+ @NonNull
+ public List<Surface> getSurfaces() {
+ // mSurfaces is a singleton list, so return it directly.
+ return ((OutputConfigurationParamsApi21) mObject).mSurfaces;
+ }
+
+ @Override
+ public int getSurfaceGroupId() {
+ // Surface groups not supported on < API 24
+ return OutputConfigurationCompat.SURFACE_GROUP_ID_NONE;
+ }
+
+ @Nullable
+ @Override
+ public Object getOutputConfiguration() {
+ return null;
+ }
+
+ /**
+ * Check if this {@link OutputConfigurationCompatBaseImpl} is equal to another
+ * {@link OutputConfigurationCompatBaseImpl}.
+ *
+ * <p>Two output configurations are only equal if and only if the underlying surfaces, surface
+ * properties (width, height, format) when the output configurations are created,
+ * and all other configuration parameters are equal. </p>
+ *
+ * @return {@code true} if the objects were equal, {@code false} otherwise
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof OutputConfigurationCompatBaseImpl)) {
+ return false;
+ }
+
+ return Objects.equals(mObject, ((OutputConfigurationCompatBaseImpl) obj).mObject);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return mObject.hashCode();
+ }
+
+ private static final class OutputConfigurationParamsApi21 {
+ /**
+ * Maximum number of surfaces supported by one {@link OutputConfigurationCompat}.
+ *
+ * <p>Always only 1 on API <= 25.
+ */
+ static final int MAX_SURFACES_COUNT = 1;
+ private static final String LEGACY_CAMERA_DEVICE_CLASS =
+ "android.hardware.camera2.legacy.LegacyCameraDevice";
+ private static final String GET_SURFACE_SIZE_METHOD = "getSurfaceSize";
+ private static final String DETECT_SURFACE_TYPE_METHOD = "detectSurfaceType";
+ // Used on class Surface
+ private static final String GET_GENERATION_ID_METHOD = "getGenerationId";
+ final List<Surface> mSurfaces;
+ // The size and format of the surface when OutputConfiguration is created.
+ final Size mConfiguredSize;
+ final int mConfiguredFormat;
+ // Surface generation ID to distinguish changes to Surface native internals
+ final int mConfiguredGenerationId;
+
+ OutputConfigurationParamsApi21(@NonNull Surface surface) {
+ Preconditions.checkNotNull(surface, "Surface must not be null");
+ mSurfaces = Collections.singletonList(surface);
+ mConfiguredSize = getSurfaceSize(surface);
+ mConfiguredFormat = getSurfaceFormat(surface);
+ mConfiguredGenerationId = getSurfaceGenerationId(surface);
+ }
+
+ // The following methods use reflection to call into the framework code, These methods are
+ // only valid up to API 24, and are not guaranteed to work on API levels greater than 23.
+ //=========================================================================================
+
+ private static Size getSurfaceSize(@NonNull Surface surface) {
+ try {
+ Class<?> legacyCameraDeviceClass = Class.forName(LEGACY_CAMERA_DEVICE_CLASS);
+ Method getSurfaceSize = legacyCameraDeviceClass.getDeclaredMethod(
+ GET_SURFACE_SIZE_METHOD, Surface.class);
+ getSurfaceSize.setAccessible(true);
+ return (Size) getSurfaceSize.invoke(null, surface);
+ } catch (ClassNotFoundException
+ | NoSuchMethodException
+ | IllegalAccessException
+ | InvocationTargetException e) {
+ Log.e(TAG, "Unable to retrieve surface size.", e);
+ return null;
+ }
+ }
+
+ private static int getSurfaceFormat(@NonNull Surface surface) {
+ try {
+ Class<?> legacyCameraDeviceClass = Class.forName(LEGACY_CAMERA_DEVICE_CLASS);
+ Method detectSurfaceType = legacyCameraDeviceClass.getDeclaredMethod(
+ DETECT_SURFACE_TYPE_METHOD, Surface.class);
+ return (int) detectSurfaceType.invoke(null, surface);
+ } catch (ClassNotFoundException
+ | NoSuchMethodException
+ | IllegalAccessException
+ | InvocationTargetException e) {
+ Log.e(TAG, "Unable to retrieve surface format.", e);
+ return ImageFormat.UNKNOWN;
+ }
+
+
+ }
+
+ private static int getSurfaceGenerationId(@NonNull Surface surface) {
+ try {
+ Method getGenerationId = Surface.class.getDeclaredMethod(GET_GENERATION_ID_METHOD);
+ return (int) getGenerationId.invoke(surface);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ Log.e(TAG, "Unable to retrieve surface generation id.", e);
+ return -1;
+ }
+ }
+
+ //=========================================================================================
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof OutputConfigurationParamsApi21)) {
+ return false;
+ }
+
+ OutputConfigurationParamsApi21 otherOutputConfig = (OutputConfigurationParamsApi21) obj;
+
+ if (!mConfiguredSize.equals(otherOutputConfig.mConfiguredSize)
+ || mConfiguredFormat != otherOutputConfig.mConfiguredFormat
+ || mConfiguredGenerationId != otherOutputConfig.mConfiguredGenerationId) {
+ return false;
+ }
+
+ int minLen = Math.min(mSurfaces.size(), otherOutputConfig.mSurfaces.size());
+ for (int i = 0; i < minLen; i++) {
+ if (mSurfaces.get(i) != otherOutputConfig.mSurfaces.get(i)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ // Strength reduction; in case the compiler has illusions about divisions being faster
+ h = ((h << 5) - h) ^ mSurfaces.hashCode(); // (h * 31) XOR mSurfaces.hashCode()
+ h = ((h << 5) - h) ^ mConfiguredGenerationId; // (h * 31) XOR mConfiguredGenerationId
+ h = ((h << 5) - h)
+ ^ mConfiguredSize.hashCode(); // (h * 31) XOR mConfiguredSize.hashCode()
+ h = ((h << 5) - h) ^ mConfiguredFormat; // (h * 31) XOR mConfiguredFormat
+
+ return h;
+ }
+ }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/SessionConfigurationCompat.java b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/SessionConfigurationCompat.java
new file mode 100644
index 0000000..7afc02a
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/impl/compat/params/SessionConfigurationCompat.java
@@ -0,0 +1,480 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.impl.compat.params;
+
+import android.annotation.TargetApi;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.InputConfiguration;
+import android.hardware.camera2.params.OutputConfiguration;
+import android.hardware.camera2.params.SessionConfiguration;
+import android.os.Build;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.camera2.impl.compat.CameraDeviceCompat;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Helper for accessing features in SessionConfiguration in a backwards compatible fashion.
+ */
+@TargetApi(21)
+public final class SessionConfigurationCompat {
+
+ /**
+ * A regular session type containing instances of {@link OutputConfigurationCompat} running
+ * at regular non high speed FPS ranges and optionally {@link InputConfigurationCompat} for
+ * reprocessable sessions.
+ *
+ * @see CameraDevice#createCaptureSession
+ * @see CameraDevice#createReprocessableCaptureSession
+ */
+ public static final int SESSION_REGULAR = CameraDeviceCompat.SESSION_OPERATION_MODE_NORMAL;
+ /**
+ * A high speed session type that can only contain instances of
+ * {@link OutputConfigurationCompat}.
+ * The outputs can run using high speed FPS ranges. Calls to {@link #setInputConfiguration}
+ * are not supported.
+ *
+ * @see CameraDevice#createConstrainedHighSpeedCaptureSession
+ */
+ public static final int SESSION_HIGH_SPEED =
+ CameraDeviceCompat.SESSION_OPERATION_MODE_CONSTRAINED_HIGH_SPEED;
+ private final SessionConfigurationCompatImpl mImpl;
+
+ /**
+ * Create a new {@link SessionConfigurationCompat}.
+ *
+ * @param sessionType The session type.
+ * @param outputsCompat A list of output configurations for the capture session.
+ * @param executor The executor which should be used to invoke the callback. In general
+ * it is
+ * recommended that camera operations are not done on the main (UI) thread.
+ * @param cb A state callback interface implementation.
+ * @see #SESSION_REGULAR
+ * @see #SESSION_HIGH_SPEED
+ */
+ public SessionConfigurationCompat(@SessionMode int sessionType,
+ @NonNull List<OutputConfigurationCompat> outputsCompat,
+ @NonNull /* @CallbackExecutor */ Executor executor,
+ @NonNull CameraCaptureSession.StateCallback cb) {
+ if (Build.VERSION.SDK_INT < 28) {
+ mImpl = new SessionConfigurationCompatBaseImpl(sessionType, outputsCompat, executor,
+ cb);
+ } else {
+ mImpl = new SessionConfigurationCompatApi28Impl(sessionType, outputsCompat, executor,
+ cb);
+ }
+ }
+
+ private SessionConfigurationCompat(@NonNull SessionConfigurationCompatImpl impl) {
+ mImpl = impl;
+ }
+
+ /**
+ * Creates an instance from a framework android.hardware.camera2.params.SessionConfiguration
+ * object.
+ *
+ * <p>This method always returns {@code null} on API <= 27.</p>
+ *
+ * @param sessionConfiguration an android.hardware.camera2.params.SessionConfiguration object,
+ * or {@code null} if none.
+ * @return an equivalent {@link SessionConfigurationCompat} object, or {@code null} if not
+ * supported.
+ */
+ @Nullable
+ public static SessionConfigurationCompat wrap(@Nullable Object sessionConfiguration) {
+ if (sessionConfiguration == null) {
+ return null;
+ }
+ if (Build.VERSION.SDK_INT < 28) {
+ return null;
+ }
+
+ return new SessionConfigurationCompat(
+ new SessionConfigurationCompatApi28Impl(sessionConfiguration));
+ }
+
+ @RequiresApi(24)
+ static List<OutputConfigurationCompat> transformToCompat(
+ @NonNull List<OutputConfiguration> outputConfigurations) {
+ ArrayList<OutputConfigurationCompat> outList = new ArrayList<>(outputConfigurations.size());
+ for (OutputConfiguration outputConfiguration : outputConfigurations) {
+ outList.add(OutputConfigurationCompat.wrap(outputConfiguration));
+ }
+
+ return outList;
+ }
+
+ @RequiresApi(24)
+ static List<OutputConfiguration> transformFromCompat(
+ @NonNull List<OutputConfigurationCompat> outputConfigurations) {
+ ArrayList<OutputConfiguration> outList = new ArrayList<>(outputConfigurations.size());
+ for (OutputConfigurationCompat outputConfiguration : outputConfigurations) {
+ outList.add((OutputConfiguration) outputConfiguration.unwrap());
+ }
+
+ return outList;
+ }
+
+ /**
+ * Retrieve the type of the capture session.
+ *
+ * @return The capture session type.
+ */
+ @SessionMode
+ public int getSessionType() {
+ return mImpl.getSessionType();
+ }
+
+ /**
+ * Retrieve the {@link OutputConfigurationCompat} list for the capture session.
+ *
+ * @return A list of output configurations for the capture session.
+ */
+ public List<OutputConfigurationCompat> getOutputConfigurations() {
+ return mImpl.getOutputConfigurations();
+ }
+
+ /**
+ * Retrieve the {@link CameraCaptureSession.StateCallback} for the capture session.
+ *
+ * @return A state callback interface implementation.
+ */
+ public CameraCaptureSession.StateCallback getStateCallback() {
+ return mImpl.getStateCallback();
+ }
+
+ /**
+ * Retrieve the {@link Executor} for the capture session.
+ *
+ * @return The Executor on which the callback will be invoked.
+ */
+ public Executor getExecutor() {
+ return mImpl.getExecutor();
+ }
+
+ /**
+ * Retrieve the {@link InputConfigurationCompat}.
+ *
+ * @return The capture session input configuration.
+ */
+ public InputConfigurationCompat getInputConfiguration() {
+ return mImpl.getInputConfiguration();
+ }
+
+ /**
+ * Sets the {@link InputConfigurationCompat} for a reprocessable session. Input configuration
+ * are not supported for {@link #SESSION_HIGH_SPEED}.
+ *
+ * @param input Input configuration.
+ * @throws UnsupportedOperationException In case it is called for {@link #SESSION_HIGH_SPEED}
+ * type session configuration.
+ */
+ public void setInputConfiguration(@NonNull InputConfigurationCompat input) {
+ mImpl.setInputConfiguration(input);
+ }
+
+ /**
+ * Retrieve the session wide camera parameters (see {@link CaptureRequest}).
+ *
+ * @return A capture request that includes the initial values for any available
+ * session wide capture keys.
+ */
+ public CaptureRequest getSessionParameters() {
+ return mImpl.getSessionParameters();
+ }
+
+ /**
+ * Sets the session wide camera parameters (see {@link CaptureRequest}). This argument can
+ * be set for every supported session type and will be passed to the camera device as part
+ * of the capture session initialization. Session parameters are a subset of the available
+ * capture request parameters (see {@code CameraCharacteristics.getAvailableSessionKeys})
+ * and their application can introduce internal camera delays. To improve camera performance
+ * it is suggested to change them sparingly within the lifetime of the capture session and
+ * to pass their initial values as part of this method.
+ *
+ * @param params A capture request that includes the initial values for any available
+ * session wide capture keys. Tags (see {@link CaptureRequest.Builder#setTag}) and
+ * output targets (see {@link CaptureRequest.Builder#addTarget}) are ignored if
+ * set. Parameter values not part of
+ * {@code CameraCharacteristics.getAvailableSessionKeys} will also be ignored. It
+ * is recommended to build the session parameters using the same template type as
+ * the initial capture request, so that the session and initial request parameters
+ * match as much as possible.
+ */
+ public void setSessionParameters(CaptureRequest params) {
+ mImpl.setSessionParameters(params);
+ }
+
+ /**
+ * Gets the underlying framework android.hardware.camera2.params.SessionConfiguration object.
+ *
+ * <p>This method always returns {@code null} on API <= 27.</p>
+ *
+ * @return an equivalent android.hardware.camera2.params.SessionConfiguration object, or
+ * {@code null} if not supported.
+ */
+ @Nullable
+ public Object unwrap() {
+ return mImpl.getSessionConfiguration();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof SessionConfigurationCompat)) {
+ return false;
+ }
+
+ return mImpl.equals(((SessionConfigurationCompat) obj).mImpl);
+ }
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value =
+ {SESSION_REGULAR, SESSION_HIGH_SPEED})
+ public @interface SessionMode {
+ }
+
+ private interface SessionConfigurationCompatImpl {
+ @SessionMode
+ int getSessionType();
+
+ List<OutputConfigurationCompat> getOutputConfigurations();
+
+ CameraCaptureSession.StateCallback getStateCallback();
+
+ Executor getExecutor();
+
+ InputConfigurationCompat getInputConfiguration();
+
+ void setInputConfiguration(@NonNull InputConfigurationCompat input);
+
+ CaptureRequest getSessionParameters();
+
+ void setSessionParameters(CaptureRequest params);
+
+ @Nullable
+ Object getSessionConfiguration();
+ }
+
+ private static final class SessionConfigurationCompatBaseImpl implements
+ SessionConfigurationCompatImpl {
+
+ private final List<OutputConfigurationCompat> mOutputConfigurations;
+ private final CameraCaptureSession.StateCallback mStateCallback;
+ private final Executor mExecutor;
+ private int mSessionType;
+ private InputConfigurationCompat mInputConfig = null;
+ private CaptureRequest mSessionParameters = null;
+
+ SessionConfigurationCompatBaseImpl(@SessionMode int sessionType,
+ @NonNull List<OutputConfigurationCompat> outputs,
+ @NonNull /* @CallbackExecutor */ Executor executor,
+ @NonNull CameraCaptureSession.StateCallback cb) {
+ mSessionType = sessionType;
+ mOutputConfigurations = Collections.unmodifiableList(new ArrayList<>(outputs));
+ mStateCallback = cb;
+ mExecutor = executor;
+ }
+
+ @Override
+ public int getSessionType() {
+ return mSessionType;
+ }
+
+ @Override
+ public List<OutputConfigurationCompat> getOutputConfigurations() {
+ return mOutputConfigurations;
+ }
+
+ @Override
+ public CameraCaptureSession.StateCallback getStateCallback() {
+ return mStateCallback;
+ }
+
+ @Override
+ public Executor getExecutor() {
+ return mExecutor;
+ }
+
+ @Nullable
+ @Override
+ public InputConfigurationCompat getInputConfiguration() {
+ return mInputConfig;
+ }
+
+ @Override
+ public void setInputConfiguration(@NonNull InputConfigurationCompat input) {
+ if (mSessionType != SESSION_HIGH_SPEED) {
+ mInputConfig = input;
+ } else {
+ throw new UnsupportedOperationException(
+ "Method not supported for high speed session types");
+ }
+ }
+
+ @Override
+ public CaptureRequest getSessionParameters() {
+ return mSessionParameters;
+ }
+
+ @Override
+ public void setSessionParameters(CaptureRequest params) {
+ mSessionParameters = params;
+ }
+
+ @Nullable
+ @Override
+ public Object getSessionConfiguration() {
+ return null;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (obj instanceof SessionConfigurationCompatBaseImpl) {
+ SessionConfigurationCompatBaseImpl other = (SessionConfigurationCompatBaseImpl) obj;
+ if (mInputConfig != other.mInputConfig
+ || mSessionType != other.mSessionType
+ || mOutputConfigurations.size() != other.mOutputConfigurations.size()) {
+ return false;
+ }
+
+ for (int i = 0; i < mOutputConfigurations.size(); i++) {
+ if (!mOutputConfigurations.get(i).equals(other.mOutputConfigurations.get(i))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ // Strength reduction; in case the compiler has illusions about divisions being faster
+ h = ((h << 5) - h)
+ ^ mOutputConfigurations.hashCode(); // (h * 31) XOR mOutputConfigurations
+ // .hashCode()
+ h = ((h << 5) - h) ^ (mInputConfig == null ? 0
+ : mInputConfig.hashCode()); // (h * 31) XOR mInputConfig.hashCode()
+ h = ((h << 5) - h) ^ mSessionType; // (h * 31) XOR mSessionType
+
+ return h;
+ }
+ }
+
+ @RequiresApi(28)
+ private static final class SessionConfigurationCompatApi28Impl implements
+ SessionConfigurationCompatImpl {
+
+ private final SessionConfiguration mObject;
+ private final List<OutputConfigurationCompat> mOutputConfigurations;
+
+ SessionConfigurationCompatApi28Impl(@NonNull Object sessionConfiguration) {
+ mObject = (SessionConfiguration) sessionConfiguration;
+ mOutputConfigurations = Collections.unmodifiableList(transformToCompat(
+ ((SessionConfiguration) sessionConfiguration).getOutputConfigurations()));
+ }
+
+ SessionConfigurationCompatApi28Impl(@SessionMode int sessionType,
+ @NonNull List<OutputConfigurationCompat> outputs,
+ @NonNull /* @CallbackExecutor */ Executor executor,
+ @NonNull CameraCaptureSession.StateCallback cb) {
+ this(new SessionConfiguration(sessionType, transformFromCompat(outputs), executor, cb));
+ }
+
+ @Override
+ public int getSessionType() {
+ return mObject.getSessionType();
+ }
+
+ @Override
+ public List<OutputConfigurationCompat> getOutputConfigurations() {
+ // Return cached compat version of list
+ return mOutputConfigurations;
+ }
+
+ @Override
+ public CameraCaptureSession.StateCallback getStateCallback() {
+ return mObject.getStateCallback();
+ }
+
+ @Override
+ public Executor getExecutor() {
+ return mObject.getExecutor();
+ }
+
+ @Override
+ public InputConfigurationCompat getInputConfiguration() {
+ return InputConfigurationCompat.wrap(mObject.getInputConfiguration());
+ }
+
+ @Override
+ public void setInputConfiguration(@NonNull InputConfigurationCompat input) {
+ mObject.setInputConfiguration((InputConfiguration) input.unwrap());
+ }
+
+ @Override
+ public CaptureRequest getSessionParameters() {
+ return mObject.getSessionParameters();
+ }
+
+ @Override
+ public void setSessionParameters(CaptureRequest params) {
+ mObject.setSessionParameters(params);
+ }
+
+ @Nullable
+ @Override
+ public Object getSessionConfiguration() {
+ return mObject;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof SessionConfigurationCompatApi28Impl)) {
+ return false;
+ }
+
+ return Objects.equals(mObject, ((SessionConfigurationCompatApi28Impl) obj).mObject);
+ }
+
+ @Override
+ public int hashCode() {
+ return mObject.hashCode();
+ }
+ }
+}
diff --git a/camera/camera2/src/test/java/androidx/camera/camera2/impl/compat/params/InputConfigurationCompatTest.java b/camera/camera2/src/test/java/androidx/camera/camera2/impl/compat/params/InputConfigurationCompatTest.java
index 3470302..6c421e4 100644
--- a/camera/camera2/src/test/java/androidx/camera/camera2/impl/compat/params/InputConfigurationCompatTest.java
+++ b/camera/camera2/src/test/java/androidx/camera/camera2/impl/compat/params/InputConfigurationCompatTest.java
@@ -34,7 +34,7 @@
@RunWith(RobolectricTestRunner.class)
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-public class InputConfigurationCompatTest {
+public final class InputConfigurationCompatTest {
private static final int WIDTH = 1024;
private static final int HEIGHT = 768;
diff --git a/camera/camera2/src/test/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatTest.java b/camera/camera2/src/test/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatTest.java
new file mode 100644
index 0000000..0b99551
--- /dev/null
+++ b/camera/camera2/src/test/java/androidx/camera/camera2/impl/compat/params/OutputConfigurationCompatTest.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.impl.compat.params;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.params.OutputConfiguration;
+import android.os.Build;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public final class OutputConfigurationCompatTest {
+
+ private static final int TEST_GROUP_ID = 100;
+ private static final String PHYSICAL_CAMERA_ID = "1";
+
+ private static void assumeSurfaceSharingAvailable(
+ OutputConfigurationCompat outputConfigCompat) {
+ Assume.assumeTrue("API level does not support surface sharing.",
+ outputConfigCompat.getMaxSharedSurfaceCount() > 1);
+ }
+
+ @Test
+ public void canRetrieveSurface() {
+ Surface surface = mock(Surface.class);
+ OutputConfigurationCompat outputConfigCompat = new OutputConfigurationCompat(surface);
+
+ assertThat(outputConfigCompat.getSurface()).isSameInstanceAs(surface);
+ }
+
+ @Test
+ public void defaultSurfaceGroupIdIsSet() {
+ Surface surface = mock(Surface.class);
+ OutputConfigurationCompat outputConfigCompat = new OutputConfigurationCompat(surface);
+
+ assertThat(outputConfigCompat.getSurfaceGroupId()).isEqualTo(
+ OutputConfigurationCompat.SURFACE_GROUP_ID_NONE);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void addSurfaceThrows_whenAddingMoreThanMax() {
+ Surface surface = mock(Surface.class);
+ OutputConfigurationCompat outputConfigCompat = new OutputConfigurationCompat(surface);
+
+ assumeSurfaceSharingAvailable(outputConfigCompat);
+ outputConfigCompat.enableSurfaceSharing();
+
+ // Since we already have 1 surface added, if we try to add the max we will be adding a
+ // total of max surfaces + 1
+ for (int i = 0; i < outputConfigCompat.getMaxSharedSurfaceCount(); ++i) {
+ outputConfigCompat.addSurface(mock(Surface.class));
+ }
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void addSurfaceThrows_whenSurfaceSharingNotEnabled() {
+ Surface surface = mock(Surface.class);
+ OutputConfigurationCompat outputConfigCompat = new OutputConfigurationCompat(surface);
+
+ // Adding a second surface should fail if OutputConfigurationCompat#enableSurfaceSharing
+ // () has not been called.
+ outputConfigCompat.addSurface(mock(Surface.class));
+ }
+
+ @Test
+ public void maxSurfaces_canBeAdded_andRetrieved() {
+ Surface surface = mock(Surface.class);
+ OutputConfigurationCompat outputConfigCompat = new OutputConfigurationCompat(surface);
+
+ outputConfigCompat.enableSurfaceSharing();
+ List<Surface> allSurfaces = new ArrayList<Surface>();
+ allSurfaces.add(surface);
+
+ for (int i = 0; i < outputConfigCompat.getMaxSharedSurfaceCount() - 1; ++i) {
+ Surface newSurface = mock(Surface.class);
+ allSurfaces.add(newSurface);
+ outputConfigCompat.addSurface(newSurface);
+ }
+
+ assertThat(outputConfigCompat.getSurfaces()).containsExactlyElementsIn(allSurfaces);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void cannotRemoveMainSurface() {
+ Surface surface = mock(Surface.class);
+ OutputConfigurationCompat outputConfigCompat = new OutputConfigurationCompat(surface);
+ outputConfigCompat.removeSurface(surface);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void removeNonAddedSurfaceThrows() {
+ Surface surface = mock(Surface.class);
+ OutputConfigurationCompat outputConfigCompat = new OutputConfigurationCompat(surface);
+ outputConfigCompat.removeSurface(mock(Surface.class));
+ }
+
+ @Test
+ public void canRemoveSharedSurface() {
+ Surface surface = mock(Surface.class);
+ OutputConfigurationCompat outputConfigCompat = new OutputConfigurationCompat(surface);
+
+ assumeSurfaceSharingAvailable(outputConfigCompat);
+ outputConfigCompat.enableSurfaceSharing();
+ List<Surface> allSurfaces = new ArrayList<Surface>();
+ allSurfaces.add(surface);
+
+ for (int i = 0; i < outputConfigCompat.getMaxSharedSurfaceCount() - 1; ++i) {
+ Surface newSurface = mock(Surface.class);
+ allSurfaces.add(newSurface);
+ outputConfigCompat.addSurface(newSurface);
+ }
+
+ Surface lastSurface = allSurfaces.remove(allSurfaces.size() - 1);
+ boolean containedBeforeRemoval = outputConfigCompat.getSurfaces().contains(lastSurface);
+
+ outputConfigCompat.removeSurface(lastSurface);
+
+ assertThat(containedBeforeRemoval).isTrue();
+ assertThat(outputConfigCompat.getSurfaces()).doesNotContain(lastSurface);
+ assertThat(outputConfigCompat.getSurfaces()).containsExactlyElementsIn(allSurfaces);
+ }
+
+ @Test
+ @Config(maxSdk = 23)
+ public void cannotRetrieveOutputConfiguration_onApi23AndBelow() {
+ Surface surface = mock(Surface.class);
+ OutputConfigurationCompat outputConfigCompat = new OutputConfigurationCompat(surface);
+
+ Object outputConfig = outputConfigCompat.unwrap();
+ assertThat(outputConfig).isNull();
+ }
+
+ @Test
+ @Config(minSdk = 24)
+ public void canRetrieveOutputConfiguration_onApi24AndUp() {
+ Surface surface = mock(Surface.class);
+ OutputConfigurationCompat outputConfigCompat = new OutputConfigurationCompat(surface);
+
+ Object outputConfig = outputConfigCompat.unwrap();
+ assertThat(outputConfig).isNotNull();
+ assertThat(outputConfig).isInstanceOf(OutputConfiguration.class);
+ }
+
+ @Test
+ @Config(minSdk = 24)
+ public void canWrapOutputConfiguration() {
+ Surface surface = mock(Surface.class);
+ OutputConfiguration outputConfig = new OutputConfiguration(surface);
+ OutputConfigurationCompat outputConfigCompat = OutputConfigurationCompat.wrap(outputConfig);
+
+ assertThat(outputConfigCompat.unwrap()).isSameInstanceAs(outputConfig);
+ }
+
+ @Test
+ @Config(minSdk = 24)
+ public void canSetGroupId_andRetrieveThroughCompatObject() {
+ Surface surface = mock(Surface.class);
+ OutputConfiguration outputConfig = new OutputConfiguration(TEST_GROUP_ID, surface);
+ OutputConfigurationCompat outputConfigCompat = OutputConfigurationCompat.wrap(outputConfig);
+
+ assertThat(outputConfigCompat.getSurfaceGroupId()).isEqualTo(TEST_GROUP_ID);
+ }
+
+ @Test
+ @Config(minSdk = 26)
+ public void canCreateDeferredOutputConfiguration_andRetrieveNullSurface() {
+ Surface surface = mock(Surface.class);
+ OutputConfigurationCompat outputConfigCompat = new OutputConfigurationCompat(
+ new Size(1024, 768), SurfaceTexture.class);
+
+ assertThat(outputConfigCompat.getSurface()).isNull();
+ }
+
+ @Test
+ @Config(minSdk = 26, maxSdk = 26)
+ public void sharedSurfaceCount_canBeRetrievedOnApi26() {
+ Surface surface = mock(Surface.class);
+ OutputConfigurationCompat outputConfigCompat = new OutputConfigurationCompat(surface);
+
+ // API 26 hard-codes max shared surface count to 2, but we have to retrieve that via
+ // reflection
+ assertThat(outputConfigCompat.getMaxSharedSurfaceCount()).isEqualTo(2);
+ }
+
+ @Test
+ @Config(minSdk = 28)
+ public void canSetPhysicalCameraId() {
+ OutputConfiguration outputConfig = mock(OutputConfiguration.class);
+
+ OutputConfigurationCompat outputConfigCompat = OutputConfigurationCompat.wrap(outputConfig);
+
+ outputConfigCompat.setPhysicalCameraId(PHYSICAL_CAMERA_ID);
+
+ verify(outputConfig, times(1)).setPhysicalCameraId(PHYSICAL_CAMERA_ID);
+ }
+}
diff --git a/camera/camera2/src/test/java/androidx/camera/camera2/impl/compat/params/SessionConfigurationCompatTest.java b/camera/camera2/src/test/java/androidx/camera/camera2/impl/compat/params/SessionConfigurationCompatTest.java
new file mode 100644
index 0000000..e122ef4
--- /dev/null
+++ b/camera/camera2/src/test/java/androidx/camera/camera2/impl/compat/params/SessionConfigurationCompatTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.impl.compat.params;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.params.OutputConfiguration;
+import android.hardware.camera2.params.SessionConfiguration;
+import android.os.Build;
+import android.view.Surface;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public final class SessionConfigurationCompatTest {
+
+ private static final int WIDTH = 1024;
+ private static final int HEIGHT = 768;
+ private static final int FORMAT = ImageFormat.YUV_420_888;
+ private static final int DEFAULT_SESSION_TYPE = SessionConfigurationCompat.SESSION_REGULAR;
+
+ private List<OutputConfigurationCompat> mOutputs;
+ private Executor mCallbackExecutor;
+ private CameraCaptureSession.StateCallback mStateCallback;
+
+ @Before
+ public void setUp() {
+ mOutputs = new ArrayList<>();
+ for (int i = 0; i < 3; ++i) {
+ Surface surface = mock(Surface.class);
+ OutputConfigurationCompat outputConfigCompat = new OutputConfigurationCompat(surface);
+ mOutputs.add(outputConfigCompat);
+ }
+
+ mCallbackExecutor = mock(Executor.class);
+
+ mStateCallback = mock(CameraCaptureSession.StateCallback.class);
+ }
+
+ private SessionConfigurationCompat createDefaultSessionConfig() {
+ return new SessionConfigurationCompat(
+ DEFAULT_SESSION_TYPE,
+ mOutputs,
+ mCallbackExecutor,
+ mStateCallback);
+ }
+
+ @Test
+ public void canCreateSessionConfiguration() {
+ SessionConfigurationCompat sessionConfigCompat = createDefaultSessionConfig();
+
+ assertThat(sessionConfigCompat.getSessionType()).isEqualTo(DEFAULT_SESSION_TYPE);
+ assertThat(sessionConfigCompat.getOutputConfigurations()).containsExactlyElementsIn(
+ mOutputs);
+ assertThat(sessionConfigCompat.getExecutor()).isSameInstanceAs(mCallbackExecutor);
+ assertThat(sessionConfigCompat.getStateCallback()).isSameInstanceAs(mStateCallback);
+ }
+
+ @Test
+ @Config(minSdk = 28)
+ public void canWrapAndUnwrapSessionConfiguration() {
+ List<OutputConfiguration> outputConfigs = new ArrayList<>(mOutputs.size());
+ for (OutputConfigurationCompat outputConfigCompat : mOutputs) {
+ outputConfigs.add((OutputConfiguration) outputConfigCompat.unwrap());
+ }
+
+ SessionConfiguration sessionConfig = new SessionConfiguration(
+ SessionConfiguration.SESSION_REGULAR,
+ outputConfigs,
+ mCallbackExecutor,
+ mStateCallback);
+
+ SessionConfigurationCompat sessionConfigCompat = SessionConfigurationCompat.wrap(
+ sessionConfig);
+
+ assertThat(sessionConfigCompat.getSessionType()).isEqualTo(
+ SessionConfigurationCompat.SESSION_REGULAR);
+ assertThat(sessionConfigCompat.getOutputConfigurations()).containsExactlyElementsIn(
+ mOutputs);
+ assertThat(sessionConfigCompat.getExecutor()).isSameInstanceAs(mCallbackExecutor);
+ assertThat(sessionConfigCompat.getStateCallback()).isSameInstanceAs(mStateCallback);
+
+ assertThat(sessionConfigCompat.unwrap()).isSameInstanceAs(sessionConfig);
+ assertThat(
+ ((SessionConfiguration) sessionConfigCompat.unwrap()).getOutputConfigurations())
+ .containsExactlyElementsIn(
+ outputConfigs);
+ }
+
+ @Test
+ public void canSetAndRetrieveInputConfiguration() {
+ SessionConfigurationCompat sessionConfigCompat = createDefaultSessionConfig();
+
+ InputConfigurationCompat inputConfigurationCompat = new InputConfigurationCompat(WIDTH,
+ HEIGHT, FORMAT);
+
+ sessionConfigCompat.setInputConfiguration(inputConfigurationCompat);
+
+ // getInputConfiguration() is not necessarily the same instance, but should be equivalent
+ // by comparison
+ assertThat(sessionConfigCompat.getInputConfiguration()).isEqualTo(inputConfigurationCompat);
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void cannotSetInputConfiguration_onHighSpeedSession() {
+ SessionConfigurationCompat sessionConfigCompat = new SessionConfigurationCompat(
+ SessionConfigurationCompat.SESSION_HIGH_SPEED,
+ mOutputs,
+ mCallbackExecutor,
+ mStateCallback);
+
+ InputConfigurationCompat inputConfigurationCompat = new InputConfigurationCompat(WIDTH,
+ HEIGHT, FORMAT);
+
+ sessionConfigCompat.setInputConfiguration(inputConfigurationCompat);
+ }
+
+ @Test
+ @Config(minSdk = 28)
+ public void constantsMatchNonCompatVersion() {
+ assertThat(SessionConfigurationCompat.SESSION_REGULAR).isEqualTo(
+ SessionConfiguration.SESSION_REGULAR);
+ assertThat(SessionConfigurationCompat.SESSION_HIGH_SPEED).isEqualTo(
+ SessionConfiguration.SESSION_HIGH_SPEED);
+ }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CaptureConfigTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CaptureConfigTest.java
index 1a332d5..b4e0c52 100644
--- a/camera/core/src/androidTest/java/androidx/camera/core/CaptureConfigTest.java
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CaptureConfigTest.java
@@ -27,7 +27,7 @@
import android.view.Surface;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.LargeTest;
import com.google.common.collect.Lists;
@@ -38,7 +38,7 @@
import java.util.List;
import java.util.Map;
-@SmallTest
+@LargeTest
@RunWith(AndroidJUnit4.class)
public class CaptureConfigTest {
private DeferrableSurface mMockSurface0;
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageReaderListenerTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageReaderListenerTest.java
index 6e9020e..cec4086 100644
--- a/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageReaderListenerTest.java
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageReaderListenerTest.java
@@ -33,7 +33,7 @@
import android.view.Surface;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.LargeTest;
import org.junit.After;
import org.junit.Before;
@@ -44,7 +44,7 @@
import java.util.List;
import java.util.concurrent.Semaphore;
-@SmallTest
+@LargeTest
@RunWith(AndroidJUnit4.class)
public final class ForwardingImageReaderListenerTest {
private static final int IMAGE_WIDTH = 640;
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ImageSaverTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ImageSaverTest.java
index 3cce59c..96173bc 100644
--- a/camera/core/src/androidTest/java/androidx/camera/core/ImageSaverTest.java
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ImageSaverTest.java
@@ -37,7 +37,7 @@
import androidx.camera.core.ImageSaver.OnImageSavedListener;
import androidx.camera.core.ImageSaver.SaveError;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.MediumTest;
import org.junit.After;
import org.junit.Before;
@@ -50,7 +50,7 @@
import java.nio.ByteBuffer;
import java.util.concurrent.Semaphore;
-@SmallTest
+@MediumTest
@RunWith(AndroidJUnit4.class)
public class ImageSaverTest {
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/QueuedImageReaderProxyTest.java b/camera/core/src/androidTest/java/androidx/camera/core/QueuedImageReaderProxyTest.java
index 21403c2..d7028b6 100644
--- a/camera/core/src/androidTest/java/androidx/camera/core/QueuedImageReaderProxyTest.java
+++ b/camera/core/src/androidTest/java/androidx/camera/core/QueuedImageReaderProxyTest.java
@@ -30,7 +30,7 @@
import android.view.Surface;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.LargeTest;
import org.junit.After;
import org.junit.Before;
@@ -41,7 +41,7 @@
import java.util.List;
import java.util.concurrent.Semaphore;
-@SmallTest
+@LargeTest
@RunWith(AndroidJUnit4.class)
public final class QueuedImageReaderProxyTest {
private static final int IMAGE_WIDTH = 640;
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/SessionConfigTest.java b/camera/core/src/androidTest/java/androidx/camera/core/SessionConfigTest.java
index 67fe0fe..df85c17a 100644
--- a/camera/core/src/androidTest/java/androidx/camera/core/SessionConfigTest.java
+++ b/camera/core/src/androidTest/java/androidx/camera/core/SessionConfigTest.java
@@ -27,7 +27,7 @@
import android.view.Surface;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.MediumTest;
import com.google.common.collect.Lists;
@@ -38,7 +38,7 @@
import java.util.List;
import java.util.Map;
-@SmallTest
+@MediumTest
@RunWith(AndroidJUnit4.class)
public class SessionConfigTest {
private DeferrableSurface mMockSurface0;
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/UseCaseAttachStateTest.java b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseAttachStateTest.java
index d48db54..7b398af 100644
--- a/camera/core/src/androidTest/java/androidx/camera/core/UseCaseAttachStateTest.java
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseAttachStateTest.java
@@ -33,7 +33,7 @@
import androidx.camera.testing.fakes.FakeUseCaseConfig;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.LargeTest;
import org.junit.Before;
import org.junit.Test;
@@ -43,7 +43,7 @@
import java.util.HashMap;
import java.util.Map;
-@SmallTest
+@LargeTest
@RunWith(AndroidJUnit4.class)
public class UseCaseAttachStateTest {
private final LensFacing mCameraLensFacing0 = LensFacing.BACK;
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageAnalysis.java b/camera/core/src/main/java/androidx/camera/core/ImageAnalysis.java
index 259b692..6c5e425 100644
--- a/camera/core/src/main/java/androidx/camera/core/ImageAnalysis.java
+++ b/camera/core/src/main/java/androidx/camera/core/ImageAnalysis.java
@@ -103,8 +103,8 @@
*
* <p>In most cases this should be set to the current rotation returned by {@link
* Display#getRotation()}. In that case, the rotation parameter sent to the analyzer will be
- * the rotation, which if applied to the output image, will make it match a correctly configured
- * preview.
+ * the rotation, which if applied to the output image, will make the image match the display
+ * orientation.
*
* <p>While rotation can also be set via
* {@link ImageAnalysisConfig.Builder#setTargetRotation(int)}, using
@@ -115,9 +115,11 @@
*
* <p>If no target rotation is set by the application, it is set to the value of
* {@link Display#getRotation()} of the default display at the time the
- * {@link ImageAnalysis} is created.
+ * use case is created.
*
- * @param rotation Desired rotation of the output image.
+ * @param rotation Target rotation of the output image, expressed as one of
+ * {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
+ * {@link Surface#ROTATION_180}, or {@link Surface#ROTATION_270}.
*/
public void setTargetRotation(@RotationValue int rotation) {
ImageAnalysisConfig oldConfig = (ImageAnalysisConfig) getUseCaseConfig();
@@ -344,6 +346,9 @@
/**
* Analyzes an image to produce a result.
*
+ * <p>This method is called once for each image from the camera, and called at the
+ * frame rate of the camera. Each analyze call is executed sequentially.
+ *
* <p>The caller is responsible for ensuring this analysis method can be executed quickly
* enough to prevent stalls in the image acquisition pipeline. Otherwise, newly available
* images will not be acquired and analyzed.
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/core/src/main/java/androidx/camera/core/ImageCapture.java
index 10fb18a..7aac19e 100644
--- a/camera/core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -293,8 +293,7 @@
*
* <p>In most cases this should be set to the current rotation returned by {@link
* Display#getRotation()}. In that case, the output rotation from takePicture calls will be the
- * rotation, which if applied to the output image, will make it match a correctly configured
- * preview.
+ * rotation, which if applied to the output image, will make it match the display orientation.
*
* <p>While rotation can also be set via
* {@link ImageCaptureConfig.Builder#setTargetRotation(int)}, using
@@ -304,9 +303,9 @@
*
* <p>If no target rotation is set by the application, it is set to the value of
* {@link Display#getRotation()} of the default display at the time the
- * {@link ImageCapture} is created.
+ * use case is created.
*
- * @param rotation Desired rotation of the output image. rotation is expressed as one of
+ * @param rotation Target rotation of the output image, expressed as one of
* {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
* {@link Surface#ROTATION_180}, or {@link Surface#ROTATION_270}.
*/
@@ -1004,10 +1003,10 @@
* rotation applied. rotationDegrees describes the magnitude of clockwise rotation, which
* if applied to the image will make it match the currently configured target rotation.
*
- * <p>For example, if the current target rotation is set to the display rotation, then for a
- * correctly configured preview, rotationDegrees is the rotation to apply to the image to
- * match what is seen in the preview. A rotation of 90 degrees would mean rotating the
- * image 90 degrees clockwise produces an image that will match the preview.
+ * <p>For example, if the current target rotation is set to the display rotation,
+ * rotationDegrees is the rotation to apply to the image to match the display orientation.
+ * A rotation of 90 degrees would mean rotating the image 90 degrees clockwise produces an
+ * image that will match the display orientation.
*
* <p>See also {@link ImageCaptureConfig.Builder#setTargetRotation(int)} and
* {@link #setTargetRotation(int)}.
diff --git a/camera/core/src/main/java/androidx/camera/core/Preview.java b/camera/core/src/main/java/androidx/camera/core/Preview.java
index 100ecce..94d371e 100644
--- a/camera/core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/core/src/main/java/androidx/camera/core/Preview.java
@@ -40,10 +40,31 @@
import java.util.Objects;
/**
- * A use case that provides a camera preview stream for a view finder.
+ * A use case that provides a camera preview stream for displaying on-screen.
*
- * <p>The preview stream is connected to an underlying {@link SurfaceTexture}. The caller is still
- * responsible for deciding how this texture is shown.
+ * <p>The preview stream is connected to an underlying {@link SurfaceTexture}. This SurfaceTexture
+ * is created by the Preview use case and provided as an output after it is configured and attached
+ * to the camera. The application receives the SurfaceTexture by setting an output listener with
+ * {@link Preview#setOnPreviewOutputUpdateListener(OnPreviewOutputUpdateListener)}. When the
+ * lifecycle becomes active, the camera will start and images will be streamed to the
+ * SurfaceTexture.
+ * {@link OnPreviewOutputUpdateListener#onUpdated(PreviewOutput)} is called when a
+ * new SurfaceTexture is created. A SurfaceTexture is created each time the use case becomes
+ * active and no previous SurfaceTexture exists.
+ *
+ * <p>The application can then decide how this texture is shown. The texture data is as received
+ * by the camera system with no rotation applied. To display the SurfaceTexture with the correct
+ * orientation, the rotation parameter sent to {@link Preview.OnPreviewOutputUpdateListener} can
+ * be used to create a correct transformation matrix for display. See
+ * {@link #setTargetRotation(int)} and {@link PreviewConfig.Builder#setTargetRotation(int)} for
+ * details. See {@link Preview#setOnPreviewOutputUpdateListener(OnPreviewOutputUpdateListener)} for
+ * notes if attaching the SurfaceTexture to {@link android.view.TextureView}.
+ *
+ * <p>The application is responsible for managing SurfaceTexture after receiving it. See
+ * {@link Preview#setOnPreviewOutputUpdateListener(OnPreviewOutputUpdateListener)} for notes on
+ * if overriding {@link
+ * android.view.TextureView.SurfaceTextureListener#onSurfaceTextureDestroyed(SurfaceTexture)}.
+ *
*/
public class Preview extends UseCase {
/**
@@ -72,7 +93,7 @@
private boolean mSurfaceDispatched = false;
/**
- * Creates a new view finder use case from the given configuration.
+ * Creates a new preview use case from the given configuration.
*
* @param config for this use case instance
*/
@@ -126,11 +147,14 @@
* data. Setting the listener to {@code null} will signal to the camera that the camera should
* no longer stream data to the last {@link PreviewOutput}.
*
- * <p>Once {@link OnPreviewOutputUpdateListener#onUpdated(PreviewOutput)} is called,
+ * <p>Once {@link OnPreviewOutputUpdateListener#onUpdated(PreviewOutput)} is called,
* ownership of the {@link PreviewOutput} and its contents is transferred to the application. It
* is the application's responsibility to release the last {@link SurfaceTexture} returned by
* {@link PreviewOutput#getSurfaceTexture()} when a new SurfaceTexture is provided via an update
- * or when the user is finished with the use case.
+ * or when the user is finished with the use case. A SurfaceTexture is created each time the
+ * use case becomes active and no previous SurfaceTexture exists.
+ * {@link OnPreviewOutputUpdateListener#onUpdated(PreviewOutput)} is called when a new
+ * SurfaceTexture is created.
*
* <p>Calling {@link android.view.TextureView#setSurfaceTexture(SurfaceTexture)} when the
* TextureView's SurfaceTexture is already created, should be preceded by calling
@@ -138,8 +162,8 @@
* {@link android.view.ViewGroup#addView(View)} on the parent view of the TextureView to ensure
* the setSurfaceTexture() call succeeds.
*
- * <p>Since {@link OnPreviewOutputUpdateListener} is called when the underlying SurfaceTexture
- * is created, applications that override and return false from {@link
+ * <p>Since {@link OnPreviewOutputUpdateListener#onUpdated(PreviewOutput)} is called when the
+ * underlying SurfaceTexture is created, applications that override and return false from {@link
* android.view.TextureView.SurfaceTextureListener#onSurfaceTextureDestroyed(SurfaceTexture)}
* should be sure to call {@link android.view.TextureView#setSurfaceTexture(SurfaceTexture)}
* with the output from the previous {@link PreviewOutput} to attach it to a new TextureView,
@@ -176,11 +200,13 @@
}
/**
- * Adjusts the view finder according to the properties in some local regions.
+ * Adjusts the preview according to the properties in some local regions.
*
* <p>The auto-focus (AF) and auto-exposure (AE) properties will be recalculated from the local
* regions.
*
+ * <p>Dimensions of the sensor coordinate frame can be found using Camera2.
+ *
* @param focus rectangle with dimensions in sensor coordinate frame for focus
* @param metering rectangle with dimensions in sensor coordinate frame for metering
*/
@@ -189,12 +215,14 @@
}
/**
- * Adjusts the view finder according to the properties in some local regions with a callback
+ * Adjusts the preview according to the properties in some local regions with a callback
* called once focus scan has completed.
*
* <p>The auto-focus (AF) and auto-exposure (AE) properties will be recalculated from the local
* regions.
*
+ * <p>Dimensions of the sensor coordinate frame can be found using Camera2.
+ *
* @param focus rectangle with dimensions in sensor coordinate frame for focus
* @param metering rectangle with dimensions in sensor coordinate frame for metering
* @param listener listener for when focus has completed
@@ -204,7 +232,12 @@
}
/**
- * Adjusts the view finder to zoom to a local region.
+ * Adjusts the preview to zoom to a local region.
+ *
+ * <p>Setting the zoom is equivalent to setting a scalar crop region (digital zoom), and zoom
+ * occurs about the center of the image.
+ *
+ * <p>Dimensions of the sensor coordinate frame can be found using Camera2.
*
* @param crop rectangle with dimensions in sensor coordinate frame for zooming
*/
@@ -215,6 +248,9 @@
/**
* Sets torch on/off.
*
+ * When the torch is on, the torch will remain on during photo capture regardless of flash
+ * setting. When the torch is off, flash will function as set by {@link ImageCapture}.
+ *
* @param torch True if turn on torch, otherwise false
*/
public void enableTorch(boolean torch) {
@@ -227,13 +263,23 @@
}
/**
- * Sets the rotation of the surface texture consumer.
+ * Sets the target rotation.
+ *
+ * <p>This informs the use case so it can adjust the rotation value sent to
+ * {@link Preview.OnPreviewOutputUpdateListener}.
*
* <p>In most cases this should be set to the current rotation returned by {@link
- * Display#getRotation()}. This will update the rotation value in {@link PreviewOutput} to
- * reflect the angle the PreviewOutput should be rotated to match the supplied rotation.
+ * Display#getRotation()}. In that case, the rotation values output by the use case will be
+ * the rotation, which if applied to the output image, will make the image match the display
+ * orientation.
*
- * @param rotation Rotation of the surface texture consumer.
+ * <p>If no target rotation is set by the application, it is set to the value of
+ * {@link Display#getRotation()} of the default display at the time the
+ * use case is created.
+ *
+ * @param rotation Rotation of the surface texture consumer expressed as one of
+ * {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
+ * {@link Surface#ROTATION_180}, or {@link Surface#ROTATION_270}.
*/
public void setTargetRotation(@RotationValue int rotation) {
ImageOutputConfig oldConfig = (ImageOutputConfig) getUseCaseConfig();
diff --git a/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java b/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
index 110cadd..540ed5d 100644
--- a/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
+++ b/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
@@ -40,4 +40,14 @@
public CaptureStageImpl getCaptureStage() {
throw new RuntimeException("Stub, replace with implementation.");
}
+
+ @Override
+ public ProcessorType getProcessorType() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
+
+ @Override
+ public RequestUpdateProcessorImpl getRequestUpdatePreviewProcessor() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
}
diff --git a/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java b/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
index 15c0e76..d3336d9 100644
--- a/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
+++ b/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
@@ -29,7 +29,7 @@
* This gets called to update where the CaptureProcessor should write the output of {@link
* #process(Map)}.
*
- * @param surface The {@link Surface} that the CaptureProcessor should write data into.
+ * @param surface The {@link Surface} that the CaptureProcessor should write data into.
* @param imageFormat The format of that the surface expects.
*/
void onOutputSurface(Surface surface, int imageFormat);
@@ -39,9 +39,10 @@
*
* <p> The result of the processing step should be written to the {@link Surface} that was
* received by {@link #onOutputSurface(Surface, int)}.
- * @param images The map of images to process. The {@link Image} that are contained within the
- * map will become invalid after this method completes, so no references to them
- * should be kept.
+ *
+ * @param images The map of images to process. The {@link Image} that are
+ * contained within the map will become invalid after this method completes,
+ * so no references to them should be kept.
*/
void process(Map<Integer, Image> images);
}
diff --git a/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java b/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
index 377f215..ff1206d 100644
--- a/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
+++ b/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
@@ -42,4 +42,14 @@
public CaptureStageImpl getCaptureStage() {
throw new RuntimeException("Stub, replace with implementation.");
}
+
+ @Override
+ public ProcessorType getProcessorType() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
+
+ @Override
+ public RequestUpdateProcessorImpl getRequestUpdatePreviewProcessor() {
+ throw new RuntimeException("Stub, replace with implementation.");
+ }
}
diff --git a/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java b/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java
index 6e11eca..9402782 100644
--- a/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java
+++ b/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java
@@ -19,10 +19,16 @@
import android.hardware.camera2.CameraCharacteristics;
/**
- * Provides abstract methods that the OEM needs to implement to enable extensions in the view
- * finder.
+ * Provides abstract methods that the OEM needs to implement to enable extensions in the preview.
*/
public interface PreviewExtenderImpl {
+ /** The different types of the preview processing. */
+ enum ProcessorType {
+ /** Processing which only updates the {@link CaptureStageImpl}. */
+ PROCESSOR_TYPE_REQUEST_UPDATE_ONLY,
+ PROCESSOR_TYPE_NONE
+ }
+
/**
* Indicates whether the extension is supported on the device.
*
@@ -40,6 +46,21 @@
*/
void enableExtension(String cameraId, CameraCharacteristics cameraCharacteristics);
- /** The set of parameters required to produce the effect on images. */
+ /**
+ * The set of parameters required to produce the effect on the preview stream.
+ *
+ * <p> This will be the initial set of parameters used for the preview
+ * {@link android.hardware.camera2.CaptureRequest}. Once the {@link RequestUpdateProcessorImpl}
+ * from {@link #getRequestUpdatePreviewProcessor()} has been called, this should be updated to
+ * reflect the new {@link CaptureStageImpl}. If the processing step returns a {@code null},
+ * meaning the required parameters has not changed, then calling this will return the previous
+ * non-null value.
+ */
CaptureStageImpl getCaptureStage();
+
+ /** The type of preview processing to use. */
+ ProcessorType getProcessorType();
+
+ /** Returns a processor which only updates the {@link CaptureStageImpl}. */
+ RequestUpdateProcessorImpl getRequestUpdatePreviewProcessor();
}
diff --git a/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/RequestUpdateProcessorImpl.java b/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/RequestUpdateProcessorImpl.java
new file mode 100644
index 0000000..5c2e952
--- /dev/null
+++ b/camera/extensions-stub/src/main/java/androidx/camera/extensions/impl/RequestUpdateProcessorImpl.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.impl;
+
+import android.hardware.camera2.TotalCaptureResult;
+
+/**
+ * Processing a {@link TotalCaptureResult} to update a CaptureStage.
+ */
+public interface RequestUpdateProcessorImpl {
+ /**
+ * Process the {@link TotalCaptureResult} to update the {@link CaptureStageImpl}
+ *
+ * @param result The metadata associated with the image. Can be null if the image and meta have
+ * not been synced.
+ * @return The updated parameters used for the repeating requests. If this is {@code null} then
+ * the previous parameters will be used.
+ */
+ CaptureStageImpl process(TotalCaptureResult result);
+}
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
index 2eadb3a..aa3516a 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
@@ -50,4 +50,14 @@
return captureStage;
}
+
+ @Override
+ public ProcessorType getProcessorType() {
+ return ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY;
+ }
+
+ @Override
+ public RequestUpdateProcessorImpl getRequestUpdatePreviewProcessor() {
+ return RequestUpdateProcessorImpls.noUpdateProcessor();
+ }
}
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
index 3602a4b..4f3cf65 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
@@ -51,4 +51,14 @@
return captureStage;
}
+
+ @Override
+ public ProcessorType getProcessorType() {
+ return ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY;
+ }
+
+ @Override
+ public RequestUpdateProcessorImpl getRequestUpdatePreviewProcessor() {
+ return RequestUpdateProcessorImpls.noUpdateProcessor();
+ }
}
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java
index 6e11eca..9402782 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewExtenderImpl.java
@@ -19,10 +19,16 @@
import android.hardware.camera2.CameraCharacteristics;
/**
- * Provides abstract methods that the OEM needs to implement to enable extensions in the view
- * finder.
+ * Provides abstract methods that the OEM needs to implement to enable extensions in the preview.
*/
public interface PreviewExtenderImpl {
+ /** The different types of the preview processing. */
+ enum ProcessorType {
+ /** Processing which only updates the {@link CaptureStageImpl}. */
+ PROCESSOR_TYPE_REQUEST_UPDATE_ONLY,
+ PROCESSOR_TYPE_NONE
+ }
+
/**
* Indicates whether the extension is supported on the device.
*
@@ -40,6 +46,21 @@
*/
void enableExtension(String cameraId, CameraCharacteristics cameraCharacteristics);
- /** The set of parameters required to produce the effect on images. */
+ /**
+ * The set of parameters required to produce the effect on the preview stream.
+ *
+ * <p> This will be the initial set of parameters used for the preview
+ * {@link android.hardware.camera2.CaptureRequest}. Once the {@link RequestUpdateProcessorImpl}
+ * from {@link #getRequestUpdatePreviewProcessor()} has been called, this should be updated to
+ * reflect the new {@link CaptureStageImpl}. If the processing step returns a {@code null},
+ * meaning the required parameters has not changed, then calling this will return the previous
+ * non-null value.
+ */
CaptureStageImpl getCaptureStage();
+
+ /** The type of preview processing to use. */
+ ProcessorType getProcessorType();
+
+ /** Returns a processor which only updates the {@link CaptureStageImpl}. */
+ RequestUpdateProcessorImpl getRequestUpdatePreviewProcessor();
}
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/RequestUpdateProcessorImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/RequestUpdateProcessorImpl.java
new file mode 100644
index 0000000..0c8d49b
--- /dev/null
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/RequestUpdateProcessorImpl.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.impl;
+
+import android.hardware.camera2.TotalCaptureResult;
+
+/**
+ * The interface for processing a {@link TotalCaptureResult} only.
+ */
+public interface RequestUpdateProcessorImpl {
+ /**
+ * Process the {@link TotalCaptureResult} to update the {@link CaptureStageImpl}
+ *
+ * @param result The metadata associated with the image. Can be null if the image and meta have
+ * not been synced.
+ * @return The updated parameters used for the repeating requests. If this is {@code null} then
+ * the previous parameters will be used.
+ */
+ CaptureStageImpl process(TotalCaptureResult result);
+}
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/RequestUpdateProcessorImpls.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/RequestUpdateProcessorImpls.java
new file mode 100644
index 0000000..86382f5
--- /dev/null
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/RequestUpdateProcessorImpls.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.impl;
+
+import android.hardware.camera2.TotalCaptureResult;
+
+class RequestUpdateProcessorImpls {
+ private static final RequestUpdateProcessorImpl sNoUpdateProcessor =
+ new RequestUpdateProcessorImpl() {
+ @Override
+ public CaptureStageImpl process(TotalCaptureResult result) {
+ return null;
+ }
+ };
+
+ static RequestUpdateProcessorImpl noUpdateProcessor() {
+ return sNoUpdateProcessor;
+ }
+
+ private RequestUpdateProcessorImpls() {
+ }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
index 5e77531..98c1feb 100644
--- a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
@@ -16,65 +16,171 @@
package androidx.camera.testing.fakes;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
import androidx.camera.core.BaseCamera;
import androidx.camera.core.CameraControl;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.CaptureConfig;
+import androidx.camera.core.DeferrableSurface;
+import androidx.camera.core.DeferrableSurfaces;
import androidx.camera.core.SessionConfig;
import androidx.camera.core.UseCase;
+import androidx.camera.core.UseCaseAttachState;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
-/** A fake camera which will not produce any data. */
+/**
+ * A fake camera which will not produce any data, but provides a valid BaseCamera implementation.
+ */
public class FakeCamera implements BaseCamera {
+ private static final String TAG = "FakeCamera";
+ private static final String DEFAULT_CAMERA_ID = "0";
private final CameraControl mCameraControl;
-
private final CameraInfo mCameraInfo;
+ private String mCameraId;
+ private UseCaseAttachState mUseCaseAttachState;
+ private State mState = State.INITIALIZED;
+
+ @Nullable
+ private SessionConfig mSessionConfig;
+ @Nullable
+ private SessionConfig mCameraControlSessionConfig;
+
+ private List<DeferrableSurface> mConfiguredDeferrableSurfaces = Collections.emptyList();
public FakeCamera() {
- this(new FakeCameraInfo(), CameraControl.DEFAULT_EMPTY_INSTANCE);
+ this(DEFAULT_CAMERA_ID, new FakeCameraInfo(), /*cameraControl=*/null);
}
- public FakeCamera(CameraInfo cameraInfo, CameraControl cameraControl) {
+ public FakeCamera(String cameraId) {
+ this(cameraId, new FakeCameraInfo(), /*cameraControl=*/null);
+ }
+
+ public FakeCamera(CameraInfo cameraInfo, @Nullable CameraControl cameraControl) {
+ this(DEFAULT_CAMERA_ID, cameraInfo, cameraControl);
+ }
+
+ public FakeCamera(String cameraId,
+ CameraInfo cameraInfo,
+ @Nullable CameraControl cameraControl) {
mCameraInfo = cameraInfo;
- mCameraControl = cameraControl;
+ mCameraId = cameraId;
+ mUseCaseAttachState = new UseCaseAttachState(cameraId);
+ mCameraControl = cameraControl == null ? new FakeCameraControl(this) : cameraControl;
}
@Override
public void open() {
+ checkNotReleased();
+ if (mState == State.INITIALIZED) {
+ mState = State.OPENED;
+ }
}
@Override
public void close() {
+ checkNotReleased();
+ if (mState == State.OPENED) {
+ mSessionConfig = null;
+ reconfigure();
+ mState = State.INITIALIZED;
+ }
}
@Override
public void release() {
+ checkNotReleased();
+ if (mState == State.OPENED) {
+ close();
+ }
+
+ mState = State.RELEASED;
}
@Override
- public void addOnlineUseCase(Collection<UseCase> useCases) {
+ public void onUseCaseActive(final UseCase useCase) {
+ Log.d(TAG, "Use case " + useCase + " ACTIVE for camera " + mCameraId);
+
+ mUseCaseAttachState.setUseCaseActive(useCase);
+ updateCaptureSessionConfig();
+ }
+
+ /** Removes the use case from a state of issuing capture requests. */
+ @Override
+ public void onUseCaseInactive(final UseCase useCase) {
+ Log.d(TAG, "Use case " + useCase + " INACTIVE for camera " + mCameraId);
+
+ mUseCaseAttachState.setUseCaseInactive(useCase);
+ updateCaptureSessionConfig();
+ }
+
+ /** Updates the capture requests based on the latest settings. */
+ @Override
+ public void onUseCaseUpdated(final UseCase useCase) {
+ Log.d(TAG, "Use case " + useCase + " UPDATED for camera " + mCameraId);
+
+ mUseCaseAttachState.updateUseCase(useCase);
+ updateCaptureSessionConfig();
}
@Override
- public void removeOnlineUseCase(Collection<UseCase> useCases) {
+ public void onUseCaseReset(final UseCase useCase) {
+ Log.d(TAG, "Use case " + useCase + " RESET for camera " + mCameraId);
+
+ mUseCaseAttachState.updateUseCase(useCase);
+ updateCaptureSessionConfig();
+ openCaptureSession();
}
+ /**
+ * Sets the use case to be in the state where the capture session will be configured to handle
+ * capture requests from the use case.
+ */
@Override
- public void onUseCaseActive(UseCase useCase) {
+ public void addOnlineUseCase(final Collection<UseCase> useCases) {
+ if (useCases.isEmpty()) {
+ return;
+ }
+
+ Log.d(TAG, "Use cases " + useCases + " ONLINE for camera " + mCameraId);
+ for (UseCase useCase : useCases) {
+ mUseCaseAttachState.setUseCaseOnline(useCase);
+ }
+
+ open();
+ updateCaptureSessionConfig();
+ openCaptureSession();
}
+ /**
+ * Removes the use case to be in the state where the capture session will be configured to
+ * handle capture requests from the use case.
+ */
@Override
- public void onUseCaseInactive(UseCase useCase) {
- }
+ public void removeOnlineUseCase(final Collection<UseCase> useCases) {
+ if (useCases.isEmpty()) {
+ return;
+ }
- @Override
- public void onUseCaseUpdated(UseCase useCase) {
- }
+ Log.d(TAG, "Use cases " + useCases + " OFFLINE for camera " + mCameraId);
+ for (UseCase useCase : useCases) {
+ mUseCaseAttachState.setUseCaseOffline(useCase);
+ }
- @Override
- public void onUseCaseReset(UseCase useCase) {
+ if (mUseCaseAttachState.getOnlineUseCases().isEmpty()) {
+ close();
+ return;
+ }
+
+ openCaptureSession();
+ updateCaptureSessionConfig();
}
// Returns fixed CameraControl instance in order to verify the instance is correctly attached.
@@ -90,9 +196,104 @@
@Override
public void onCameraControlUpdateSessionConfig(SessionConfig sessionConfig) {
+ mCameraControlSessionConfig = sessionConfig;
+ updateCaptureSessionConfig();
}
@Override
public void onCameraControlCaptureRequests(List<CaptureConfig> captureConfigs) {
+ Log.d(TAG, "Capture requests submitted:\n " + TextUtils.join("\n ", captureConfigs));
}
+
+ private void checkNotReleased() {
+ if (mState == State.RELEASED) {
+ throw new IllegalStateException("Camera has been released.");
+ }
+ }
+
+ private void openCaptureSession() {
+ SessionConfig.ValidatingBuilder validatingBuilder;
+ validatingBuilder = mUseCaseAttachState.getOnlineBuilder();
+ if (!validatingBuilder.isValid()) {
+ Log.d(TAG, "Unable to create capture session due to conflicting configurations");
+ return;
+ }
+
+ if (mState != State.OPENED) {
+ Log.d(TAG, "CameraDevice is not opened");
+ return;
+ }
+
+ mSessionConfig = validatingBuilder.build();
+ reconfigure();
+ }
+
+ private void updateCaptureSessionConfig() {
+ SessionConfig.ValidatingBuilder validatingBuilder;
+ validatingBuilder = mUseCaseAttachState.getActiveAndOnlineBuilder();
+
+ if (validatingBuilder.isValid()) {
+ // Apply CameraControl's SessionConfig to let CameraControl be able to control
+ // Repeating Request and process results.
+ validatingBuilder.add(mCameraControlSessionConfig);
+
+ mSessionConfig = validatingBuilder.build();
+ }
+ }
+
+ private void reconfigure() {
+ notifySurfaceDetached();
+
+ if (mSessionConfig != null) {
+ List<DeferrableSurface> surfaces = mSessionConfig.getSurfaces();
+
+ // Before creating capture session, some surfaces may need to refresh.
+ DeferrableSurfaces.refresh(surfaces);
+
+ mConfiguredDeferrableSurfaces = new ArrayList<>(surfaces);
+
+ List<Surface> configuredSurfaces = new ArrayList<>(
+ DeferrableSurfaces.surfaceSet(
+ mConfiguredDeferrableSurfaces));
+ if (configuredSurfaces.isEmpty()) {
+ Log.e(TAG, "Unable to open capture session with no surfaces. ");
+ return;
+ }
+ }
+
+ notifySurfaceAttached();
+ }
+
+ // Notify the surface is attached to a new capture session.
+ private void notifySurfaceAttached() {
+ for (DeferrableSurface deferrableSurface : mConfiguredDeferrableSurfaces) {
+ deferrableSurface.notifySurfaceAttached();
+ }
+ }
+
+ // Notify the surface is detached from current capture session.
+ private void notifySurfaceDetached() {
+ for (DeferrableSurface deferredSurface : mConfiguredDeferrableSurfaces) {
+ deferredSurface.notifySurfaceDetached();
+ }
+ // Clears the mConfiguredDeferrableSurfaces to prevent from duplicate
+ // notifySurfaceDetached calls.
+ mConfiguredDeferrableSurfaces.clear();
+ }
+
+ enum State {
+ /**
+ * Stable state once the camera has been constructed.
+ */
+ INITIALIZED,
+ /**
+ * A stable state where the camera has been opened.
+ */
+ OPENED,
+ /**
+ * A stable state where the camera has been permanently closed.
+ */
+ RELEASED
+ }
+
}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
new file mode 100644
index 0000000..e4c1da6
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import android.graphics.Rect;
+import android.os.Handler;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.CaptureConfig;
+import androidx.camera.core.FlashMode;
+import androidx.camera.core.OnFocusListener;
+import androidx.camera.core.SessionConfig;
+
+import java.util.List;
+
+/**
+ * A fake implementation for the CameraControl interface.
+ */
+public final class FakeCameraControl implements CameraControl {
+ private static final String TAG = "FakeCameraControl";
+ private final ControlUpdateListener mControlUpdateListener;
+ private final SessionConfig.Builder mSessionConfigBuilder = new SessionConfig.Builder();
+ private boolean mIsTorchOn = false;
+ private FlashMode mFlashMode = FlashMode.OFF;
+
+ public FakeCameraControl(ControlUpdateListener controlUpdateListener) {
+ mControlUpdateListener = controlUpdateListener;
+ updateSessionConfig();
+ }
+
+ @Override
+ public void setCropRegion(final Rect crop) {
+ Log.d(TAG, "setCropRegion(" + crop + ")");
+ }
+
+ @Override
+ public void focus(
+ final Rect focus,
+ final Rect metering,
+ @Nullable final OnFocusListener listener,
+ @Nullable final Handler listenerHandler) {
+ Log.d(TAG, "focus(\n " + focus + ",\n " + metering + ")");
+ }
+
+ @Override
+ public void focus(Rect focus, Rect metering) {
+ focus(focus, metering, null, null);
+ }
+
+ @Override
+ public FlashMode getFlashMode() {
+ return mFlashMode;
+ }
+
+ @Override
+ public void setFlashMode(FlashMode flashMode) {
+ mFlashMode = flashMode;
+ Log.d(TAG, "setFlashMode(" + mFlashMode + ")");
+ }
+
+ @Override
+ public void enableTorch(boolean torch) {
+ mIsTorchOn = torch;
+ Log.d(TAG, "enableTorch(" + torch + ")");
+ }
+
+ @Override
+ public boolean isTorchOn() {
+ return mIsTorchOn;
+ }
+
+ @Override
+ public boolean isFocusLocked() {
+ return false;
+ }
+
+ @Override
+ public void triggerAf() {
+ Log.d(TAG, "triggerAf()");
+ }
+
+ @Override
+ public void triggerAePrecapture() {
+ Log.d(TAG, "triggerAePrecapture()");
+ }
+
+ @Override
+ public void cancelAfAeTrigger(final boolean cancelAfTrigger,
+ final boolean cancelAePrecaptureTrigger) {
+ Log.d(TAG, "cancelAfAeTrigger(" + cancelAfTrigger + ", "
+ + cancelAePrecaptureTrigger + ")");
+ }
+
+ @Override
+ public void submitCaptureRequests(List<CaptureConfig> captureConfigs) {
+ mControlUpdateListener.onCameraControlCaptureRequests(captureConfigs);
+ }
+
+ private void updateSessionConfig() {
+ mControlUpdateListener.onCameraControlUpdateSessionConfig(mSessionConfigBuilder.build());
+ }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
index 36a96be..8d59757 100644
--- a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
@@ -28,11 +28,27 @@
import java.util.Map;
/** A CameraDeviceSurfaceManager which has no supported SurfaceConfigs. */
-public class FakeCameraDeviceSurfaceManager implements CameraDeviceSurfaceManager {
+public final class FakeCameraDeviceSurfaceManager implements CameraDeviceSurfaceManager {
- private static final Size MAX_OUTPUT_SIZE = new Size(0, 0);
+ private static final Size MAX_OUTPUT_SIZE = new Size(4032, 3024); // 12.2 MP
private static final Size PREVIEW_SIZE = new Size(1920, 1080);
+ private Map<String, Map<Class<? extends UseCase>, Size>> mDefinedResolutions = new HashMap<>();
+
+ /**
+ * Sets the given suggested resolutions for the specified camera Id and use case type.
+ */
+ public void setSuggestedResolution(String cameraId, Class<? extends UseCase> type, Size size) {
+ Map<Class<? extends UseCase>, Size> useCaseTypeToSizeMap =
+ mDefinedResolutions.get(cameraId);
+ if (useCaseTypeToSizeMap == null) {
+ useCaseTypeToSizeMap = new HashMap<>();
+ mDefinedResolutions.put(cameraId, useCaseTypeToSizeMap);
+ }
+
+ useCaseTypeToSizeMap.put(type, size);
+ }
+
@Override
public boolean checkSupported(String cameraId, List<SurfaceConfig> surfaceConfigList) {
return false;
@@ -54,7 +70,17 @@
String cameraId, List<UseCase> originalUseCases, List<UseCase> newUseCases) {
Map<UseCase, Size> suggestedSizes = new HashMap<>();
for (UseCase useCase : newUseCases) {
- suggestedSizes.put(useCase, MAX_OUTPUT_SIZE);
+ Size resolution = MAX_OUTPUT_SIZE;
+ Map<Class<? extends UseCase>, Size> definedResolutions =
+ mDefinedResolutions.get(cameraId);
+ if (definedResolutions != null) {
+ Size definedResolution = definedResolutions.get(useCase.getClass());
+ if (definedResolution != null) {
+ resolution = definedResolution;
+ }
+ }
+
+ suggestedSizes.put(useCase, resolution);
}
return suggestedSizes;
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfo.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfo.java
index abba36a..091ba75 100644
--- a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfo.java
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfo.java
@@ -20,6 +20,7 @@
import androidx.annotation.Nullable;
import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraOrientationUtil;
import androidx.camera.core.CameraX.LensFacing;
import androidx.camera.core.ImageOutputConfig.RotationValue;
@@ -28,7 +29,7 @@
*
* <p>This camera info can be constructed with fake values.
*/
-public class FakeCameraInfo implements CameraInfo {
+public final class FakeCameraInfo implements CameraInfo {
private final int mSensorRotation;
private final LensFacing mLensFacing;
@@ -50,7 +51,16 @@
@Override
public int getSensorRotationDegrees(@RotationValue int relativeRotation) {
- return mSensorRotation;
+ int relativeRotationDegrees =
+ CameraOrientationUtil.surfaceRotationToDegrees(relativeRotation);
+ // Currently this assumes that a back-facing camera is always opposite to the screen.
+ // This may not be the case for all devices, so in the future we may need to handle that
+ // scenario.
+ boolean isOppositeFacingScreen = LensFacing.BACK.equals(getLensFacing());
+ return CameraOrientationUtil.getRelativeImageRotation(
+ relativeRotationDegrees,
+ mSensorRotation,
+ isOppositeFacingScreen);
}
@Override
diff --git a/camera/view/build.gradle b/camera/view/build.gradle
index f5e787a4..2a1caa1 100644
--- a/camera/view/build.gradle
+++ b/camera/view/build.gradle
@@ -36,7 +36,7 @@
}
androidx {
name = "Jetpack Camera View Library"
- publish = true
+ publish = false
mavenVersion = LibraryVersions.CAMERA
mavenGroup = LibraryGroups.CAMERA
inceptionYear = "2019"
diff --git a/car/core/api/1.0.0-alpha8.txt b/car/core/api/1.0.0-alpha8.txt
index 6ee7690..90326b8 100644
--- a/car/core/api/1.0.0-alpha8.txt
+++ b/car/core/api/1.0.0-alpha8.txt
@@ -316,6 +316,24 @@
method public void setTitleTextAppearance(@StyleRes int);
}
+ public final class CheckBoxListItem extends androidx.car.widget.CompoundButtonListItem<androidx.car.widget.CheckBoxListItem.ViewHolder> {
+ ctor public CheckBoxListItem(android.content.Context);
+ method public static androidx.car.widget.CheckBoxListItem.ViewHolder createViewHolder(android.view.View);
+ method public int getViewType();
+ method public boolean isCompoundButtonPositionEnd();
+ }
+
+ public static final class CheckBoxListItem.ViewHolder extends androidx.car.widget.CompoundButtonListItem.ViewHolder {
+ ctor public CheckBoxListItem.ViewHolder(android.view.View);
+ method public android.widget.TextView getBody();
+ method public android.widget.CompoundButton getCompoundButton();
+ method public android.view.View getCompoundButtonDivider();
+ method public android.view.ViewGroup getContainerLayout();
+ method public android.widget.ImageView getPrimaryIcon();
+ method public android.widget.TextView getTitle();
+ method public void onUxRestrictionsChanged(androidx.car.uxrestrictions.CarUxRestrictions);
+ }
+
public final class ColumnCardView extends androidx.cardview.widget.CardView {
ctor public ColumnCardView(android.content.Context!);
ctor public ColumnCardView(android.content.Context!, android.util.AttributeSet!);
@@ -325,6 +343,36 @@
method public void setColumnSpan(int);
}
+ public abstract class CompoundButtonListItem<VH extends androidx.car.widget.CompoundButtonListItem.ViewHolder> extends androidx.car.widget.ListItem<VH> {
+ ctor public CompoundButtonListItem(android.content.Context);
+ method public abstract boolean isCompoundButtonPositionEnd();
+ method public void onBind(VH);
+ method @CallSuper protected void resolveDirtyState();
+ method public void setBody(CharSequence?);
+ method public void setChecked(boolean);
+ method public void setClickable(boolean);
+ method public void setEnabled(boolean);
+ method public void setOnCheckedChangeListener(android.widget.CompoundButton.OnCheckedChangeListener?);
+ method public void setPrimaryActionEmptyIcon();
+ method public void setPrimaryActionIcon(android.graphics.drawable.Drawable, int);
+ method public void setPrimaryActionIcon(@DrawableRes int, int);
+ method public void setPrimaryActionNoIcon();
+ method public void setShowCompoundButtonDivider(boolean);
+ method public void setTitle(CharSequence?);
+ field public static final int PRIMARY_ACTION_ICON_SIZE_LARGE = 2; // 0x2
+ field public static final int PRIMARY_ACTION_ICON_SIZE_MEDIUM = 1; // 0x1
+ field public static final int PRIMARY_ACTION_ICON_SIZE_SMALL = 0; // 0x0
+ }
+
+ public abstract static class CompoundButtonListItem.ViewHolder extends androidx.car.widget.ListItem.ViewHolder {
+ ctor public CompoundButtonListItem.ViewHolder(android.view.View);
+ method public abstract android.widget.TextView getBody();
+ method public abstract android.widget.CompoundButton getCompoundButton();
+ method public abstract android.view.View getCompoundButtonDivider();
+ method public abstract android.widget.ImageView getPrimaryIcon();
+ method public abstract android.widget.TextView getTitle();
+ }
+
public abstract class ListItem<VH extends androidx.car.widget.ListItem.ViewHolder> {
ctor public ListItem();
method public final void addViewBinder(androidx.car.widget.ListItem.ViewBinder<VH>!);
@@ -496,38 +544,22 @@
field public static final int PAGE_UP = 0; // 0x0
}
- public class RadioButtonListItem extends androidx.car.widget.ListItem<androidx.car.widget.RadioButtonListItem.ViewHolder> {
+ public final class RadioButtonListItem extends androidx.car.widget.CompoundButtonListItem<androidx.car.widget.RadioButtonListItem.ViewHolder> {
ctor public RadioButtonListItem(android.content.Context);
method public static androidx.car.widget.RadioButtonListItem.ViewHolder createViewHolder(android.view.View);
- method protected android.content.Context getContext();
method public int getViewType();
- method public boolean isChecked();
- method protected void onBind(androidx.car.widget.RadioButtonListItem.ViewHolder!);
- method protected void resolveDirtyState();
- method public void setBody(CharSequence?);
- method public void setChecked(boolean);
- method public void setEnabled(boolean);
- method public void setOnCheckedChangeListener(android.widget.CompoundButton.OnCheckedChangeListener);
- method public void setPrimaryActionIcon(android.graphics.drawable.Drawable, int);
- method public void setPrimaryActionIcon(@DrawableRes int, int);
- method public void setPrimaryActionNoIcon();
- method public void setShowRadioButtonDivider(boolean);
- method public void setTextStartMargin(@DimenRes int);
- method public void setTitle(CharSequence?);
- field public static final int PRIMARY_ACTION_ICON_SIZE_LARGE = 2; // 0x2
- field public static final int PRIMARY_ACTION_ICON_SIZE_MEDIUM = 1; // 0x1
- field public static final int PRIMARY_ACTION_ICON_SIZE_SMALL = 0; // 0x0
+ method public boolean isCompoundButtonPositionEnd();
}
- public static final class RadioButtonListItem.ViewHolder extends androidx.car.widget.ListItem.ViewHolder {
+ public static final class RadioButtonListItem.ViewHolder extends androidx.car.widget.CompoundButtonListItem.ViewHolder {
ctor public RadioButtonListItem.ViewHolder(android.view.View);
method public android.widget.TextView getBody();
+ method public android.widget.CompoundButton getCompoundButton();
+ method public android.view.View getCompoundButtonDivider();
method public android.view.ViewGroup getContainerLayout();
method public android.widget.ImageView getPrimaryIcon();
- method public android.widget.RadioButton getRadioButton();
- method public android.view.View getRadioButtonDivider();
method public android.widget.TextView getTitle();
- method public void onUxRestrictionsChanged(androidx.car.uxrestrictions.CarUxRestrictions!);
+ method public void onUxRestrictionsChanged(androidx.car.uxrestrictions.CarUxRestrictions);
}
public class SeekbarListItem extends androidx.car.widget.ListItem<androidx.car.widget.SeekbarListItem.ViewHolder> {
@@ -588,38 +620,22 @@
method public void onUxRestrictionsChanged(androidx.car.uxrestrictions.CarUxRestrictions!);
}
- public class SwitchListItem extends androidx.car.widget.ListItem<androidx.car.widget.SwitchListItem.ViewHolder> {
+ public final class SwitchListItem extends androidx.car.widget.CompoundButtonListItem<androidx.car.widget.SwitchListItem.ViewHolder> {
ctor public SwitchListItem(android.content.Context);
- method public static androidx.car.widget.SwitchListItem.ViewHolder createViewHolder(android.view.View!);
- method protected final android.content.Context getContext();
+ method public static androidx.car.widget.SwitchListItem.ViewHolder createViewHolder(android.view.View);
method public int getViewType();
- method public void onBind(androidx.car.widget.SwitchListItem.ViewHolder!);
- method @CallSuper protected void resolveDirtyState();
- method public void setBody(CharSequence?);
- method public void setChecked(boolean);
- method public void setClickable(boolean);
- method public void setEnabled(boolean);
- method public void setPrimaryActionEmptyIcon();
- method public void setPrimaryActionIcon(android.graphics.drawable.Drawable, int);
- method public void setPrimaryActionIcon(@DrawableRes int, int);
- method public void setPrimaryActionNoIcon();
- method public void setShowSwitchDivider(boolean);
- method public void setSwitchOnCheckedChangeListener(android.widget.CompoundButton.OnCheckedChangeListener?);
- method @Deprecated public void setSwitchState(boolean);
- method public void setTitle(CharSequence?);
- field public static final int PRIMARY_ACTION_ICON_SIZE_LARGE = 2; // 0x2
- field public static final int PRIMARY_ACTION_ICON_SIZE_MEDIUM = 1; // 0x1
- field public static final int PRIMARY_ACTION_ICON_SIZE_SMALL = 0; // 0x0
+ method public boolean isCompoundButtonPositionEnd();
}
- public static final class SwitchListItem.ViewHolder extends androidx.car.widget.ListItem.ViewHolder {
+ public static final class SwitchListItem.ViewHolder extends androidx.car.widget.CompoundButtonListItem.ViewHolder {
ctor public SwitchListItem.ViewHolder(android.view.View);
method public android.widget.TextView getBody();
+ method public android.widget.CompoundButton getCompoundButton();
+ method public android.view.View getCompoundButtonDivider();
+ method public android.view.ViewGroup getContainerLayout();
method public android.widget.ImageView getPrimaryIcon();
- method public android.widget.Switch getSwitch();
- method public android.view.View getSwitchDivider();
method public android.widget.TextView getTitle();
- method public void onUxRestrictionsChanged(androidx.car.uxrestrictions.CarUxRestrictions!);
+ method public void onUxRestrictionsChanged(androidx.car.uxrestrictions.CarUxRestrictions);
}
public class TextListItem extends androidx.car.widget.ListItem<androidx.car.widget.TextListItem.ViewHolder> {
diff --git a/car/core/api/current.txt b/car/core/api/current.txt
index 6ee7690..90326b8 100644
--- a/car/core/api/current.txt
+++ b/car/core/api/current.txt
@@ -316,6 +316,24 @@
method public void setTitleTextAppearance(@StyleRes int);
}
+ public final class CheckBoxListItem extends androidx.car.widget.CompoundButtonListItem<androidx.car.widget.CheckBoxListItem.ViewHolder> {
+ ctor public CheckBoxListItem(android.content.Context);
+ method public static androidx.car.widget.CheckBoxListItem.ViewHolder createViewHolder(android.view.View);
+ method public int getViewType();
+ method public boolean isCompoundButtonPositionEnd();
+ }
+
+ public static final class CheckBoxListItem.ViewHolder extends androidx.car.widget.CompoundButtonListItem.ViewHolder {
+ ctor public CheckBoxListItem.ViewHolder(android.view.View);
+ method public android.widget.TextView getBody();
+ method public android.widget.CompoundButton getCompoundButton();
+ method public android.view.View getCompoundButtonDivider();
+ method public android.view.ViewGroup getContainerLayout();
+ method public android.widget.ImageView getPrimaryIcon();
+ method public android.widget.TextView getTitle();
+ method public void onUxRestrictionsChanged(androidx.car.uxrestrictions.CarUxRestrictions);
+ }
+
public final class ColumnCardView extends androidx.cardview.widget.CardView {
ctor public ColumnCardView(android.content.Context!);
ctor public ColumnCardView(android.content.Context!, android.util.AttributeSet!);
@@ -325,6 +343,36 @@
method public void setColumnSpan(int);
}
+ public abstract class CompoundButtonListItem<VH extends androidx.car.widget.CompoundButtonListItem.ViewHolder> extends androidx.car.widget.ListItem<VH> {
+ ctor public CompoundButtonListItem(android.content.Context);
+ method public abstract boolean isCompoundButtonPositionEnd();
+ method public void onBind(VH);
+ method @CallSuper protected void resolveDirtyState();
+ method public void setBody(CharSequence?);
+ method public void setChecked(boolean);
+ method public void setClickable(boolean);
+ method public void setEnabled(boolean);
+ method public void setOnCheckedChangeListener(android.widget.CompoundButton.OnCheckedChangeListener?);
+ method public void setPrimaryActionEmptyIcon();
+ method public void setPrimaryActionIcon(android.graphics.drawable.Drawable, int);
+ method public void setPrimaryActionIcon(@DrawableRes int, int);
+ method public void setPrimaryActionNoIcon();
+ method public void setShowCompoundButtonDivider(boolean);
+ method public void setTitle(CharSequence?);
+ field public static final int PRIMARY_ACTION_ICON_SIZE_LARGE = 2; // 0x2
+ field public static final int PRIMARY_ACTION_ICON_SIZE_MEDIUM = 1; // 0x1
+ field public static final int PRIMARY_ACTION_ICON_SIZE_SMALL = 0; // 0x0
+ }
+
+ public abstract static class CompoundButtonListItem.ViewHolder extends androidx.car.widget.ListItem.ViewHolder {
+ ctor public CompoundButtonListItem.ViewHolder(android.view.View);
+ method public abstract android.widget.TextView getBody();
+ method public abstract android.widget.CompoundButton getCompoundButton();
+ method public abstract android.view.View getCompoundButtonDivider();
+ method public abstract android.widget.ImageView getPrimaryIcon();
+ method public abstract android.widget.TextView getTitle();
+ }
+
public abstract class ListItem<VH extends androidx.car.widget.ListItem.ViewHolder> {
ctor public ListItem();
method public final void addViewBinder(androidx.car.widget.ListItem.ViewBinder<VH>!);
@@ -496,38 +544,22 @@
field public static final int PAGE_UP = 0; // 0x0
}
- public class RadioButtonListItem extends androidx.car.widget.ListItem<androidx.car.widget.RadioButtonListItem.ViewHolder> {
+ public final class RadioButtonListItem extends androidx.car.widget.CompoundButtonListItem<androidx.car.widget.RadioButtonListItem.ViewHolder> {
ctor public RadioButtonListItem(android.content.Context);
method public static androidx.car.widget.RadioButtonListItem.ViewHolder createViewHolder(android.view.View);
- method protected android.content.Context getContext();
method public int getViewType();
- method public boolean isChecked();
- method protected void onBind(androidx.car.widget.RadioButtonListItem.ViewHolder!);
- method protected void resolveDirtyState();
- method public void setBody(CharSequence?);
- method public void setChecked(boolean);
- method public void setEnabled(boolean);
- method public void setOnCheckedChangeListener(android.widget.CompoundButton.OnCheckedChangeListener);
- method public void setPrimaryActionIcon(android.graphics.drawable.Drawable, int);
- method public void setPrimaryActionIcon(@DrawableRes int, int);
- method public void setPrimaryActionNoIcon();
- method public void setShowRadioButtonDivider(boolean);
- method public void setTextStartMargin(@DimenRes int);
- method public void setTitle(CharSequence?);
- field public static final int PRIMARY_ACTION_ICON_SIZE_LARGE = 2; // 0x2
- field public static final int PRIMARY_ACTION_ICON_SIZE_MEDIUM = 1; // 0x1
- field public static final int PRIMARY_ACTION_ICON_SIZE_SMALL = 0; // 0x0
+ method public boolean isCompoundButtonPositionEnd();
}
- public static final class RadioButtonListItem.ViewHolder extends androidx.car.widget.ListItem.ViewHolder {
+ public static final class RadioButtonListItem.ViewHolder extends androidx.car.widget.CompoundButtonListItem.ViewHolder {
ctor public RadioButtonListItem.ViewHolder(android.view.View);
method public android.widget.TextView getBody();
+ method public android.widget.CompoundButton getCompoundButton();
+ method public android.view.View getCompoundButtonDivider();
method public android.view.ViewGroup getContainerLayout();
method public android.widget.ImageView getPrimaryIcon();
- method public android.widget.RadioButton getRadioButton();
- method public android.view.View getRadioButtonDivider();
method public android.widget.TextView getTitle();
- method public void onUxRestrictionsChanged(androidx.car.uxrestrictions.CarUxRestrictions!);
+ method public void onUxRestrictionsChanged(androidx.car.uxrestrictions.CarUxRestrictions);
}
public class SeekbarListItem extends androidx.car.widget.ListItem<androidx.car.widget.SeekbarListItem.ViewHolder> {
@@ -588,38 +620,22 @@
method public void onUxRestrictionsChanged(androidx.car.uxrestrictions.CarUxRestrictions!);
}
- public class SwitchListItem extends androidx.car.widget.ListItem<androidx.car.widget.SwitchListItem.ViewHolder> {
+ public final class SwitchListItem extends androidx.car.widget.CompoundButtonListItem<androidx.car.widget.SwitchListItem.ViewHolder> {
ctor public SwitchListItem(android.content.Context);
- method public static androidx.car.widget.SwitchListItem.ViewHolder createViewHolder(android.view.View!);
- method protected final android.content.Context getContext();
+ method public static androidx.car.widget.SwitchListItem.ViewHolder createViewHolder(android.view.View);
method public int getViewType();
- method public void onBind(androidx.car.widget.SwitchListItem.ViewHolder!);
- method @CallSuper protected void resolveDirtyState();
- method public void setBody(CharSequence?);
- method public void setChecked(boolean);
- method public void setClickable(boolean);
- method public void setEnabled(boolean);
- method public void setPrimaryActionEmptyIcon();
- method public void setPrimaryActionIcon(android.graphics.drawable.Drawable, int);
- method public void setPrimaryActionIcon(@DrawableRes int, int);
- method public void setPrimaryActionNoIcon();
- method public void setShowSwitchDivider(boolean);
- method public void setSwitchOnCheckedChangeListener(android.widget.CompoundButton.OnCheckedChangeListener?);
- method @Deprecated public void setSwitchState(boolean);
- method public void setTitle(CharSequence?);
- field public static final int PRIMARY_ACTION_ICON_SIZE_LARGE = 2; // 0x2
- field public static final int PRIMARY_ACTION_ICON_SIZE_MEDIUM = 1; // 0x1
- field public static final int PRIMARY_ACTION_ICON_SIZE_SMALL = 0; // 0x0
+ method public boolean isCompoundButtonPositionEnd();
}
- public static final class SwitchListItem.ViewHolder extends androidx.car.widget.ListItem.ViewHolder {
+ public static final class SwitchListItem.ViewHolder extends androidx.car.widget.CompoundButtonListItem.ViewHolder {
ctor public SwitchListItem.ViewHolder(android.view.View);
method public android.widget.TextView getBody();
+ method public android.widget.CompoundButton getCompoundButton();
+ method public android.view.View getCompoundButtonDivider();
+ method public android.view.ViewGroup getContainerLayout();
method public android.widget.ImageView getPrimaryIcon();
- method public android.widget.Switch getSwitch();
- method public android.view.View getSwitchDivider();
method public android.widget.TextView getTitle();
- method public void onUxRestrictionsChanged(androidx.car.uxrestrictions.CarUxRestrictions!);
+ method public void onUxRestrictionsChanged(androidx.car.uxrestrictions.CarUxRestrictions);
}
public class TextListItem extends androidx.car.widget.ListItem<androidx.car.widget.TextListItem.ViewHolder> {
diff --git a/car/core/api/restricted_1.0.0-alpha8.txt b/car/core/api/restricted_1.0.0-alpha8.txt
index b88575a..d79bca5 100644
--- a/car/core/api/restricted_1.0.0-alpha8.txt
+++ b/car/core/api/restricted_1.0.0-alpha8.txt
@@ -75,6 +75,7 @@
package androidx.car.widget {
+
}
package androidx.car.widget.itemdecorators {
diff --git a/car/core/api/restricted_current.txt b/car/core/api/restricted_current.txt
index b88575a..d79bca5 100644
--- a/car/core/api/restricted_current.txt
+++ b/car/core/api/restricted_current.txt
@@ -75,6 +75,7 @@
package androidx.car.widget {
+
}
package androidx.car.widget.itemdecorators {
diff --git a/car/core/res/layout/car_list_item_check_box_content.xml b/car/core/res/layout/car_list_item_check_box_content.xml
new file mode 100644
index 0000000..7b1d8c6
--- /dev/null
+++ b/car/core/res/layout/car_list_item_check_box_content.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:foreground="@drawable/car_card_ripple_background"
+ android:minHeight="@dimen/car_single_line_list_item_height">
+
+ <!-- Primary Action. -->
+ <ImageView
+ android:id="@+id/primary_icon"
+ android:layout_width="@dimen/car_single_line_list_item_height"
+ android:layout_height="@dimen/car_single_line_list_item_height"
+ android:tint="?attr/listItemPrimaryIconTint"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <!-- Text. -->
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAppearance="?attr/listItemTitleTextAppearance"
+ app:layout_constraintBottom_toTopOf="@+id/body"
+ app:layout_constraintEnd_toStartOf="@+id/barrier"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="packed" />
+
+ <TextView
+ android:id="@+id/body"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="?attr/listItemBodyTextAppearance"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/barrier"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/title" />
+
+ <!-- A barrier between the text and checkbox. -->
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/barrier"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:barrierAllowsGoneWidgets="false"
+ app:barrierDirection="start"
+ app:constraint_referenced_ids="checkbox_divider,checkbox_widget" />
+
+ <!-- Guideline that the checkbox is centered upon. -->
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/supplemental_actions_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_percent="0.5" />
+
+ <!-- Checkbox with divider. -->
+ <View
+ android:id="@+id/checkbox_divider"
+ style="@style/CarListVerticalDivider"
+ android:layout_marginEnd="@dimen/car_padding_4"
+ android:background="?attr/listItemActionDividerColor"
+ app:layout_constraintBottom_toBottomOf="@+id/supplemental_actions_guideline"
+ app:layout_constraintEnd_toStartOf="@+id/checkbox_widget"
+ app:layout_constraintTop_toTopOf="@+id/supplemental_actions_guideline" />
+
+ <CheckBox
+ android:id="@+id/checkbox_widget"
+ style="?attr/checkboxStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/car_keyline_1"
+ app:layout_constraintBottom_toBottomOf="@+id/supplemental_actions_guideline"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="@+id/supplemental_actions_guideline" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car/core/res/layout/car_list_item_radio_content.xml b/car/core/res/layout/car_list_item_radio_content.xml
index df30fcb..dfb8a15 100644
--- a/car/core/res/layout/car_list_item_radio_content.xml
+++ b/car/core/res/layout/car_list_item_radio_content.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2019 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -14,78 +14,85 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<!-- This layout should only be used by class RadioButtonListItem, as it requires layout params
- being set programmatically depending on item data/view configuration. -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:minHeight="@dimen/car_single_line_list_item_height"
- android:paddingEnd="@dimen/car_keyline_1">
+ android:foreground="@drawable/car_card_ripple_background"
+ android:minHeight="@dimen/car_single_line_list_item_height">
- <!-- Radio button. -->
+ <!-- Radio button with divider. -->
<RadioButton
- android:id="@+id/radio_button"
- android:layout_width="@dimen/car_primary_icon_size"
- android:layout_height="@dimen/car_primary_icon_size"
+ android:id="@+id/radiobutton_widget"
+ style="?attr/radioButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
android:layout_marginStart="@dimen/car_keyline_1"
- app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintBottom_toBottomOf="@+id/supplemental_actions_guideline"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintTop_toTopOf="@+id/supplemental_actions_guideline" />
<View
- android:id="@+id/radio_button_divider"
+ android:id="@+id/radiobutton_divider"
style="@style/CarListVerticalDivider"
+ android:layout_marginEnd="@dimen/car_padding_4"
android:background="?attr/listItemActionDividerColor"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toEndOf="@+id/radio_button"
- app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintBottom_toBottomOf="@+id/supplemental_actions_guideline"
+ app:layout_constraintStart_toEndOf="@+id/radiobutton_widget"
+ app:layout_constraintTop_toTopOf="@+id/supplemental_actions_guideline" />
<!-- Text. -->
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginEnd="@dimen/car_keyline_3"
- android:layout_marginStart="@dimen/car_keyline_3"
- android:layout_marginTop="@dimen/car_padding_2"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/listItemTitleTextAppearance"
app:layout_constraintBottom_toTopOf="@+id/body"
- app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/barrier"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/body"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/car_padding_2"
- android:layout_marginEnd="@dimen/car_keyline_3"
- android:layout_marginStart="@dimen/car_keyline_3"
- android:ellipsize="end"
- android:singleLine="false"
android:textAppearance="?attr/listItemBodyTextAppearance"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/barrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" />
- <!-- Primary Icon. -->
+ <!-- A barrier between the text and icon. -->
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/barrier"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:barrierAllowsGoneWidgets="false"
+ app:barrierDirection="start"
+ app:constraint_referenced_ids="primary_icon" />
+
+ <!-- Guideline that the checkbox is centered upon. -->
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/supplemental_actions_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_percent="0.5" />
+
+ <!-- Primary Action. -->
<ImageView
android:id="@+id/primary_icon"
- android:layout_width="@dimen/car_primary_icon_size"
- android:layout_height="@dimen/car_primary_icon_size"
+ android:layout_width="@dimen/car_single_line_list_item_height"
+ android:layout_height="@dimen/car_single_line_list_item_height"
android:layout_marginEnd="@dimen/car_keyline_1"
- android:layout_marginStart="@dimen/car_padding_4"
android:tint="?attr/listItemPrimaryIconTint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
-
</androidx.constraintlayout.widget.ConstraintLayout>
-
diff --git a/car/core/res/layout/car_list_item_switch_content.xml b/car/core/res/layout/car_list_item_switch_content.xml
index 5897a78..0f8edd6 100644
--- a/car/core/res/layout/car_list_item_switch_content.xml
+++ b/car/core/res/layout/car_list_item_switch_content.xml
@@ -17,13 +17,11 @@
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- tools:ignore="MissingConstraints"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:minHeight="@dimen/car_single_line_list_item_height"
- android:foreground="@drawable/car_card_ripple_background" >
+ android:foreground="@drawable/car_card_ripple_background"
+ android:minHeight="@dimen/car_single_line_list_item_height">
<!-- Primary Action. -->
<ImageView
@@ -31,41 +29,41 @@
android:layout_width="@dimen/car_single_line_list_item_height"
android:layout_height="@dimen/car_single_line_list_item_height"
android:tint="?attr/listItemPrimaryIconTint"
- app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toStartOf="parent"/>
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
<!-- Text. -->
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:singleLine="true"
android:ellipsize="end"
+ android:singleLine="true"
android:textAppearance="?attr/listItemTitleTextAppearance"
- app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/body"
- app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/barrier"
- app:layout_constraintVertical_chainStyle="packed"/>
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?attr/listItemBodyTextAppearance"
- app:layout_constraintTop_toBottomOf="@+id/title"
app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/barrier"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toStartOf="@+id/barrier"/>
+ app:layout_constraintTop_toBottomOf="@+id/title" />
<!-- A barrier between the text and switch. -->
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- app:barrierDirection="start"
app:barrierAllowsGoneWidgets="false"
+ app:barrierDirection="start"
app:constraint_referenced_ids="switch_divider,switch_widget" />
<!-- Guideline that the switch is centered upon. -->
@@ -79,20 +77,20 @@
<!-- Switch with divider. -->
<View
android:id="@+id/switch_divider"
- android:background="?attr/listItemActionDividerColor"
+ style="@style/CarListVerticalDivider"
android:layout_marginEnd="@dimen/car_padding_4"
- app:layout_constraintTop_toTopOf="@+id/supplemental_actions_guideline"
+ android:background="?attr/listItemActionDividerColor"
app:layout_constraintBottom_toBottomOf="@+id/supplemental_actions_guideline"
app:layout_constraintEnd_toStartOf="@+id/switch_widget"
- style="@style/CarListVerticalDivider"/>
+ app:layout_constraintTop_toTopOf="@+id/supplemental_actions_guideline" />
<Switch
android:id="@+id/switch_widget"
+ style="?attr/switchStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/car_keyline_1"
- app:layout_constraintTop_toTopOf="@+id/supplemental_actions_guideline"
app:layout_constraintBottom_toBottomOf="@+id/supplemental_actions_guideline"
app:layout_constraintEnd_toEndOf="parent"
- style="?attr/switchStyle" />
+ app:layout_constraintTop_toTopOf="@+id/supplemental_actions_guideline" />
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car/core/src/androidTest/java/androidx/car/widget/CheckBoxListItemTest.java b/car/core/src/androidTest/java/androidx/car/widget/CheckBoxListItemTest.java
new file mode 100644
index 0000000..c12a356
--- /dev/null
+++ b/car/core/src/androidTest/java/androidx/car/widget/CheckBoxListItemTest.java
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.widget;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
+import static androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.lessThan;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.hamcrest.number.IsCloseTo.closeTo;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.text.InputFilter;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+
+import androidx.car.test.R;
+import androidx.car.util.CarUxRestrictionsTestUtils;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.espresso.UiController;
+import androidx.test.espresso.ViewAction;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+
+import org.hamcrest.Matcher;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Tests the layout configuration and checkbox functionality of {@link CheckBoxListItem}.
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class CheckBoxListItemTest {
+
+ @Rule
+ public ActivityTestRule<PagedListViewTestActivity> mActivityRule =
+ new ActivityTestRule<>(PagedListViewTestActivity.class);
+
+ private PagedListViewTestActivity mActivity;
+ private PagedListView mPagedListView;
+ private ListItemAdapter mAdapter;
+
+ private boolean isAutoDevice() {
+ PackageManager packageManager = mActivityRule.getActivity().getPackageManager();
+ return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+ }
+
+ @Before
+ public void setUp() {
+ Assume.assumeTrue(isAutoDevice());
+
+ mActivity = mActivityRule.getActivity();
+ mPagedListView = mActivity.findViewById(R.id.paged_list_view);
+ }
+
+ @Test
+ public void testDefaultVisibility_EmptyItemShowsCheckBox() {
+ CheckBoxListItem item = new CheckBoxListItem(mActivity);
+ setupPagedListView(Arrays.asList(item));
+
+ ViewGroup itemView = (ViewGroup)
+ mPagedListView.getRecyclerView().getLayoutManager().getChildAt(0);
+ int childCount = itemView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View view = itemView.getChildAt(i);
+ // |view| could be container in view holder, so exempt ViewGroup.
+ if (view instanceof CheckBox || view instanceof ViewGroup) {
+ assertThat(view.getVisibility(), is(equalTo(View.VISIBLE)));
+ } else {
+ assertThat("Visibility of view "
+ + mActivity.getResources().getResourceEntryName(view.getId())
+ + " by default should be GONE.",
+ view.getVisibility(), is(equalTo(View.GONE)));
+ }
+ }
+ }
+
+ @Test
+ public void testItemIsEnabledByDefault() {
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+
+ List<CheckBoxListItem> items = Arrays.asList(item0);
+ setupPagedListView(items);
+
+ assertTrue(getViewHolderAtPosition(0).itemView.isEnabled());
+ }
+
+ @Test
+ public void testDisablingItem() {
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+
+ List<CheckBoxListItem> items = Arrays.asList(item0);
+ setupPagedListView(items);
+
+ item0.setEnabled(false);
+ refreshUi();
+
+ assertFalse(getViewHolderAtPosition(0).itemView.isEnabled());
+ }
+
+ @Test
+ public void testClickableItem_DefaultNotClickable() {
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+
+ List<CheckBoxListItem> items = Arrays.asList(item0);
+ setupPagedListView(items);
+
+ assertFalse(getViewHolderAtPosition(0).itemView.isClickable());
+ }
+
+ @Test
+ public void testClickableItem_setClickable() {
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setClickable(true);
+
+ List<CheckBoxListItem> items = Arrays.asList(item0);
+ setupPagedListView(items);
+
+ assertTrue(getViewHolderAtPosition(0).itemView.isClickable());
+ }
+
+ @Test
+ public void testClickableItem_ClickingTogglesCheckBox() {
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setClickable(true);
+
+ List<CheckBoxListItem> items = Arrays.asList(item0);
+ setupPagedListView(items);
+
+ onView(withId(R.id.recycler_view)).perform(actionOnItemAtPosition(0, click()));
+
+ assertTrue(getViewHolderAtPosition(0).getCompoundButton().isChecked());
+ }
+
+ @Test
+ public void testCheckBoxStatePersistsOnRebind() {
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ // CheckBox initially checked.
+ item0.setChecked(true);
+
+ setupPagedListView(Collections.singletonList(item0));
+ CheckBoxListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+
+ toggleChecked(viewHolder.getCompoundButton());
+
+ viewHolder = getViewHolderAtPosition(0);
+ assertThat(viewHolder.getCompoundButton().isChecked(), is(equalTo(false)));
+ }
+
+ @Test
+ public void testSetCheckBoxState() {
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setChecked(true);
+
+ setupPagedListView(Arrays.asList(item0));
+
+ item0.setChecked(false);
+ refreshUi();
+
+ CheckBoxListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+ assertThat(viewHolder.getCompoundButton().getVisibility(), is(equalTo(View.VISIBLE)));
+ assertThat(viewHolder.getCompoundButton().isChecked(), is(equalTo(false)));
+ }
+
+ @Test
+ public void testSetCheckBoxStateCallsListener() {
+ CompoundButton.OnCheckedChangeListener listener =
+ mock(CompoundButton.OnCheckedChangeListener.class);
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setOnCheckedChangeListener(listener);
+
+ setupPagedListView(Collections.singletonList(item0));
+
+ item0.setChecked(true);
+ refreshUi();
+ verify(listener).onCheckedChanged(any(CompoundButton.class), eq(true));
+ }
+
+ @Test
+ public void testRefreshingUiDoesNotCallListener() {
+ CompoundButton.OnCheckedChangeListener listener =
+ mock(CompoundButton.OnCheckedChangeListener.class);
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setOnCheckedChangeListener(listener);
+
+ setupPagedListView(Collections.singletonList(item0));
+
+ refreshUi();
+ verify(listener, never()).onCheckedChanged(any(CompoundButton.class), anyBoolean());
+ }
+
+ @Test
+ public void testSetCheckBoxStateBeforeFirstBindCallsListener() {
+ CompoundButton.OnCheckedChangeListener listener =
+ mock(CompoundButton.OnCheckedChangeListener.class);
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setOnCheckedChangeListener(listener);
+ item0.setChecked(true);
+
+ setupPagedListView(Collections.singletonList(item0));
+
+ verify(listener).onCheckedChanged(any(CompoundButton.class), eq(true));
+ }
+
+ @Test
+ public void testCheckBoxToggleCallsListener() {
+ CompoundButton.OnCheckedChangeListener listener =
+ mock(CompoundButton.OnCheckedChangeListener.class);
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setOnCheckedChangeListener(listener);
+
+ setupPagedListView(Collections.singletonList(item0));
+
+ CheckBoxListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+ toggleChecked(viewHolder.getCompoundButton());
+
+ // Expect true because checkbox defaults to false.
+ verify(listener).onCheckedChanged(any(CompoundButton.class), eq(true));
+ }
+
+ @Test
+ public void testSetCheckBoxStateNotDirtyDoesNotCallListener() {
+ CompoundButton.OnCheckedChangeListener listener =
+ mock(CompoundButton.OnCheckedChangeListener.class);
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setChecked(true);
+ item0.setOnCheckedChangeListener(listener);
+
+ setupPagedListView(Collections.singletonList(item0));
+
+ item0.setChecked(true);
+ refreshUi();
+
+ verify(listener, never()).onCheckedChanged(any(CompoundButton.class), anyBoolean());
+ }
+
+ @Test
+ public void testCheckingCheckBox() {
+ final boolean[] clicked = {false};
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setOnCheckedChangeListener((button, isChecked) -> {
+ // Initial value is false.
+ assertTrue(isChecked);
+ clicked[0] = true;
+ });
+
+ List<CheckBoxListItem> items = Arrays.asList(item0);
+ setupPagedListView(items);
+
+ onView(withId(R.id.recycler_view)).perform(
+ actionOnItemAtPosition(0, clickChildViewWithId(R.id.checkbox_widget)));
+ assertTrue(clicked[0]);
+ }
+
+ @Test
+ public void testDividerVisibility() {
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setShowCompoundButtonDivider(true);
+
+ CheckBoxListItem item1 = new CheckBoxListItem(mActivity);
+ item0.setShowCompoundButtonDivider(false);
+
+ List<CheckBoxListItem> items = Arrays.asList(item0, item1);
+ setupPagedListView(items);
+
+ CheckBoxListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+ assertThat(viewHolder.getCompoundButton().getVisibility(), is(equalTo(View.VISIBLE)));
+ assertThat(viewHolder.getCompoundButton().getVisibility(), is(equalTo(View.VISIBLE)));
+
+ viewHolder = getViewHolderAtPosition(1);
+ assertThat(viewHolder.getCompoundButton().getVisibility(), is(equalTo(View.VISIBLE)));
+ assertThat(viewHolder.getCompoundButtonDivider().getVisibility(), is(equalTo(View.GONE)));
+ }
+
+ @Test
+ public void testPrimaryActionVisible() {
+ CheckBoxListItem largeIcon = new CheckBoxListItem(mActivity);
+ largeIcon.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_LARGE);
+
+ CheckBoxListItem mediumIcon = new CheckBoxListItem(mActivity);
+ mediumIcon.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_MEDIUM);
+
+ CheckBoxListItem smallIcon = new CheckBoxListItem(mActivity);
+ smallIcon.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
+
+ List<CheckBoxListItem> items = Arrays.asList(largeIcon, mediumIcon, smallIcon);
+ setupPagedListView(items);
+
+ assertThat(getViewHolderAtPosition(0).getPrimaryIcon().getVisibility(),
+ is(equalTo(View.VISIBLE)));
+ assertThat(getViewHolderAtPosition(1).getPrimaryIcon().getVisibility(),
+ is(equalTo(View.VISIBLE)));
+ assertThat(getViewHolderAtPosition(2).getPrimaryIcon().getVisibility(),
+ is(equalTo(View.VISIBLE)));
+ }
+
+ @Test
+ public void testTextVisible() {
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setTitle("title");
+
+ CheckBoxListItem item1 = new CheckBoxListItem(mActivity);
+ item1.setBody("body");
+
+ List<CheckBoxListItem> items = Arrays.asList(item0, item1);
+ setupPagedListView(items);
+
+ assertThat(getViewHolderAtPosition(0).getTitle().getVisibility(),
+ is(equalTo(View.VISIBLE)));
+ assertThat(getViewHolderAtPosition(1).getBody().getVisibility(),
+ is(equalTo(View.VISIBLE)));
+ }
+
+ @Test
+ public void testTextStartMarginMatchesPrimaryActionType() {
+ CheckBoxListItem largeIcon = new CheckBoxListItem(mActivity);
+ largeIcon.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_LARGE);
+
+ CheckBoxListItem mediumIcon = new CheckBoxListItem(mActivity);
+ mediumIcon.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_MEDIUM);
+
+ CheckBoxListItem smallIcon = new CheckBoxListItem(mActivity);
+ smallIcon.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
+
+ CheckBoxListItem emptyIcon = new CheckBoxListItem(mActivity);
+ emptyIcon.setPrimaryActionEmptyIcon();
+
+ CheckBoxListItem noIcon = new CheckBoxListItem(mActivity);
+ noIcon.setPrimaryActionNoIcon();
+
+ List<CheckBoxListItem> items = Arrays.asList(
+ largeIcon, mediumIcon, smallIcon, emptyIcon, noIcon);
+ List<Integer> expectedStartMargin = Arrays.asList(
+ R.dimen.car_keyline_4, // Large icon.
+ R.dimen.car_keyline_3, // Medium icon.
+ R.dimen.car_keyline_3, // Small icon.
+ R.dimen.car_keyline_3, // Empty icon.
+ R.dimen.car_keyline_1); // No icon.
+ setupPagedListView(items);
+
+ for (int i = 0; i < items.size(); i++) {
+ CheckBoxListItem.ViewHolder viewHolder = getViewHolderAtPosition(i);
+
+ int expected = ApplicationProvider.getApplicationContext().getResources()
+ .getDimensionPixelSize(expectedStartMargin.get(i));
+ assertThat(((ViewGroup.MarginLayoutParams) viewHolder.getTitle().getLayoutParams())
+ .getMarginStart(), is(equalTo(expected)));
+ assertThat(((ViewGroup.MarginLayoutParams) viewHolder.getBody().getLayoutParams())
+ .getMarginStart(), is(equalTo(expected)));
+ }
+ }
+
+ @Test
+ public void testItemWithOnlyTitleIsSingleLine() {
+ // Only space.
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setTitle(" ");
+
+ // Underscore.
+ CheckBoxListItem item1 = new CheckBoxListItem(mActivity);
+ item1.setTitle("______");
+
+ CheckBoxListItem item2 = new CheckBoxListItem(mActivity);
+ item2.setTitle("ALL UPPER CASE");
+
+ // String wouldn't fit in one line.
+ CheckBoxListItem item3 = new CheckBoxListItem(mActivity);
+ item3.setTitle(ApplicationProvider.getApplicationContext().getResources().getString(
+ R.string.over_uxr_text_length_limit));
+
+ List<CheckBoxListItem> items = Arrays.asList(item0, item1, item2, item3);
+ setupPagedListView(items);
+
+ double singleLineHeight =
+ ApplicationProvider.getApplicationContext().getResources().getDimension(
+ R.dimen.car_single_line_list_item_height);
+
+ LinearLayoutManager layoutManager =
+ (LinearLayoutManager) mPagedListView.getRecyclerView().getLayoutManager();
+ for (int i = 0; i < items.size(); i++) {
+ assertThat((double) layoutManager.findViewByPosition(i).getHeight(),
+ is(closeTo(singleLineHeight, 1.0d)));
+ }
+ }
+
+ @Test
+ public void testItemWithBodyTextIsAtLeastDoubleLine() {
+ // Only space.
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setBody(" ");
+
+ // Underscore.
+ CheckBoxListItem item1 = new CheckBoxListItem(mActivity);
+ item1.setBody("____");
+
+ // String wouldn't fit in one line.
+ CheckBoxListItem item2 = new CheckBoxListItem(mActivity);
+ item2.setBody(ApplicationProvider.getApplicationContext().getResources().getString(
+ R.string.over_uxr_text_length_limit));
+
+ List<CheckBoxListItem> items = Arrays.asList(item0, item1, item2);
+ setupPagedListView(items);
+
+ final int doubleLineHeight =
+ (int) ApplicationProvider.getApplicationContext().getResources().getDimension(
+ R.dimen.car_double_line_list_item_height);
+
+ LinearLayoutManager layoutManager =
+ (LinearLayoutManager) mPagedListView.getRecyclerView().getLayoutManager();
+ for (int i = 0; i < items.size(); i++) {
+ assertThat(layoutManager.findViewByPosition(i).getHeight(),
+ is(greaterThanOrEqualTo(doubleLineHeight)));
+ }
+ }
+
+ @Test
+ public void testSetPrimaryActionIcon_withIcon() {
+ CheckBoxListItem item = new CheckBoxListItem(mActivity);
+ item.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_LARGE);
+
+ List<CheckBoxListItem> items = Arrays.asList(item);
+ setupPagedListView(items);
+
+ assertThat(getViewHolderAtPosition(0).getPrimaryIcon().getDrawable(), is(notNullValue()));
+ }
+
+ @Test
+ public void testSetPrimaryActionIcon_withDrawable() {
+ CheckBoxListItem item = new CheckBoxListItem(mActivity);
+ item.setPrimaryActionIcon(
+ mActivity.getDrawable(android.R.drawable.sym_def_app_icon),
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_LARGE);
+
+ List<CheckBoxListItem> items = Arrays.asList(item);
+ setupPagedListView(items);
+
+ assertThat(getViewHolderAtPosition(0).getPrimaryIcon().getDrawable(), is(notNullValue()));
+ }
+
+ @Test
+ public void testPrimaryIconSizesInIncreasingOrder() {
+ CheckBoxListItem small = new CheckBoxListItem(mActivity);
+ small.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
+
+ CheckBoxListItem medium = new CheckBoxListItem(mActivity);
+ medium.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_MEDIUM);
+
+ CheckBoxListItem large = new CheckBoxListItem(mActivity);
+ large.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_LARGE);
+
+ List<CheckBoxListItem> items = Arrays.asList(small, medium, large);
+ setupPagedListView(items);
+
+ CheckBoxListItem.ViewHolder smallVH = getViewHolderAtPosition(0);
+ CheckBoxListItem.ViewHolder mediumVH = getViewHolderAtPosition(1);
+ CheckBoxListItem.ViewHolder largeVH = getViewHolderAtPosition(2);
+
+ assertThat(largeVH.getPrimaryIcon().getHeight(), is(greaterThan(
+ mediumVH.getPrimaryIcon().getHeight())));
+ assertThat(mediumVH.getPrimaryIcon().getHeight(), is(greaterThan(
+ smallVH.getPrimaryIcon().getHeight())));
+ }
+
+ @Test
+ public void testLargePrimaryIconHasNoStartMargin() {
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_LARGE);
+
+ List<CheckBoxListItem> items = Arrays.asList(item0);
+ setupPagedListView(items);
+
+ CheckBoxListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+ assertThat(((ViewGroup.MarginLayoutParams) viewHolder.getPrimaryIcon().getLayoutParams())
+ .getMarginStart(), is(equalTo(0)));
+ }
+
+ @Test
+ public void testSmallAndMediumPrimaryIconStartMargin() {
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setPrimaryActionIcon(
+ android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
+
+ CheckBoxListItem item1 = new CheckBoxListItem(mActivity);
+ item1.setPrimaryActionIcon(
+ android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_MEDIUM);
+
+ List<CheckBoxListItem> items = Arrays.asList(item0, item1);
+ setupPagedListView(items);
+
+ int expected =
+ ApplicationProvider.getApplicationContext().getResources().getDimensionPixelSize(
+ R.dimen.car_keyline_1);
+
+ CheckBoxListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+ assertThat(((ViewGroup.MarginLayoutParams) viewHolder.getPrimaryIcon().getLayoutParams())
+ .getMarginStart(), is(equalTo(expected)));
+
+ viewHolder = getViewHolderAtPosition(1);
+ assertThat(((ViewGroup.MarginLayoutParams) viewHolder.getPrimaryIcon().getLayoutParams())
+ .getMarginStart(), is(equalTo(expected)));
+ }
+
+ @Test
+ public void testSmallPrimaryIconTopMarginRemainsTheSameRegardlessOfTextLength() {
+ final String longText =
+ ApplicationProvider.getApplicationContext().getResources().getString(
+ R.string.over_uxr_text_length_limit);
+
+ // Single line item.
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setPrimaryActionIcon(
+ android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
+ item0.setTitle("one line text");
+
+ // Double line item with one line text.
+ CheckBoxListItem item1 = new CheckBoxListItem(mActivity);
+ item1.setPrimaryActionIcon(
+ android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
+ item1.setTitle("one line text");
+ item1.setBody("one line text");
+
+ // Double line item with long text.
+ CheckBoxListItem item2 = new CheckBoxListItem(mActivity);
+ item2.setPrimaryActionIcon(
+ android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
+ item2.setTitle("one line text");
+ item2.setBody(longText);
+
+ // Body text only - long text.
+ CheckBoxListItem item3 = new CheckBoxListItem(mActivity);
+ item3.setPrimaryActionIcon(
+ android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
+ item3.setBody(longText);
+
+ // Body text only - one line text.
+ CheckBoxListItem item4 = new CheckBoxListItem(mActivity);
+ item4.setPrimaryActionIcon(
+ android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
+ item4.setBody("one line text");
+
+ List<CheckBoxListItem> items = Arrays.asList(item0, item1, item2, item3, item4);
+ setupPagedListView(items);
+
+ for (int i = 1; i < items.size(); i++) {
+ onView(withId(R.id.recycler_view)).perform(scrollToPosition(i));
+ // Implementation uses integer division so it may be off by 1 vs centered vertically.
+ assertThat((double) getViewHolderAtPosition(i - 1).getPrimaryIcon().getTop(),
+ is(closeTo(
+ (double) getViewHolderAtPosition(i).getPrimaryIcon().getTop(), 1.0d)));
+ }
+ }
+
+ @Test
+ public void testCustomViewBinderBindsLast() {
+ final String updatedTitle = "updated title";
+
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setTitle("original title");
+ item0.addViewBinder((viewHolder) -> viewHolder.getTitle().setText(updatedTitle));
+
+ List<CheckBoxListItem> items = Arrays.asList(item0);
+ setupPagedListView(items);
+
+ CheckBoxListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+ assertThat(viewHolder.getTitle().getText(), is(equalTo(updatedTitle)));
+ }
+
+ @Test
+ public void testCustomViewBinderOnUnusedViewsHasNoEffect() {
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.addViewBinder((viewHolder) -> viewHolder.getBody().setText("text"));
+
+ List<CheckBoxListItem> items = Arrays.asList(item0);
+ setupPagedListView(items);
+
+ CheckBoxListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+ assertThat(viewHolder.getBody().getVisibility(), is(equalTo(View.GONE)));
+ // Custom binder interacts with body but has no effect.
+ // Expect card height to remain single line.
+ assertThat((double) viewHolder.itemView.getHeight(), is(closeTo(
+ ApplicationProvider.getApplicationContext().getResources().getDimension(
+ R.dimen.car_single_line_list_item_height), 1.0d)));
+ }
+
+ @Test
+ public void testRevertingViewBinder() throws Throwable {
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setBody("one item");
+ item0.addViewBinder(
+ (viewHolder) -> viewHolder.getBody().setEllipsize(TextUtils.TruncateAt.END),
+ (viewHolder -> viewHolder.getBody().setEllipsize(null)));
+
+ List<CheckBoxListItem> items = Arrays.asList(item0);
+ setupPagedListView(items);
+
+ CheckBoxListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+
+ // Bind view holder to a new item - the customization made by item0 should be reverted.
+ CheckBoxListItem item1 = new CheckBoxListItem(mActivity);
+ item1.setBody("new item");
+ mActivityRule.runOnUiThread(() -> item1.bind(viewHolder));
+
+ assertThat(viewHolder.getBody().getEllipsize(), is(equalTo(null)));
+ }
+
+ @Test
+ public void testRemovingViewBinder() {
+ CheckBoxListItem item0 = new CheckBoxListItem(mActivity);
+ item0.setBody("one item");
+ ListItem.ViewBinder<CheckBoxListItem.ViewHolder> binder =
+ (viewHolder) -> viewHolder.getTitle().setEllipsize(TextUtils.TruncateAt.END);
+ item0.addViewBinder(binder);
+
+ assertTrue(item0.removeViewBinder(binder));
+
+ List<CheckBoxListItem> items = Arrays.asList(item0);
+ setupPagedListView(items);
+
+ assertThat(getViewHolderAtPosition(0).getBody().getEllipsize(), is(equalTo(null)));
+ }
+
+ @Test
+ public void testUpdateItem() {
+ CheckBoxListItem item = new CheckBoxListItem(mActivity);
+ setupPagedListView(Arrays.asList(item));
+
+ String title = "updated title";
+ item.setTitle(title);
+
+ refreshUi();
+
+ CheckBoxListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+ assertThat(viewHolder.getTitle().getText(), is(equalTo(title)));
+ }
+
+ @Test
+ public void testUxRestrictionsChange() {
+ String longText = mActivity.getString(R.string.over_uxr_text_length_limit);
+ CheckBoxListItem item = new CheckBoxListItem(mActivity);
+ item.setBody(longText);
+
+ setupPagedListView(Arrays.asList(item));
+
+ CheckBoxListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+ // Default behavior without UXR is unrestricted.
+ assertThat(viewHolder.getBody().getText(), is(equalTo(longText)));
+
+ viewHolder.onUxRestrictionsChanged(CarUxRestrictionsTestUtils.getFullyRestricted());
+ refreshUi();
+
+ // Verify that the body text length is limited.
+ assertThat(viewHolder.getBody().getText().length(), is(lessThan(longText.length())));
+ }
+
+ @Test
+ public void testUxRestrictionsChangesDoNotAlterExistingInputFilters() {
+ InputFilter filter = new InputFilter.AllCaps(Locale.US);
+ String bodyText = "body_text";
+ CheckBoxListItem item = new CheckBoxListItem(mActivity);
+ item.setBody(bodyText);
+ item.addViewBinder(vh -> vh.getBody().setFilters(new InputFilter[]{filter}));
+
+ setupPagedListView(Arrays.asList(item));
+
+ CheckBoxListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+
+ // Toggle UX restrictions between fully restricted and unrestricted should not affect
+ // existing filters.
+ viewHolder.onUxRestrictionsChanged(CarUxRestrictionsTestUtils.getFullyRestricted());
+ refreshUi();
+ assertTrue(Arrays.asList(viewHolder.getBody().getFilters()).contains(filter));
+
+ viewHolder.onUxRestrictionsChanged(CarUxRestrictionsTestUtils.getBaseline());
+ refreshUi();
+ assertTrue(Arrays.asList(viewHolder.getBody().getFilters()).contains(filter));
+ }
+
+ @Test
+ public void testDisabledItemDisablesViewHolder() {
+ CheckBoxListItem item = new CheckBoxListItem(mActivity);
+ item.setTitle("title");
+ item.setBody("body");
+ item.setEnabled(false);
+
+ setupPagedListView(Arrays.asList(item));
+
+ CheckBoxListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+ assertFalse(viewHolder.getTitle().isEnabled());
+ assertFalse(viewHolder.getBody().isEnabled());
+ assertFalse(viewHolder.getCompoundButton().isEnabled());
+ }
+
+ @Test
+ public void testDisabledItemDoesNotRespondToClick() {
+ // Disabled view will not respond to touch event.
+ // Current test setup makes it hard to test, since clickChildViewWithId() directly calls
+ // performClick() on a view, bypassing the way UI handles disabled state.
+
+ // We are explicitly setting itemView so test it here.
+ boolean[] clicked = new boolean[]{false};
+ CheckBoxListItem item = new CheckBoxListItem(mActivity);
+ item.setEnabled(false);
+
+ setupPagedListView(Arrays.asList(item));
+
+ onView(withId(R.id.recycler_view)).perform(
+ actionOnItemAtPosition(0, click()));
+
+ assertFalse(clicked[0]);
+ }
+
+ private Context getContext() {
+ return mActivity;
+ }
+
+ private void refreshUi() {
+ try {
+ mActivityRule.runOnUiThread(() -> mAdapter.notifyDataSetChanged());
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ throw new RuntimeException(throwable);
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+
+ private void setupPagedListView(List<CheckBoxListItem> items) {
+ ListItemProvider provider = new ListItemProvider.ListProvider(new ArrayList<>(items));
+ try {
+ mAdapter = new ListItemAdapter(mActivity, provider);
+ mActivityRule.runOnUiThread(() -> mPagedListView.setAdapter(mAdapter));
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ throw new RuntimeException(throwable);
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+
+ private CheckBoxListItem.ViewHolder getViewHolderAtPosition(int position) {
+ return (CheckBoxListItem.ViewHolder) mPagedListView.getRecyclerView()
+ .findViewHolderForAdapterPosition(position);
+ }
+
+ private void toggleChecked(CompoundButton button) {
+ try {
+ mActivityRule.runOnUiThread(button::toggle);
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ throw new RuntimeException(throwable);
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+
+ private static ViewAction clickChildViewWithId(final int id) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return null;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Click on a child view with specific id.";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ View v = view.findViewById(id);
+ v.performClick();
+ }
+ };
+ }
+}
diff --git a/car/core/src/androidTest/java/androidx/car/widget/RadioButtonListItemTest.java b/car/core/src/androidTest/java/androidx/car/widget/RadioButtonListItemTest.java
index dfcbd471..0a16219 100644
--- a/car/core/src/androidTest/java/androidx/car/widget/RadioButtonListItemTest.java
+++ b/car/core/src/androidTest/java/androidx/car/widget/RadioButtonListItemTest.java
@@ -82,7 +82,7 @@
item.setEnabled(false);
setupPagedListView(Arrays.asList(item));
- assertFalse(getViewHolderAtPosition(0).getRadioButton().isEnabled());
+ assertFalse(getViewHolderAtPosition(0).getCompoundButton().isEnabled());
}
@Test
@@ -145,7 +145,7 @@
}
@Test
- public void testSetTextStartMargin_DefaultMargin() {
+ public void testSetTextStartMargin_Margin() {
RadioButtonListItem item = new RadioButtonListItem(mActivity);
item.setPrimaryActionIcon(null, RadioButtonListItem.PRIMARY_ACTION_ICON_SIZE_LARGE);
item.setTitle("text");
@@ -156,21 +156,7 @@
assertThat(getViewHolderAtPosition(0).getTitle().getLeft(), is(equalTo(expected)));
}
- @Test
- public void testSetTextStartMargin_CustomMargin() {
- RadioButtonListItem item = new RadioButtonListItem(mActivity);
- item.setPrimaryActionIcon(
- android.R.drawable.sym_def_app_icon,
- RadioButtonListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
- item.setTitle("text");
- item.setTextStartMargin(R.dimen.car_keyline_4);
- setupPagedListView(Arrays.asList(item));
- int expected = ApplicationProvider.getApplicationContext().getResources()
- .getDimensionPixelSize(R.dimen.car_keyline_4);
-
- assertThat(getViewHolderAtPosition(0).getTitle().getLeft(), is(equalTo(expected)));
- }
@Test
public void testSetChecked() {
@@ -178,7 +164,7 @@
item.setChecked(true);
setupPagedListView(Arrays.asList(item));
- assertTrue(getViewHolderAtPosition(0).getRadioButton().isChecked());
+ assertTrue(getViewHolderAtPosition(0).getCompoundButton().isChecked());
}
@Test
@@ -190,28 +176,28 @@
item.setChecked(false);
refreshUi();
- assertFalse(getViewHolderAtPosition(0).getRadioButton().isChecked());
+ assertFalse(getViewHolderAtPosition(0).getCompoundButton().isChecked());
}
@Test
public void testSetShowRadioButtonDivider() {
RadioButtonListItem show = new RadioButtonListItem(mActivity);
- show.setShowRadioButtonDivider(true);
+ show.setShowCompoundButtonDivider(true);
setupPagedListView(Arrays.asList(show));
- assertThat(getViewHolderAtPosition(0).getRadioButtonDivider().getVisibility(),
+ assertThat(getViewHolderAtPosition(0).getCompoundButtonDivider().getVisibility(),
is(equalTo(View.VISIBLE)));
}
@Test
public void testSetShowRadioButtonDivider_noDivider() {
RadioButtonListItem noShow = new RadioButtonListItem(mActivity);
- noShow.setShowRadioButtonDivider(false);
+ noShow.setShowCompoundButtonDivider(false);
setupPagedListView(Arrays.asList(noShow));
- assertThat(getViewHolderAtPosition(0).getRadioButtonDivider().getVisibility(),
+ assertThat(getViewHolderAtPosition(0).getCompoundButtonDivider().getVisibility(),
is(equalTo(View.GONE)));
}
@@ -220,24 +206,7 @@
RadioButtonListItem item = new RadioButtonListItem(mActivity);
setupPagedListView(Arrays.asList(item));
- assertFalse(getViewHolderAtPosition(0).getRadioButton().isChecked());
- }
-
- @Test
- public void testClickingItemAlwaysCheckRadioButton() {
- boolean[] clicked = new boolean[]{false};
-
- RadioButtonListItem item = new RadioButtonListItem(mActivity);
- // Set radio button listener, but we will click the item.
- item.setOnCheckedChangeListener((compoundButton, checked) -> clicked[0] = true);
- setupPagedListView(Arrays.asList(item));
-
- onView(withId(R.id.recycler_view)).perform(
- RecyclerViewActions.actionOnItemAtPosition(0, click()));
-
- assertTrue(getViewHolderAtPosition(0).getRadioButton().isChecked());
- // Verify the listener is also triggered.
- assertTrue(clicked[0]);
+ assertFalse(getViewHolderAtPosition(0).getCompoundButton().isChecked());
}
@Test
@@ -252,7 +221,7 @@
item.setChecked(false);
refreshUi();
- assertFalse(getViewHolderAtPosition(0).getRadioButton().isChecked());
+ assertFalse(getViewHolderAtPosition(0).getCompoundButton().isChecked());
}
@Test
@@ -263,7 +232,7 @@
setupPagedListView(Arrays.asList(item));
onView(withId(R.id.recycler_view)).perform(
- actionOnItemAtPosition(0, clickChildViewWithId(R.id.radio_button)));
+ actionOnItemAtPosition(0, clickChildViewWithId(R.id.radiobutton_widget)));
assertTrue(clicked[0]);
}
diff --git a/car/core/src/androidTest/java/androidx/car/widget/SwitchListItemTest.java b/car/core/src/androidTest/java/androidx/car/widget/SwitchListItemTest.java
index ffc6ed1..f5b2e5b 100644
--- a/car/core/src/androidTest/java/androidx/car/widget/SwitchListItemTest.java
+++ b/car/core/src/androidTest/java/androidx/car/widget/SwitchListItemTest.java
@@ -176,7 +176,7 @@
onView(withId(R.id.recycler_view)).perform(actionOnItemAtPosition(0, click()));
- assertTrue(getViewHolderAtPosition(0).getSwitch().isChecked());
+ assertTrue(getViewHolderAtPosition(0).getCompoundButton().isChecked());
}
@Test
@@ -188,10 +188,10 @@
setupPagedListView(Collections.singletonList(item0));
SwitchListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
- toggleChecked(viewHolder.getSwitch());
+ toggleChecked(viewHolder.getCompoundButton());
viewHolder = getViewHolderAtPosition(0);
- assertThat(viewHolder.getSwitch().isChecked(), is(equalTo(false)));
+ assertThat(viewHolder.getCompoundButton().isChecked(), is(equalTo(false)));
}
@Test
@@ -205,8 +205,8 @@
refreshUi();
SwitchListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
- assertThat(viewHolder.getSwitch().getVisibility(), is(equalTo(View.VISIBLE)));
- assertThat(viewHolder.getSwitch().isChecked(), is(equalTo(false)));
+ assertThat(viewHolder.getCompoundButton().getVisibility(), is(equalTo(View.VISIBLE)));
+ assertThat(viewHolder.getCompoundButton().isChecked(), is(equalTo(false)));
}
@Test
@@ -214,7 +214,7 @@
CompoundButton.OnCheckedChangeListener listener =
mock(CompoundButton.OnCheckedChangeListener.class);
SwitchListItem item0 = new SwitchListItem(mActivity);
- item0.setSwitchOnCheckedChangeListener(listener);
+ item0.setOnCheckedChangeListener(listener);
setupPagedListView(Collections.singletonList(item0));
@@ -228,7 +228,7 @@
CompoundButton.OnCheckedChangeListener listener =
mock(CompoundButton.OnCheckedChangeListener.class);
SwitchListItem item0 = new SwitchListItem(mActivity);
- item0.setSwitchOnCheckedChangeListener(listener);
+ item0.setOnCheckedChangeListener(listener);
setupPagedListView(Collections.singletonList(item0));
@@ -241,7 +241,7 @@
CompoundButton.OnCheckedChangeListener listener =
mock(CompoundButton.OnCheckedChangeListener.class);
SwitchListItem item0 = new SwitchListItem(mActivity);
- item0.setSwitchOnCheckedChangeListener(listener);
+ item0.setOnCheckedChangeListener(listener);
item0.setChecked(true);
setupPagedListView(Collections.singletonList(item0));
@@ -254,12 +254,12 @@
CompoundButton.OnCheckedChangeListener listener =
mock(CompoundButton.OnCheckedChangeListener.class);
SwitchListItem item0 = new SwitchListItem(mActivity);
- item0.setSwitchOnCheckedChangeListener(listener);
+ item0.setOnCheckedChangeListener(listener);
setupPagedListView(Collections.singletonList(item0));
SwitchListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
- toggleChecked(viewHolder.getSwitch());
+ toggleChecked(viewHolder.getCompoundButton());
// Expect true because switch defaults to false.
verify(listener).onCheckedChanged(any(CompoundButton.class), eq(true));
@@ -271,7 +271,7 @@
mock(CompoundButton.OnCheckedChangeListener.class);
SwitchListItem item0 = new SwitchListItem(mActivity);
item0.setChecked(true);
- item0.setSwitchOnCheckedChangeListener(listener);
+ item0.setOnCheckedChangeListener(listener);
setupPagedListView(Collections.singletonList(item0));
@@ -285,7 +285,7 @@
public void testCheckingSwitch() {
final boolean[] clicked = {false};
SwitchListItem item0 = new SwitchListItem(mActivity);
- item0.setSwitchOnCheckedChangeListener((button, isChecked) -> {
+ item0.setOnCheckedChangeListener((button, isChecked) -> {
// Initial value is false.
assertTrue(isChecked);
clicked[0] = true;
@@ -302,21 +302,21 @@
@Test
public void testDividerVisibility() {
SwitchListItem item0 = new SwitchListItem(mActivity);
- item0.setShowSwitchDivider(true);
+ item0.setShowCompoundButtonDivider(true);
SwitchListItem item1 = new SwitchListItem(mActivity);
- item0.setShowSwitchDivider(false);
+ item0.setShowCompoundButtonDivider(false);
List<SwitchListItem> items = Arrays.asList(item0, item1);
setupPagedListView(items);
SwitchListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
- assertThat(viewHolder.getSwitch().getVisibility(), is(equalTo(View.VISIBLE)));
- assertThat(viewHolder.getSwitch().getVisibility(), is(equalTo(View.VISIBLE)));
+ assertThat(viewHolder.getCompoundButton().getVisibility(), is(equalTo(View.VISIBLE)));
+ assertThat(viewHolder.getCompoundButton().getVisibility(), is(equalTo(View.VISIBLE)));
viewHolder = getViewHolderAtPosition(1);
- assertThat(viewHolder.getSwitch().getVisibility(), is(equalTo(View.VISIBLE)));
- assertThat(viewHolder.getSwitchDivider().getVisibility(), is(equalTo(View.GONE)));
+ assertThat(viewHolder.getCompoundButton().getVisibility(), is(equalTo(View.VISIBLE)));
+ assertThat(viewHolder.getCompoundButtonDivider().getVisibility(), is(equalTo(View.GONE)));
}
@Test
@@ -752,7 +752,7 @@
SwitchListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
assertFalse(viewHolder.getTitle().isEnabled());
assertFalse(viewHolder.getBody().isEnabled());
- assertFalse(viewHolder.getSwitch().isEnabled());
+ assertFalse(viewHolder.getCompoundButton().isEnabled());
}
@Test
diff --git a/car/core/src/main/java/androidx/car/app/CarSingleChoiceDialog.java b/car/core/src/main/java/androidx/car/app/CarSingleChoiceDialog.java
index 73e7307..23bef3c 100644
--- a/car/core/src/main/java/androidx/car/app/CarSingleChoiceDialog.java
+++ b/car/core/src/main/java/androidx/car/app/CarSingleChoiceDialog.java
@@ -227,10 +227,10 @@
RadioButtonListItem item = new RadioButtonListItem(getContext());
item.setTitle(selectionItem.mTitle);
item.setBody(selectionItem.mBody);
- item.setShowRadioButtonDivider(false);
+ item.setShowCompoundButtonDivider(false);
item.addViewBinder(vh -> {
- vh.getRadioButton().setChecked(mSelectedItem == position);
- vh.getRadioButton().setOnCheckedChangeListener(
+ vh.getCompoundButton().setChecked(mSelectedItem == position);
+ vh.getCompoundButton().setOnCheckedChangeListener(
(buttonView, isChecked) -> {
mSelectedItem = position;
// Refresh other radio button list items.
diff --git a/car/core/src/main/java/androidx/car/widget/CheckBoxListItem.java b/car/core/src/main/java/androidx/car/widget/CheckBoxListItem.java
new file mode 100644
index 0000000..be99348
--- /dev/null
+++ b/car/core/src/main/java/androidx/car/widget/CheckBoxListItem.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.widget;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.car.R;
+import androidx.car.util.CarUxRestrictionsUtils;
+import androidx.car.uxrestrictions.CarUxRestrictions;
+import androidx.car.widget.ListItemAdapter.ListItemType;
+import androidx.constraintlayout.widget.Guideline;
+
+/**
+ * Class to build a list item with {@link CheckBox}.
+ *
+ * <p>A checkbox list item is visually composed of 5 parts.
+ * <ul>
+ * <li>optional {@code Primary Action Icon}.
+ * <li>optional {@code Title}.
+ * <li>optional {@code Body}.
+ * <li>optional {@code Divider}.
+ * <li>A {@link CheckBox}.
+ * </ul>
+ */
+public final class CheckBoxListItem extends CompoundButtonListItem<CheckBoxListItem.ViewHolder> {
+
+ /**
+ * Creates a {@link ViewHolder}.
+ *
+ * @return a {@link ViewHolder} for this {@link CheckBoxListItem}.
+ */
+ @NonNull
+ public static ViewHolder createViewHolder(@NonNull View itemView) {
+ return new ViewHolder(itemView);
+ }
+
+ /**
+ * Creates a {@link CheckBoxListItem} that will be used to display a list item with a
+ * {@link CheckBox}.
+ *
+ * @param context The context to be used by this {@link CheckBoxListItem}.
+ */
+ public CheckBoxListItem(@NonNull Context context) {
+ super(context);
+ }
+
+ /**
+ * Used by {@link ListItemAdapter} to choose layout to inflate for view holder.
+ *
+ * @return Type of this {@link CompoundButtonListItem}.
+ */
+ @ListItemType
+ @Override
+ public int getViewType() {
+ return ListItemAdapter.LIST_ITEM_TYPE_CHECK_BOX;
+ }
+
+ /**
+ * Returns whether the compound button will be placed at the end of the list item layout. This
+ * value is used to determine start margins for the {@code Title} and {@code Body}.
+ *
+ * @return Whether compound button is placed at the end of the list item layout.
+ */
+ @Override
+ public boolean isCompoundButtonPositionEnd() {
+ return true;
+ }
+
+ /**
+ * ViewHolder that contains necessary widgets for {@link CheckBoxListItem}.
+ */
+ public static final class ViewHolder extends CompoundButtonListItem.ViewHolder {
+
+ private View[] mWidgetViews;
+
+ private ViewGroup mContainerLayout;
+
+ private ImageView mPrimaryIcon;
+
+ private TextView mTitle;
+ private TextView mBody;
+
+ private Guideline mSupplementalGuideline;
+
+ private CompoundButton mCompoundButton;
+ private View mCompoundButtonDivider;
+
+ /**
+ * Creates a {@link ViewHolder} for a {@link CheckBoxListItem}.
+ *
+ * @param itemView The view to be used to display a {@link CheckBoxListItem}.
+ */
+ public ViewHolder(@NonNull View itemView) {
+ super(itemView);
+
+ mContainerLayout = itemView.findViewById(R.id.container);
+
+ mPrimaryIcon = itemView.findViewById(R.id.primary_icon);
+
+ mTitle = itemView.findViewById(R.id.title);
+ mBody = itemView.findViewById(R.id.body);
+
+ mSupplementalGuideline = itemView.findViewById(R.id.supplemental_actions_guideline);
+
+ mCompoundButton = itemView.findViewById(R.id.checkbox_widget);
+ mCompoundButtonDivider = itemView.findViewById(R.id.checkbox_divider);
+
+ int minTouchSize = itemView.getContext().getResources()
+ .getDimensionPixelSize(R.dimen.car_touch_target_size);
+ MinTouchTargetHelper.ensureThat(mCompoundButton).hasMinTouchSize(minTouchSize);
+
+ // Each line groups relevant child views in an effort to help keep this view array
+ // updated with actual child views in the ViewHolder.
+ mWidgetViews = new View[]{
+ mPrimaryIcon,
+ mTitle, mBody,
+ mCompoundButton, mCompoundButtonDivider,
+ };
+ }
+
+ /**
+ * Updates child views with current car UX restrictions.
+ *
+ * <p>{@code Text} might be truncated to meet length limit required by regulation.
+ *
+ * @param restrictionsInfo current car UX restrictions.
+ */
+ @Override
+ public void onUxRestrictionsChanged(@NonNull CarUxRestrictions restrictionsInfo) {
+ CarUxRestrictionsUtils.apply(itemView.getContext(), restrictionsInfo, getBody());
+ }
+
+ /**
+ * Returns the primary icon view within this view holder's view.
+ *
+ * @return Icon view within this view holder's view.
+ */
+ @NonNull
+ public ImageView getPrimaryIcon() {
+ return mPrimaryIcon;
+ }
+
+ /**
+ * Returns the title view within this view holder's view.
+ *
+ * @return Title view within this view holder's view.
+ */
+ @NonNull
+ public TextView getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Returns the body view within this view holder's view.
+ *
+ * @return Body view within this view holder's view.
+ */
+ @NonNull
+ public TextView getBody() {
+ return mBody;
+ }
+
+ /**
+ * Returns the compound button divider view within this view holder's view.
+ *
+ * @return Compound button divider view within this view holder's view.
+ */
+ @NonNull
+ public View getCompoundButtonDivider() {
+ return mCompoundButtonDivider;
+ }
+
+ /**
+ * Returns the compound button within this view holder's view.
+ *
+ * @return Compound button within this view holder's view.
+ */
+ @NonNull
+ public CompoundButton getCompoundButton() {
+ return mCompoundButton;
+ }
+
+ @NonNull
+ Guideline getSupplementalGuideline() {
+ return mSupplementalGuideline;
+ }
+
+ @NonNull
+ View[] getWidgetViews() {
+ return mWidgetViews;
+ }
+
+ /**
+ * Returns the container layout of this view holder.
+ *
+ * @return Container layout of this view holder.
+ */
+ @NonNull
+ public ViewGroup getContainerLayout() {
+ return mContainerLayout;
+ }
+ }
+}
diff --git a/car/core/src/main/java/androidx/car/widget/CompoundButtonListItem.java b/car/core/src/main/java/androidx/car/widget/CompoundButtonListItem.java
new file mode 100644
index 0000000..42876f6
--- /dev/null
+++ b/car/core/src/main/java/androidx/car/widget/CompoundButtonListItem.java
@@ -0,0 +1,660 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.widget;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.DimenRes;
+import androidx.annotation.Dimension;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.R;
+import androidx.car.widget.ListItemAdapter.ListItemType;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.constraintlayout.widget.Guideline;
+
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * Class to build a list item with {@link CompoundButton}.
+ *
+ * <p>A compound button list item is visually composed of 5 parts.
+ * <ul>
+ * <li>optional {@code Primary Action Icon}.
+ * <li>optional {@code Title}.
+ * <li>optional {@code Body}.
+ * <li>optional {@code Divider}.
+ * <li>A {@link CompoundButton}.
+ * </ul>
+ *
+ * @param <VH> ViewHolder that extends {@link CompoundButtonListItem.ViewHolder}.
+ */
+public abstract class CompoundButtonListItem<VH extends CompoundButtonListItem.ViewHolder> extends
+ ListItem<VH> {
+
+ @Retention(SOURCE)
+ @IntDef({
+ PRIMARY_ACTION_ICON_SIZE_SMALL, PRIMARY_ACTION_ICON_SIZE_MEDIUM,
+ PRIMARY_ACTION_ICON_SIZE_LARGE})
+ private @interface PrimaryActionIconSize {
+ }
+
+ /**
+ * Small sized icon is the mostly commonly used size. It's the same as supplemental action icon.
+ */
+ public static final int PRIMARY_ACTION_ICON_SIZE_SMALL = 0;
+ /**
+ * Medium sized icon is slightly bigger than {@code SMALL} ones. It is intended for profile
+ * pictures (avatar), in which case caller is responsible for passing in a circular image.
+ */
+ public static final int PRIMARY_ACTION_ICON_SIZE_MEDIUM = 1;
+ /**
+ * Large sized icon is as tall as a list item with only {@code title} text. It is intended for
+ * album art.
+ */
+ public static final int PRIMARY_ACTION_ICON_SIZE_LARGE = 2;
+
+ @Retention(SOURCE)
+ @IntDef({
+ PRIMARY_ACTION_TYPE_NO_ICON, PRIMARY_ACTION_TYPE_EMPTY_ICON,
+ PRIMARY_ACTION_TYPE_ICON})
+ private @interface PrimaryActionType {
+ }
+
+ private static final int PRIMARY_ACTION_TYPE_NO_ICON = 0;
+ private static final int PRIMARY_ACTION_TYPE_EMPTY_ICON = 1;
+ private static final int PRIMARY_ACTION_TYPE_ICON = 2;
+
+ private final Context mContext;
+ private boolean mIsEnabled = true;
+ private boolean mIsClickable;
+
+ private final List<ViewBinder<ViewHolder>> mBinders = new ArrayList<>();
+
+ @PrimaryActionType
+ private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
+ private Drawable mPrimaryActionIconDrawable;
+ @PrimaryActionIconSize
+ private int mPrimaryActionIconSize = PRIMARY_ACTION_ICON_SIZE_SMALL;
+
+ private CharSequence mTitle;
+ private CharSequence mBody;
+
+ @Dimension
+ private final int mSupplementalGuidelineBegin;
+
+ private boolean mIsChecked;
+ /**
+ * {@code true} if the checked state of the item has changed programmatically and
+ * {@link #mOnCheckedChangeListener} needs to be notified.
+ */
+ private boolean mShouldNotifyChecked;
+ private boolean mShowCompoundButtonDivider;
+ private CompoundButton.OnCheckedChangeListener mOnCheckedChangeListener;
+
+ /**
+ * Creates a {@link CompoundButtonListItem} that will be used to display a list item with a
+ * {@link CompoundButton}.
+ *
+ * @param context The context to be used by this {@link CompoundButtonListItem}.
+ */
+ public CompoundButtonListItem(@NonNull Context context) {
+ mContext = context;
+ Resources res = mContext.getResources();
+ mSupplementalGuidelineBegin = res.getDimensionPixelSize(
+ R.dimen.car_list_item_supplemental_guideline_top);
+ markDirty();
+ }
+
+ /**
+ * Classes that extend {@link CompoundButtonListItem} should register its view type in
+ * {@link ListItemAdapter#registerListItemViewType(int, int, Function)}.
+ *
+ * @return Type of this {@link CompoundButtonListItem}.
+ */
+ @ListItemType
+ @Override
+ public abstract int getViewType();
+
+ /**
+ * Calculates the layout params for views in {@link ViewHolder}.
+ */
+ @Override
+ @CallSuper
+ protected void resolveDirtyState() {
+ mBinders.clear();
+
+ // Create binders that adjust layout params of each view.
+ setPrimaryAction();
+ setText();
+ setCompoundButton();
+ setItemClickable();
+ }
+
+ /**
+ * Hides all views in {@link ViewHolder} then applies ViewBinders to adjust view layout params.
+ */
+ @Override
+ public void onBind(@NonNull VH viewHolder) {
+ hideSubViews(viewHolder);
+ for (ViewBinder binder : mBinders) {
+ binder.bind(viewHolder);
+ }
+
+ for (View v : viewHolder.getWidgetViews()) {
+ v.setEnabled(mIsEnabled);
+ }
+ // SwitchListItem supports clicking on the item so we also update the entire itemView.
+ viewHolder.itemView.setEnabled(mIsEnabled);
+ }
+
+ @Override
+ public void setEnabled(boolean isEnabled) {
+ mIsEnabled = isEnabled;
+ }
+
+ /**
+ * Sets whether the item is clickable. If {@code true}, clicking item toggles the compound
+ * button.
+ */
+ public void setClickable(boolean isClickable) {
+ mIsClickable = isClickable;
+ markDirty();
+ }
+
+ /**
+ * Sets {@code Primary Action} to be represented by an icon.
+ *
+ * @param drawable the {@link Drawable} to set.
+ * @param size small/medium/large. Available as {@link #PRIMARY_ACTION_ICON_SIZE_SMALL},
+ * {@link #PRIMARY_ACTION_ICON_SIZE_MEDIUM},
+ * {@link #PRIMARY_ACTION_ICON_SIZE_LARGE}.
+ */
+ public void setPrimaryActionIcon(@NonNull Drawable drawable, @PrimaryActionIconSize int size) {
+ mPrimaryActionType = PRIMARY_ACTION_TYPE_ICON;
+ mPrimaryActionIconDrawable = drawable;
+ mPrimaryActionIconSize = size;
+ markDirty();
+ }
+
+ /**
+ * Sets {@code Primary Action} to be represented by an icon.
+ *
+ * @param iconResId the resource identifier of the drawable.
+ * @param size small/medium/large. Available as {@link #PRIMARY_ACTION_ICON_SIZE_SMALL},
+ * {@link #PRIMARY_ACTION_ICON_SIZE_MEDIUM},
+ * {@link #PRIMARY_ACTION_ICON_SIZE_LARGE}.
+ */
+ public void setPrimaryActionIcon(@DrawableRes int iconResId, @PrimaryActionIconSize int size) {
+ setPrimaryActionIcon(mContext.getDrawable(iconResId), size);
+ }
+
+ /**
+ * Sets {@code Primary Action} to be empty icon.
+ *
+ * <p>{@code Text} would have a start margin as if {@code Primary Action} were set to primary
+ * icon.
+ */
+ public void setPrimaryActionEmptyIcon() {
+ mPrimaryActionType = PRIMARY_ACTION_TYPE_EMPTY_ICON;
+ markDirty();
+ }
+
+ /**
+ * Sets {@code Primary Action} to have no icon. Text would align to the start of item.
+ */
+ public void setPrimaryActionNoIcon() {
+ mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
+ markDirty();
+ }
+
+ /**
+ * Sets the title of item.
+ *
+ * <p>{@code Title} text is limited to one line, and ellipses at the end.
+ *
+ * @param title text to display as title.
+ */
+ public void setTitle(@Nullable CharSequence title) {
+ mTitle = title;
+ markDirty();
+ }
+
+ /**
+ * Sets the body text of item.
+ *
+ * <p>Text beyond length required by regulation will be truncated.
+ *
+ * @param body text to be displayed.
+ */
+ public void setBody(@Nullable CharSequence body) {
+ mBody = body;
+ markDirty();
+ }
+
+ /**
+ * Sets the state of {@link CompoundButton}.
+ *
+ * @param isChecked sets the "checked/unchecked, namely on/off" state of compound button.
+ */
+ public void setChecked(boolean isChecked) {
+ if (mIsChecked == isChecked) {
+ return;
+ }
+ mIsChecked = isChecked;
+ mShouldNotifyChecked = true;
+ markDirty();
+ }
+
+ /**
+ * Registers a callback to be invoked when the checked state of compound button changes.
+ *
+ * @param listener callback to be invoked when the checked state shown in the UI changes.
+ */
+ public void setOnCheckedChangeListener(
+ @Nullable CompoundButton.OnCheckedChangeListener listener) {
+ mOnCheckedChangeListener = listener;
+ // This method invalidates previous listener. Reset so that we *only*
+ // notify when the checked state changes and not on the initial bind.
+ mShouldNotifyChecked = false;
+ markDirty();
+ }
+
+ /**
+ * Sets whether to display a vertical bar between compound button and text.
+ */
+ public void setShowCompoundButtonDivider(boolean showCompoundButtonDivider) {
+ mShowCompoundButtonDivider = showCompoundButtonDivider;
+ markDirty();
+ }
+
+ private void hideSubViews(ViewHolder vh) {
+ for (View v : vh.getWidgetViews()) {
+ v.setVisibility(View.GONE);
+ }
+ }
+
+ private void setPrimaryAction() {
+ setPrimaryIconContent();
+ setPrimaryIconLayout();
+ }
+
+ private void setText() {
+ setTextContent();
+ setTextVerticalMargin();
+ setTextStartMargin();
+ setTextEndMargin();
+ }
+
+ private void setPrimaryIconContent() {
+ switch (mPrimaryActionType) {
+ case PRIMARY_ACTION_TYPE_ICON:
+ mBinders.add(vh -> {
+ vh.getPrimaryIcon().setVisibility(View.VISIBLE);
+ vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
+ });
+ break;
+ case PRIMARY_ACTION_TYPE_EMPTY_ICON:
+ // Do nothing.
+ break;
+ case PRIMARY_ACTION_TYPE_NO_ICON:
+ mBinders.add(vh -> {
+ vh.getPrimaryIcon().setVisibility(View.GONE);
+ });
+ break;
+ default:
+ throw new IllegalStateException("Unknown primary action type.");
+ }
+ }
+
+ /**
+ * Returns whether the compound button will be placed at the end of the list item layout. This
+ * value is used to determine start margins for the {@code Title} and {@code Body}.
+ *
+ * @return Whether compound button is placed at the end of the list item layout.
+ */
+ public abstract boolean isCompoundButtonPositionEnd();
+
+ /**
+ * Sets the size, start margin, and vertical position of primary icon.
+ *
+ * <p>Large icon will have no start margin, and always align center vertically.
+ *
+ * <p>Small/medium icon will have start margin, and uses a top margin such that it is "pinned"
+ * at the same position in list item regardless of item height.
+ */
+ private void setPrimaryIconLayout() {
+ if (mPrimaryActionType == PRIMARY_ACTION_TYPE_EMPTY_ICON
+ || mPrimaryActionType == PRIMARY_ACTION_TYPE_NO_ICON) {
+ return;
+ }
+
+ // Size of icon.
+ @DimenRes int sizeResId;
+ switch (mPrimaryActionIconSize) {
+ case PRIMARY_ACTION_ICON_SIZE_SMALL:
+ sizeResId = R.dimen.car_primary_icon_size;
+ break;
+ case PRIMARY_ACTION_ICON_SIZE_MEDIUM:
+ sizeResId = R.dimen.car_avatar_icon_size;
+ break;
+ case PRIMARY_ACTION_ICON_SIZE_LARGE:
+ sizeResId = R.dimen.car_single_line_list_item_height;
+ break;
+ default:
+ throw new IllegalStateException("Unknown primary action icon size.");
+ }
+
+ int iconSize = mContext.getResources().getDimensionPixelSize(sizeResId);
+
+ // Start margin of icon.
+ int startMargin;
+ switch (mPrimaryActionIconSize) {
+ case PRIMARY_ACTION_ICON_SIZE_SMALL:
+ case PRIMARY_ACTION_ICON_SIZE_MEDIUM:
+ startMargin = mContext.getResources().getDimensionPixelSize(R.dimen.car_keyline_1);
+ break;
+ case PRIMARY_ACTION_ICON_SIZE_LARGE:
+ startMargin = 0;
+ break;
+ default:
+ throw new IllegalStateException("Unknown primary action icon size.");
+ }
+
+ mBinders.add(vh -> {
+ ConstraintLayout.LayoutParams layoutParams =
+ (ConstraintLayout.LayoutParams) vh.getPrimaryIcon().getLayoutParams();
+ layoutParams.height = layoutParams.width = iconSize;
+ layoutParams.setMarginStart(startMargin);
+
+ if (mPrimaryActionIconSize == PRIMARY_ACTION_ICON_SIZE_LARGE) {
+ // A large icon is always vertically centered.
+ layoutParams.verticalBias = 0.5f;
+ layoutParams.topMargin = 0;
+ } else {
+ // Align the icon to the top of the parent. This allows the topMargin to shift it
+ // down relative to the top.
+ layoutParams.verticalBias = 0f;
+
+ // For all other icon sizes, the icon should be centered within the height of
+ // car_double_line_list_item_height. Note: the actual height of the item can be
+ // larger than this.
+ int itemHeight = mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_double_line_list_item_height);
+ layoutParams.topMargin = (itemHeight - iconSize) / 2;
+ }
+
+ vh.getPrimaryIcon().requestLayout();
+ });
+ }
+
+ private void setTextContent() {
+ boolean hasTitle = !TextUtils.isEmpty(mTitle);
+ boolean hasBody = !TextUtils.isEmpty(mBody);
+
+ if (!hasTitle && !hasBody) {
+ return;
+ }
+
+ mBinders.add(vh -> {
+ if (hasTitle) {
+ vh.getTitle().setVisibility(View.VISIBLE);
+ vh.getTitle().setText(mTitle);
+ }
+
+ if (hasBody) {
+ vh.getBody().setVisibility(View.VISIBLE);
+ vh.getBody().setText(mBody);
+ }
+
+ if (hasTitle && !hasBody) {
+ // If only title, then center the supplemental actions.
+ vh.getSupplementalGuideline().setGuidelineBegin(
+ ConstraintLayout.LayoutParams.UNSET);
+ vh.getSupplementalGuideline().setGuidelinePercent(0.5f);
+ } else {
+ // Otherwise, position it a fixed distance from the top.
+ vh.getSupplementalGuideline().setGuidelinePercent(
+ ConstraintLayout.LayoutParams.UNSET);
+ vh.getSupplementalGuideline().setGuidelineBegin(
+ mSupplementalGuidelineBegin);
+ }
+ });
+ }
+
+ /**
+ * Sets start margin of text view depending on icon type.
+ */
+ private void setTextStartMargin() {
+ @DimenRes int startMarginResId;
+ if (!isCompoundButtonPositionEnd()) {
+ startMarginResId = R.dimen.car_keyline_3;
+ } else {
+ switch (mPrimaryActionType) {
+ case PRIMARY_ACTION_TYPE_NO_ICON:
+ startMarginResId = R.dimen.car_keyline_1;
+ break;
+ case PRIMARY_ACTION_TYPE_EMPTY_ICON:
+ startMarginResId = R.dimen.car_keyline_3;
+ break;
+ case PRIMARY_ACTION_TYPE_ICON:
+ startMarginResId = mPrimaryActionIconSize == PRIMARY_ACTION_ICON_SIZE_LARGE
+ ? R.dimen.car_keyline_4
+ : R.dimen.car_keyline_3; // Small and medium sized icon.
+ break;
+ default:
+ throw new IllegalStateException("Unknown primary action type.");
+ }
+ }
+
+ int startMargin = mContext.getResources().getDimensionPixelSize(startMarginResId);
+ mBinders.add(vh -> {
+ MarginLayoutParams titleLayoutParams =
+ (MarginLayoutParams) vh.getTitle().getLayoutParams();
+ titleLayoutParams.setMarginStart(startMargin);
+ vh.getTitle().requestLayout();
+
+ MarginLayoutParams bodyLayoutParams =
+ (MarginLayoutParams) vh.getBody().getLayoutParams();
+ bodyLayoutParams.setMarginStart(startMargin);
+ vh.getBody().requestLayout();
+ });
+ }
+
+ private void setTextEndMargin() {
+ int endMargin = mContext.getResources().getDimensionPixelSize(R.dimen.car_padding_4);
+
+ mBinders.add(vh -> {
+ MarginLayoutParams titleLayoutParams =
+ (MarginLayoutParams) vh.getTitle().getLayoutParams();
+ titleLayoutParams.setMarginEnd(endMargin);
+
+ MarginLayoutParams bodyLayoutParams =
+ (MarginLayoutParams) vh.getBody().getLayoutParams();
+ bodyLayoutParams.setMarginEnd(endMargin);
+ });
+ }
+
+ /**
+ * Sets top/bottom margins of {@code Title} and {@code Body}.
+ */
+ private void setTextVerticalMargin() {
+ // Set all relevant fields in layout params to avoid carried over params when the item
+ // gets bound to a recycled view holder.
+ if (!TextUtils.isEmpty(mTitle) && TextUtils.isEmpty(mBody)) {
+ // Title only - view is aligned center vertically by itself.
+ mBinders.add(vh -> {
+ MarginLayoutParams layoutParams =
+ (MarginLayoutParams) vh.getTitle().getLayoutParams();
+ layoutParams.topMargin = 0;
+ vh.getTitle().requestLayout();
+ });
+ } else if (TextUtils.isEmpty(mTitle) && !TextUtils.isEmpty(mBody)) {
+ mBinders.add(vh -> {
+ // Body uses top and bottom margin.
+ int margin = mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_padding_3);
+ MarginLayoutParams layoutParams =
+ (MarginLayoutParams) vh.getBody().getLayoutParams();
+ layoutParams.topMargin = margin;
+ layoutParams.bottomMargin = margin;
+ vh.getBody().requestLayout();
+ });
+ } else {
+ mBinders.add(vh -> {
+ Resources resources = mContext.getResources();
+ int padding2 = resources.getDimensionPixelSize(R.dimen.car_padding_2);
+
+ // Title has a top margin
+ MarginLayoutParams titleLayoutParams =
+ (MarginLayoutParams) vh.getTitle().getLayoutParams();
+ titleLayoutParams.topMargin = padding2;
+ vh.getTitle().requestLayout();
+
+ // Body is below title with no margin and has bottom margin.
+ MarginLayoutParams bodyLayoutParams =
+ (MarginLayoutParams) vh.getBody().getLayoutParams();
+ bodyLayoutParams.topMargin = 0;
+ bodyLayoutParams.bottomMargin = padding2;
+ vh.getBody().requestLayout();
+ });
+ }
+ }
+
+ /**
+ * Sets up view(s) for supplemental action.
+ */
+ private void setCompoundButton() {
+ mBinders.add(vh -> {
+ vh.getCompoundButton().setVisibility(View.VISIBLE);
+ vh.getCompoundButton().setOnCheckedChangeListener(null);
+ vh.getCompoundButton().setChecked(mIsChecked);
+ vh.getCompoundButton().setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (mOnCheckedChangeListener != null) {
+ // The checked state changed via user interaction with the compound button.
+ mOnCheckedChangeListener.onCheckedChanged(buttonView, isChecked);
+ }
+ mIsChecked = isChecked;
+ });
+ if (mShouldNotifyChecked && mOnCheckedChangeListener != null) {
+ // The checked state was changed programmatically.
+ mOnCheckedChangeListener.onCheckedChanged(vh.getCompoundButton(),
+ mIsChecked);
+ mShouldNotifyChecked = false;
+ }
+
+ if (mShowCompoundButtonDivider) {
+ vh.getCompoundButtonDivider().setVisibility(View.VISIBLE);
+ }
+ });
+ }
+
+ private void setItemClickable() {
+ mBinders.add(vh -> {
+ // If applicable (namely item is clickable), clicking item always toggles the
+ // compound button.
+ vh.itemView.setOnClickListener(v -> vh.getCompoundButton().toggle());
+ vh.itemView.setClickable(mIsClickable);
+ });
+ }
+
+ /**
+ * Holds views of CompoundButtonListItem.
+ */
+ public abstract static class ViewHolder extends ListItem.ViewHolder {
+
+ /**
+ * Creates a {@link ViewHolder} for a {@link CompoundButtonListItem}.
+ *
+ * @param itemView The view to be used to display a {@link CompoundButtonListItem}.
+ */
+ public ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+
+ /**
+ * Returns the primary icon view within this view holder's view.
+ *
+ * @return Icon view within this view holder's view.
+ */
+ @NonNull
+ public abstract ImageView getPrimaryIcon();
+
+ /**
+ * Returns the title view within this view holder's view.
+ *
+ * @return Title view within this view holder's view.
+ */
+ @NonNull
+ public abstract TextView getTitle();
+
+ /**
+ * Returns the body view within this view holder's view.
+ *
+ * @return Body view within this view holder's view.
+ */
+ @NonNull
+ public abstract TextView getBody();
+
+ /**
+ * Returns the compound button divider view within this view holder's view.
+ *
+ * @return Compound button divider view within this view holder's view.
+ */
+ @NonNull
+ public abstract View getCompoundButtonDivider();
+
+ /**
+ * Returns the compound button within this view holder's view.
+ *
+ * @return Compound button within this view holder's view.
+ */
+ @NonNull
+ public abstract CompoundButton getCompoundButton();
+
+ @NonNull
+ abstract Guideline getSupplementalGuideline();
+
+ @NonNull
+ abstract ViewGroup getContainerLayout();
+
+ /**
+ * Returns the container layout of this view holder.
+ *
+ * @return Container layout of this view holder.
+ */
+ @NonNull
+ abstract View[] getWidgetViews();
+ }
+}
diff --git a/car/core/src/main/java/androidx/car/widget/ListItemAdapter.java b/car/core/src/main/java/androidx/car/widget/ListItemAdapter.java
index 5bf1e6a..916bddb 100644
--- a/car/core/src/main/java/androidx/car/widget/ListItemAdapter.java
+++ b/car/core/src/main/java/androidx/car/widget/ListItemAdapter.java
@@ -95,12 +95,27 @@
*/
public static final int BACKGROUND_STYLE_PANEL = 3;
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @IntDef({
+ LIST_ITEM_TYPE_TEXT,
+ LIST_ITEM_TYPE_SEEKBAR,
+ LIST_ITEM_TYPE_SUBHEADER,
+ LIST_ITEM_TYPE_ACTION,
+ LIST_ITEM_TYPE_RADIO,
+ LIST_ITEM_TYPE_SWITCH,
+ LIST_ITEM_TYPE_CHECK_BOX})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ListItemType {
+ }
+
static final int LIST_ITEM_TYPE_TEXT = 1;
static final int LIST_ITEM_TYPE_SEEKBAR = 2;
static final int LIST_ITEM_TYPE_SUBHEADER = 3;
static final int LIST_ITEM_TYPE_ACTION = 4;
static final int LIST_ITEM_TYPE_RADIO = 5;
static final int LIST_ITEM_TYPE_SWITCH = 6;
+ static final int LIST_ITEM_TYPE_CHECK_BOX = 7;
private final SparseIntArray mViewHolderLayoutResIds = new SparseIntArray();
@@ -146,6 +161,8 @@
R.layout.car_list_item_radio_content, RadioButtonListItem::createViewHolder);
registerListItemViewTypeInternal(LIST_ITEM_TYPE_SWITCH,
R.layout.car_list_item_switch_content, SwitchListItem::createViewHolder);
+ registerListItemViewTypeInternal(LIST_ITEM_TYPE_CHECK_BOX,
+ R.layout.car_list_item_check_box_content, CheckBoxListItem::createViewHolder);
mUxRestrictionsHelper =
new CarUxRestrictionsHelper(context, carUxRestrictions -> {
diff --git a/car/core/src/main/java/androidx/car/widget/RadioButtonListItem.java b/car/core/src/main/java/androidx/car/widget/RadioButtonListItem.java
index 36b05fc..84b20c8 100644
--- a/car/core/src/main/java/androidx/car/widget/RadioButtonListItem.java
+++ b/car/core/src/main/java/androidx/car/widget/RadioButtonListItem.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 The Android Open Source Project
+ * Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,11 +16,7 @@
package androidx.car.widget;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
@@ -28,483 +24,187 @@
import android.widget.RadioButton;
import android.widget.TextView;
-import androidx.annotation.DimenRes;
-import androidx.annotation.Dimension;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.car.R;
import androidx.car.util.CarUxRestrictionsUtils;
-
-import java.lang.annotation.Retention;
-import java.util.ArrayList;
-import java.util.List;
+import androidx.car.uxrestrictions.CarUxRestrictions;
+import androidx.car.widget.ListItemAdapter.ListItemType;
+import androidx.constraintlayout.widget.Guideline;
/**
* Class to build a list item with {@link RadioButton}.
*
- * <p>A radio button list item visually composes of 4 parts.
+ * <p>A radio button list item is visually composed of 5 parts.
* <ul>
+ * <li>A {@link RadioButton}.
+ * <li>optional {@code Divider}.
* <li>optional {@code Primary Action Icon}.
* <li>optional {@code Title}.
* <li>optional {@code Body}.
- * <li>A {@link RadioButton}.
* </ul>
- *
- * <p>Clicking the item always checks the radio button.
*/
-public class RadioButtonListItem extends ListItem<RadioButtonListItem.ViewHolder> {
-
- @Retention(SOURCE)
- @IntDef({
- PRIMARY_ACTION_ICON_SIZE_SMALL, PRIMARY_ACTION_ICON_SIZE_MEDIUM,
- PRIMARY_ACTION_ICON_SIZE_LARGE})
- private @interface PrimaryActionIconSize {
- }
+public final class RadioButtonListItem extends
+ CompoundButtonListItem<RadioButtonListItem.ViewHolder> {
/**
- * Small sized icon is the mostly commonly used size.
- */
- public static final int PRIMARY_ACTION_ICON_SIZE_SMALL = 0;
- /**
- * Medium sized icon is slightly bigger than {@code SMALL} ones. It is intended for profile
- * pictures (avatar), in which case caller is responsible for passing in a circular image.
- */
- public static final int PRIMARY_ACTION_ICON_SIZE_MEDIUM = 1;
- /**
- * Large sized icon is as tall as a list item with only {@code title} text. It is intended for
- * album art.
- */
- public static final int PRIMARY_ACTION_ICON_SIZE_LARGE = 2;
-
- private final List<ViewBinder<ViewHolder>> mBinders = new ArrayList<>();
- private final Context mContext;
- private boolean mIsEnabled = true;
-
- private Drawable mPrimaryActionIconDrawable;
- @PrimaryActionIconSize private int mPrimaryActionIconSize = PRIMARY_ACTION_ICON_SIZE_SMALL;
-
- @Dimension(unit = Dimension.PX)
- private int mTextStartMargin;
- private CharSequence mTitle;
- private CharSequence mBody;
-
- private boolean mIsChecked;
- private boolean mShowRadioButtonDivider;
- private CompoundButton.OnCheckedChangeListener mRadioButtonOnCheckedChangeListener;
-
- /**
- * Creates a {@link RadioButtonListItem.ViewHolder}.
+ * Creates a {@link ViewHolder}.
+ *
+ * @return a {@link ViewHolder} for this {@link RadioButtonListItem}.
*/
@NonNull
public static ViewHolder createViewHolder(@NonNull View itemView) {
return new ViewHolder(itemView);
}
+ /**
+ * Creates a {@link RadioButtonListItem} that will be used to display a list item with a
+ * {@link RadioButton}.
+ *
+ * @param context The context to be used by this {@link RadioButtonListItem}.
+ */
public RadioButtonListItem(@NonNull Context context) {
- mContext = context;
- mTextStartMargin = mContext.getResources().getDimensionPixelSize(R.dimen.car_keyline_3);
- markDirty();
+ super(context);
+ }
+
+ /**
+ * Returns whether the compound button will be placed at the end of the list item layout. This
+ * value is used to determine start margins for the {@code Title} and {@code Body}.
+ *
+ * @return Whether compound button is placed at the end of the list item layout.
+ */
+ @Override
+ public boolean isCompoundButtonPositionEnd() {
+ return false;
}
/**
* Used by {@link ListItemAdapter} to choose layout to inflate for view holder.
+ *
+ * @return Type of this {@link CompoundButtonListItem}.
*/
+ @ListItemType
@Override
public int getViewType() {
return ListItemAdapter.LIST_ITEM_TYPE_RADIO;
}
- @Override
- public void setEnabled(boolean enabled) {
- mIsEnabled = enabled;
- }
-
- @NonNull
- protected Context getContext() {
- return mContext;
- }
-
/**
- * Sets the state of radio button.
- *
- * @param isChecked {@code true} to check the button; {@code false} to uncheck it.
+ * ViewHolder that contains necessary widgets for {@link RadioButtonListItem}.
*/
- public void setChecked(boolean isChecked) {
- if (mIsChecked == isChecked) {
- return;
- }
- mIsChecked = isChecked;
- markDirty();
- }
+ public static final class ViewHolder extends CompoundButtonListItem.ViewHolder {
- /**
- * Get whether the radio button is checked.
- *
- * <p>The return value is in sync with UI state.
- *
- * @return {@code true} if the widget is checked; {@code false} otherwise.
- */
- public boolean isChecked() {
- return mIsChecked;
- }
-
- /**
- * Sets {@code Primary Action} to be represented by an icon. The size of icon automatically
- * adjusts the start of {@code Text}.
- *
- * <p>Call {@link #setPrimaryActionNoIcon()} to clear the content and aligns text to the start
- * of list item
- *
- * @param drawable the Drawable to set as primary action.
- * @param size constant that represents the size of icon. See
- * {@link #PRIMARY_ACTION_ICON_SIZE_SMALL},
- * {@link #PRIMARY_ACTION_ICON_SIZE_MEDIUM}, and
- * {@link #PRIMARY_ACTION_ICON_SIZE_LARGE}.
- */
- public void setPrimaryActionIcon(@NonNull Drawable drawable, @PrimaryActionIconSize int size) {
- mPrimaryActionIconDrawable = drawable;
- mPrimaryActionIconSize = size;
- markDirty();
- }
-
- /**
- * Sets {@code Primary Action} to be represented by an icon. The size of icon automatically
- * adjusts the start of {@code Text}.
- *
- * @param iconResId the resource identifier of the drawable.
- * @param size constant that represents the size of icon. See
- * {@link #PRIMARY_ACTION_ICON_SIZE_SMALL},
- * {@link #PRIMARY_ACTION_ICON_SIZE_MEDIUM}, and
- * {@link #PRIMARY_ACTION_ICON_SIZE_LARGE}.
- */
- public void setPrimaryActionIcon(@DrawableRes int iconResId, @PrimaryActionIconSize int size) {
- setPrimaryActionIcon(getContext().getDrawable(iconResId), size);
- }
-
- /**
- * Sets {@code Primary Action} to have no icon. Text would align to the start of list item.
- */
- public void setPrimaryActionNoIcon() {
- mPrimaryActionIconDrawable = null;
- markDirty();
- }
-
- /**
- * Sets title text to be displayed next to icon.
- *
- * @param text Text to be displayed, or {@code null} to clear the content.
- */
- public void setTitle(@Nullable CharSequence text) {
- mTitle = text;
- markDirty();
- }
-
- /**
- * Sets body text to be displayed next to radio button.
- *
- * @param text Text to be displayed, or {@code null} to clear the content.
- */
- public void setBody(@Nullable CharSequence text) {
- mBody = text;
- markDirty();
- }
-
- /**
- * Sets the start margin of text.
- */
- public void setTextStartMargin(@DimenRes int dimenRes) {
- mTextStartMargin = mContext.getResources().getDimensionPixelSize(dimenRes);
- markDirty();
- }
-
- /**
- * Sets whether to display a vertical bar that separates {@code text} and radio button.
- */
- public void setShowRadioButtonDivider(boolean showDivider) {
- mShowRadioButtonDivider = showDivider;
- markDirty();
- }
-
- /**
- * Sets {@link android.widget.CompoundButton.OnCheckedChangeListener} of radio button.
- */
- public void setOnCheckedChangeListener(
- @NonNull CompoundButton.OnCheckedChangeListener listener) {
- mRadioButtonOnCheckedChangeListener = listener;
- markDirty();
- }
-
- /**
- * Calculates the layout params for views in {@link ViewHolder}.
- */
- @Override
- protected void resolveDirtyState() {
- mBinders.clear();
-
- // Create binders that adjust layout params of each view.
- setPrimaryAction();
- setTextInternal();
- setRadioButton();
- setOnClickListenerToCheckRadioButton();
- }
-
- private void setPrimaryAction() {
- setPrimaryIconContent();
- setPrimaryIconLayout();
- }
-
- private void setTextInternal() {
- setTextContent();
- setTextVerticalMargins();
- setTextStartMargin();
- }
-
- private void setRadioButton() {
- mBinders.add(vh -> {
- // Clear listener before setting checked to avoid listener is notified every time
- // we bind to view holder.
- vh.getRadioButton().setOnCheckedChangeListener(null);
- vh.getRadioButton().setChecked(mIsChecked);
- // Keep internal checked state in sync with UI by wrapping listener.
- vh.getRadioButton().setOnCheckedChangeListener((buttonView, isChecked) -> {
- mIsChecked = isChecked;
- if (mRadioButtonOnCheckedChangeListener != null) {
- mRadioButtonOnCheckedChangeListener.onCheckedChanged(buttonView, isChecked);
- }
- });
-
- vh.getRadioButtonDivider().setVisibility(
- mShowRadioButtonDivider ? View.VISIBLE : View.GONE);
- });
- }
-
- private void setPrimaryIconContent() {
- mBinders.add(vh -> {
- if (mPrimaryActionIconDrawable == null) {
- vh.getPrimaryIcon().setVisibility(View.GONE);
- } else {
- vh.getPrimaryIcon().setVisibility(View.VISIBLE);
- vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
- }
- });
- }
-
- /**
- * Sets the size, start margin, and vertical position of primary icon.
- *
- * <p>Large icon will have no start margin, and always align center vertically.
- *
- * <p>Small/medium icon will have start margin, and uses a top margin such that it is "pinned"
- * at the same position in list item regardless of item height.
- */
- private void setPrimaryIconLayout() {
- if (mPrimaryActionIconDrawable == null) {
- return;
- }
-
- // Size of icon.
- @DimenRes int sizeResId;
- switch (mPrimaryActionIconSize) {
- case PRIMARY_ACTION_ICON_SIZE_SMALL:
- sizeResId = R.dimen.car_primary_icon_size;
- break;
- case PRIMARY_ACTION_ICON_SIZE_MEDIUM:
- sizeResId = R.dimen.car_avatar_icon_size;
- break;
- case PRIMARY_ACTION_ICON_SIZE_LARGE:
- sizeResId = R.dimen.car_single_line_list_item_height;
- break;
- default:
- throw new IllegalStateException("Unknown primary action icon size.");
- }
-
- int iconSize = mContext.getResources().getDimensionPixelSize(sizeResId);
-
- // Start margin of icon.
- int startMargin;
- switch (mPrimaryActionIconSize) {
- case PRIMARY_ACTION_ICON_SIZE_SMALL:
- case PRIMARY_ACTION_ICON_SIZE_MEDIUM:
- startMargin = mContext.getResources().getDimensionPixelSize(R.dimen.car_keyline_1);
- break;
- case PRIMARY_ACTION_ICON_SIZE_LARGE:
- startMargin = 0;
- break;
- default:
- throw new IllegalStateException("Unknown primary action icon size.");
- }
-
- mBinders.add(vh -> {
- ViewGroup.MarginLayoutParams layoutParams =
- (ViewGroup.MarginLayoutParams) vh.getPrimaryIcon().getLayoutParams();
- layoutParams.height = layoutParams.width = iconSize;
- layoutParams.setMarginStart(startMargin);
-
- vh.getPrimaryIcon().requestLayout();
- });
- }
-
- private void setTextContent() {
- if (!TextUtils.isEmpty(mTitle)) {
- mBinders.add(vh -> {
- vh.getTitle().setVisibility(View.VISIBLE);
- vh.getTitle().setText(mTitle);
- });
- }
-
- if (!TextUtils.isEmpty(mBody)) {
- mBinders.add(vh -> {
- vh.getBody().setVisibility(View.VISIBLE);
- vh.getBody().setText(mBody);
- });
- } else {
- mBinders.add(vh -> vh.getBody().setVisibility(View.GONE));
- }
- }
-
- /**
- * Sets top and bottom margins of text views depending on existence of other text view.
- */
- private void setTextVerticalMargins() {
- if (TextUtils.isEmpty(mBody)) {
- mBinders.add(vh -> {
- ViewGroup.MarginLayoutParams textViewLayoutParams =
- (ViewGroup.MarginLayoutParams) vh.getTitle().getLayoutParams();
- textViewLayoutParams.topMargin = 0;
- vh.getTitle().requestLayout();
- });
- }
-
- if (TextUtils.isEmpty(mTitle)) {
- mBinders.add(vh -> {
- ViewGroup.MarginLayoutParams textViewLayoutParams =
- (ViewGroup.MarginLayoutParams) vh.getBody().getLayoutParams();
- textViewLayoutParams.bottomMargin = 0;
- vh.getBody().requestLayout();
- });
- }
- }
-
- /**
- * Sets start margin of text views.
- */
- private void setTextStartMargin() {
- mBinders.add(vh -> {
- ViewGroup.MarginLayoutParams textViewLayoutParams =
- (ViewGroup.MarginLayoutParams) vh.getTitle().getLayoutParams();
- textViewLayoutParams.setMarginStart(mTextStartMargin);
- vh.getTitle().requestLayout();
-
- ViewGroup.MarginLayoutParams bodyTextViewLayoutParams =
- (ViewGroup.MarginLayoutParams) vh.getBody().getLayoutParams();
- bodyTextViewLayoutParams.setMarginStart(mTextStartMargin);
- vh.getBody().requestLayout();
- });
- }
-
- // Clicking the item always checks radio button.
- private void setOnClickListenerToCheckRadioButton() {
- mBinders.add(vh -> {
- vh.itemView.setClickable(true);
- vh.itemView.setOnClickListener(v -> vh.getRadioButton().setChecked(true));
- });
- }
-
- /**
- * Hides all views in {@link ViewHolder} then applies ViewBinders to adjust view layout params.
- */
- @Override
- protected void onBind(ViewHolder viewHolder) {
- // Hide all subviews then apply view binders to adjust subviews.
- hideSubViews(viewHolder);
- for (ViewBinder binder : mBinders) {
- binder.bind(viewHolder);
- }
-
- for (View v : viewHolder.getWidgetViews()) {
- v.setEnabled(mIsEnabled);
- }
- }
-
- private void hideSubViews(ViewHolder vh) {
- for (View v : vh.getWidgetViews()) {
- v.setVisibility(View.GONE);
- }
- // Radio button is always visible.
- vh.getRadioButton().setVisibility(View.VISIBLE);
- }
-
- /**
- * Holds views of RadioButtonListItem.
- */
- public static final class ViewHolder extends ListItem.ViewHolder {
-
- private final View[] mWidgetViews;
+ private View[] mWidgetViews;
private ViewGroup mContainerLayout;
private ImageView mPrimaryIcon;
+
private TextView mTitle;
private TextView mBody;
- private View mRadioButtonDivider;
- private RadioButton mRadioButton;
+ private Guideline mSupplementalGuideline;
+ private CompoundButton mCompoundButton;
+ private View mCompoundButtonDivider;
+
+ /**
+ * Creates a {@link ViewHolder} for a {@link RadioButtonListItem}.
+ *
+ * @param itemView The view to be used to display a {@link RadioButtonListItem}.
+ */
public ViewHolder(@NonNull View itemView) {
super(itemView);
mContainerLayout = itemView.findViewById(R.id.container);
mPrimaryIcon = itemView.findViewById(R.id.primary_icon);
+
mTitle = itemView.findViewById(R.id.title);
mBody = itemView.findViewById(R.id.body);
- mRadioButton = itemView.findViewById(R.id.radio_button);
- mRadioButtonDivider = itemView.findViewById(R.id.radio_button_divider);
+ mSupplementalGuideline = itemView.findViewById(R.id.supplemental_actions_guideline);
+
+ mCompoundButton = itemView.findViewById(R.id.radiobutton_widget);
+ mCompoundButtonDivider = itemView.findViewById(R.id.radiobutton_divider);
int minTouchSize = itemView.getContext().getResources()
.getDimensionPixelSize(R.dimen.car_touch_target_size);
-
- MinTouchTargetHelper.ensureThat(mRadioButton)
- .hasMinTouchSize(minTouchSize);
+ MinTouchTargetHelper.ensureThat(mCompoundButton).hasMinTouchSize(minTouchSize);
// Each line groups relevant child views in an effort to help keep this view array
// updated with actual child views in the ViewHolder.
mWidgetViews = new View[]{
- mPrimaryIcon, mTitle, mBody,
- mRadioButton, mRadioButtonDivider};
+ mPrimaryIcon,
+ mTitle, mBody,
+ mCompoundButton, mCompoundButtonDivider,
+ };
}
- @NonNull
- public ViewGroup getContainerLayout() {
- return mContainerLayout;
+ /**
+ * Updates child views with current car UX restrictions.
+ *
+ * <p>{@code Text} might be truncated to meet length limit required by regulation.
+ *
+ * @param restrictionsInfo current car UX restrictions.
+ */
+ @Override
+ public void onUxRestrictionsChanged(@NonNull CarUxRestrictions restrictionsInfo) {
+ CarUxRestrictionsUtils.apply(itemView.getContext(), restrictionsInfo, getBody());
}
+ /**
+ * Returns the primary icon view within this view holder's view.
+ *
+ * @return Icon view within this view holder's view.
+ */
@NonNull
public ImageView getPrimaryIcon() {
return mPrimaryIcon;
}
+ /**
+ * Returns the title view within this view holder's view.
+ *
+ * @return Title view within this view holder's view.
+ */
@NonNull
public TextView getTitle() {
return mTitle;
}
+ /**
+ * Returns the body view within this view holder's view.
+ *
+ * @return Body view within this view holder's view.
+ */
@NonNull
public TextView getBody() {
return mBody;
}
+ /**
+ * Returns the compound button divider view within this view holder's view.
+ *
+ * @return Compound button divider view within this view holder's view.
+ */
@NonNull
- public RadioButton getRadioButton() {
- return mRadioButton;
+ public View getCompoundButtonDivider() {
+ return mCompoundButtonDivider;
+ }
+
+ /**
+ * Returns the compound button within this view holder's view.
+ *
+ * @return Compound button within this view holder's view.
+ */
+ @NonNull
+ public CompoundButton getCompoundButton() {
+ return mCompoundButton;
}
@NonNull
- public View getRadioButtonDivider() {
- return mRadioButtonDivider;
+ Guideline getSupplementalGuideline() {
+ return mSupplementalGuideline;
}
@NonNull
@@ -512,11 +212,14 @@
return mWidgetViews;
}
- @Override
- public void onUxRestrictionsChanged(
- androidx.car.uxrestrictions.CarUxRestrictions restrictionInfo) {
- CarUxRestrictionsUtils.apply(itemView.getContext(), restrictionInfo, getTitle());
- CarUxRestrictionsUtils.apply(itemView.getContext(), restrictionInfo, getBody());
+ /**
+ * Returns the container layout of this view holder.
+ *
+ * @return Container layout of this view holder.
+ */
+ @NonNull
+ public ViewGroup getContainerLayout() {
+ return mContainerLayout;
}
}
}
diff --git a/car/core/src/main/java/androidx/car/widget/SwitchListItem.java b/car/core/src/main/java/androidx/car/widget/SwitchListItem.java
index b461334..801486d 100644
--- a/car/core/src/main/java/androidx/car/widget/SwitchListItem.java
+++ b/car/core/src/main/java/androidx/car/widget/SwitchListItem.java
@@ -16,595 +16,85 @@
package androidx.car.widget;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
import android.view.View;
-import android.view.ViewGroup.MarginLayoutParams;
+import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.Switch;
import android.widget.TextView;
-import androidx.annotation.CallSuper;
-import androidx.annotation.DimenRes;
-import androidx.annotation.Dimension;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.car.R;
import androidx.car.util.CarUxRestrictionsUtils;
import androidx.car.uxrestrictions.CarUxRestrictions;
-import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.car.widget.ListItemAdapter.ListItemType;
import androidx.constraintlayout.widget.Guideline;
-import java.lang.annotation.Retention;
-import java.util.ArrayList;
-import java.util.List;
-
/**
* Class to build a list item with {@link Switch}.
*
- * <p>An item supports primary action and a switch as supplemental action.
- *
- * <p>An item visually composes of 3 parts; each part may contain multiple views.
+ * <p>A switch list item is visually composed of 5 parts.
* <ul>
- * <li>{@code Primary Action}: represented by an icon of following types.
- * <ul>
- * <li>Primary Icon - icon size could be large or small.
- * <li>No Icon - no icon is shown.
- * <li>Empty Icon - {@code Text} offsets start space as if there was an icon.
+ * <li>A {@link Switch}.
+ * <li>optional {@code Divider}.
+ * <li>optional {@code Primary Action Icon}.
+ * <li>optional {@code Title}.
+ * <li>optional {@code Body}.
* </ul>
- * <li>{@code Text}: supports any combination of the following text views.
- * <ul>
- * <li>Title
- * <li>Body
- * </ul>
- * <li>{@code Supplemental Action}: represented by {@link Switch}.
- * </ul>
- *
- * <p>{@code SwitchListItem} binds data to {@link ViewHolder} based on components selected.
- *
- * <p>When conflicting setter methods are called (e.g. setting primary action to both primary icon
- * and no icon), the last called method wins.
*/
-public class SwitchListItem extends ListItem<SwitchListItem.ViewHolder> {
-
- @Retention(SOURCE)
- @IntDef({
- PRIMARY_ACTION_ICON_SIZE_SMALL, PRIMARY_ACTION_ICON_SIZE_MEDIUM,
- PRIMARY_ACTION_ICON_SIZE_LARGE})
- private @interface PrimaryActionIconSize {
- }
-
- /**
- * Small sized icon is the mostly commonly used size. It's the same as supplemental action icon.
- */
- public static final int PRIMARY_ACTION_ICON_SIZE_SMALL = 0;
- /**
- * Medium sized icon is slightly bigger than {@code SMALL} ones. It is intended for profile
- * pictures (avatar), in which case caller is responsible for passing in a circular image.
- */
- public static final int PRIMARY_ACTION_ICON_SIZE_MEDIUM = 1;
- /**
- * Large sized icon is as tall as a list item with only {@code title} text. It is intended for
- * album art.
- */
- public static final int PRIMARY_ACTION_ICON_SIZE_LARGE = 2;
-
- @Retention(SOURCE)
- @IntDef({
- PRIMARY_ACTION_TYPE_NO_ICON, PRIMARY_ACTION_TYPE_EMPTY_ICON,
- PRIMARY_ACTION_TYPE_ICON})
- private @interface PrimaryActionType {
- }
-
- private static final int PRIMARY_ACTION_TYPE_NO_ICON = 0;
- private static final int PRIMARY_ACTION_TYPE_EMPTY_ICON = 1;
- private static final int PRIMARY_ACTION_TYPE_ICON = 2;
-
- private final Context mContext;
- private boolean mIsEnabled = true;
- private boolean mIsClickable;
-
- private final List<ViewBinder<ViewHolder>> mBinders = new ArrayList<>();
-
- @PrimaryActionType
- private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
- private Drawable mPrimaryActionIconDrawable;
- @PrimaryActionIconSize
- private int mPrimaryActionIconSize = PRIMARY_ACTION_ICON_SIZE_SMALL;
-
- private CharSequence mTitle;
- private CharSequence mBody;
-
- @Dimension
- private final int mSupplementalGuidelineBegin;
-
- private boolean mSwitchChecked;
- /**
- * {@code true} if the checked state of the switch has changed programmatically and
- * {@link #mSwitchOnCheckedChangeListener} needs to be notified.
- */
- private boolean mShouldNotifySwitchChecked;
- private boolean mShowSwitchDivider;
- private CompoundButton.OnCheckedChangeListener mSwitchOnCheckedChangeListener;
+public final class SwitchListItem extends CompoundButtonListItem<SwitchListItem.ViewHolder> {
/**
* Creates a {@link ViewHolder}.
+ *
+ * @return a {@link ViewHolder} for this {@link SwitchListItem}.
*/
@NonNull
- public static ViewHolder createViewHolder(View itemView) {
+ public static ViewHolder createViewHolder(@NonNull View itemView) {
return new ViewHolder(itemView);
}
- public SwitchListItem(@NonNull Context context) {
- mContext = context;
- mSupplementalGuidelineBegin = mContext.getResources().getDimensionPixelSize(
- R.dimen.car_list_item_supplemental_guideline_top);
- markDirty();
- }
-
/**
* Used by {@link ListItemAdapter} to choose layout to inflate for view holder.
+ *
+ * @return Type of this {@link CompoundButtonListItem}.
*/
+ @ListItemType
@Override
public int getViewType() {
return ListItemAdapter.LIST_ITEM_TYPE_SWITCH;
}
/**
- * Calculates the layout params for views in {@link ViewHolder}.
+ * Creates a {@link SwitchListItem} that will be used to display a list item with a
+ * {@link Switch}.
+ *
+ * @param context The context to be used by this {@link SwitchListItem}.
+ */
+ public SwitchListItem(@NonNull Context context) {
+ super(context);
+ }
+
+ /**
+ * Returns whether the compound button will be placed at the end of the list item layout. This
+ * value is used to determine start margins for the {@code Title} and {@code Body}.
+ *
+ * @return Whether compound button is placed at the end of the list item layout.
*/
@Override
- @CallSuper
- protected void resolveDirtyState() {
- mBinders.clear();
-
- // Create binders that adjust layout params of each view.
- setPrimaryAction();
- setText();
- setSwitch();
- setItemClickable();
+ public boolean isCompoundButtonPositionEnd() {
+ return true;
}
/**
- * Hides all views in {@link ViewHolder} then applies ViewBinders to adjust view layout params.
+ * ViewHolder that contains necessary widgets for {@link SwitchListItem}.
*/
- @Override
- public void onBind(ViewHolder viewHolder) {
- hideSubViews(viewHolder);
- for (ViewBinder binder : mBinders) {
- binder.bind(viewHolder);
- }
+ public static final class ViewHolder extends CompoundButtonListItem.ViewHolder {
- for (View v : viewHolder.getWidgetViews()) {
- v.setEnabled(mIsEnabled);
- }
- // SwitchListItem supports clicking on the item so we also update the entire itemView.
- viewHolder.itemView.setEnabled(mIsEnabled);
- }
+ private View[] mWidgetViews;
- @Override
- public void setEnabled(boolean enabled) {
- mIsEnabled = enabled;
- }
-
- /**
- * Sets whether the item is clickable. If {@code true}, clicking item toggles the switch.
- */
- public void setClickable(boolean isClickable) {
- mIsClickable = isClickable;
- markDirty();
- }
-
- /**
- * Sets {@code Primary Action} to be represented by an icon.
- *
- * @param drawable the Drawable to set.
- * @param size small/medium/large. Available as {@link #PRIMARY_ACTION_ICON_SIZE_SMALL},
- * {@link #PRIMARY_ACTION_ICON_SIZE_MEDIUM},
- * {@link #PRIMARY_ACTION_ICON_SIZE_LARGE}.
- */
- public void setPrimaryActionIcon(@NonNull Drawable drawable, @PrimaryActionIconSize int size) {
- mPrimaryActionType = PRIMARY_ACTION_TYPE_ICON;
- mPrimaryActionIconDrawable = drawable;
- mPrimaryActionIconSize = size;
- markDirty();
- }
-
- /**
- * Sets {@code Primary Action} to be represented by an icon.
- *
- * @param iconResId the resource identifier of the drawable.
- * @param size small/medium/large. Available as {@link #PRIMARY_ACTION_ICON_SIZE_SMALL},
- * {@link #PRIMARY_ACTION_ICON_SIZE_MEDIUM},
- * {@link #PRIMARY_ACTION_ICON_SIZE_LARGE}.
- */
- public void setPrimaryActionIcon(@DrawableRes int iconResId, @PrimaryActionIconSize int size) {
- setPrimaryActionIcon(getContext().getDrawable(iconResId), size);
- }
-
- /**
- * Sets {@code Primary Action} to be empty icon.
- *
- * <p>{@code Text} would have a start margin as if {@code Primary Action} were set to primary
- * icon.
- */
- public void setPrimaryActionEmptyIcon() {
- mPrimaryActionType = PRIMARY_ACTION_TYPE_EMPTY_ICON;
- markDirty();
- }
-
- /**
- * Sets {@code Primary Action} to have no icon. Text would align to the start of item.
- */
- public void setPrimaryActionNoIcon() {
- mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
- markDirty();
- }
-
- /**
- * Sets the title of item.
- *
- * <p>Primary text is {@code Title} by default. It can be set by
- * {@link #setBody(CharSequence)}
- *
- * <p>{@code Title} text is limited to one line, and ellipses at the end.
- *
- * @param title text to display as title.
- */
- public void setTitle(@Nullable CharSequence title) {
- mTitle = title;
- markDirty();
- }
-
- /**
- * Sets the body text of item.
- *
- * <p>Text beyond length required by regulation will be truncated.
- *
- * @param body text to be displayed.
- */
- public void setBody(@Nullable CharSequence body) {
- mBody = body;
- markDirty();
- }
-
- /**
- * Sets the state of {@code Switch}.
- *
- * @param isChecked sets the "checked/unchecked, namely on/off" state of switch.
- * @deprecated Use {@link #setChecked(boolean)} to set checked state of {@code Switch}.
- */
- @Deprecated
- public void setSwitchState(boolean isChecked) {
- setChecked(isChecked);
- }
-
- /**
- * Sets whether the switch is checked.
- *
- * @param isChecked {@code true} to check the switch or {@code false} to uncheck it.
- */
- public void setChecked(boolean isChecked) {
- if (mSwitchChecked == isChecked) {
- return;
- }
- mSwitchChecked = isChecked;
- mShouldNotifySwitchChecked = true;
- markDirty();
- }
-
- /**
- * Registers a callback to be invoked when the checked state of switch changes.
- *
- * @param listener callback to be invoked when the checked state shown in the UI changes.
- */
- public void setSwitchOnCheckedChangeListener(
- @Nullable CompoundButton.OnCheckedChangeListener listener) {
- mSwitchOnCheckedChangeListener = listener;
- // This method invalidates previous listener. Reset so that we *only*
- // notify when the checked state changes and not on the initial bind.
- mShouldNotifySwitchChecked = false;
- markDirty();
- }
-
- /**
- * Sets whether to display a vertical bar between switch and text.
- */
- public void setShowSwitchDivider(boolean showSwitchDivider) {
- mShowSwitchDivider = showSwitchDivider;
- markDirty();
- }
-
- @NonNull
- protected final Context getContext() {
- return mContext;
- }
-
- private void hideSubViews(ViewHolder vh) {
- for (View v : vh.getWidgetViews()) {
- v.setVisibility(View.GONE);
- }
- }
-
- private void setPrimaryAction() {
- setPrimaryIconContent();
- setPrimaryIconLayout();
- }
-
- private void setText() {
- setTextContent();
- setTextVerticalMargin();
- setTextStartMargin();
- setTextEndMargin();
- }
-
- private void setPrimaryIconContent() {
- switch (mPrimaryActionType) {
- case PRIMARY_ACTION_TYPE_ICON:
- mBinders.add(vh -> {
- vh.getPrimaryIcon().setVisibility(View.VISIBLE);
- vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
- });
- break;
- case PRIMARY_ACTION_TYPE_EMPTY_ICON:
- case PRIMARY_ACTION_TYPE_NO_ICON:
- // Do nothing.
- break;
- default:
- throw new IllegalStateException("Unknown primary action type.");
- }
- }
-
- /**
- * Sets the size, start margin, and vertical position of primary icon.
- *
- * <p>Large icon will have no start margin, and always align center vertically.
- *
- * <p>Small/medium icon will have start margin, and uses a top margin such that it is "pinned"
- * at the same position in list item regardless of item height.
- */
- private void setPrimaryIconLayout() {
- if (mPrimaryActionType == PRIMARY_ACTION_TYPE_EMPTY_ICON
- || mPrimaryActionType == PRIMARY_ACTION_TYPE_NO_ICON) {
- return;
- }
-
- // Size of icon.
- @DimenRes int sizeResId;
- switch (mPrimaryActionIconSize) {
- case PRIMARY_ACTION_ICON_SIZE_SMALL:
- sizeResId = R.dimen.car_primary_icon_size;
- break;
- case PRIMARY_ACTION_ICON_SIZE_MEDIUM:
- sizeResId = R.dimen.car_avatar_icon_size;
- break;
- case PRIMARY_ACTION_ICON_SIZE_LARGE:
- sizeResId = R.dimen.car_single_line_list_item_height;
- break;
- default:
- throw new IllegalStateException("Unknown primary action icon size.");
- }
-
- int iconSize = mContext.getResources().getDimensionPixelSize(sizeResId);
-
- // Start margin of icon.
- int startMargin;
- switch (mPrimaryActionIconSize) {
- case PRIMARY_ACTION_ICON_SIZE_SMALL:
- case PRIMARY_ACTION_ICON_SIZE_MEDIUM:
- startMargin = mContext.getResources().getDimensionPixelSize(R.dimen.car_keyline_1);
- break;
- case PRIMARY_ACTION_ICON_SIZE_LARGE:
- startMargin = 0;
- break;
- default:
- throw new IllegalStateException("Unknown primary action icon size.");
- }
-
- mBinders.add(vh -> {
- ConstraintLayout.LayoutParams layoutParams =
- (ConstraintLayout.LayoutParams) vh.getPrimaryIcon().getLayoutParams();
- layoutParams.height = layoutParams.width = iconSize;
- layoutParams.setMarginStart(startMargin);
-
- if (mPrimaryActionIconSize == PRIMARY_ACTION_ICON_SIZE_LARGE) {
- // A large icon is always vertically centered.
- layoutParams.verticalBias = 0.5f;
- layoutParams.topMargin = 0;
- } else {
- // Align the icon to the top of the parent. This allows the topMargin to shift it
- // down relative to the top.
- layoutParams.verticalBias = 0f;
-
- // For all other icon sizes, the icon should be centered within the height of
- // car_double_line_list_item_height. Note: the actual height of the item can be
- // larger than this.
- int itemHeight = mContext.getResources().getDimensionPixelSize(
- R.dimen.car_double_line_list_item_height);
- layoutParams.topMargin = (itemHeight - iconSize) / 2;
- }
-
- vh.getPrimaryIcon().requestLayout();
- });
- }
-
- private void setTextContent() {
- boolean hasTitle = !TextUtils.isEmpty(mTitle);
- boolean hasBody = !TextUtils.isEmpty(mBody);
-
- if (!hasTitle && !hasBody) {
- return;
- }
-
- mBinders.add(vh -> {
- if (hasTitle) {
- vh.getTitle().setVisibility(View.VISIBLE);
- vh.getTitle().setText(mTitle);
- }
-
- if (hasBody) {
- vh.getBody().setVisibility(View.VISIBLE);
- vh.getBody().setText(mBody);
- }
-
- if (hasTitle && !hasBody) {
- // If only title, then center the supplemental actions.
- vh.getSupplementalGuideline().setGuidelineBegin(
- ConstraintLayout.LayoutParams.UNSET);
- vh.getSupplementalGuideline().setGuidelinePercent(0.5f);
- } else {
- // Otherwise, position it a fixed distance from the top.
- vh.getSupplementalGuideline().setGuidelinePercent(
- ConstraintLayout.LayoutParams.UNSET);
- vh.getSupplementalGuideline().setGuidelineBegin(
- mSupplementalGuidelineBegin);
- }
- });
- }
-
- /**
- * Sets start margin of text view depending on icon type.
- */
- private void setTextStartMargin() {
- @DimenRes int startMarginResId;
- switch (mPrimaryActionType) {
- case PRIMARY_ACTION_TYPE_NO_ICON:
- startMarginResId = R.dimen.car_keyline_1;
- break;
- case PRIMARY_ACTION_TYPE_EMPTY_ICON:
- startMarginResId = R.dimen.car_keyline_3;
- break;
- case PRIMARY_ACTION_TYPE_ICON:
- startMarginResId = mPrimaryActionIconSize == PRIMARY_ACTION_ICON_SIZE_LARGE
- ? R.dimen.car_keyline_4
- : R.dimen.car_keyline_3; // Small and medium sized icon.
- break;
- default:
- throw new IllegalStateException("Unknown primary action type.");
- }
- int startMargin = mContext.getResources().getDimensionPixelSize(startMarginResId);
- mBinders.add(vh -> {
- MarginLayoutParams titleLayoutParams =
- (MarginLayoutParams) vh.getTitle().getLayoutParams();
- titleLayoutParams.setMarginStart(startMargin);
- vh.getTitle().requestLayout();
-
- MarginLayoutParams bodyLayoutParams =
- (MarginLayoutParams) vh.getBody().getLayoutParams();
- bodyLayoutParams.setMarginStart(startMargin);
- vh.getBody().requestLayout();
- });
- }
-
- private void setTextEndMargin() {
- int endMargin = mContext.getResources().getDimensionPixelSize(R.dimen.car_padding_4);
-
- mBinders.add(vh -> {
- MarginLayoutParams titleLayoutParams =
- (MarginLayoutParams) vh.getTitle().getLayoutParams();
- titleLayoutParams.setMarginEnd(endMargin);
-
- MarginLayoutParams bodyLayoutParams =
- (MarginLayoutParams) vh.getBody().getLayoutParams();
- bodyLayoutParams.setMarginEnd(endMargin);
- });
- }
-
- /**
- * Sets top/bottom margins of {@code Title} and {@code Body}.
- */
- private void setTextVerticalMargin() {
- // Set all relevant fields in layout params to avoid carried over params when the item
- // gets bound to a recycled view holder.
- if (!TextUtils.isEmpty(mTitle) && TextUtils.isEmpty(mBody)) {
- // Title only - view is aligned center vertically by itself.
- mBinders.add(vh -> {
- MarginLayoutParams layoutParams =
- (MarginLayoutParams) vh.getTitle().getLayoutParams();
- layoutParams.topMargin = 0;
- vh.getTitle().requestLayout();
- });
- } else if (TextUtils.isEmpty(mTitle) && !TextUtils.isEmpty(mBody)) {
- mBinders.add(vh -> {
- // Body uses top and bottom margin.
- int margin = mContext.getResources().getDimensionPixelSize(
- R.dimen.car_padding_3);
- MarginLayoutParams layoutParams =
- (MarginLayoutParams) vh.getBody().getLayoutParams();
- layoutParams.topMargin = margin;
- layoutParams.bottomMargin = margin;
- vh.getBody().requestLayout();
- });
- } else {
- mBinders.add(vh -> {
- Resources resources = mContext.getResources();
- int padding2 = resources.getDimensionPixelSize(R.dimen.car_padding_2);
-
- // Title has a top margin
- MarginLayoutParams titleLayoutParams =
- (MarginLayoutParams) vh.getTitle().getLayoutParams();
- titleLayoutParams.topMargin = padding2;
- vh.getTitle().requestLayout();
-
- // Body is below title with no margin and has bottom margin.
- MarginLayoutParams bodyLayoutParams =
- (MarginLayoutParams) vh.getBody().getLayoutParams();
- bodyLayoutParams.topMargin = 0;
- bodyLayoutParams.bottomMargin = padding2;
- vh.getBody().requestLayout();
- });
- }
- }
-
- /**
- * Sets up view(s) for supplemental action.
- */
- private void setSwitch() {
- mBinders.add(vh -> {
- vh.getSwitch().setVisibility(View.VISIBLE);
- vh.getSwitch().setOnCheckedChangeListener(null);
- vh.getSwitch().setChecked(mSwitchChecked);
- vh.getSwitch().setOnCheckedChangeListener((buttonView, isChecked) -> {
- if (mSwitchOnCheckedChangeListener != null) {
- // The checked state changed via user interaction with the switch.
- mSwitchOnCheckedChangeListener.onCheckedChanged(buttonView, isChecked);
- }
- mSwitchChecked = isChecked;
- });
- if (mShouldNotifySwitchChecked && mSwitchOnCheckedChangeListener != null) {
- // The checked state was changed programmatically.
- mSwitchOnCheckedChangeListener.onCheckedChanged(vh.getSwitch(),
- mSwitchChecked);
- mShouldNotifySwitchChecked = false;
- }
-
- if (mShowSwitchDivider) {
- vh.getSwitchDivider().setVisibility(View.VISIBLE);
- }
- });
- }
-
- private void setItemClickable() {
- mBinders.add(vh -> {
- // If applicable (namely item is clickable), clicking item always toggles the switch.
- vh.itemView.setOnClickListener(v -> vh.getSwitch().toggle());
- vh.itemView.setClickable(mIsClickable);
- });
- }
-
- /**
- * Holds views of SwitchListItem.
- */
- public static final class ViewHolder extends ListItem.ViewHolder {
-
- private final View[] mWidgetViews;
+ private ViewGroup mContainerLayout;
private ImageView mPrimaryIcon;
@@ -613,15 +103,19 @@
private Guideline mSupplementalGuideline;
- private Switch mSwitch;
- private View mSwitchDivider;
+ private CompoundButton mCompoundButton;
+ private View mCompoundButtonDivider;
/**
- * ViewHolder that contains necessary widgets for {@link SwitchListItem}.
+ * Creates a {@link ViewHolder} for a {@link SwitchListItem}.
+ *
+ * @param itemView The view to be used to display a {@link SwitchListItem}.
*/
public ViewHolder(@NonNull View itemView) {
super(itemView);
+ mContainerLayout = itemView.findViewById(R.id.container);
+
mPrimaryIcon = itemView.findViewById(R.id.primary_icon);
mTitle = itemView.findViewById(R.id.title);
@@ -629,19 +123,19 @@
mSupplementalGuideline = itemView.findViewById(R.id.supplemental_actions_guideline);
- mSwitch = itemView.findViewById(R.id.switch_widget);
- mSwitchDivider = itemView.findViewById(R.id.switch_divider);
+ mCompoundButton = itemView.findViewById(R.id.switch_widget);
+ mCompoundButtonDivider = itemView.findViewById(R.id.switch_divider);
int minTouchSize = itemView.getContext().getResources()
.getDimensionPixelSize(R.dimen.car_touch_target_size);
- MinTouchTargetHelper.ensureThat(mSwitch).hasMinTouchSize(minTouchSize);
+ MinTouchTargetHelper.ensureThat(mCompoundButton).hasMinTouchSize(minTouchSize);
// Each line groups relevant child views in an effort to help keep this view array
// updated with actual child views in the ViewHolder.
mWidgetViews = new View[]{
mPrimaryIcon,
mTitle, mBody,
- mSwitch, mSwitchDivider,
+ mCompoundButton, mCompoundButtonDivider,
};
}
@@ -653,43 +147,86 @@
* @param restrictionsInfo current car UX restrictions.
*/
@Override
- public void onUxRestrictionsChanged(CarUxRestrictions restrictionsInfo) {
+ public void onUxRestrictionsChanged(@NonNull CarUxRestrictions restrictionsInfo) {
CarUxRestrictionsUtils.apply(itemView.getContext(), restrictionsInfo, getBody());
}
+ /**
+ * Returns the primary icon view within this view holder's view.
+ *
+ * @return Icon view within this view holder's view.
+ */
@NonNull
+ @Override
public ImageView getPrimaryIcon() {
return mPrimaryIcon;
}
+ /**
+ * Returns the title view within this view holder's view.
+ *
+ * @return Title view within this view holder's view.
+ */
@NonNull
+ @Override
public TextView getTitle() {
return mTitle;
}
+ /**
+ * Returns the body view within this view holder's view.
+ *
+ * @return Body view within this view holder's view.
+ */
@NonNull
+ @Override
public TextView getBody() {
return mBody;
}
+ /**
+ * Returns the compound button divider view within this view holder's view.
+ *
+ * @return Compound button divider view within this view holder's view.
+ */
@NonNull
- public View getSwitchDivider() {
- return mSwitchDivider;
+ @Override
+ public View getCompoundButtonDivider() {
+ return mCompoundButtonDivider;
+ }
+
+ /**
+ * Returns the compound button within this view holder's view.
+ *
+ * @return Compound button within this view holder's view.
+ */
+ @NonNull
+ @Override
+ public CompoundButton getCompoundButton() {
+ return mCompoundButton;
}
@NonNull
- public Switch getSwitch() {
- return mSwitch;
- }
-
- @NonNull
+ @Override
Guideline getSupplementalGuideline() {
return mSupplementalGuideline;
}
@NonNull
+ @Override
View[] getWidgetViews() {
return mWidgetViews;
}
+
+ /**
+ * Returns the container layout of this view holder.
+ *
+ * @return Container layout of this view holder.
+ */
+ @NonNull
+ @Override
+ public ViewGroup getContainerLayout() {
+ return mContainerLayout;
+ }
}
}
diff --git a/jetifier/jetifier/migration.config b/jetifier/jetifier/migration.config
index e0b1250..6aa7518 100644
--- a/jetifier/jetifier/migration.config
+++ b/jetifier/jetifier/migration.config
@@ -292,22 +292,6 @@
"to": "android/support/v4/media/{0}"
},
{
- "from": "android/support/v4/widget/CursorAdapter(.*)",
- "to": "androidx/cursoradapter/widget/CursorAdapter{0}"
- },
- {
- "from": "android/support/v4/widget/CursorFilter(.*)",
- "to": "androidx/cursoradapter/widget/CursorFilter{0}"
- },
- {
- "from": "android/support/v4/widget/ResourceCursorAdapter(.*)",
- "to": "androidx/cursoradapter/widget/ResourceCursorAdapter{0}"
- },
- {
- "from": "android/support/v4/widget/SimpleCursorAdapter(.*)",
- "to": "androidx/cursoradapter/widget/SimpleCursorAdapter{0}"
- },
- {
"from": "android/support/v4/app/LoaderManager(.*)",
"to": "androidx/loader/app/LoaderManager{0}"
},
@@ -642,6 +626,22 @@
"to": "ignore"
},
{
+ "from": "androidx/cursoradapter/widget/CursorAdapter(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/cursoradapter/widget/CursorFilter(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/cursoradapter/widget/ResourceCursorAdapter(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/cursoradapter/widget/SimpleCursorAdapter(.*)",
+ "to": "ignore"
+ },
+ {
"from": "androidx/textclassifier/(.*)",
"to": "ignore"
},
@@ -1016,7 +1016,7 @@
"to": "androidx/loader"
},
{
- "from": "android/support/cursoradapter",
+ "from": "androidx/cursoradapter",
"to": "androidx/cursoradapter"
},
{
@@ -1991,9 +1991,9 @@
},
{
"from": {
- "groupId": "com.android.support",
+ "groupId": "androidx.cursoradapter",
"artifactId": "cursoradapter",
- "version": "{oldSlVersion}"
+ "version": "{newSlVersion}"
},
"to": {
"groupId": "androidx.cursoradapter",
@@ -4240,8 +4240,6 @@
"android/support/v4/widget/AutoSizeableTextView": "androidx/core/widget/AutoSizeableTextView",
"android/support/v4/widget/CompoundButtonCompat": "androidx/core/widget/CompoundButtonCompat",
"android/support/v4/widget/ContentLoadingProgressBar": "androidx/core/widget/ContentLoadingProgressBar",
- "android/support/v4/widget/CursorAdapter": "androidx/cursoradapter/widget/CursorAdapter",
- "android/support/v4/widget/CursorFilter": "androidx/cursoradapter/widget/CursorFilter",
"android/support/v4/widget/DirectedAcyclicGraph": "androidx/coordinatorlayout/widget/DirectedAcyclicGraph",
"android/support/v4/widget/DrawerLayout": "androidx/drawerlayout/widget/DrawerLayout",
"android/support/v4/widget/EdgeEffectCompat": "androidx/core/widget/EdgeEffectCompat",
@@ -4254,9 +4252,7 @@
"android/support/v4/widget/NestedScrollView": "androidx/core/widget/NestedScrollView",
"android/support/v4/widget/PopupMenuCompat": "androidx/core/widget/PopupMenuCompat",
"android/support/v4/widget/PopupWindowCompat": "androidx/core/widget/PopupWindowCompat",
- "android/support/v4/widget/ResourceCursorAdapter": "androidx/cursoradapter/widget/ResourceCursorAdapter",
"android/support/v4/widget/ScrollerCompat": "androidx/core/widget/ScrollerCompat",
- "android/support/v4/widget/SimpleCursorAdapter": "androidx/cursoradapter/widget/SimpleCursorAdapter",
"android/support/v4/widget/TextViewCompat": "androidx/core/widget/TextViewCompat",
"android/support/v4/widget/TintableCompoundButton": "androidx/core/widget/TintableCompoundButton",
"android/support/v4/widget/TintableImageSourceView": "androidx/core/widget/TintableImageSourceView",
diff --git a/jetifier/jetifier/standalone/build.gradle b/jetifier/jetifier/standalone/build.gradle
index 83d059d..126f449 100644
--- a/jetifier/jetifier/standalone/build.gradle
+++ b/jetifier/jetifier/standalone/build.gradle
@@ -34,5 +34,4 @@
destinationDir BuildServerConfigurationKt.getDistributionDirectory(rootProject)
}
-rootProject.tasks["createArchive"].dependsOn(dist)
diff --git a/media2/player/src/androidTest/java/androidx/media2/player/MediaPlayerTest.java b/media2/player/src/androidTest/java/androidx/media2/player/MediaPlayerTest.java
index 89e2ebc..de69fc9 100644
--- a/media2/player/src/androidTest/java/androidx/media2/player/MediaPlayerTest.java
+++ b/media2/player/src/androidTest/java/androidx/media2/player/MediaPlayerTest.java
@@ -25,6 +25,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -1133,6 +1134,23 @@
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+ public void testSetPlaybackSpeedWithIllegalArguments() throws Throwable {
+ // Zero is not allowed.
+ ListenableFuture<PlayerResult> future = mPlayer.setPlaybackSpeed(0.0f);
+ PlayerResult result = future.get();
+ assertNotNull(result);
+ assertEquals(RESULT_ERROR_BAD_VALUE, result.getResultCode());
+
+ // Negative values are not allowed.
+ future = mPlayer.setPlaybackSpeed(-1.0f);
+ result = future.get();
+ assertNotNull(result);
+ assertEquals(RESULT_ERROR_BAD_VALUE, result.getResultCode());
+ }
+
+ @Test
+ @LargeTest
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void testClose() throws Exception {
assertTrue(loadResource(R.raw.testmp3_2));
AudioAttributesCompat attributes = new AudioAttributesCompat.Builder()
diff --git a/media2/player/src/main/java/androidx/media2/player/MediaPlayer.java b/media2/player/src/main/java/androidx/media2/player/MediaPlayer.java
index de7cc0a..20ac028 100644
--- a/media2/player/src/main/java/androidx/media2/player/MediaPlayer.java
+++ b/media2/player/src/main/java/androidx/media2/player/MediaPlayer.java
@@ -823,6 +823,9 @@
PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) {
@Override
List<ResolvableFuture<PlayerResult>> onExecute() {
+ if (playbackSpeed <= 0.0f) {
+ return createFuturesForResultCode(RESULT_ERROR_BAD_VALUE);
+ }
ArrayList<ResolvableFuture<PlayerResult>> futures = new ArrayList<>();
ResolvableFuture<PlayerResult> future = ResolvableFuture.create();
synchronized (mPendingCommands) {
diff --git a/samples/SupportCarDemos/src/main/AndroidManifest.xml b/samples/SupportCarDemos/src/main/AndroidManifest.xml
index 7a205fc..34c0422 100644
--- a/samples/SupportCarDemos/src/main/AndroidManifest.xml
+++ b/samples/SupportCarDemos/src/main/AndroidManifest.xml
@@ -270,5 +270,13 @@
<category android:name="android.intent.category.SAMPLE_CODE"/>
</intent-filter>
</activity>
+ <activity android:name=".CheckBoxListItemActivity"
+ android:label="CheckBoxListItem Demo"
+ android:parentActivityName=".SupportCarDemoActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.SAMPLE_CODE"/>
+ </intent-filter>
+ </activity>
</application>
</manifest>
diff --git a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CheckBoxListItemActivity.java b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CheckBoxListItemActivity.java
new file mode 100644
index 0000000..2a1bed5
--- /dev/null
+++ b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CheckBoxListItemActivity.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.androidx.car;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.widget.CompoundButton;
+import android.widget.Toast;
+
+import androidx.car.widget.CarToolbar;
+import androidx.car.widget.CheckBoxListItem;
+import androidx.car.widget.ListItem;
+import androidx.car.widget.ListItemAdapter;
+import androidx.car.widget.ListItemProvider;
+import androidx.car.widget.PagedListView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Demo activity for {@link androidx.car.widget.CheckBoxListItem}.
+ */
+public class CheckBoxListItemActivity extends Activity {
+
+ private PagedListView mPagedListView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_paged_list_view);
+
+ CarToolbar toolbar = findViewById(R.id.car_toolbar);
+ toolbar.setTitle(R.string.checkbox_list_item_title);
+ toolbar.setNavigationIconOnClickListener(v -> finish());
+
+ mPagedListView = findViewById(R.id.paged_list_view);
+
+ SampleProvider provider = new SampleProvider(this);
+ ListItemAdapter adapter = new ListItemAdapter(this, provider);
+
+ mPagedListView.setAdapter(adapter);
+ mPagedListView.setMaxPages(PagedListView.UNLIMITED_PAGES);
+
+ CheckBoxListItem item = new CheckBoxListItem(this);
+ item.setTitle("Clicking me to set checked state of item above");
+ item.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ int size = adapter.getItemCount();
+ // -2 to get second to last item (the one above).
+ ((CheckBoxListItem) provider.mItems.get(size - 2)).setChecked(isChecked);
+ adapter.notifyDataSetChanged();
+ });
+ provider.mItems.add(item);
+ }
+
+ private static class SampleProvider extends ListItemProvider {
+ private Context mContext;
+
+ private final List<ListItem> mItems = new ArrayList<>();
+ private final ListItemProvider.ListProvider mListProvider =
+ new ListItemProvider.ListProvider(mItems);
+ private final CompoundButton.OnCheckedChangeListener mListener = (button, isChecked) ->
+ Toast.makeText(mContext,
+ "Checkbox is " + (isChecked ? "checked" : "unchecked"),
+ Toast.LENGTH_SHORT).show();
+
+ SampleProvider(Context context) {
+ mContext = context;
+
+ String longText = mContext.getString(R.string.long_text);
+
+ CheckBoxListItem item;
+
+ item = new CheckBoxListItem(mContext);
+ item.setTitle("Title - show divider");
+ item.setShowCompoundButtonDivider(true);
+ item.setOnCheckedChangeListener(mListener);
+ mItems.add(item);
+
+ item = new CheckBoxListItem(mContext);
+ item.setBody("Body text");
+ item.setOnCheckedChangeListener(mListener);
+ mItems.add(item);
+
+ item = new CheckBoxListItem(mContext);
+ item.setTitle("Long body text");
+ item.setBody(longText);
+ item.setOnCheckedChangeListener(mListener);
+ mItems.add(item);
+
+ item = new CheckBoxListItem(mContext);
+ item.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon,
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
+ item.setTitle("CheckBox with Icon");
+ item.setBody(longText);
+ item.setOnCheckedChangeListener(mListener);
+ mItems.add(item);
+
+ item = new CheckBoxListItem(mContext);
+ item.setTitle("CheckBox with Drawable");
+ item.setPrimaryActionIcon(
+ mContext.getDrawable(android.R.drawable.sym_def_app_icon),
+ CheckBoxListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
+ item.setBody(longText);
+ item.setOnCheckedChangeListener(mListener);
+ mItems.add(item);
+
+ item = new CheckBoxListItem(mContext);
+ item.setTitle("Clicking item toggles checkbox");
+ item.setClickable(true);
+ item.setOnCheckedChangeListener(mListener);
+ mItems.add(item);
+
+ item = new CheckBoxListItem(mContext);
+ item.setTitle("Disabled item");
+ item.setEnabled(false);
+ item.setOnCheckedChangeListener(mListener);
+ mItems.add(item);
+ }
+
+ @Override
+ public ListItem get(int position) {
+ return mListProvider.get(position);
+ }
+
+ @Override
+ public int size() {
+ return mListProvider.size();
+ }
+ }
+}
+
diff --git a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/RadioButtonListItemActivity.java b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/RadioButtonListItemActivity.java
index 2b9f396..69e5f89 100644
--- a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/RadioButtonListItemActivity.java
+++ b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/RadioButtonListItemActivity.java
@@ -21,6 +21,7 @@
import android.os.Bundle;
import androidx.car.app.CarListDialog;
+import androidx.car.widget.CarToolbar;
import androidx.car.widget.ListItem;
import androidx.car.widget.ListItemAdapter;
import androidx.car.widget.ListItemProvider;
@@ -44,6 +45,9 @@
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_paged_list_view);
+ CarToolbar toolbar = findViewById(R.id.car_toolbar);
+ toolbar.setTitle(R.string.radio_button_list_item_title);
+ toolbar.setNavigationIconOnClickListener(v -> finish());
mPagedListView = findViewById(R.id.paged_list_view);
RadioButtonSelectionAdapter adapter = new RadioButtonSelectionAdapter(
@@ -67,7 +71,6 @@
item = new RadioButtonListItem(this);
item.setPrimaryActionNoIcon();
- item.setTextStartMargin(R.dimen.car_keyline_3);
item.setTitle("No icon");
items.add(item);
@@ -75,7 +78,7 @@
item.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon,
RadioButtonListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
item.setTitle("Small icon - with action divider");
- item.setShowRadioButtonDivider(true);
+ item.setShowCompoundButtonDivider(true);
items.add(item);
item = new RadioButtonListItem(this);
@@ -83,7 +86,7 @@
RadioButtonListItem.PRIMARY_ACTION_ICON_SIZE_MEDIUM);
item.setTitle("Medium icon - with body text");
item.setBody("Sample body text");
- item.setShowRadioButtonDivider(true);
+ item.setShowCompoundButtonDivider(true);
items.add(item);
item = new RadioButtonListItem(this);
@@ -162,12 +165,12 @@
RadioButtonListItem.ViewHolder viewHolder = (RadioButtonListItem.ViewHolder) vh;
- viewHolder.getRadioButton().setChecked(mSelectionController.isChecked(position));
- viewHolder.getRadioButton().setOnCheckedChangeListener((buttonView, isChecked) -> {
+ viewHolder.getCompoundButton().setChecked(mSelectionController.isChecked(position));
+ viewHolder.getCompoundButton().setOnCheckedChangeListener((buttonView, isChecked) -> {
mSelectionController.setChecked(position);
// Refresh other radio button list items.
notifyDataSetChanged();
});
}
}
-}
+}
\ No newline at end of file
diff --git a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SwitchListItemActivity.java b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SwitchListItemActivity.java
index 63e9eb5..3dacd22 100644
--- a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SwitchListItemActivity.java
+++ b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SwitchListItemActivity.java
@@ -58,7 +58,7 @@
SwitchListItem item = new SwitchListItem(this);
item.setTitle("Clicking me to set checked state of item above");
- item.setSwitchOnCheckedChangeListener((buttonView, isChecked) -> {
+ item.setOnCheckedChangeListener((buttonView, isChecked) -> {
int size = adapter.getItemCount();
// -2 to get second to last item (the one above).
((SwitchListItem) provider.mItems.get(size - 2)).setChecked(isChecked);
@@ -87,19 +87,19 @@
item = new SwitchListItem(mContext);
item.setTitle("Title - show divider");
- item.setShowSwitchDivider(true);
- item.setSwitchOnCheckedChangeListener(mListener);
+ item.setShowCompoundButtonDivider(true);
+ item.setOnCheckedChangeListener(mListener);
mItems.add(item);
item = new SwitchListItem(mContext);
item.setBody("Body text");
- item.setSwitchOnCheckedChangeListener(mListener);
+ item.setOnCheckedChangeListener(mListener);
mItems.add(item);
item = new SwitchListItem(mContext);
item.setTitle("Long body text");
item.setBody(longText);
- item.setSwitchOnCheckedChangeListener(mListener);
+ item.setOnCheckedChangeListener(mListener);
mItems.add(item);
item = new SwitchListItem(mContext);
@@ -107,7 +107,7 @@
SwitchListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
item.setTitle("Switch with Icon");
item.setBody(longText);
- item.setSwitchOnCheckedChangeListener(mListener);
+ item.setOnCheckedChangeListener(mListener);
mItems.add(item);
item = new SwitchListItem(mContext);
@@ -116,19 +116,19 @@
mContext.getDrawable(android.R.drawable.sym_def_app_icon),
SwitchListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
item.setBody(longText);
- item.setSwitchOnCheckedChangeListener(mListener);
+ item.setOnCheckedChangeListener(mListener);
mItems.add(item);
item = new SwitchListItem(mContext);
item.setTitle("Clicking item toggles switch");
item.setClickable(true);
- item.setSwitchOnCheckedChangeListener(mListener);
+ item.setOnCheckedChangeListener(mListener);
mItems.add(item);
item = new SwitchListItem(mContext);
item.setTitle("Disabled item");
item.setEnabled(false);
- item.setSwitchOnCheckedChangeListener(mListener);
+ item.setOnCheckedChangeListener(mListener);
mItems.add(item);
}
diff --git a/samples/SupportCarDemos/src/main/res/values/strings.xml b/samples/SupportCarDemos/src/main/res/values/strings.xml
index d75f7c5..fd26a43 100644
--- a/samples/SupportCarDemos/src/main/res/values/strings.xml
+++ b/samples/SupportCarDemos/src/main/res/values/strings.xml
@@ -22,7 +22,9 @@
<string name="paged_list_view_shrink_title">PagedListViewShrinkDemo</string>
<string name="text_list_item_title">TextListItem</string>
<string name="seekbar_list_item_title">SeekbarListItem</string>
+ <string name="radio_button_list_item_title">RadioButtonListItem</string>
<string name="switch_list_item_title">SwitchListItem</string>
+ <string name="checkbox_list_item_title">CheckBoxListItem</string>
<string name="sub_header_list_item_title">SubheaderListItem</string>
<string name="alpha_jump_title">Alpha Jump Demo</string>
<string name="single_selection_dialog_title">CarSingleChoiceDialog Demo</string>
diff --git a/security/crypto/api/1.0.0-alpha01.txt b/security/crypto/api/1.0.0-alpha01.txt
index 261883a..44f0de6 100644
--- a/security/crypto/api/1.0.0-alpha01.txt
+++ b/security/crypto/api/1.0.0-alpha01.txt
@@ -1,295 +1,49 @@
// Signature format: 3.0
-package androidx.security {
-
- public class SecureConfig {
- method public String getAndroidCAStore();
- method public String getAndroidKeyStore();
- method public String getAsymmetricBlockModes();
- method public String getAsymmetricCipherTransformation();
- method public String getAsymmetricKeyPairAlgorithm();
- method public int getAsymmetricKeyPurposes();
- method public int getAsymmetricKeySize();
- method public String getAsymmetricPaddings();
- method public boolean getAsymmetricRequireUserAuthEnabled();
- method public int getAsymmetricRequireUserValiditySeconds();
- method public boolean getAsymmetricSensitiveDataProtectionEnabled();
- method public androidx.security.biometric.BiometricKeyAuthCallback getBiometricKeyAuthCallback();
- method public String getCertPath();
- method public String getCertPathValidator();
- method public String[] getClientCertAlgorithms();
- method public static androidx.security.SecureConfig getDefault();
- method public String getKeystoreType();
- method public static androidx.security.SecureConfig getNiapConfig();
- method public static androidx.security.SecureConfig getNiapConfig(androidx.security.biometric.BiometricKeyAuthCallback);
- method public String getSignatureAlgorithm();
- method public String[] getStrongSSLCiphers();
- method public String getSymmetricBlockModes();
- method public String getSymmetricCipherTransformation();
- method public int getSymmetricGcmTagLength();
- method public String getSymmetricKeyAlgorithm();
- method public int getSymmetricKeyPurposes();
- method public int getSymmetricKeySize();
- method public String getSymmetricPaddings();
- method public boolean getSymmetricRequireUserAuthEnabled();
- method public int getSymmetricRequireUserValiditySeconds();
- method public boolean getSymmetricSensitiveDataProtectionEnabled();
- method public androidx.security.config.TrustAnchorOptions getTrustAnchorOptions();
- method public boolean getUseStrongSSLCiphers();
- method public boolean getUseStrongSSLCiphersEnabled();
- method public void setAndroidCAStore(String);
- method public void setAndroidKeyStore(String);
- method public void setAsymmetricBlockModes(String);
- method public void setAsymmetricCipherTransformation(String);
- method public void setAsymmetricKeyPairAlgorithm(String);
- method public void setAsymmetricKeyPurposes(int);
- method public void setAsymmetricKeySize(int);
- method public void setAsymmetricPaddings(String);
- method public void setAsymmetricRequireUserAuth(boolean);
- method public void setAsymmetricRequireUserValiditySeconds(int);
- method public void setAsymmetricSensitiveDataProtection(boolean);
- method public void setBiometricKeyAuthCallback(androidx.security.biometric.BiometricKeyAuthCallback);
- method public void setCertPath(String);
- method public void setCertPathValidator(String);
- method public void setClientCertAlgorithms(String[]);
- method public void setKeystoreType(String);
- method public void setSignatureAlgorithm(String);
- method public void setStrongSSLCiphers(String[]);
- method public void setSymmetricBlockModes(String);
- method public void setSymmetricCipherTransformation(String);
- method public void setSymmetricGcmTagLength(int);
- method public void setSymmetricKeyAlgorithm(String);
- method public void setSymmetricKeyPurposes(int);
- method public void setSymmetricKeySize(int);
- method public void setSymmetricPaddings(String);
- method public void setSymmetricRequireUserAuth(boolean);
- method public void setSymmetricRequireUserValiditySeconds(int);
- method public void setSymmetricSensitiveDataProtection(boolean);
- method public void setTrustAnchorOptions(androidx.security.config.TrustAnchorOptions);
- method public void setUseStrongSSLCiphers(boolean);
- field public static final int AES_IV_SIZE_BYTES = 16; // 0x10
- field public static final String ANDROID_CA_STORE = "AndroidCAStore";
- field public static final String ANDROID_KEYSTORE = "AndroidKeyStore";
- field public static final String SSL_TLS = "TLS";
- }
-
- public static class SecureConfig.Builder {
- ctor public SecureConfig.Builder();
- method public androidx.security.SecureConfig build();
- method public androidx.security.SecureConfig.Builder forKeyStoreType(String);
- method public androidx.security.SecureConfig.Builder setAsymmetricBlockModes(String);
- method public androidx.security.SecureConfig.Builder setAsymmetricCipherTransformation(String);
- method public androidx.security.SecureConfig.Builder setAsymmetricKeyPairAlgorithm(String);
- method public androidx.security.SecureConfig.Builder setAsymmetricKeyPurposes(int);
- method public androidx.security.SecureConfig.Builder setAsymmetricKeySize(int);
- method public androidx.security.SecureConfig.Builder setAsymmetricPaddings(String);
- method public androidx.security.SecureConfig.Builder setAsymmetricRequireUserAuth(boolean);
- method public androidx.security.SecureConfig.Builder setAsymmetricRequireUserValiditySeconds(int);
- method public androidx.security.SecureConfig.Builder setAsymmetricSensitiveDataProtection(boolean);
- method public androidx.security.SecureConfig.Builder setBiometricKeyAuthCallback(androidx.security.biometric.BiometricKeyAuthCallback);
- method public androidx.security.SecureConfig.Builder setCertPath(String);
- method public androidx.security.SecureConfig.Builder setCertPathValidator(String);
- method public androidx.security.SecureConfig.Builder setClientCertAlgorithms(String[]);
- method public androidx.security.SecureConfig.Builder setSignatureAlgorithm(String);
- method public androidx.security.SecureConfig.Builder setStrongSSLCiphers(String[]);
- method public androidx.security.SecureConfig.Builder setSymmetricBlockModes(String);
- method public androidx.security.SecureConfig.Builder setSymmetricCipherTransformation(String);
- method public androidx.security.SecureConfig.Builder setSymmetricGcmTagLength(int);
- method public androidx.security.SecureConfig.Builder setSymmetricKeyAlgorithm(String);
- method public androidx.security.SecureConfig.Builder setSymmetricKeyPurposes(int);
- method public androidx.security.SecureConfig.Builder setSymmetricKeySize(int);
- method public androidx.security.SecureConfig.Builder setSymmetricPaddings(String);
- method public androidx.security.SecureConfig.Builder setSymmetricRequireUserAuth(boolean);
- method public androidx.security.SecureConfig.Builder setSymmetricRequireUserValiditySeconds(int);
- method public androidx.security.SecureConfig.Builder setSymmetricSensitiveDataProtection(boolean);
- method public androidx.security.SecureConfig.Builder setTrustAnchorOptions(androidx.security.config.TrustAnchorOptions);
- method public androidx.security.SecureConfig.Builder setUseStrongSSLCiphers(boolean);
- }
-
-}
-
-package androidx.security.biometric {
-
- public class BiometricKeyAuth extends androidx.biometric.BiometricPrompt.AuthenticationCallback {
- ctor public BiometricKeyAuth(androidx.fragment.app.FragmentActivity, androidx.security.biometric.BiometricKeyAuthCallback);
- method public void authenticateKey(javax.crypto.Cipher, androidx.biometric.BiometricPrompt.PromptInfo, androidx.security.crypto.SecureCipher.SecureAuthListener);
- method public void authenticateKey(java.security.Signature, androidx.biometric.BiometricPrompt.PromptInfo, androidx.security.crypto.SecureCipher.SecureAuthListener);
- method public void authenticateKey(javax.crypto.Cipher, androidx.security.crypto.SecureCipher.SecureAuthListener);
- method public void authenticateKey(java.security.Signature, androidx.security.crypto.SecureCipher.SecureAuthListener);
- }
-
- public abstract class BiometricKeyAuthCallback {
- ctor public BiometricKeyAuthCallback();
- method public abstract void authenticateKey(javax.crypto.Cipher, androidx.security.crypto.SecureCipher.SecureAuthListener);
- method public abstract void authenticateKey(java.security.Signature, androidx.security.crypto.SecureCipher.SecureAuthListener);
- method public abstract void onAuthenticationError(int, CharSequence);
- method public abstract void onAuthenticationFailed();
- method public abstract void onAuthenticationSucceeded();
- method public abstract void onMessage(String);
- }
-
- public enum BiometricKeyAuthCallback.BiometricStatus {
- method public static androidx.security.biometric.BiometricKeyAuthCallback.BiometricStatus fromId(int);
- method public int getType();
- enum_constant public static final androidx.security.biometric.BiometricKeyAuthCallback.BiometricStatus ERROR;
- enum_constant public static final androidx.security.biometric.BiometricKeyAuthCallback.BiometricStatus FAILED;
- enum_constant public static final androidx.security.biometric.BiometricKeyAuthCallback.BiometricStatus SUCCESS;
- }
-
-}
-
-package androidx.security.config {
-
- public final class TldConstants {
- field public static final java.util.List<java.lang.String> VALID_TLDS;
- }
-
- public enum TrustAnchorOptions {
- method public static androidx.security.config.TrustAnchorOptions fromId(int);
- method public int getType();
- enum_constant public static final androidx.security.config.TrustAnchorOptions LIMITED_SYSTEM;
- enum_constant public static final androidx.security.config.TrustAnchorOptions SYSTEM_ONLY;
- enum_constant public static final androidx.security.config.TrustAnchorOptions USER_ONLY;
- enum_constant public static final androidx.security.config.TrustAnchorOptions USER_SYSTEM;
- }
-
-}
-
-package androidx.security.context {
-
- public class SecureContextCompat {
- ctor public SecureContextCompat(android.content.Context);
- ctor public SecureContextCompat(android.content.Context, androidx.security.SecureConfig);
- method public boolean deviceLocked();
- method public void openEncryptedFileInput(String, java.util.concurrent.Executor, androidx.security.context.SecureContextCompat.EncryptedFileInputStreamListener) throws java.io.IOException;
- method public java.io.FileOutputStream openEncryptedFileOutput(String, int) throws java.io.IOException;
- method public java.io.FileOutputStream openEncryptedFileOutput(String, int, String) throws java.io.IOException;
- }
-
- public static interface SecureContextCompat.EncryptedFileInputStreamListener {
- method public void onEncryptedFileInput(java.io.FileInputStream);
- }
-
-}
-
package androidx.security.crypto {
- public class EphemeralSecretKey implements java.security.spec.KeySpec javax.crypto.SecretKey {
- ctor public EphemeralSecretKey(byte[]);
- ctor public EphemeralSecretKey(byte[], String);
- ctor public EphemeralSecretKey(byte[], androidx.security.SecureConfig);
- ctor public EphemeralSecretKey(byte[], String, androidx.security.SecureConfig);
- ctor public EphemeralSecretKey(byte[], int, int, String);
- method public void destroy();
- method public void destroyCipherKey(javax.crypto.Cipher, int);
- method public String getAlgorithm();
- method public byte[] getEncoded();
- method public String getFormat();
+ public final class EncryptedFile {
+ method public java.io.FileInputStream openFileInput() throws java.security.GeneralSecurityException, java.io.IOException;
+ method public java.io.FileOutputStream openFileOutput() throws java.security.GeneralSecurityException, java.io.IOException;
}
- public class SecureCipher {
- ctor public SecureCipher(androidx.security.SecureConfig);
- method public void decrypt(String, byte[], byte[], androidx.security.crypto.SecureCipher.SecureDecryptionListener);
- method public void decryptAsymmetric(String, byte[], androidx.security.crypto.SecureCipher.SecureDecryptionListener);
- method public void decryptEncodedData(byte[], androidx.security.crypto.SecureCipher.SecureDecryptionListener);
- method public byte[] decryptEphemeralData(androidx.security.crypto.EphemeralSecretKey, byte[], byte[]);
- method public byte[] encodeAsymmetricData(byte[], byte[]);
- method public byte[] encodeEphemeralData(byte[], byte[], byte[], byte[]);
- method public byte[] encodeSymmetricData(byte[], byte[], byte[]);
- method public void encrypt(String, byte[], androidx.security.crypto.SecureCipher.SecureSymmetricEncryptionListener);
- method public void encryptAsymmetric(String, byte[], androidx.security.crypto.SecureCipher.SecureAsymmetricEncryptionListener);
- method public android.util.Pair<byte[],byte[]> encryptEphemeralData(androidx.security.crypto.EphemeralSecretKey, byte[]);
- method public static androidx.security.crypto.SecureCipher getDefault();
- method public static androidx.security.crypto.SecureCipher getDefault(androidx.security.biometric.BiometricKeyAuthCallback);
- method public static androidx.security.crypto.SecureCipher getInstance(androidx.security.SecureConfig);
- method public void sign(String, byte[], androidx.security.crypto.SecureCipher.SecureSignListener);
- method public boolean verify(String, byte[], byte[]);
+ public static final class EncryptedFile.Builder {
+ ctor public EncryptedFile.Builder(java.io.File, android.content.Context, String, androidx.security.crypto.EncryptedFile.FileEncryptionScheme);
+ method public androidx.security.crypto.EncryptedFile build() throws java.security.GeneralSecurityException, java.io.IOException;
+ method public androidx.security.crypto.EncryptedFile.Builder setKeysetAlias(String);
+ method public androidx.security.crypto.EncryptedFile.Builder setKeysetPrefName(String);
}
- public static interface SecureCipher.SecureAsymmetricEncryptionListener {
- method public void encryptionComplete(byte[]);
+ public enum EncryptedFile.FileEncryptionScheme {
+ enum_constant public static final androidx.security.crypto.EncryptedFile.FileEncryptionScheme AES256_GCM_HKDF_4KB;
}
- public static interface SecureCipher.SecureAuthListener {
- method public void authComplete(androidx.security.biometric.BiometricKeyAuthCallback.BiometricStatus);
+ public final class EncryptedSharedPreferences implements android.content.SharedPreferences {
+ method public boolean contains(String?);
+ method public static android.content.SharedPreferences create(String, String, android.content.Context, androidx.security.crypto.EncryptedSharedPreferences.PrefKeyEncryptionScheme, androidx.security.crypto.EncryptedSharedPreferences.PrefValueEncryptionScheme) throws java.security.GeneralSecurityException, java.io.IOException;
+ method public android.content.SharedPreferences.Editor edit();
+ method public java.util.Map<java.lang.String,?> getAll();
+ method public boolean getBoolean(String?, boolean);
+ method public float getFloat(String?, float);
+ method public int getInt(String?, int);
+ method public long getLong(String?, long);
+ method public String? getString(String?, String?);
+ method public java.util.Set<java.lang.String>? getStringSet(String?, java.util.Set<java.lang.String>?);
+ method public void registerOnSharedPreferenceChangeListener(android.content.SharedPreferences.OnSharedPreferenceChangeListener);
+ method public void unregisterOnSharedPreferenceChangeListener(android.content.SharedPreferences.OnSharedPreferenceChangeListener);
}
- public static interface SecureCipher.SecureDecryptionListener {
- method public void decryptionComplete(byte[]);
+ public enum EncryptedSharedPreferences.PrefKeyEncryptionScheme {
+ enum_constant public static final androidx.security.crypto.EncryptedSharedPreferences.PrefKeyEncryptionScheme AES256_SIV;
}
- public enum SecureCipher.SecureFileEncodingType {
- method public static androidx.security.crypto.SecureCipher.SecureFileEncodingType fromId(int);
- method public int getType();
- enum_constant public static final androidx.security.crypto.SecureCipher.SecureFileEncodingType ASYMMETRIC;
- enum_constant public static final androidx.security.crypto.SecureCipher.SecureFileEncodingType EPHEMERAL;
- enum_constant public static final androidx.security.crypto.SecureCipher.SecureFileEncodingType NOT_ENCRYPTED;
- enum_constant public static final androidx.security.crypto.SecureCipher.SecureFileEncodingType SYMMETRIC;
+ public enum EncryptedSharedPreferences.PrefValueEncryptionScheme {
+ enum_constant public static final androidx.security.crypto.EncryptedSharedPreferences.PrefValueEncryptionScheme AES256_GCM;
}
- public static interface SecureCipher.SecureSignListener {
- method public void signComplete(byte[]);
- }
-
- public static interface SecureCipher.SecureSymmetricEncryptionListener {
- method public void encryptionComplete(byte[], byte[]);
- }
-
- public class SecureKeyGenerator {
- method public boolean generateAsymmetricKeyPair(String);
- method public androidx.security.crypto.EphemeralSecretKey generateEphemeralDataKey();
- method public boolean generateKey(String);
- method public static androidx.security.crypto.SecureKeyGenerator getDefault();
- method public static androidx.security.crypto.SecureKeyGenerator getInstance(androidx.security.SecureConfig);
- }
-
- public class SecureKeyStore {
- method public boolean checkKeyInsideSecureHardware(String);
- method public boolean checkKeyInsideSecureHardwareAsymmetric(String);
- method public void deleteKey(String);
- method public static androidx.security.crypto.SecureKeyStore getDefault();
- method public static androidx.security.crypto.SecureKeyStore getInstance(androidx.security.SecureConfig);
- method public boolean keyExists(String);
- }
-
-}
-
-package androidx.security.net {
-
- public class SecureKeyManager implements android.security.KeyChainAliasCallback javax.net.ssl.X509KeyManager {
- ctor public SecureKeyManager(String, androidx.security.SecureConfig);
- method public void alias(String);
- method public String chooseClientAlias(String[], java.security.Principal[], java.net.Socket);
- method public final String chooseServerAlias(String, java.security.Principal[], java.net.Socket);
- method public java.security.cert.X509Certificate[] getCertificateChain(String);
- method public final String[] getClientAliases(String, java.security.Principal[]);
- method public static androidx.security.net.SecureKeyManager getDefault(String);
- method public static androidx.security.net.SecureKeyManager getDefault(String, androidx.security.SecureConfig);
- method public java.security.PrivateKey getPrivateKey(String);
- method public final String[] getServerAliases(String, java.security.Principal[]);
- method public static androidx.security.net.SecureKeyManager installCertManually(androidx.security.net.SecureKeyManager.CertType, byte[], String, androidx.security.SecureConfig);
- method public void setCertChain(java.security.cert.X509Certificate[]);
- method public static void setContext(android.app.Activity);
- method public void setPrivateKey(java.security.PrivateKey);
- }
-
- public enum SecureKeyManager.CertType {
- method public static androidx.security.net.SecureKeyManager.CertType fromId(int);
- method public int getType();
- enum_constant public static final androidx.security.net.SecureKeyManager.CertType NOT_SUPPORTED;
- enum_constant public static final androidx.security.net.SecureKeyManager.CertType PKCS12;
- enum_constant public static final androidx.security.net.SecureKeyManager.CertType X509;
- }
-
- public class SecureURL {
- ctor public SecureURL(String) throws java.net.MalformedURLException;
- ctor public SecureURL(String, String) throws java.net.MalformedURLException;
- ctor public SecureURL(String, String, androidx.security.SecureConfig) throws java.net.MalformedURLException;
- method public String getClientCertAlias();
- method public String getHostname();
- method public int getPort();
- method public boolean isValid(javax.net.ssl.HttpsURLConnection);
- method public java.net.URLConnection openConnection() throws java.io.IOException;
- method public java.net.URLConnection openUserTrustedCertConnection(java.util.Map<java.lang.String,java.io.InputStream>) throws java.io.IOException;
+ public final class MasterKeys {
+ ctor public MasterKeys();
+ method public static String getOrCreate(android.security.keystore.KeyGenParameterSpec) throws java.security.GeneralSecurityException, java.io.IOException;
+ field public static final android.security.keystore.KeyGenParameterSpec AES256_GCM_SPEC;
}
}
diff --git a/security/crypto/api/current.txt b/security/crypto/api/current.txt
index 261883a..44f0de6 100644
--- a/security/crypto/api/current.txt
+++ b/security/crypto/api/current.txt
@@ -1,295 +1,49 @@
// Signature format: 3.0
-package androidx.security {
-
- public class SecureConfig {
- method public String getAndroidCAStore();
- method public String getAndroidKeyStore();
- method public String getAsymmetricBlockModes();
- method public String getAsymmetricCipherTransformation();
- method public String getAsymmetricKeyPairAlgorithm();
- method public int getAsymmetricKeyPurposes();
- method public int getAsymmetricKeySize();
- method public String getAsymmetricPaddings();
- method public boolean getAsymmetricRequireUserAuthEnabled();
- method public int getAsymmetricRequireUserValiditySeconds();
- method public boolean getAsymmetricSensitiveDataProtectionEnabled();
- method public androidx.security.biometric.BiometricKeyAuthCallback getBiometricKeyAuthCallback();
- method public String getCertPath();
- method public String getCertPathValidator();
- method public String[] getClientCertAlgorithms();
- method public static androidx.security.SecureConfig getDefault();
- method public String getKeystoreType();
- method public static androidx.security.SecureConfig getNiapConfig();
- method public static androidx.security.SecureConfig getNiapConfig(androidx.security.biometric.BiometricKeyAuthCallback);
- method public String getSignatureAlgorithm();
- method public String[] getStrongSSLCiphers();
- method public String getSymmetricBlockModes();
- method public String getSymmetricCipherTransformation();
- method public int getSymmetricGcmTagLength();
- method public String getSymmetricKeyAlgorithm();
- method public int getSymmetricKeyPurposes();
- method public int getSymmetricKeySize();
- method public String getSymmetricPaddings();
- method public boolean getSymmetricRequireUserAuthEnabled();
- method public int getSymmetricRequireUserValiditySeconds();
- method public boolean getSymmetricSensitiveDataProtectionEnabled();
- method public androidx.security.config.TrustAnchorOptions getTrustAnchorOptions();
- method public boolean getUseStrongSSLCiphers();
- method public boolean getUseStrongSSLCiphersEnabled();
- method public void setAndroidCAStore(String);
- method public void setAndroidKeyStore(String);
- method public void setAsymmetricBlockModes(String);
- method public void setAsymmetricCipherTransformation(String);
- method public void setAsymmetricKeyPairAlgorithm(String);
- method public void setAsymmetricKeyPurposes(int);
- method public void setAsymmetricKeySize(int);
- method public void setAsymmetricPaddings(String);
- method public void setAsymmetricRequireUserAuth(boolean);
- method public void setAsymmetricRequireUserValiditySeconds(int);
- method public void setAsymmetricSensitiveDataProtection(boolean);
- method public void setBiometricKeyAuthCallback(androidx.security.biometric.BiometricKeyAuthCallback);
- method public void setCertPath(String);
- method public void setCertPathValidator(String);
- method public void setClientCertAlgorithms(String[]);
- method public void setKeystoreType(String);
- method public void setSignatureAlgorithm(String);
- method public void setStrongSSLCiphers(String[]);
- method public void setSymmetricBlockModes(String);
- method public void setSymmetricCipherTransformation(String);
- method public void setSymmetricGcmTagLength(int);
- method public void setSymmetricKeyAlgorithm(String);
- method public void setSymmetricKeyPurposes(int);
- method public void setSymmetricKeySize(int);
- method public void setSymmetricPaddings(String);
- method public void setSymmetricRequireUserAuth(boolean);
- method public void setSymmetricRequireUserValiditySeconds(int);
- method public void setSymmetricSensitiveDataProtection(boolean);
- method public void setTrustAnchorOptions(androidx.security.config.TrustAnchorOptions);
- method public void setUseStrongSSLCiphers(boolean);
- field public static final int AES_IV_SIZE_BYTES = 16; // 0x10
- field public static final String ANDROID_CA_STORE = "AndroidCAStore";
- field public static final String ANDROID_KEYSTORE = "AndroidKeyStore";
- field public static final String SSL_TLS = "TLS";
- }
-
- public static class SecureConfig.Builder {
- ctor public SecureConfig.Builder();
- method public androidx.security.SecureConfig build();
- method public androidx.security.SecureConfig.Builder forKeyStoreType(String);
- method public androidx.security.SecureConfig.Builder setAsymmetricBlockModes(String);
- method public androidx.security.SecureConfig.Builder setAsymmetricCipherTransformation(String);
- method public androidx.security.SecureConfig.Builder setAsymmetricKeyPairAlgorithm(String);
- method public androidx.security.SecureConfig.Builder setAsymmetricKeyPurposes(int);
- method public androidx.security.SecureConfig.Builder setAsymmetricKeySize(int);
- method public androidx.security.SecureConfig.Builder setAsymmetricPaddings(String);
- method public androidx.security.SecureConfig.Builder setAsymmetricRequireUserAuth(boolean);
- method public androidx.security.SecureConfig.Builder setAsymmetricRequireUserValiditySeconds(int);
- method public androidx.security.SecureConfig.Builder setAsymmetricSensitiveDataProtection(boolean);
- method public androidx.security.SecureConfig.Builder setBiometricKeyAuthCallback(androidx.security.biometric.BiometricKeyAuthCallback);
- method public androidx.security.SecureConfig.Builder setCertPath(String);
- method public androidx.security.SecureConfig.Builder setCertPathValidator(String);
- method public androidx.security.SecureConfig.Builder setClientCertAlgorithms(String[]);
- method public androidx.security.SecureConfig.Builder setSignatureAlgorithm(String);
- method public androidx.security.SecureConfig.Builder setStrongSSLCiphers(String[]);
- method public androidx.security.SecureConfig.Builder setSymmetricBlockModes(String);
- method public androidx.security.SecureConfig.Builder setSymmetricCipherTransformation(String);
- method public androidx.security.SecureConfig.Builder setSymmetricGcmTagLength(int);
- method public androidx.security.SecureConfig.Builder setSymmetricKeyAlgorithm(String);
- method public androidx.security.SecureConfig.Builder setSymmetricKeyPurposes(int);
- method public androidx.security.SecureConfig.Builder setSymmetricKeySize(int);
- method public androidx.security.SecureConfig.Builder setSymmetricPaddings(String);
- method public androidx.security.SecureConfig.Builder setSymmetricRequireUserAuth(boolean);
- method public androidx.security.SecureConfig.Builder setSymmetricRequireUserValiditySeconds(int);
- method public androidx.security.SecureConfig.Builder setSymmetricSensitiveDataProtection(boolean);
- method public androidx.security.SecureConfig.Builder setTrustAnchorOptions(androidx.security.config.TrustAnchorOptions);
- method public androidx.security.SecureConfig.Builder setUseStrongSSLCiphers(boolean);
- }
-
-}
-
-package androidx.security.biometric {
-
- public class BiometricKeyAuth extends androidx.biometric.BiometricPrompt.AuthenticationCallback {
- ctor public BiometricKeyAuth(androidx.fragment.app.FragmentActivity, androidx.security.biometric.BiometricKeyAuthCallback);
- method public void authenticateKey(javax.crypto.Cipher, androidx.biometric.BiometricPrompt.PromptInfo, androidx.security.crypto.SecureCipher.SecureAuthListener);
- method public void authenticateKey(java.security.Signature, androidx.biometric.BiometricPrompt.PromptInfo, androidx.security.crypto.SecureCipher.SecureAuthListener);
- method public void authenticateKey(javax.crypto.Cipher, androidx.security.crypto.SecureCipher.SecureAuthListener);
- method public void authenticateKey(java.security.Signature, androidx.security.crypto.SecureCipher.SecureAuthListener);
- }
-
- public abstract class BiometricKeyAuthCallback {
- ctor public BiometricKeyAuthCallback();
- method public abstract void authenticateKey(javax.crypto.Cipher, androidx.security.crypto.SecureCipher.SecureAuthListener);
- method public abstract void authenticateKey(java.security.Signature, androidx.security.crypto.SecureCipher.SecureAuthListener);
- method public abstract void onAuthenticationError(int, CharSequence);
- method public abstract void onAuthenticationFailed();
- method public abstract void onAuthenticationSucceeded();
- method public abstract void onMessage(String);
- }
-
- public enum BiometricKeyAuthCallback.BiometricStatus {
- method public static androidx.security.biometric.BiometricKeyAuthCallback.BiometricStatus fromId(int);
- method public int getType();
- enum_constant public static final androidx.security.biometric.BiometricKeyAuthCallback.BiometricStatus ERROR;
- enum_constant public static final androidx.security.biometric.BiometricKeyAuthCallback.BiometricStatus FAILED;
- enum_constant public static final androidx.security.biometric.BiometricKeyAuthCallback.BiometricStatus SUCCESS;
- }
-
-}
-
-package androidx.security.config {
-
- public final class TldConstants {
- field public static final java.util.List<java.lang.String> VALID_TLDS;
- }
-
- public enum TrustAnchorOptions {
- method public static androidx.security.config.TrustAnchorOptions fromId(int);
- method public int getType();
- enum_constant public static final androidx.security.config.TrustAnchorOptions LIMITED_SYSTEM;
- enum_constant public static final androidx.security.config.TrustAnchorOptions SYSTEM_ONLY;
- enum_constant public static final androidx.security.config.TrustAnchorOptions USER_ONLY;
- enum_constant public static final androidx.security.config.TrustAnchorOptions USER_SYSTEM;
- }
-
-}
-
-package androidx.security.context {
-
- public class SecureContextCompat {
- ctor public SecureContextCompat(android.content.Context);
- ctor public SecureContextCompat(android.content.Context, androidx.security.SecureConfig);
- method public boolean deviceLocked();
- method public void openEncryptedFileInput(String, java.util.concurrent.Executor, androidx.security.context.SecureContextCompat.EncryptedFileInputStreamListener) throws java.io.IOException;
- method public java.io.FileOutputStream openEncryptedFileOutput(String, int) throws java.io.IOException;
- method public java.io.FileOutputStream openEncryptedFileOutput(String, int, String) throws java.io.IOException;
- }
-
- public static interface SecureContextCompat.EncryptedFileInputStreamListener {
- method public void onEncryptedFileInput(java.io.FileInputStream);
- }
-
-}
-
package androidx.security.crypto {
- public class EphemeralSecretKey implements java.security.spec.KeySpec javax.crypto.SecretKey {
- ctor public EphemeralSecretKey(byte[]);
- ctor public EphemeralSecretKey(byte[], String);
- ctor public EphemeralSecretKey(byte[], androidx.security.SecureConfig);
- ctor public EphemeralSecretKey(byte[], String, androidx.security.SecureConfig);
- ctor public EphemeralSecretKey(byte[], int, int, String);
- method public void destroy();
- method public void destroyCipherKey(javax.crypto.Cipher, int);
- method public String getAlgorithm();
- method public byte[] getEncoded();
- method public String getFormat();
+ public final class EncryptedFile {
+ method public java.io.FileInputStream openFileInput() throws java.security.GeneralSecurityException, java.io.IOException;
+ method public java.io.FileOutputStream openFileOutput() throws java.security.GeneralSecurityException, java.io.IOException;
}
- public class SecureCipher {
- ctor public SecureCipher(androidx.security.SecureConfig);
- method public void decrypt(String, byte[], byte[], androidx.security.crypto.SecureCipher.SecureDecryptionListener);
- method public void decryptAsymmetric(String, byte[], androidx.security.crypto.SecureCipher.SecureDecryptionListener);
- method public void decryptEncodedData(byte[], androidx.security.crypto.SecureCipher.SecureDecryptionListener);
- method public byte[] decryptEphemeralData(androidx.security.crypto.EphemeralSecretKey, byte[], byte[]);
- method public byte[] encodeAsymmetricData(byte[], byte[]);
- method public byte[] encodeEphemeralData(byte[], byte[], byte[], byte[]);
- method public byte[] encodeSymmetricData(byte[], byte[], byte[]);
- method public void encrypt(String, byte[], androidx.security.crypto.SecureCipher.SecureSymmetricEncryptionListener);
- method public void encryptAsymmetric(String, byte[], androidx.security.crypto.SecureCipher.SecureAsymmetricEncryptionListener);
- method public android.util.Pair<byte[],byte[]> encryptEphemeralData(androidx.security.crypto.EphemeralSecretKey, byte[]);
- method public static androidx.security.crypto.SecureCipher getDefault();
- method public static androidx.security.crypto.SecureCipher getDefault(androidx.security.biometric.BiometricKeyAuthCallback);
- method public static androidx.security.crypto.SecureCipher getInstance(androidx.security.SecureConfig);
- method public void sign(String, byte[], androidx.security.crypto.SecureCipher.SecureSignListener);
- method public boolean verify(String, byte[], byte[]);
+ public static final class EncryptedFile.Builder {
+ ctor public EncryptedFile.Builder(java.io.File, android.content.Context, String, androidx.security.crypto.EncryptedFile.FileEncryptionScheme);
+ method public androidx.security.crypto.EncryptedFile build() throws java.security.GeneralSecurityException, java.io.IOException;
+ method public androidx.security.crypto.EncryptedFile.Builder setKeysetAlias(String);
+ method public androidx.security.crypto.EncryptedFile.Builder setKeysetPrefName(String);
}
- public static interface SecureCipher.SecureAsymmetricEncryptionListener {
- method public void encryptionComplete(byte[]);
+ public enum EncryptedFile.FileEncryptionScheme {
+ enum_constant public static final androidx.security.crypto.EncryptedFile.FileEncryptionScheme AES256_GCM_HKDF_4KB;
}
- public static interface SecureCipher.SecureAuthListener {
- method public void authComplete(androidx.security.biometric.BiometricKeyAuthCallback.BiometricStatus);
+ public final class EncryptedSharedPreferences implements android.content.SharedPreferences {
+ method public boolean contains(String?);
+ method public static android.content.SharedPreferences create(String, String, android.content.Context, androidx.security.crypto.EncryptedSharedPreferences.PrefKeyEncryptionScheme, androidx.security.crypto.EncryptedSharedPreferences.PrefValueEncryptionScheme) throws java.security.GeneralSecurityException, java.io.IOException;
+ method public android.content.SharedPreferences.Editor edit();
+ method public java.util.Map<java.lang.String,?> getAll();
+ method public boolean getBoolean(String?, boolean);
+ method public float getFloat(String?, float);
+ method public int getInt(String?, int);
+ method public long getLong(String?, long);
+ method public String? getString(String?, String?);
+ method public java.util.Set<java.lang.String>? getStringSet(String?, java.util.Set<java.lang.String>?);
+ method public void registerOnSharedPreferenceChangeListener(android.content.SharedPreferences.OnSharedPreferenceChangeListener);
+ method public void unregisterOnSharedPreferenceChangeListener(android.content.SharedPreferences.OnSharedPreferenceChangeListener);
}
- public static interface SecureCipher.SecureDecryptionListener {
- method public void decryptionComplete(byte[]);
+ public enum EncryptedSharedPreferences.PrefKeyEncryptionScheme {
+ enum_constant public static final androidx.security.crypto.EncryptedSharedPreferences.PrefKeyEncryptionScheme AES256_SIV;
}
- public enum SecureCipher.SecureFileEncodingType {
- method public static androidx.security.crypto.SecureCipher.SecureFileEncodingType fromId(int);
- method public int getType();
- enum_constant public static final androidx.security.crypto.SecureCipher.SecureFileEncodingType ASYMMETRIC;
- enum_constant public static final androidx.security.crypto.SecureCipher.SecureFileEncodingType EPHEMERAL;
- enum_constant public static final androidx.security.crypto.SecureCipher.SecureFileEncodingType NOT_ENCRYPTED;
- enum_constant public static final androidx.security.crypto.SecureCipher.SecureFileEncodingType SYMMETRIC;
+ public enum EncryptedSharedPreferences.PrefValueEncryptionScheme {
+ enum_constant public static final androidx.security.crypto.EncryptedSharedPreferences.PrefValueEncryptionScheme AES256_GCM;
}
- public static interface SecureCipher.SecureSignListener {
- method public void signComplete(byte[]);
- }
-
- public static interface SecureCipher.SecureSymmetricEncryptionListener {
- method public void encryptionComplete(byte[], byte[]);
- }
-
- public class SecureKeyGenerator {
- method public boolean generateAsymmetricKeyPair(String);
- method public androidx.security.crypto.EphemeralSecretKey generateEphemeralDataKey();
- method public boolean generateKey(String);
- method public static androidx.security.crypto.SecureKeyGenerator getDefault();
- method public static androidx.security.crypto.SecureKeyGenerator getInstance(androidx.security.SecureConfig);
- }
-
- public class SecureKeyStore {
- method public boolean checkKeyInsideSecureHardware(String);
- method public boolean checkKeyInsideSecureHardwareAsymmetric(String);
- method public void deleteKey(String);
- method public static androidx.security.crypto.SecureKeyStore getDefault();
- method public static androidx.security.crypto.SecureKeyStore getInstance(androidx.security.SecureConfig);
- method public boolean keyExists(String);
- }
-
-}
-
-package androidx.security.net {
-
- public class SecureKeyManager implements android.security.KeyChainAliasCallback javax.net.ssl.X509KeyManager {
- ctor public SecureKeyManager(String, androidx.security.SecureConfig);
- method public void alias(String);
- method public String chooseClientAlias(String[], java.security.Principal[], java.net.Socket);
- method public final String chooseServerAlias(String, java.security.Principal[], java.net.Socket);
- method public java.security.cert.X509Certificate[] getCertificateChain(String);
- method public final String[] getClientAliases(String, java.security.Principal[]);
- method public static androidx.security.net.SecureKeyManager getDefault(String);
- method public static androidx.security.net.SecureKeyManager getDefault(String, androidx.security.SecureConfig);
- method public java.security.PrivateKey getPrivateKey(String);
- method public final String[] getServerAliases(String, java.security.Principal[]);
- method public static androidx.security.net.SecureKeyManager installCertManually(androidx.security.net.SecureKeyManager.CertType, byte[], String, androidx.security.SecureConfig);
- method public void setCertChain(java.security.cert.X509Certificate[]);
- method public static void setContext(android.app.Activity);
- method public void setPrivateKey(java.security.PrivateKey);
- }
-
- public enum SecureKeyManager.CertType {
- method public static androidx.security.net.SecureKeyManager.CertType fromId(int);
- method public int getType();
- enum_constant public static final androidx.security.net.SecureKeyManager.CertType NOT_SUPPORTED;
- enum_constant public static final androidx.security.net.SecureKeyManager.CertType PKCS12;
- enum_constant public static final androidx.security.net.SecureKeyManager.CertType X509;
- }
-
- public class SecureURL {
- ctor public SecureURL(String) throws java.net.MalformedURLException;
- ctor public SecureURL(String, String) throws java.net.MalformedURLException;
- ctor public SecureURL(String, String, androidx.security.SecureConfig) throws java.net.MalformedURLException;
- method public String getClientCertAlias();
- method public String getHostname();
- method public int getPort();
- method public boolean isValid(javax.net.ssl.HttpsURLConnection);
- method public java.net.URLConnection openConnection() throws java.io.IOException;
- method public java.net.URLConnection openUserTrustedCertConnection(java.util.Map<java.lang.String,java.io.InputStream>) throws java.io.IOException;
+ public final class MasterKeys {
+ ctor public MasterKeys();
+ method public static String getOrCreate(android.security.keystore.KeyGenParameterSpec) throws java.security.GeneralSecurityException, java.io.IOException;
+ field public static final android.security.keystore.KeyGenParameterSpec AES256_GCM_SPEC;
}
}
diff --git a/security/crypto/api/restricted_1.0.0-alpha01.txt b/security/crypto/api/restricted_1.0.0-alpha01.txt
index 23579e8..da4f6cc 100644
--- a/security/crypto/api/restricted_1.0.0-alpha01.txt
+++ b/security/crypto/api/restricted_1.0.0-alpha01.txt
@@ -1,6 +1 @@
// Signature format: 3.0
-package androidx.security.crypto {
-
-
-}
-
diff --git a/security/crypto/api/restricted_current.txt b/security/crypto/api/restricted_current.txt
index 23579e8..da4f6cc 100644
--- a/security/crypto/api/restricted_current.txt
+++ b/security/crypto/api/restricted_current.txt
@@ -1,6 +1 @@
// Signature format: 3.0
-package androidx.security.crypto {
-
-
-}
-
diff --git a/security/crypto/build.gradle b/security/crypto/build.gradle
index edcda67..02c34ea 100644
--- a/security/crypto/build.gradle
+++ b/security/crypto/build.gradle
@@ -28,12 +28,16 @@
api("androidx.annotation:annotation:1.0.0") { transitive = true}
api("androidx.core:core:1.0.0") { transitive = true}
api("androidx.fragment:fragment:1.0.0") { transitive = true}
- api("androidx.biometric:biometric:1.0.0-alpha03")
+ api("com.google.crypto.tink:tink-android:1.2.2")
+
+ androidTestImplementation("androidx.test.ext:junit:1.1.0")
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
androidTestImplementation(ANDROIDX_TEST_CORE)
androidTestImplementation(ANDROIDX_TEST_RUNNER)
+ androidTestImplementation(ANDROIDX_TEST_RULES)
androidTestImplementation(MOCKITO_CORE)
+
}
android {
diff --git a/security/crypto/src/androidTest/AndroidManifest.xml b/security/crypto/src/androidTest/AndroidManifest.xml
index 4c91032..39f2d9c 100644
--- a/security/crypto/src/androidTest/AndroidManifest.xml
+++ b/security/crypto/src/androidTest/AndroidManifest.xml
@@ -18,4 +18,7 @@
package="androidx.security.tests">
<uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
<uses-permission android:name="android.permission.INTERNET" />
+ <application>
+ </application>
+
</manifest>
diff --git a/security/crypto/src/androidTest/java/androidx/security/context/SecureContextCompatTest.java b/security/crypto/src/androidTest/java/androidx/security/context/SecureContextCompatTest.java
deleted file mode 100644
index c526f13..0000000
--- a/security/crypto/src/androidTest/java/androidx/security/context/SecureContextCompatTest.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.security.context;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.security.SecureConfig;
-import androidx.security.crypto.SecureKeyGenerator;
-import androidx.test.core.app.ApplicationProvider;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-
-@SuppressWarnings("unchecked")
-@RunWith(JUnit4.class)
-public class SecureContextCompatTest {
-
-
- private Context mContext;
- private static final String KEYPAIR = "file_key";
-
-
- @Before
- public void setup() {
- mContext = ApplicationProvider.getApplicationContext();
- SecureKeyGenerator keyGenerator = SecureKeyGenerator.getInstance(SecureConfig.getDefault());
- keyGenerator.generateAsymmetricKeyPair(KEYPAIR);
- }
-
- @Test
- public void testWriteEncryptedFile() {
- String fileContent = "SOME TEST DATA!";
- String fileName = "test_file";
-
- SecureContextCompat secureContextCompat = new SecureContextCompat(mContext);
- try {
- FileOutputStream outputStream = secureContextCompat.openEncryptedFileOutput(fileName,
- Context.MODE_PRIVATE, KEYPAIR);
- outputStream.write(fileContent.getBytes("UTF-8"));
- outputStream.flush();
- outputStream.close();
-
- FileInputStream fileInputStream = mContext.openFileInput(fileName);
- byte[] rawBytes = new byte[fileInputStream.available()];
- fileInputStream.read(rawBytes);
- Assert.assertNotEquals("Contents should differ, data was not encrypted.",
- fileContent, new String(rawBytes, "UTF-8"));
- } catch (IOException ex) {
- ex.printStackTrace();
- }
- }
-
- @Test
- public void testReadEncryptedFile() {
- final String fileContent = "SOME TEST DATA!";
- final String fileName = "test_file";
- SecureContextCompat secureContextCompat = new SecureContextCompat(mContext);
- try {
- secureContextCompat.openEncryptedFileInput(fileName,
- null,
- new SecureContextCompat.EncryptedFileInputStreamListener() {
- @Override
- public void onEncryptedFileInput(@NonNull FileInputStream inputStream) {
- try {
- byte[] rawBytes = new byte[inputStream.available()];
- inputStream.read(rawBytes);
- Assert.assertNotEquals(
- "Contents should be equal, data was encrypted.",
- fileContent, new String(rawBytes, "UTF-8"));
- } catch (Exception ex) {
- ex.printStackTrace();
- }
- }
- });
-
- } catch (IOException ex) {
- ex.printStackTrace();
- }
- }
-
-}
-
diff --git a/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedFileTest.java b/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedFileTest.java
new file mode 100644
index 0000000..3f1b6666
--- /dev/null
+++ b/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedFileTest.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.security.crypto;
+
+import static androidx.security.crypto.MasterKeys.KEYSTORE_PATH_URI;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.StreamingAead;
+import com.google.crypto.tink.config.TinkConfig;
+import com.google.crypto.tink.integration.android.AndroidKeysetManager;
+import com.google.crypto.tink.streamingaead.StreamingAeadFactory;
+import com.google.crypto.tink.streamingaead.StreamingAeadKeyTemplates;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.KeyStore;
+
+@SuppressWarnings("unchecked")
+@RunWith(JUnit4.class)
+public class EncryptedFileTest {
+
+
+ private Context mContext;
+ private String mMasterKeyAlias;
+
+ @Before
+ public void setup() throws Exception {
+ mContext = ApplicationProvider.getApplicationContext();
+
+ SharedPreferences sharedPreferences = mContext.getSharedPreferences(
+ "__androidx_security_crypto_encrypted_file_pref__", Context.MODE_PRIVATE);
+ sharedPreferences.edit().clear().commit();
+
+
+ // Delete old keys for testing
+ String filePath = mContext.getFilesDir().getParent() + "/shared_prefs/"
+ + "__androidx_security_crypto_encrypted_file_pref__";
+ File deletePrefFile = new File(filePath);
+ deletePrefFile.delete();
+
+ filePath = mContext.getFilesDir().getParent() + "nothing_to_see_here";
+ deletePrefFile = new File(filePath);
+ deletePrefFile.delete();
+
+ File dataFile = new File(mContext.getFilesDir(), "nothing_to_see_here");
+ dataFile.delete();
+
+ dataFile = new File(mContext.getFilesDir(), "nothing_to_see_here_custom");
+ dataFile.delete();
+
+ dataFile = new File(mContext.getFilesDir(), "tink_test_file");
+ dataFile.delete();
+
+ // Delete MasterKeys
+ KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+ keyStore.load(null);
+ keyStore.deleteEntry(MasterKeys.MASTER_KEY_ALIAS);
+
+ mMasterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
+ }
+
+ @Test
+ public void testWriteReadEncryptedFile() throws Exception {
+ final String fileContent = "Don't tell anyone...";
+ final String fileName = "nothing_to_see_here";
+
+ // Write
+
+ EncryptedFile encryptedFile = new EncryptedFile.Builder(new File(mContext.getFilesDir(),
+ fileName), mContext, mMasterKeyAlias,
+ EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB)
+ .build();
+
+ OutputStream outputStream = encryptedFile.openFileOutput();
+ outputStream.write(fileContent.getBytes("UTF-8"));
+ outputStream.flush();
+ outputStream.close();
+
+ FileInputStream rawStream = mContext.openFileInput(fileName);
+ ByteArrayOutputStream rawByteArrayOutputStream = new ByteArrayOutputStream();
+ int rawNextByte = rawStream.read();
+ while (rawNextByte != -1) {
+ rawByteArrayOutputStream.write(rawNextByte);
+ rawNextByte = rawStream.read();
+ }
+ byte[] rawCipherText = rawByteArrayOutputStream.toByteArray();
+ System.out.println("Raw CipherText = " + new String(rawCipherText,
+ UTF_8));
+ rawStream.close();
+
+ InputStream inputStream = encryptedFile.openFileInput();
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ int nextByte = inputStream.read();
+ while (nextByte != -1) {
+ byteArrayOutputStream.write(nextByte);
+ nextByte = inputStream.read();
+ }
+
+ byte[] plainText = byteArrayOutputStream.toByteArray();
+
+ System.out.println("Decrypted Data: " + new String(plainText,
+ UTF_8));
+
+ Assert.assertEquals(
+ "Contents should be equal, data was encrypted.",
+ fileContent, new String(plainText, "UTF-8"));
+ inputStream.close();
+
+
+ EncryptedFile existingFileInputCheck = new EncryptedFile.Builder(
+ new File(mContext.getFilesDir(), "FAKE_FILE"), mContext, mMasterKeyAlias,
+ EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB)
+ .build();
+ boolean inputFailed = false;
+ try {
+ existingFileInputCheck.openFileInput();
+ } catch (IOException ex) {
+ inputFailed = true;
+ }
+ Assert.assertTrue("File should have failed opening.", inputFailed);
+
+ EncryptedFile existingFileOutputCheck = new EncryptedFile.Builder(
+ new File(mContext.getFilesDir(), fileName), mContext, mMasterKeyAlias,
+ EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB)
+ .build();
+ boolean outputFailed = false;
+ try {
+ existingFileOutputCheck.openFileOutput();
+ } catch (IOException ex) {
+ outputFailed = true;
+ }
+ Assert.assertTrue("File should have failed writing.", outputFailed);
+
+ }
+
+ @Test
+ public void testWriteReadEncryptedFileCustomPrefs() throws Exception {
+ final String fileContent = "Don't tell anyone...!!!!!";
+ final String fileName = "nothing_to_see_here_custom";
+
+ // Write
+ EncryptedFile encryptedFile = new EncryptedFile.Builder(new File(mContext.getFilesDir(),
+ fileName), mContext, mMasterKeyAlias,
+ EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB)
+ .setKeysetAlias("CustomKEYALIAS")
+ .setKeysetPrefName("CUSTOMPREFNAME")
+ .build();
+
+ OutputStream outputStream = encryptedFile.openFileOutput();
+ outputStream.write(fileContent.getBytes("UTF-8"));
+ outputStream.flush();
+ outputStream.close();
+
+ FileInputStream rawStream = mContext.openFileInput(fileName);
+ ByteArrayOutputStream rawByteArrayOutputStream = new ByteArrayOutputStream();
+ int rawNextByte = rawStream.read();
+ while (rawNextByte != -1) {
+ rawByteArrayOutputStream.write(rawNextByte);
+ rawNextByte = rawStream.read();
+ }
+ byte[] rawCipherText = rawByteArrayOutputStream.toByteArray();
+ System.out.println("Raw CipherText = " + new String(rawCipherText,
+ UTF_8));
+ rawStream.close();
+
+ InputStream inputStream = encryptedFile.openFileInput();
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ int nextByte = inputStream.read();
+ while (nextByte != -1) {
+ byteArrayOutputStream.write(nextByte);
+ nextByte = inputStream.read();
+ }
+
+ byte[] plainText = byteArrayOutputStream.toByteArray();
+
+ System.out.println("Decrypted Data: " + new String(plainText,
+ UTF_8));
+
+ Assert.assertEquals(
+ "Contents should be equal, data was encrypted.",
+ fileContent, new String(plainText, "UTF-8"));
+ inputStream.close();
+
+ SharedPreferences sharedPreferences = mContext.getSharedPreferences("CUSTOMPREFNAME",
+ Context.MODE_PRIVATE);
+ boolean containsKeyset = sharedPreferences.contains("CustomKEYALIAS");
+ Assert.assertTrue("Keyset should have existed.", containsKeyset);
+ }
+
+ @Test
+ public void tinkTest() throws Exception {
+ final String fileContent = "Don't tell anyone...";
+ final String fileName = "tink_test_file";
+ File file = new File(mContext.getFilesDir(), fileName);
+
+ // Write
+ EncryptedFile encryptedFile = new EncryptedFile.Builder(file, mContext, mMasterKeyAlias,
+ EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB)
+ .build();
+
+ OutputStream outputStream = encryptedFile.openFileOutput();
+ outputStream.write(fileContent.getBytes(UTF_8));
+ outputStream.flush();
+ outputStream.close();
+
+ TinkConfig.register();
+ KeysetHandle streadmingAeadKeysetHandle = new AndroidKeysetManager.Builder()
+ .withKeyTemplate(StreamingAeadKeyTemplates.AES256_GCM_HKDF_4KB)
+ .withSharedPref(mContext,
+ "__androidx_security_crypto_encrypted_file_keyset__",
+ "__androidx_security_crypto_encrypted_file_pref__")
+ .withMasterKeyUri(KEYSTORE_PATH_URI + mMasterKeyAlias)
+ .build().getKeysetHandle();
+
+ StreamingAead streamingAead = StreamingAeadFactory.getPrimitive(
+ streadmingAeadKeysetHandle);
+
+ FileInputStream fileInputStream = new FileInputStream(file);
+ InputStream inputStream = streamingAead.newDecryptingStream(fileInputStream,
+ file.getName().getBytes(UTF_8));
+
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ int nextByte = inputStream.read();
+ while (nextByte != -1) {
+ byteArrayOutputStream.write(nextByte);
+ nextByte = inputStream.read();
+ }
+
+ byte[] plainText = byteArrayOutputStream.toByteArray();
+
+ System.out.println("Decrypted Data: " + new String(plainText,
+ UTF_8));
+
+ Assert.assertEquals(
+ "Contents should be equal, data was encrypted.",
+ fileContent, new String(plainText, "UTF-8"));
+ inputStream.close();
+ }
+
+}
+
diff --git a/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java b/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java
new file mode 100644
index 0000000..cf405fc
--- /dev/null
+++ b/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.security.crypto;
+
+import static android.content.Context.MODE_PRIVATE;
+
+import static androidx.security.crypto.MasterKeys.KEYSTORE_PATH_URI;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.ArraySet;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.DeterministicAead;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.aead.AeadFactory;
+import com.google.crypto.tink.aead.AeadKeyTemplates;
+import com.google.crypto.tink.config.TinkConfig;
+import com.google.crypto.tink.daead.DeterministicAeadFactory;
+import com.google.crypto.tink.daead.DeterministicAeadKeyTemplates;
+import com.google.crypto.tink.integration.android.AndroidKeysetManager;
+import com.google.crypto.tink.subtle.Base64;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.security.KeyStore;
+import java.util.Set;
+
+@SuppressWarnings("unchecked")
+@RunWith(JUnit4.class)
+public class EncryptedSharedPreferencesTest {
+
+ private Context mContext;
+ private String mKeyAlias;
+
+ private static final String PREFS_FILE = "test_shared_prefs";
+
+ @Before
+ public void setup() throws Exception {
+
+ mContext = ApplicationProvider.getApplicationContext();
+
+ // Delete all previous keys and shared preferences.
+
+ String filePath = mContext.getFilesDir().getParent() + "/shared_prefs/"
+ + "__androidx_security__crypto_encrypted_prefs__";
+ File deletePrefFile = new File(filePath);
+ deletePrefFile.delete();
+
+ SharedPreferences notEncryptedSharedPrefs = mContext.getSharedPreferences(PREFS_FILE,
+ MODE_PRIVATE);
+ notEncryptedSharedPrefs.edit().clear().commit();
+
+ filePath = mContext.getFilesDir().getParent() + "/shared_prefs/"
+ + PREFS_FILE;
+ deletePrefFile = new File(filePath);
+ deletePrefFile.delete();
+
+ SharedPreferences encryptedSharedPrefs = mContext.getSharedPreferences("TinkTestPrefs",
+ MODE_PRIVATE);
+ encryptedSharedPrefs.edit().clear().commit();
+
+ filePath = mContext.getFilesDir().getParent() + "/shared_prefs/"
+ + "TinkTestPrefs";
+ deletePrefFile = new File(filePath);
+ deletePrefFile.delete();
+
+ // Delete MasterKeys
+ KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+ keyStore.load(null);
+ keyStore.deleteEntry("_androidx_security_master_key_");
+
+ mKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
+ }
+
+ @Test
+ public void testWriteSharedPrefs() throws Exception {
+
+ SharedPreferences sharedPreferences = EncryptedSharedPreferences
+ .create(PREFS_FILE,
+ mKeyAlias, mContext,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM);
+
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+
+ // String Test
+ final String stringTestKey = "StringTest";
+ final String stringTestValue = "THIS IS A TEST STRING";
+ editor.putString(stringTestKey, stringTestValue);
+
+ SharedPreferences.OnSharedPreferenceChangeListener listener =
+ new SharedPreferences.OnSharedPreferenceChangeListener() {
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+ Assert.assertEquals(stringTestValue,
+ sharedPreferences.getString(stringTestKey, null));
+ }
+ };
+
+ sharedPreferences.registerOnSharedPreferenceChangeListener(listener);
+
+
+ // String Set Test
+ String stringSetTestKey = "StringSetTest";
+ Set<String> stringSetValue = new ArraySet<>();
+ stringSetValue.add("Test1");
+ stringSetValue.add("Test2");
+ editor.putStringSet(stringSetTestKey, stringSetValue);
+
+
+ // Int Test
+ String intTestKey = "IntTest";
+ int intTestValue = 1000;
+ editor.putInt(intTestKey, intTestValue);
+
+ // Long Test
+ String longTestKey = "LongTest";
+ long longTestValue = 500L;
+ editor.putLong(longTestKey, longTestValue);
+
+ // Boolean Test
+ String booleanTestKey = "BooleanTest";
+ boolean booleanTestValue = true;
+ editor.putBoolean(booleanTestKey, booleanTestValue);
+
+ // Float Test
+ String floatTestKey = "FloatTest";
+ float floatTestValue = 250.5f;
+ editor.putFloat(floatTestKey, floatTestValue);
+
+ // Null Key Test
+ String nullKey = null;
+ String nullStringValue = "NULL_KEY";
+ editor.putString(nullKey, nullStringValue);
+
+ editor.commit();
+
+ // String Test Assertion
+ Assert.assertEquals(stringTestKey + " has the wrong value",
+ stringTestValue,
+ sharedPreferences.getString(stringTestKey, null));
+
+ // StringSet Test Assertion
+ Set<String> stringSetPrefsValue = sharedPreferences.getStringSet(stringSetTestKey, null);
+ String stringSetTestValue = null;
+ if (!stringSetPrefsValue.isEmpty()) {
+ stringSetTestValue = stringSetPrefsValue.iterator().next();
+ }
+ Assert.assertEquals(stringSetTestKey + " has the wrong value",
+ ((ArraySet<String>) stringSetValue).valueAt(0),
+ stringSetTestValue);
+
+ // Int Test Assertion
+ Assert.assertEquals(intTestKey + " has the wrong value",
+ intTestValue,
+ sharedPreferences.getInt(intTestKey, 0));
+
+ // Long Test Assertion
+ Assert.assertEquals(longTestKey + " has the wrong value",
+ longTestValue,
+ sharedPreferences.getLong(longTestKey, 0L));
+
+ // Boolean Test Assertion
+ Assert.assertEquals(booleanTestKey + " has the wrong value",
+ booleanTestValue,
+ sharedPreferences.getBoolean(booleanTestKey, false));
+
+ // Float Test Assertion
+ Assert.assertEquals(floatTestValue,
+ sharedPreferences.getFloat(floatTestKey, 0.0f),
+ 0.0f);
+
+ // Null Key Test Assertion
+ Assert.assertEquals(nullKey + " has the wrong value",
+ nullStringValue,
+ sharedPreferences.getString(nullKey, null));
+
+ Assert.assertTrue(nullKey + " should exist", sharedPreferences.contains(nullKey));
+
+ // Test Remove
+ editor.remove(nullKey);
+ editor.commit();
+
+ Assert.assertEquals(nullKey + " should have been removed.",
+ null,
+ sharedPreferences.getString(nullKey, null));
+
+ Assert.assertFalse(nullKey + " should not exist",
+ sharedPreferences.contains(nullKey));
+
+ // Null String Key and value Test Assertion
+ editor.putString(null, null);
+ editor.putStringSet(null, null);
+ editor.commit();
+ Assert.assertEquals(null + " should not have a value",
+ null,
+ sharedPreferences.getString(null, null));
+
+ // Null StringSet Key and value Test Assertion
+
+ Assert.assertEquals(null + " should not have a value",
+ null,
+ sharedPreferences.getStringSet(null, null));
+
+ // Test overwriting keys
+ String twiceKey = "KeyTwice";
+ String twiceVal1 = "FirstVal";
+ String twiceVal2 = "SecondVal";
+ editor.putString(twiceKey, twiceVal1);
+ editor.commit();
+
+ Assert.assertEquals(twiceVal1 + " should be the value",
+ twiceVal1,
+ sharedPreferences.getString(twiceKey, null));
+
+ editor.putString(twiceKey, twiceVal2);
+ editor.commit();
+
+ Assert.assertEquals(twiceVal2 + " should be the value",
+ twiceVal2,
+ sharedPreferences.getString(twiceKey, null));
+ }
+
+ @Test
+ public void testWriteSharedPrefsTink() throws Exception {
+ String tinkTestPrefs = "TinkTestPrefs";
+ String testKey = "TestKey";
+ String testValue = "TestValue";
+
+ SharedPreferences encryptedSharedPreferences = EncryptedSharedPreferences
+ .create(tinkTestPrefs,
+ mKeyAlias, mContext,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM);
+
+ SharedPreferences.Editor encryptedEditor = encryptedSharedPreferences.edit();
+ encryptedEditor.putString(testKey, testValue);
+ encryptedEditor.commit();
+
+ // Set up Tink
+ TinkConfig.register();
+
+ KeysetHandle daeadKeysetHandle = new AndroidKeysetManager.Builder()
+ .withKeyTemplate(DeterministicAeadKeyTemplates.AES256_SIV)
+ .withSharedPref(mContext,
+ "__androidx_security_crypto_encrypted_prefs_key_keyset__", tinkTestPrefs)
+ .withMasterKeyUri(KEYSTORE_PATH_URI + "_androidx_security_master_key_")
+ .build().getKeysetHandle();
+
+ DeterministicAead deterministicAead = DeterministicAeadFactory.getPrimitive(
+ daeadKeysetHandle);
+ byte[] encryptedKey = deterministicAead.encryptDeterministically(testKey.getBytes(UTF_8),
+ tinkTestPrefs.getBytes());
+ String encodedKey = Base64.encode(encryptedKey);
+ SharedPreferences sharedPreferences = mContext.getSharedPreferences(tinkTestPrefs,
+ MODE_PRIVATE);
+
+ boolean keyExists = sharedPreferences.contains(encodedKey);
+ Assert.assertTrue("Key should exist if Tink is compatible.", keyExists);
+
+ KeysetHandle aeadKeysetHandle = new AndroidKeysetManager.Builder()
+ .withKeyTemplate(AeadKeyTemplates.AES256_GCM)
+ .withSharedPref(mContext,
+ "__androidx_security_crypto_encrypted_prefs_value_keyset__", tinkTestPrefs)
+ .withMasterKeyUri(KEYSTORE_PATH_URI + "_androidx_security_master_key_")
+ .build().getKeysetHandle();
+
+ Aead aead = AeadFactory.getPrimitive(aeadKeysetHandle);
+
+ String encryptedValue = sharedPreferences.getString(encodedKey, null);
+ byte[] cipherText = Base64.decode(encryptedValue);
+ ByteBuffer values = ByteBuffer.wrap(aead.decrypt(cipherText, encodedKey.getBytes(UTF_8)));
+ values.getInt(); // throw type away, we know its a String
+ int length = values.getInt();
+ ByteBuffer stringSlice = values.slice();
+ stringSlice.limit(length);
+ String actualValue = UTF_8.decode(stringSlice).toString();
+ Assert.assertEquals("String should have been equal to original",
+ actualValue,
+ testValue);
+ }
+
+}
diff --git a/security/crypto/src/androidTest/java/androidx/security/crypto/SecureCipherTest.java b/security/crypto/src/androidTest/java/androidx/security/crypto/SecureCipherTest.java
deleted file mode 100644
index 2529319..0000000
--- a/security/crypto/src/androidTest/java/androidx/security/crypto/SecureCipherTest.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.security.crypto;
-
-import androidx.annotation.NonNull;
-import androidx.security.SecureConfig;
-
-import org.junit.Assert;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-
-@SuppressWarnings("unchecked")
-@RunWith(JUnit4.class)
-public class SecureCipherTest {
-
- @Test
- public void testEncryptDecryptSymmetricData() {
- final String keyAlias = "test_signing_key";
- final String original = "It's a secret...";
- try {
- SecureConfig config = SecureConfig.getDefault();
- final SecureCipher cipher = new SecureCipher(config);
- SecureKeyGenerator keyGenerator = SecureKeyGenerator.getInstance(config);
- keyGenerator.generateKey(keyAlias);
- cipher.encrypt(keyAlias, original.getBytes("UTF-8"),
- new SecureCipher.SecureSymmetricEncryptionListener() {
- @Override
- public void encryptionComplete(@NonNull byte[] cipherText,
- @NonNull byte[] iv) {
- cipher.decrypt(keyAlias, cipherText, iv,
- new SecureCipher.SecureDecryptionListener() {
- @Override
- public void decryptionComplete(
- @NonNull byte[] clearText) {
- try {
- Assert.assertEquals(
- "Original should match"
- + "encrypted/decrypted data",
- original,
- new String(clearText,
- "UTF-8"));
- } catch (UnsupportedEncodingException ex) {
- ex.printStackTrace();
- }
- }
- });
-
- }
- });
-
- } catch (IOException ex) {
- ex.printStackTrace();
- }
-
- }
-
- @Test
- public void testSignVerifyData() {
- final String keyAlias = "test_signing_key";
- final String original = "It's a secret...";
- try {
- SecureConfig config = SecureConfig.getDefault();
- final SecureCipher cipher = new SecureCipher(config);
- SecureKeyGenerator keyGenerator = SecureKeyGenerator.getInstance(config);
- keyGenerator.generateAsymmetricKeyPair(keyAlias);
- cipher.sign(keyAlias, original.getBytes("UTF-8"),
- new SecureCipher.SecureSignListener() {
- @Override
- public void signComplete(@NonNull byte[] signature) {
- try {
- Assert.assertTrue(
- "Signature should verify",
- cipher.verify(keyAlias,
- original.getBytes("UTF-8"),
- signature));
- } catch (UnsupportedEncodingException ex) {
- ex.printStackTrace();
- }
- }
- });
- } catch (IOException ex) {
- ex.printStackTrace();
- }
-
- }
-
-}
diff --git a/security/crypto/src/androidTest/java/androidx/security/crypto/SecureKeyStoreTest.java b/security/crypto/src/androidTest/java/androidx/security/crypto/SecureKeyStoreTest.java
deleted file mode 100644
index 293f9a2..0000000
--- a/security/crypto/src/androidTest/java/androidx/security/crypto/SecureKeyStoreTest.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.security.crypto;
-
-import org.junit.Assert;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@RunWith(JUnit4.class)
-public class SecureKeyStoreTest {
-
- @Test
- public void testKeyDoesNotExist() throws Throwable {
- SecureKeyStore secureKeyStore = SecureKeyStore.getDefault();
- boolean keyExists = secureKeyStore.keyExists("Not_a_real_key");
- Assert.assertFalse("Key has not been created and should not exist!", keyExists);
- }
-
- @Test
- public void testSymmetricKeyExists() throws Throwable {
- SecureKeyGenerator keyGenerator = SecureKeyGenerator.getDefault();
- keyGenerator.generateKey("symmetric_key");
-
- SecureKeyStore keyStore = SecureKeyStore.getDefault();
- boolean keyExists = keyStore.keyExists("symmetric_key");
-
- Assert.assertTrue("Symmetric Key should exist!", keyExists);
- }
-
- @Test
- public void testDeleteSymmetricKey() throws Throwable {
- SecureKeyStore keyStore = SecureKeyStore.getDefault();
- keyStore.deleteKey("symmetric_key");
-
- boolean keyExists = keyStore.keyExists("symmetric_key");
-
- Assert.assertFalse("Symmetric Key should have been deleted!", keyExists);
- }
-
- @Test
- public void testAsymmetricKeyExists() throws Throwable {
- SecureKeyGenerator keyGenerator = SecureKeyGenerator.getDefault();
- keyGenerator.generateAsymmetricKeyPair("asymmetric_key_pair");
-
- SecureKeyStore keyStore = SecureKeyStore.getDefault();
- boolean keyExists = keyStore.keyExists("asymmetric_key_pair");
-
- Assert.assertTrue("Asymmetric Key should exist!", keyExists);
- }
-
- @Test
- public void testDeleteAsymmetricKey() throws Throwable {
- SecureKeyStore keyStore = SecureKeyStore.getDefault();
- keyStore.deleteKey("asymmetric_key_pair");
-
- boolean keyExists = keyStore.keyExists("asymmetric_key_pair");
-
- Assert.assertFalse("Asymmetric Key should have been deleted!", keyExists);
- }
-
-}
diff --git a/security/crypto/src/androidTest/java/androidx/security/net/SecureURLTest.java b/security/crypto/src/androidTest/java/androidx/security/net/SecureURLTest.java
deleted file mode 100644
index 793b8aa..0000000
--- a/security/crypto/src/androidTest/java/androidx/security/net/SecureURLTest.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.security.net;
-
-import org.junit.Assert;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.IOException;
-
-import javax.net.ssl.HttpsURLConnection;
-
-@SuppressWarnings("unchecked")
-@RunWith(JUnit4.class)
-public class SecureURLTest {
-
- @Test
- public void testValidHttpsUrlConnection() {
- String url = "https://www.google.com";
- try {
- SecureURL secureURL = new SecureURL(url);
- HttpsURLConnection connection = (HttpsURLConnection) secureURL.openConnection();
-
-
- boolean valid = secureURL.isValid(connection);
-
- Assert.assertTrue("Connection to " + url + " should be valid.",
- valid);
- } catch (IOException ex) {
- ex.printStackTrace();
- }
-
-
- }
-
- @Test
- public void testInValidHttpsUrlConnection() {
- String url = "https://revoked.badssl.com";
- try {
- SecureURL secureURL = new SecureURL(url);
- HttpsURLConnection connection = (HttpsURLConnection) secureURL.openConnection();
-
- boolean valid = secureURL.isValid(connection);
-
- Assert.assertFalse("Connection to " + url
- + " should be invalid, revoked cert.",
- valid);
-
- } catch (IOException ex) {
- ex.printStackTrace();
- }
-
-
- }
-}
diff --git a/security/crypto/src/main/AndroidManifest.xml b/security/crypto/src/main/AndroidManifest.xml
index 7e7b70b..25bdf7a 100644
--- a/security/crypto/src/main/AndroidManifest.xml
+++ b/security/crypto/src/main/AndroidManifest.xml
@@ -15,5 +15,4 @@
-->
<manifest package="androidx.security"
xmlns:android="http://schemas.android.com/apk/res/android">
- <uses-permission android:name="android.permission.USE_BIOMETRIC" />
</manifest>
\ No newline at end of file
diff --git a/security/crypto/src/main/java/androidx/security/SecureConfig.java b/security/crypto/src/main/java/androidx/security/SecureConfig.java
deleted file mode 100644
index 8dc6ba9..0000000
--- a/security/crypto/src/main/java/androidx/security/SecureConfig.java
+++ /dev/null
@@ -1,1016 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package androidx.security;
-
-import android.security.keystore.KeyProperties;
-
-import androidx.annotation.NonNull;
-import androidx.security.biometric.BiometricKeyAuthCallback;
-import androidx.security.config.TrustAnchorOptions;
-
-/**
- * Class that defines constants used by the library. Includes predefined configurations for:
- *
- * Default:
- * SecureConfig.getDefault() provides a good basic security configuration for encrypting data
- * both in transit and at rest.
- *
- * NIAP:
- * For government use, SecureConfig.getNiapConfig(biometric auth) which returns compliant security
- * settings for NIAP use cases.
- *
- *
- */
-public class SecureConfig {
-
- public static final String ANDROID_KEYSTORE = "AndroidKeyStore";
- public static final String ANDROID_CA_STORE = "AndroidCAStore";
- public static final int AES_IV_SIZE_BYTES = 16;
- public static final String SSL_TLS = "TLS";
-
- String mAndroidKeyStore;
- String mAndroidCAStore;
- String mKeystoreType;
-
- // Asymmetric Encryption Constants
- String mAsymmetricKeyPairAlgorithm;
- int mAsymmetricKeySize;
- String mAsymmetricCipherTransformation;
- String mAsymmetricBlockModes;
- String mAsymmetricPaddings;
- int mAsymmetricKeyPurposes;
- // Sets KeyGenBuilder#setUnlockedDeviceRequired to true, requires Android 9 Pie.
- boolean mAsymmetricSensitiveDataProtection;
- boolean mAsymmetricRequireUserAuth;
- int mAsymmetricRequireUserValiditySeconds;
- String mAsymmetricDigests;
-
- // Symmetric Encryption Constants
- String mSymmetricKeyAlgorithm;
- String mSymmetricBlockModes;
- String mSymmetricPaddings;
- int mSymmetricKeySize;
- int mSymmetricGcmTagLength;
- int mSymmetricKeyPurposes;
- String mSymmetricCipherTransformation;
- // Sets KeyGenBuilder#setUnlockedDeviceRequired to true, requires Android 9 Pie.
- boolean mSymmetricSensitiveDataProtection;
- boolean mSymmetricRequireUserAuth;
- int mSymmetricRequireUserValiditySeconds;
- private String mSymmetricDigests;
-
- // Certificate Constants
- String mCertPath;
- String mCertPathValidator;
- boolean mUseStrongSSLCiphers;
- String[] mStrongSSLCiphers;
- String[] mClientCertAlgorithms;
- TrustAnchorOptions mTrustAnchorOptions;
-
- BiometricKeyAuthCallback mBiometricKeyAuthCallback;
-
- String mSignatureAlgorithm;
-
- SecureConfig() {
- }
-
- /**
- * SecureConfig.Builder configures SecureConfig.
- */
- public static class Builder {
-
- public Builder() {
- }
-
- // Keystore Constants
- String mAndroidKeyStore;
- String mAndroidCAStore;
- String mKeystoreType;
-
- // Asymmetric Encryption Constants
- String mAsymmetricKeyPairAlgorithm;
- int mAsymmetricKeySize;
- String mAsymmetricCipherTransformation;
- int mAsymmetricKeyPurposes;
- String mAsymmetricBlockModes;
- String mAsymmetricPaddings;
- boolean mAsymmetricSensitiveDataProtection;
- boolean mAsymmetricRequireUserAuth;
- int mAsymmetricRequireUserValiditySeconds;
-
- /**
- * Sets the keystore type
- *
- * @param keystoreType the KeystoreType to set
- * @return
- */
- @NonNull
- public Builder forKeyStoreType(@NonNull String keystoreType) {
- this.mKeystoreType = keystoreType;
- return this;
- }
-
- /**
- * Sets the key pair algorithm.
- *
- * @param keyPairAlgorithm the key pair algorithm
- * @return The configured builder
- */
- @NonNull
- public Builder setAsymmetricKeyPairAlgorithm(@NonNull String keyPairAlgorithm) {
- this.mAsymmetricKeyPairAlgorithm = keyPairAlgorithm;
- return this;
- }
-
- /**
- * @param keySize
- * @return The configured builder
- */
- @NonNull
- public Builder setAsymmetricKeySize(int keySize) {
- this.mAsymmetricKeySize = keySize;
- return this;
- }
-
- /**
- * @param cipherTransformation
- * @return The configured builder
- */
- @NonNull
- public Builder setAsymmetricCipherTransformation(@NonNull String cipherTransformation) {
- this.mAsymmetricCipherTransformation = cipherTransformation;
- return this;
- }
-
- /**
- * @param purposes
- * @return The configured builder
- */
- @NonNull
- public Builder setAsymmetricKeyPurposes(int purposes) {
- this.mAsymmetricKeyPurposes = purposes;
- return this;
- }
-
- /**
- * @param blockModes
- * @return The configured builder
- */
- @NonNull
- public Builder setAsymmetricBlockModes(@NonNull String blockModes) {
- this.mAsymmetricBlockModes = blockModes;
- return this;
- }
-
- /**
- * @param paddings
- * @return The configured builder
- */
- @NonNull
- public Builder setAsymmetricPaddings(@NonNull String paddings) {
- this.mAsymmetricPaddings = paddings;
- return this;
- }
-
- /**
- * @param dataProtection
- * @return The configured builder
- */
- @NonNull
- public Builder setAsymmetricSensitiveDataProtection(boolean dataProtection) {
- this.mAsymmetricSensitiveDataProtection = dataProtection;
- return this;
- }
-
- /**
- * @param userAuth
- * @return The configured builder
- */
- @NonNull
- public Builder setAsymmetricRequireUserAuth(boolean userAuth) {
- this.mAsymmetricRequireUserAuth = userAuth;
- return this;
- }
-
- /**
- * @param authValiditySeconds
- * @return The configured builder
- */
- @NonNull
- public Builder setAsymmetricRequireUserValiditySeconds(int authValiditySeconds) {
- this.mAsymmetricRequireUserValiditySeconds = authValiditySeconds;
- return this;
- }
-
- // Symmetric Encryption Constants
- String mSymmetricKeyAlgorithm;
- String mSymmetricBlockModes;
- String mSymmetricPaddings;
- int mSymmetricKeySize;
- int mSymmetricGcmTagLength;
- int mSymmetricKeyPurposes;
- String mSymmetricCipherTransformation;
- boolean mSymmetricSensitiveDataProtection;
- boolean mSymmetricRequireUserAuth;
- int mSymmetricRequireUserValiditySeconds;
-
- /**
- * @param keyAlgorithm
- * @return The configured builder
- */
- @NonNull
- public Builder setSymmetricKeyAlgorithm(@NonNull String keyAlgorithm) {
- this.mSymmetricKeyAlgorithm = keyAlgorithm;
- return this;
- }
-
- /**
- * @param keySize
- * @return The configured builder
- */
- @NonNull
- public Builder setSymmetricKeySize(int keySize) {
- this.mSymmetricKeySize = keySize;
- return this;
- }
-
- /**
- * @param cipherTransformation
- * @return The configured builder
- */
- @NonNull
- public Builder setSymmetricCipherTransformation(@NonNull String cipherTransformation) {
- this.mSymmetricCipherTransformation = cipherTransformation;
- return this;
- }
-
- /**
- * @param purposes
- * @return The configured builder
- */
- @NonNull
- public Builder setSymmetricKeyPurposes(int purposes) {
- this.mSymmetricKeyPurposes = purposes;
- return this;
- }
-
- /**
- * @param gcmTagLength
- * @return The configured builder
- */
- @NonNull
- public Builder setSymmetricGcmTagLength(int gcmTagLength) {
- this.mSymmetricGcmTagLength = gcmTagLength;
- return this;
- }
-
- /**
- * @param blockModes
- * @return The configured builder
- */
- @NonNull
- public Builder setSymmetricBlockModes(@NonNull String blockModes) {
- this.mSymmetricBlockModes = blockModes;
- return this;
- }
-
- /**
- * @param paddings
- * @return The configured builder
- */
- @NonNull
- public Builder setSymmetricPaddings(@NonNull String paddings) {
- this.mSymmetricPaddings = paddings;
- return this;
- }
-
- /**
- * @param dataProtection
- * @return The configured builder
- */
- @NonNull
- public Builder setSymmetricSensitiveDataProtection(boolean dataProtection) {
- this.mSymmetricSensitiveDataProtection = dataProtection;
- return this;
- }
-
- /**
- * @param userAuth
- * @return The configured builder
- */
- @NonNull
- public Builder setSymmetricRequireUserAuth(boolean userAuth) {
- this.mSymmetricRequireUserAuth = userAuth;
- return this;
- }
-
- /**
- * @param authValiditySeconds
- * @return The configured builder
- */
- @NonNull
- public Builder setSymmetricRequireUserValiditySeconds(int authValiditySeconds) {
- this.mSymmetricRequireUserValiditySeconds = authValiditySeconds;
- return this;
- }
-
- // Certificate Constants
- String mCertPath;
- String mCertPathValidator;
- boolean mUseStrongSSLCiphers;
- String[] mStrongSSLCiphers;
- String[] mClientCertAlgorithms;
- TrustAnchorOptions mAnchorOptions;
- BiometricKeyAuthCallback mBiometricKeyAuthCallback;
- String mSignatureAlgorithm;
-
- /**
- * @param certPath
- * @return The configured builder
- */
- @NonNull
- public Builder setCertPath(@NonNull String certPath) {
- this.mCertPath = certPath;
- return this;
- }
-
- /**
- * @param certPathValidator
- * @return The configured builder
- */
- @NonNull
- public Builder setCertPathValidator(@NonNull String certPathValidator) {
- this.mCertPathValidator = certPathValidator;
- return this;
- }
-
- /**
- * @param strongSSLCiphers
- * @return The configured builder
- */
- @NonNull
- public Builder setUseStrongSSLCiphers(boolean strongSSLCiphers) {
- this.mUseStrongSSLCiphers = strongSSLCiphers;
- return this;
- }
-
- /**
- * @param strongSSLCiphers
- * @return The configured builder
- */
- @NonNull
- public Builder setStrongSSLCiphers(@NonNull String[] strongSSLCiphers) {
- this.mStrongSSLCiphers = strongSSLCiphers;
- return this;
- }
-
- /**
- * @param clientCertAlgorithms
- * @return The configured builder
- */
- @NonNull
- public Builder setClientCertAlgorithms(@NonNull String[] clientCertAlgorithms) {
- this.mClientCertAlgorithms = clientCertAlgorithms;
- return this;
- }
-
- /**
- * @param trustAnchorOptions
- * @return The configured builder
- */
- @NonNull
- public Builder setTrustAnchorOptions(@NonNull TrustAnchorOptions trustAnchorOptions) {
- this.mAnchorOptions = trustAnchorOptions;
- return this;
- }
-
- /**
- * @param biometricKeyAuthCallback
- * @return The configured builder
- */
- @NonNull
- public Builder setBiometricKeyAuthCallback(
- @NonNull BiometricKeyAuthCallback biometricKeyAuthCallback) {
- this.mBiometricKeyAuthCallback = biometricKeyAuthCallback;
- return this;
- }
-
- /**
- * @param signatureAlgorithm
- * @return The configured builder
- */
- @NonNull
- public Builder setSignatureAlgorithm(
- @NonNull String signatureAlgorithm) {
- this.mSignatureAlgorithm = signatureAlgorithm;
- return this;
- }
-
-
-
- /**
- * @return The configured builder
- */
- @NonNull
- public SecureConfig build() {
- SecureConfig secureConfig = new SecureConfig();
- secureConfig.mAndroidKeyStore = this.mAndroidKeyStore;
- secureConfig.mAndroidCAStore = this.mAndroidCAStore;
- secureConfig.mKeystoreType = this.mKeystoreType;
-
- secureConfig.mAsymmetricKeyPairAlgorithm = this.mAsymmetricKeyPairAlgorithm;
- secureConfig.mAsymmetricKeySize = this.mAsymmetricKeySize;
- secureConfig.mAsymmetricCipherTransformation = this.mAsymmetricCipherTransformation;
- secureConfig.mAsymmetricKeyPurposes = this.mAsymmetricKeyPurposes;
- secureConfig.mAsymmetricBlockModes = this.mAsymmetricBlockModes;
- secureConfig.mAsymmetricPaddings = this.mAsymmetricPaddings;
- secureConfig.mAsymmetricSensitiveDataProtection =
- this.mAsymmetricSensitiveDataProtection;
- secureConfig.mAsymmetricRequireUserAuth = this.mAsymmetricRequireUserAuth;
- secureConfig.mAsymmetricRequireUserValiditySeconds =
- this.mAsymmetricRequireUserValiditySeconds;
-
- secureConfig.mSymmetricKeyAlgorithm = this.mSymmetricKeyAlgorithm;
- secureConfig.mSymmetricBlockModes = this.mSymmetricBlockModes;
- secureConfig.mSymmetricPaddings = this.mSymmetricPaddings;
- secureConfig.mSymmetricKeySize = this.mSymmetricKeySize;
- secureConfig.mSymmetricGcmTagLength = this.mSymmetricGcmTagLength;
- secureConfig.mSymmetricKeyPurposes = this.mSymmetricKeyPurposes;
- secureConfig.mSymmetricCipherTransformation = this.mSymmetricCipherTransformation;
- secureConfig.mSymmetricSensitiveDataProtection =
- this.mSymmetricSensitiveDataProtection;
- secureConfig.mSymmetricRequireUserAuth = this.mSymmetricRequireUserAuth;
- secureConfig.mSymmetricRequireUserValiditySeconds =
- this.mSymmetricRequireUserValiditySeconds;
-
- secureConfig.mCertPath = this.mCertPath;
- secureConfig.mCertPathValidator = this.mCertPathValidator;
- secureConfig.mUseStrongSSLCiphers = this.mUseStrongSSLCiphers;
- secureConfig.mStrongSSLCiphers = this.mStrongSSLCiphers;
- secureConfig.mClientCertAlgorithms = this.mClientCertAlgorithms;
- secureConfig.mTrustAnchorOptions = this.mAnchorOptions;
- secureConfig.mBiometricKeyAuthCallback = this.mBiometricKeyAuthCallback;
- secureConfig.mSignatureAlgorithm = this.mSignatureAlgorithm;
- return secureConfig;
- }
- }
-
- /**
- * @return A NIAP compliant configuration.
- */
- @NonNull
- public static SecureConfig getNiapConfig() {
- return getNiapConfig(null);
- }
-
- /**
- * @return A default configuration with for consumer applications.
- */
- @NonNull
- public static SecureConfig getDefault() {
- SecureConfig.Builder builder = new SecureConfig.Builder();
- builder.mAndroidKeyStore = SecureConfig.ANDROID_KEYSTORE;
- builder.mAndroidCAStore = SecureConfig.ANDROID_CA_STORE;
- builder.mKeystoreType = "PKCS12";
-
- builder.mAsymmetricKeyPairAlgorithm = KeyProperties.KEY_ALGORITHM_RSA;
- builder.mAsymmetricKeySize = 2048;
- builder.mAsymmetricCipherTransformation = "RSA/ECB/PKCS1Padding";
- builder.mAsymmetricBlockModes = KeyProperties.BLOCK_MODE_ECB;
- builder.mAsymmetricPaddings = KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1;
- builder.mAsymmetricKeyPurposes = KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_SIGN;
- builder.mAsymmetricSensitiveDataProtection = false;
- builder.mAsymmetricRequireUserAuth = false;
- builder.mAsymmetricRequireUserValiditySeconds = -1;
-
- builder.mSymmetricKeyAlgorithm = KeyProperties.KEY_ALGORITHM_AES;
- builder.mSymmetricBlockModes = KeyProperties.BLOCK_MODE_GCM;
- builder.mSymmetricPaddings = KeyProperties.ENCRYPTION_PADDING_NONE;
- builder.mSymmetricKeySize = 256;
- builder.mSymmetricGcmTagLength = 128;
- builder.mSymmetricKeyPurposes =
- KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT;
- builder.mSymmetricCipherTransformation = "AES/GCM/NoPadding";
- builder.mSymmetricSensitiveDataProtection = false;
- builder.mSymmetricRequireUserAuth = false;
- builder.mSymmetricRequireUserValiditySeconds = -1;
-
- builder.mCertPath = "X.509";
- builder.mCertPathValidator = "PKIX";
- builder.mUseStrongSSLCiphers = false;
- builder.mStrongSSLCiphers = null;
- builder.mClientCertAlgorithms = new String[]{"RSA"};
- builder.mAnchorOptions = TrustAnchorOptions.USER_SYSTEM;
- builder.mBiometricKeyAuthCallback = null;
- builder.mSignatureAlgorithm = "SHA256withECDSA";
-
- return builder.build();
- }
-
- /**
- * Create a Niap compliant configuration
- *
- * -Insert Link to Spec
- *
- * @param biometricKeyAuthCallback
- * @return The NIAP compliant configuration
- */
- @NonNull
- public static SecureConfig getNiapConfig(
- @NonNull BiometricKeyAuthCallback biometricKeyAuthCallback) {
- SecureConfig.Builder builder = new SecureConfig.Builder();
- builder.mAndroidKeyStore = SecureConfig.ANDROID_KEYSTORE;
- builder.mAndroidCAStore = SecureConfig.ANDROID_CA_STORE;
- builder.mKeystoreType = "PKCS12";
-
- builder.mAsymmetricKeyPairAlgorithm = KeyProperties.KEY_ALGORITHM_RSA;
- builder.mAsymmetricKeySize = 4096;
- builder.mAsymmetricCipherTransformation = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
- builder.mAsymmetricBlockModes = KeyProperties.BLOCK_MODE_ECB;
- builder.mAsymmetricPaddings = KeyProperties.ENCRYPTION_PADDING_RSA_OAEP;
- builder.mAsymmetricKeyPurposes = KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_SIGN;
- builder.mAsymmetricSensitiveDataProtection = true;
- builder.mAsymmetricRequireUserAuth = true;
- builder.mAsymmetricRequireUserValiditySeconds = -1;
-
- builder.mSymmetricKeyAlgorithm = KeyProperties.KEY_ALGORITHM_AES;
- builder.mSymmetricBlockModes = KeyProperties.BLOCK_MODE_GCM;
- builder.mSymmetricPaddings = KeyProperties.ENCRYPTION_PADDING_NONE;
- builder.mSymmetricKeySize = 256;
- builder.mSymmetricGcmTagLength = 128;
- builder.mSymmetricKeyPurposes =
- KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT;
- builder.mSymmetricCipherTransformation = "AES/GCM/NoPadding";
- builder.mSymmetricSensitiveDataProtection = true;
- builder.mSymmetricRequireUserAuth = true;
- builder.mSymmetricRequireUserValiditySeconds = -1;
-
- builder.mCertPath = "X.509";
- builder.mCertPathValidator = "PKIX";
- builder.mUseStrongSSLCiphers = false;
- builder.mStrongSSLCiphers = new String[]{
- "TLS_RSA_WITH_AES_128_CBC_SHA256",
- "TLS_RSA_WITH_AES_256_CBC_SHA256",
- "TLS_RSA_WITH_AES_128_GCM_SHA256",
- "TLS_RSA_WITH_AES_256_GCM_SHA384",
- "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
- "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
- "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
- "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
- "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
- "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
- "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
- };
- builder.mClientCertAlgorithms = new String[]{"RSA"};
- builder.mAnchorOptions = TrustAnchorOptions.USER_SYSTEM;
- builder.mBiometricKeyAuthCallback = biometricKeyAuthCallback;
- builder.mSignatureAlgorithm = "SHA512withRSA/PSS";
-
- return builder.build();
- }
-
- /**
- * @return
- */
- @NonNull
- public String getAndroidKeyStore() {
- return mAndroidKeyStore;
- }
-
- public void setAndroidKeyStore(@NonNull String androidKeyStore) {
- this.mAndroidKeyStore = androidKeyStore;
- }
-
- /**
- * @return
- */
- @NonNull
- public String getAndroidCAStore() {
- return mAndroidCAStore;
- }
-
- public void setAndroidCAStore(@NonNull String androidCAStore) {
- this.mAndroidCAStore = androidCAStore;
- }
-
- /**
- * @return
- */
- @NonNull
- public String getKeystoreType() {
- return mKeystoreType;
- }
-
- /**
- * @param keystoreType
- */
- @NonNull
- public void setKeystoreType(@NonNull String keystoreType) {
- this.mKeystoreType = keystoreType;
- }
-
- /**
- * @return
- */
- @NonNull
- public String getAsymmetricKeyPairAlgorithm() {
- return mAsymmetricKeyPairAlgorithm;
- }
-
- /**
- * @param asymmetricKeyPairAlgorithm
- */
- public void setAsymmetricKeyPairAlgorithm(@NonNull String asymmetricKeyPairAlgorithm) {
- this.mAsymmetricKeyPairAlgorithm = asymmetricKeyPairAlgorithm;
- }
-
- /**
- * @return
- */
- public int getAsymmetricKeySize() {
- return mAsymmetricKeySize;
- }
-
- /**
- * @param asymmetricKeySize
- */
- public void setAsymmetricKeySize(int asymmetricKeySize) {
- this.mAsymmetricKeySize = asymmetricKeySize;
- }
-
- /**
- * @return
- */
- @NonNull
- public String getAsymmetricCipherTransformation() {
- return mAsymmetricCipherTransformation;
- }
-
- /**
- * @param asymmetricCipherTransformation
- */
- public void setAsymmetricCipherTransformation(@NonNull String asymmetricCipherTransformation) {
- this.mAsymmetricCipherTransformation = asymmetricCipherTransformation;
- }
-
- /**
- * @return
- */
- @NonNull
- public String getAsymmetricBlockModes() {
- return mAsymmetricBlockModes;
- }
-
- /**
- * @param asymmetricBlockModes
- */
- public void setAsymmetricBlockModes(@NonNull String asymmetricBlockModes) {
- this.mAsymmetricBlockModes = asymmetricBlockModes;
- }
-
- /**
- * @return
- */
- @NonNull
- public String getAsymmetricPaddings() {
- return mAsymmetricPaddings;
- }
-
- /**
- * @param asymmetricPaddings
- */
- public void setAsymmetricPaddings(@NonNull String asymmetricPaddings) {
- this.mAsymmetricPaddings = asymmetricPaddings;
- }
-
- /**
- * @return
- */
- public int getAsymmetricKeyPurposes() {
- return mAsymmetricKeyPurposes;
- }
-
- /**
- * @param asymmetricKeyPurposes
- */
- public void setAsymmetricKeyPurposes(int asymmetricKeyPurposes) {
- this.mAsymmetricKeyPurposes = asymmetricKeyPurposes;
- }
-
- /**
- * @return
- */
- public boolean getAsymmetricSensitiveDataProtectionEnabled() {
- return mAsymmetricSensitiveDataProtection;
- }
-
- /**
- * @param asymmetricSensitiveDataProtection
- */
- public void setAsymmetricSensitiveDataProtection(boolean asymmetricSensitiveDataProtection) {
- this.mAsymmetricSensitiveDataProtection = asymmetricSensitiveDataProtection;
- }
-
- /**
- * @return
- */
- public boolean getAsymmetricRequireUserAuthEnabled() {
- return mAsymmetricRequireUserAuth && mBiometricKeyAuthCallback != null;
- }
-
- /**
- * @param requireUserAuth
- */
- public void setAsymmetricRequireUserAuth(boolean requireUserAuth) {
- this.mAsymmetricRequireUserAuth = requireUserAuth;
- }
-
- /**
- * @return
- */
- public int getAsymmetricRequireUserValiditySeconds() {
- return this.mAsymmetricRequireUserValiditySeconds;
- }
-
- /**
- * @param userValiditySeconds
- */
- public void setAsymmetricRequireUserValiditySeconds(int userValiditySeconds) {
- this.mAsymmetricRequireUserValiditySeconds = userValiditySeconds;
- }
-
- /**
- * @return
- */
- @NonNull
- public String getSymmetricKeyAlgorithm() {
- return mSymmetricKeyAlgorithm;
- }
-
- /**
- * @param symmetricKeyAlgorithm
- */
- public void setSymmetricKeyAlgorithm(@NonNull String symmetricKeyAlgorithm) {
- this.mSymmetricKeyAlgorithm = symmetricKeyAlgorithm;
- }
-
- /**
- * @return
- */
- @NonNull
- public String getSymmetricBlockModes() {
- return mSymmetricBlockModes;
- }
-
- /**
- * @param symmetricBlockModes
- */
- public void setSymmetricBlockModes(@NonNull String symmetricBlockModes) {
- this.mSymmetricBlockModes = symmetricBlockModes;
- }
-
- /**
- * @return
- */
- @NonNull
- public String getSymmetricPaddings() {
- return mSymmetricPaddings;
- }
-
- /**
- * @param symmetricPaddings
- */
- public void setSymmetricPaddings(@NonNull String symmetricPaddings) {
- this.mSymmetricPaddings = symmetricPaddings;
- }
-
- /**
- * @return
- */
- public int getSymmetricKeySize() {
- return mSymmetricKeySize;
- }
-
- /**
- * @param symmetricKeySize
- */
- public void setSymmetricKeySize(int symmetricKeySize) {
- this.mSymmetricKeySize = symmetricKeySize;
- }
-
- /**
- * @return
- */
- public int getSymmetricGcmTagLength() {
- return mSymmetricGcmTagLength;
- }
-
- /**
- * @param symmetricGcmTagLength
- */
- public void setSymmetricGcmTagLength(int symmetricGcmTagLength) {
- this.mSymmetricGcmTagLength = symmetricGcmTagLength;
- }
-
- /**
- * @return
- */
- public int getSymmetricKeyPurposes() {
- return mSymmetricKeyPurposes;
- }
-
- /**
- * @param symmetricKeyPurposes
- */
- public void setSymmetricKeyPurposes(int symmetricKeyPurposes) {
- this.mSymmetricKeyPurposes = symmetricKeyPurposes;
- }
-
- /**
- * @return
- */
- @NonNull
- public String getSymmetricCipherTransformation() {
- return mSymmetricCipherTransformation;
- }
-
- /**
- * @param symmetricCipherTransformation
- */
- public void setSymmetricCipherTransformation(@NonNull String symmetricCipherTransformation) {
- this.mSymmetricCipherTransformation = symmetricCipherTransformation;
- }
-
- /**
- * @return
- */
- public boolean getSymmetricSensitiveDataProtectionEnabled() {
- return mSymmetricSensitiveDataProtection;
- }
-
- /**
- * @param symmetricSensitiveDataProtection
- */
- public void setSymmetricSensitiveDataProtection(boolean symmetricSensitiveDataProtection) {
- this.mSymmetricSensitiveDataProtection = symmetricSensitiveDataProtection;
- }
-
- public boolean getSymmetricRequireUserAuthEnabled() {
- return mSymmetricRequireUserAuth && mBiometricKeyAuthCallback != null;
- }
-
- /**
- * @param requireUserAuth
- */
- public void setSymmetricRequireUserAuth(boolean requireUserAuth) {
- this.mSymmetricRequireUserAuth = requireUserAuth;
- }
-
- /**
- * @return
- */
- public int getSymmetricRequireUserValiditySeconds() {
- return this.mSymmetricRequireUserValiditySeconds;
- }
-
- /**
- * @param userValiditySeconds
- */
- public void setSymmetricRequireUserValiditySeconds(int userValiditySeconds) {
- this.mSymmetricRequireUserValiditySeconds = userValiditySeconds;
- }
-
- /**
- * @return
- */
- @NonNull
- public String getCertPath() {
- return mCertPath;
- }
-
- public void setCertPath(@NonNull String certPath) {
- this.mCertPath = certPath;
- }
-
- /**
- * @return
- */
- @NonNull
- public String getCertPathValidator() {
- return mCertPathValidator;
- }
-
- /**
- * @param certPathValidator
- */
- public void setCertPathValidator(@NonNull String certPathValidator) {
- this.mCertPathValidator = certPathValidator;
- }
-
- /**
- * @return
- */
- public boolean getUseStrongSSLCiphersEnabled() {
- return mUseStrongSSLCiphers;
- }
-
- /**
- * @param useStrongSSLCiphers
- */
- public void setUseStrongSSLCiphers(boolean useStrongSSLCiphers) {
- this.mUseStrongSSLCiphers = useStrongSSLCiphers;
- }
-
- public boolean getUseStrongSSLCiphers() {
- return mUseStrongSSLCiphers;
- }
-
- /**
- * @return
- */
- @NonNull
- public String[] getStrongSSLCiphers() {
- return mStrongSSLCiphers;
- }
-
- /**
- * @param strongSSLCiphers
- */
- public void setStrongSSLCiphers(@NonNull String[] strongSSLCiphers) {
- this.mStrongSSLCiphers = strongSSLCiphers;
- }
-
- /**
- * @return
- */
- @NonNull
- public String[] getClientCertAlgorithms() {
- return mClientCertAlgorithms;
- }
-
- public void setClientCertAlgorithms(@NonNull String[] clientCertAlgorithms) {
- this.mClientCertAlgorithms = clientCertAlgorithms;
- }
-
- /**
- * @return
- */
- @NonNull
- public TrustAnchorOptions getTrustAnchorOptions() {
- return mTrustAnchorOptions;
- }
-
- /**
- * @param trustAnchorOptions
- */
- public void setTrustAnchorOptions(@NonNull TrustAnchorOptions trustAnchorOptions) {
- this.mTrustAnchorOptions = trustAnchorOptions;
- }
-
- /**
- * @return
- */
- @NonNull
- public BiometricKeyAuthCallback getBiometricKeyAuthCallback() {
- return mBiometricKeyAuthCallback;
- }
-
- /**
- * @param biometricKeyAuthCallback
- */
- public void setBiometricKeyAuthCallback(
- @NonNull BiometricKeyAuthCallback biometricKeyAuthCallback) {
- this.mBiometricKeyAuthCallback = biometricKeyAuthCallback;
- }
-
- /**
- * @return
- */
- @NonNull
- public String getSignatureAlgorithm() {
- return mSignatureAlgorithm;
- }
-
- /**
- * @param signatureAlgorithm
- */
- public void setSignatureAlgorithm(
- @NonNull String signatureAlgorithm) {
- this.mSignatureAlgorithm = signatureAlgorithm;
- }
-}
diff --git a/security/crypto/src/main/java/androidx/security/biometric/BiometricKeyAuth.java b/security/crypto/src/main/java/androidx/security/biometric/BiometricKeyAuth.java
deleted file mode 100644
index f15fbb3..0000000
--- a/security/crypto/src/main/java/androidx/security/biometric/BiometricKeyAuth.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.security.biometric;
-
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.biometric.BiometricPrompt;
-import androidx.fragment.app.FragmentActivity;
-import androidx.security.crypto.SecureCipher;
-
-import java.security.Signature;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executors;
-
-import javax.crypto.Cipher;
-
-/**
- * Class that handles authenticating Ciphers using Biometric Prompt.
- */
-public class BiometricKeyAuth extends BiometricPrompt.AuthenticationCallback {
-
- private static final String TAG = "BiometricKeyAuth";
-
- private FragmentActivity mActivity;
- private SecureCipher.SecureAuthListener mSecureAuthListener;
- private CountDownLatch mCountDownLatch = null;
- private BiometricKeyAuthCallback mBiometricKeyAuthCallback;
-
- /**
- * @param activity The activity to use a parent
- * @param callback Callback to reply with when complete.
- */
- public BiometricKeyAuth(@NonNull FragmentActivity activity,
- @NonNull BiometricKeyAuthCallback callback) {
- this.mActivity = activity;
- this.mBiometricKeyAuthCallback = callback;
- }
-
- @Override
- public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
- super.onAuthenticationSucceeded(result);
- mBiometricKeyAuthCallback.onAuthenticationSucceeded();
- Log.i(TAG, "Fingerprint success!");
- mSecureAuthListener.authComplete(BiometricKeyAuthCallback.BiometricStatus.SUCCESS);
- mCountDownLatch.countDown();
- }
-
- @Override
- public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
- super.onAuthenticationError(errorCode, errString);
- mBiometricKeyAuthCallback.onMessage(String.valueOf(errString));
- mBiometricKeyAuthCallback.onAuthenticationError(errorCode, errString);
- mSecureAuthListener.authComplete(BiometricKeyAuthCallback.BiometricStatus.FAILED);
- mCountDownLatch.countDown();
- }
-
- @Override
- public void onAuthenticationFailed() {
- super.onAuthenticationFailed();
- mBiometricKeyAuthCallback.onAuthenticationFailed();
- mSecureAuthListener.authComplete(BiometricKeyAuthCallback.BiometricStatus.FAILED);
- mCountDownLatch.countDown();
- }
-
- /**
- * Authenticates a key, via the Cipher it's used with
- *
- * @param cipher The cipher to authenticate
- * @param promptInfo The prompt info for the auth fragment
- * @param listener the listener to call back when complete
- */
- public void authenticateKey(@NonNull Cipher cipher,
- @NonNull BiometricPrompt.PromptInfo promptInfo,
- @NonNull SecureCipher.SecureAuthListener listener) {
- authenticateKeyObject(cipher, promptInfo, listener);
- }
-
- /**
- * Authenticates a key, via the Cipher it's used with
- *
- * @param signature The signature to authenticate
- * @param promptInfo The prompt info for the auth fragment
- * @param listener the listener to call back when complete
- */
- public void authenticateKey(@NonNull Signature signature,
- @NonNull BiometricPrompt.PromptInfo promptInfo,
- @NonNull SecureCipher.SecureAuthListener listener) {
- authenticateKeyObject(signature, promptInfo, listener);
- }
-
- private void authenticateKeyObject(Object crypto,
- BiometricPrompt.PromptInfo promptInfo,
- SecureCipher.SecureAuthListener listener) {
- mCountDownLatch = new CountDownLatch(1);
- mSecureAuthListener = listener;
-
- BiometricPrompt prompt = new BiometricPrompt(mActivity,
- Executors.newSingleThreadExecutor(),
- this);
- BiometricPrompt.CryptoObject cryptoObject;
- if (crypto instanceof Cipher) {
- cryptoObject = new BiometricPrompt.CryptoObject((Cipher) crypto);
- } else { /*if(crypto instanceof Signature) {*/
- cryptoObject = new BiometricPrompt.CryptoObject((Signature) crypto);
- }
- prompt.authenticate(promptInfo, cryptoObject);
- try {
- mCountDownLatch.await();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- /**
- * Authenticates a key, via the Cipher it's used with. Provides a default implementation of
- * PromptInfo.
- *
- * @param cipher The cipher to authenticate
- * @param listener the listener to call back when complete
- */
- public void authenticateKey(@NonNull Cipher cipher,
- @NonNull SecureCipher.SecureAuthListener listener) {
- authenticateKeyObject(cipher, listener);
- }
-
- /**
- * Authenticates a key, via the Signature it's used with. Provides a default implementation of
- * PromptInfo.
- *
- * @param signature The signature to authenticate
- * @param listener the listener to call back when complete
- */
- public void authenticateKey(@NonNull Signature signature,
- @NonNull SecureCipher.SecureAuthListener listener) {
- authenticateKeyObject(signature, listener);
- }
-
- private void authenticateKeyObject(Object crypto,
- @NonNull SecureCipher.SecureAuthListener listener) {
- authenticateKeyObject(crypto, new BiometricPrompt.PromptInfo.Builder()
- .setTitle("Please Auth for key usage.")
- .setSubtitle("Key used for encrypting files")
- .setDescription("User authentication required to access key.")
- .setNegativeButtonText("Cancel")
- .build(), listener);
- }
-
-}
diff --git a/security/crypto/src/main/java/androidx/security/biometric/BiometricKeyAuthCallback.java b/security/crypto/src/main/java/androidx/security/biometric/BiometricKeyAuthCallback.java
deleted file mode 100644
index ff60d31..0000000
--- a/security/crypto/src/main/java/androidx/security/biometric/BiometricKeyAuthCallback.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.security.biometric;
-
-import androidx.annotation.NonNull;
-import androidx.security.crypto.SecureCipher;
-
-import java.security.Signature;
-
-import javax.crypto.Cipher;
-
-
-
-/**
- * Callback for Biometric Key auth.
- *
- */
-public abstract class BiometricKeyAuthCallback {
-
- /**
- * Statuses of Biometric auth
- */
- public enum BiometricStatus {
- SUCCESS(0),
- FAILED(1),
- ERROR(2);
-
- private final int mType;
-
- BiometricStatus(int type) {
- this.mType = type;
- }
-
- /**
- * @return the mType
- */
- public int getType() {
- return this.mType;
- }
-
- /**
- * @return the status that matches the id
- */
- @NonNull
- public static BiometricStatus fromId(int id) {
- switch (id) {
- case 0:
- return SUCCESS;
- case 1:
- return FAILED;
- case 2:
- return ERROR;
- }
- return ERROR;
- }
- }
-
- /**
- *
- */
- public abstract void onAuthenticationSucceeded();
-
- /**
- *
- */
- public abstract void onAuthenticationError(int errorCode, @NonNull CharSequence errString);
-
- /**
- *
- */
- public abstract void onAuthenticationFailed();
-
- /**
- * @param message the message to send
- */
- public abstract void onMessage(@NonNull String message);
-
- /**
- * @param cipher The cipher to authenticate
- * @param listener The listener to call back
- */
- public abstract void authenticateKey(@NonNull Cipher cipher,
- @NonNull SecureCipher.SecureAuthListener listener);
-
- /**
- * @param signature The signature to authenticate
- * @param listener The listener to call back
- */
- public abstract void authenticateKey(@NonNull Signature signature,
- @NonNull SecureCipher.SecureAuthListener listener);
-
-}
diff --git a/security/crypto/src/main/java/androidx/security/config/TldConstants.java b/security/crypto/src/main/java/androidx/security/config/TldConstants.java
deleted file mode 100644
index d118cb5c..0000000
--- a/security/crypto/src/main/java/androidx/security/config/TldConstants.java
+++ /dev/null
@@ -1,1575 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package androidx.security.config;
-
-import androidx.annotation.NonNull;
-
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Class that contains all known TLDs.
- */
-public final class TldConstants {
-
- private TldConstants() {
- }
-
- @NonNull
- public static final List<String> VALID_TLDS = Arrays.asList(
- "*.AAA",
- "*.AARP",
- "*.ABARTH",
- "*.ABB",
- "*.ABBOTT",
- "*.ABBVIE",
- "*.ABC",
- "*.ABLE",
- "*.ABOGADO",
- "*.ABUDHABI",
- "*.AC",
- "*.ACADEMY",
- "*.ACCENTURE",
- "*.ACCOUNTANT",
- "*.ACCOUNTANTS",
- "*.ACO",
- "*.ACTIVE",
- "*.ACTOR",
- "*.AD",
- "*.ADAC",
- "*.ADS",
- "*.ADULT",
- "*.AE",
- "*.AEG",
- "*.AERO",
- "*.AETNA",
- "*.AF",
- "*.AFAMILYCOMPANY",
- "*.AFL",
- "*.AFRICA",
- "*.AG",
- "*.AGAKHAN",
- "*.AGENCY",
- "*.AI",
- "*.AIG",
- "*.AIGO",
- "*.AIRBUS",
- "*.AIRFORCE",
- "*.AIRTEL",
- "*.AKDN",
- "*.AL",
- "*.ALFAROMEO",
- "*.ALIBABA",
- "*.ALIPAY",
- "*.ALLFINANZ",
- "*.ALLSTATE",
- "*.ALLY",
- "*.ALSACE",
- "*.ALSTOM",
- "*.AM",
- "*.AMERICANEXPRESS",
- "*.AMERICANFAMILY",
- "*.AMEX",
- "*.AMFAM",
- "*.AMICA",
- "*.AMSTERDAM",
- "*.ANALYTICS",
- "*.ANDROID",
- "*.ANQUAN",
- "*.ANZ",
- "*.AO",
- "*.AOL",
- "*.APARTMENTS",
- "*.APP",
- "*.APPLE",
- "*.AQ",
- "*.AQUARELLE",
- "*.AR",
- "*.ARAB",
- "*.ARAMCO",
- "*.ARCHI",
- "*.ARMY",
- "*.ARPA",
- "*.ART",
- "*.ARTE",
- "*.AS",
- "*.ASDA",
- "*.ASIA",
- "*.ASSOCIATES",
- "*.AT",
- "*.ATHLETA",
- "*.ATTORNEY",
- "*.AU",
- "*.AUCTION",
- "*.AUDI",
- "*.AUDIBLE",
- "*.AUDIO",
- "*.AUSPOST",
- "*.AUTHOR",
- "*.AUTO",
- "*.AUTOS",
- "*.AVIANCA",
- "*.AW",
- "*.AWS",
- "*.AX",
- "*.AXA",
- "*.AZ",
- "*.AZURE",
- "*.BA",
- "*.BABY",
- "*.BAIDU",
- "*.BANAMEX",
- "*.BANANAREPUBLIC",
- "*.BAND",
- "*.BANK",
- "*.BAR",
- "*.BARCELONA",
- "*.BARCLAYCARD",
- "*.BARCLAYS",
- "*.BAREFOOT",
- "*.BARGAINS",
- "*.BASEBALL",
- "*.BASKETBALL",
- "*.BAUHAUS",
- "*.BAYERN",
- "*.BB",
- "*.BBC",
- "*.BBT",
- "*.BBVA",
- "*.BCG",
- "*.BCN",
- "*.BD",
- "*.BE",
- "*.BEATS",
- "*.BEAUTY",
- "*.BEER",
- "*.BENTLEY",
- "*.BERLIN",
- "*.BEST",
- "*.BESTBUY",
- "*.BET",
- "*.BF",
- "*.BG",
- "*.BH",
- "*.BHARTI",
- "*.BI",
- "*.BIBLE",
- "*.BID",
- "*.BIKE",
- "*.BING",
- "*.BINGO",
- "*.BIO",
- "*.BIZ",
- "*.BJ",
- "*.BLACK",
- "*.BLACKFRIDAY",
- "*.BLANCO",
- "*.BLOCKBUSTER",
- "*.BLOG",
- "*.BLOOMBERG",
- "*.BLUE",
- "*.BM",
- "*.BMS",
- "*.BMW",
- "*.BN",
- "*.BNL",
- "*.BNPPARIBAS",
- "*.BO",
- "*.BOATS",
- "*.BOEHRINGER",
- "*.BOFA",
- "*.BOM",
- "*.BOND",
- "*.BOO",
- "*.BOOK",
- "*.BOOKING",
- "*.BOSCH",
- "*.BOSTIK",
- "*.BOSTON",
- "*.BOT",
- "*.BOUTIQUE",
- "*.BOX",
- "*.BR",
- "*.BRADESCO",
- "*.BRIDGESTONE",
- "*.BROADWAY",
- "*.BROKER",
- "*.BROTHER",
- "*.BRUSSELS",
- "*.BS",
- "*.BT",
- "*.BUDAPEST",
- "*.BUGATTI",
- "*.BUILD",
- "*.BUILDERS",
- "*.BUSINESS",
- "*.BUY",
- "*.BUZZ",
- "*.BV",
- "*.BW",
- "*.BY",
- "*.BZ",
- "*.BZH",
- "*.CA",
- "*.CAB",
- "*.CAFE",
- "*.CAL",
- "*.CALL",
- "*.CALVINKLEIN",
- "*.CAM",
- "*.CAMERA",
- "*.CAMP",
- "*.CANCERRESEARCH",
- "*.CANON",
- "*.CAPETOWN",
- "*.CAPITAL",
- "*.CAPITALONE",
- "*.CAR",
- "*.CARAVAN",
- "*.CARDS",
- "*.CARE",
- "*.CAREER",
- "*.CAREERS",
- "*.CARS",
- "*.CARTIER",
- "*.CASA",
- "*.CASE",
- "*.CASEIH",
- "*.CASH",
- "*.CASINO",
- "*.CAT",
- "*.CATERING",
- "*.CATHOLIC",
- "*.CBA",
- "*.CBN",
- "*.CBRE",
- "*.CBS",
- "*.CC",
- "*.CD",
- "*.CEB",
- "*.CENTER",
- "*.CEO",
- "*.CERN",
- "*.CF",
- "*.CFA",
- "*.CFD",
- "*.CG",
- "*.CH",
- "*.CHANEL",
- "*.CHANNEL",
- "*.CHARITY",
- "*.CHASE",
- "*.CHAT",
- "*.CHEAP",
- "*.CHINTAI",
- "*.CHRISTMAS",
- "*.CHROME",
- "*.CHRYSLER",
- "*.CHURCH",
- "*.CI",
- "*.CIPRIANI",
- "*.CIRCLE",
- "*.CISCO",
- "*.CITADEL",
- "*.CITI",
- "*.CITIC",
- "*.CITY",
- "*.CITYEATS",
- "*.CK",
- "*.CL",
- "*.CLAIMS",
- "*.CLEANING",
- "*.CLICK",
- "*.CLINIC",
- "*.CLINIQUE",
- "*.CLOTHING",
- "*.CLOUD",
- "*.CLUB",
- "*.CLUBMED",
- "*.CM",
- "*.CN",
- "*.CO",
- "*.COACH",
- "*.CODES",
- "*.COFFEE",
- "*.COLLEGE",
- "*.COLOGNE",
- "*.COM",
- "*.COMCAST",
- "*.COMMBANK",
- "*.COMMUNITY",
- "*.COMPANY",
- "*.COMPARE",
- "*.COMPUTER",
- "*.COMSEC",
- "*.CONDOS",
- "*.CONSTRUCTION",
- "*.CONSULTING",
- "*.CONTACT",
- "*.CONTRACTORS",
- "*.COOKING",
- "*.COOKINGCHANNEL",
- "*.COOL",
- "*.COOP",
- "*.CORSICA",
- "*.COUNTRY",
- "*.COUPON",
- "*.COUPONS",
- "*.COURSES",
- "*.CR",
- "*.CREDIT",
- "*.CREDITCARD",
- "*.CREDITUNION",
- "*.CRICKET",
- "*.CROWN",
- "*.CRS",
- "*.CRUISE",
- "*.CRUISES",
- "*.CSC",
- "*.CU",
- "*.CUISINELLA",
- "*.CV",
- "*.CW",
- "*.CX",
- "*.CY",
- "*.CYMRU",
- "*.CYOU",
- "*.CZ",
- "*.DABUR",
- "*.DAD",
- "*.DANCE",
- "*.DATA",
- "*.DATE",
- "*.DATING",
- "*.DATSUN",
- "*.DAY",
- "*.DCLK",
- "*.DDS",
- "*.DE",
- "*.DEAL",
- "*.DEALER",
- "*.DEALS",
- "*.DEGREE",
- "*.DELIVERY",
- "*.DELL",
- "*.DELOITTE",
- "*.DELTA",
- "*.DEMOCRAT",
- "*.DENTAL",
- "*.DENTIST",
- "*.DESI",
- "*.DESIGN",
- "*.DEV",
- "*.DHL",
- "*.DIAMONDS",
- "*.DIET",
- "*.DIGITAL",
- "*.DIRECT",
- "*.DIRECTORY",
- "*.DISCOUNT",
- "*.DISCOVER",
- "*.DISH",
- "*.DIY",
- "*.DJ",
- "*.DK",
- "*.DM",
- "*.DNP",
- "*.DO",
- "*.DOCS",
- "*.DOCTOR",
- "*.DODGE",
- "*.DOG",
- "*.DOHA",
- "*.DOMAINS",
- "*.DOT",
- "*.DOWNLOAD",
- "*.DRIVE",
- "*.DTV",
- "*.DUBAI",
- "*.DUCK",
- "*.DUNLOP",
- "*.DUNS",
- "*.DUPONT",
- "*.DURBAN",
- "*.DVAG",
- "*.DVR",
- "*.DZ",
- "*.EARTH",
- "*.EAT",
- "*.EC",
- "*.ECO",
- "*.EDEKA",
- "*.EDU",
- "*.EDUCATION",
- "*.EE",
- "*.EG",
- "*.EMAIL",
- "*.EMERCK",
- "*.ENERGY",
- "*.ENGINEER",
- "*.ENGINEERING",
- "*.ENTERPRISES",
- "*.EPOST",
- "*.EPSON",
- "*.EQUIPMENT",
- "*.ER",
- "*.ERICSSON",
- "*.ERNI",
- "*.ES",
- "*.ESQ",
- "*.ESTATE",
- "*.ESURANCE",
- "*.ET",
- "*.ETISALAT",
- "*.EU",
- "*.EUROVISION",
- "*.EUS",
- "*.EVENTS",
- "*.EVERBANK",
- "*.EXCHANGE",
- "*.EXPERT",
- "*.EXPOSED",
- "*.EXPRESS",
- "*.EXTRASPACE",
- "*.FAGE",
- "*.FAIL",
- "*.FAIRWINDS",
- "*.FAITH",
- "*.FAMILY",
- "*.FAN",
- "*.FANS",
- "*.FARM",
- "*.FARMERS",
- "*.FASHION",
- "*.FAST",
- "*.FEDEX",
- "*.FEEDBACK",
- "*.FERRARI",
- "*.FERRERO",
- "*.FI",
- "*.FIAT",
- "*.FIDELITY",
- "*.FIDO",
- "*.FILM",
- "*.FINAL",
- "*.FINANCE",
- "*.FINANCIAL",
- "*.FIRE",
- "*.FIRESTONE",
- "*.FIRMDALE",
- "*.FISH",
- "*.FISHING",
- "*.FIT",
- "*.FITNESS",
- "*.FJ",
- "*.FK",
- "*.FLICKR",
- "*.FLIGHTS",
- "*.FLIR",
- "*.FLORIST",
- "*.FLOWERS",
- "*.FLY",
- "*.FM",
- "*.FO",
- "*.FOO",
- "*.FOOD",
- "*.FOODNETWORK",
- "*.FOOTBALL",
- "*.FORD",
- "*.FOREX",
- "*.FORSALE",
- "*.FORUM",
- "*.FOUNDATION",
- "*.FOX",
- "*.FR",
- "*.FREE",
- "*.FRESENIUS",
- "*.FRL",
- "*.FROGANS",
- "*.FRONTDOOR",
- "*.FRONTIER",
- "*.FTR",
- "*.FUJITSU",
- "*.FUJIXEROX",
- "*.FUN",
- "*.FUND",
- "*.FURNITURE",
- "*.FUTBOL",
- "*.FYI",
- "*.GA",
- "*.GAL",
- "*.GALLERY",
- "*.GALLO",
- "*.GALLUP",
- "*.GAME",
- "*.GAMES",
- "*.GAP",
- "*.GARDEN",
- "*.GB",
- "*.GBIZ",
- "*.GD",
- "*.GDN",
- "*.GE",
- "*.GEA",
- "*.GENT",
- "*.GENTING",
- "*.GEORGE",
- "*.GF",
- "*.GG",
- "*.GGEE",
- "*.GH",
- "*.GI",
- "*.GIFT",
- "*.GIFTS",
- "*.GIVES",
- "*.GIVING",
- "*.GL",
- "*.GLADE",
- "*.GLASS",
- "*.GLE",
- "*.GLOBAL",
- "*.GLOBO",
- "*.GM",
- "*.GMAIL",
- "*.GMBH",
- "*.GMO",
- "*.GMX",
- "*.GN",
- "*.GODADDY",
- "*.GOLD",
- "*.GOLDPOINT",
- "*.GOLF",
- "*.GOO",
- "*.GOODHANDS",
- "*.GOODYEAR",
- "*.GOOG",
- "*.GOOGLE",
- "*.GOP",
- "*.GOT",
- "*.GOV",
- "*.GP",
- "*.GQ",
- "*.GR",
- "*.GRAINGER",
- "*.GRAPHICS",
- "*.GRATIS",
- "*.GREEN",
- "*.GRIPE",
- "*.GROCERY",
- "*.GROUP",
- "*.GS",
- "*.GT",
- "*.GU",
- "*.GUARDIAN",
- "*.GUCCI",
- "*.GUGE",
- "*.GUIDE",
- "*.GUITARS",
- "*.GURU",
- "*.GW",
- "*.GY",
- "*.HAIR",
- "*.HAMBURG",
- "*.HANGOUT",
- "*.HAUS",
- "*.HBO",
- "*.HDFC",
- "*.HDFCBANK",
- "*.HEALTH",
- "*.HEALTHCARE",
- "*.HELP",
- "*.HELSINKI",
- "*.HERE",
- "*.HERMES",
- "*.HGTV",
- "*.HIPHOP",
- "*.HISAMITSU",
- "*.HITACHI",
- "*.HIV",
- "*.HK",
- "*.HKT",
- "*.HM",
- "*.HN",
- "*.HOCKEY",
- "*.HOLDINGS",
- "*.HOLIDAY",
- "*.HOMEDEPOT",
- "*.HOMEGOODS",
- "*.HOMES",
- "*.HOMESENSE",
- "*.HONDA",
- "*.HONEYWELL",
- "*.HORSE",
- "*.HOSPITAL",
- "*.HOST",
- "*.HOSTING",
- "*.HOT",
- "*.HOTELES",
- "*.HOTELS",
- "*.HOTMAIL",
- "*.HOUSE",
- "*.HOW",
- "*.HR",
- "*.HSBC",
- "*.HT",
- "*.HU",
- "*.HUGHES",
- "*.HYATT",
- "*.HYUNDAI",
- "*.IBM",
- "*.ICBC",
- "*.ICE",
- "*.ICU",
- "*.ID",
- "*.IE",
- "*.IEEE",
- "*.IFM",
- "*.IKANO",
- "*.IL",
- "*.IM",
- "*.IMAMAT",
- "*.IMDB",
- "*.IMMO",
- "*.IMMOBILIEN",
- "*.IN",
- "*.INC",
- "*.INDUSTRIES",
- "*.INFINITI",
- "*.INFO",
- "*.ING",
- "*.INK",
- "*.INSTITUTE",
- "*.INSURANCE",
- "*.INSURE",
- "*.INT",
- "*.INTEL",
- "*.INTERNATIONAL",
- "*.INTUIT",
- "*.INVESTMENTS",
- "*.IO",
- "*.IPIRANGA",
- "*.IQ",
- "*.IR",
- "*.IRISH",
- "*.IS",
- "*.ISELECT",
- "*.ISMAILI",
- "*.IST",
- "*.ISTANBUL",
- "*.IT",
- "*.ITAU",
- "*.ITV",
- "*.IVECO",
- "*.JAGUAR",
- "*.JAVA",
- "*.JCB",
- "*.JCP",
- "*.JE",
- "*.JEEP",
- "*.JETZT",
- "*.JEWELRY",
- "*.JIO",
- "*.JLC",
- "*.JLL",
- "*.JM",
- "*.JMP",
- "*.JNJ",
- "*.JO",
- "*.JOBS",
- "*.JOBURG",
- "*.JOT",
- "*.JOY",
- "*.JP",
- "*.JPMORGAN",
- "*.JPRS",
- "*.JUEGOS",
- "*.JUNIPER",
- "*.KAUFEN",
- "*.KDDI",
- "*.KE",
- "*.KERRYHOTELS",
- "*.KERRYLOGISTICS",
- "*.KERRYPROPERTIES",
- "*.KFH",
- "*.KG",
- "*.KH",
- "*.KI",
- "*.KIA",
- "*.KIM",
- "*.KINDER",
- "*.KINDLE",
- "*.KITCHEN",
- "*.KIWI",
- "*.KM",
- "*.KN",
- "*.KOELN",
- "*.KOMATSU",
- "*.KOSHER",
- "*.KP",
- "*.KPMG",
- "*.KPN",
- "*.KR",
- "*.KRD",
- "*.KRED",
- "*.KUOKGROUP",
- "*.KW",
- "*.KY",
- "*.KYOTO",
- "*.KZ",
- "*.LA",
- "*.LACAIXA",
- "*.LADBROKES",
- "*.LAMBORGHINI",
- "*.LAMER",
- "*.LANCASTER",
- "*.LANCIA",
- "*.LANCOME",
- "*.LAND",
- "*.LANDROVER",
- "*.LANXESS",
- "*.LASALLE",
- "*.LAT",
- "*.LATINO",
- "*.LATROBE",
- "*.LAW",
- "*.LAWYER",
- "*.LB",
- "*.LC",
- "*.LDS",
- "*.LEASE",
- "*.LECLERC",
- "*.LEFRAK",
- "*.LEGAL",
- "*.LEGO",
- "*.LEXUS",
- "*.LGBT",
- "*.LI",
- "*.LIAISON",
- "*.LIDL",
- "*.LIFE",
- "*.LIFEINSURANCE",
- "*.LIFESTYLE",
- "*.LIGHTING",
- "*.LIKE",
- "*.LILLY",
- "*.LIMITED",
- "*.LIMO",
- "*.LINCOLN",
- "*.LINDE",
- "*.LINK",
- "*.LIPSY",
- "*.LIVE",
- "*.LIVING",
- "*.LIXIL",
- "*.LK",
- "*.LLC",
- "*.LOAN",
- "*.LOANS",
- "*.LOCKER",
- "*.LOCUS",
- "*.LOFT",
- "*.LOL",
- "*.LONDON",
- "*.LOTTE",
- "*.LOTTO",
- "*.LOVE",
- "*.LPL",
- "*.LPLFINANCIAL",
- "*.LR",
- "*.LS",
- "*.LT",
- "*.LTD",
- "*.LTDA",
- "*.LU",
- "*.LUNDBECK",
- "*.LUPIN",
- "*.LUXE",
- "*.LUXURY",
- "*.LV",
- "*.LY",
- "*.MA",
- "*.MACYS",
- "*.MADRID",
- "*.MAIF",
- "*.MAISON",
- "*.MAKEUP",
- "*.MAN",
- "*.MANAGEMENT",
- "*.MANGO",
- "*.MAP",
- "*.MARKET",
- "*.MARKETING",
- "*.MARKETS",
- "*.MARRIOTT",
- "*.MARSHALLS",
- "*.MASERATI",
- "*.MATTEL",
- "*.MBA",
- "*.MC",
- "*.MCKINSEY",
- "*.MD",
- "*.ME",
- "*.MED",
- "*.MEDIA",
- "*.MEET",
- "*.MELBOURNE",
- "*.MEME",
- "*.MEMORIAL",
- "*.MEN",
- "*.MENU",
- "*.MERCKMSD",
- "*.METLIFE",
- "*.MG",
- "*.MH",
- "*.MIAMI",
- "*.MICROSOFT",
- "*.MIL",
- "*.MINI",
- "*.MINT",
- "*.MIT",
- "*.MITSUBISHI",
- "*.MK",
- "*.ML",
- "*.MLB",
- "*.MLS",
- "*.MM",
- "*.MMA",
- "*.MN",
- "*.MO",
- "*.MOBI",
- "*.MOBILE",
- "*.MOBILY",
- "*.MODA",
- "*.MOE",
- "*.MOI",
- "*.MOM",
- "*.MONASH",
- "*.MONEY",
- "*.MONSTER",
- "*.MOPAR",
- "*.MORMON",
- "*.MORTGAGE",
- "*.MOSCOW",
- "*.MOTO",
- "*.MOTORCYCLES",
- "*.MOV",
- "*.MOVIE",
- "*.MOVISTAR",
- "*.MP",
- "*.MQ",
- "*.MR",
- "*.MS",
- "*.MSD",
- "*.MT",
- "*.MTN",
- "*.MTR",
- "*.MU",
- "*.MUSEUM",
- "*.MUTUAL",
- "*.MV",
- "*.MW",
- "*.MX",
- "*.MY",
- "*.MZ",
- "*.NA",
- "*.NAB",
- "*.NADEX",
- "*.NAGOYA",
- "*.NAME",
- "*.NATIONWIDE",
- "*.NATURA",
- "*.NAVY",
- "*.NBA",
- "*.NC",
- "*.NE",
- "*.NEC",
- "*.NET",
- "*.NETBANK",
- "*.NETFLIX",
- "*.NETWORK",
- "*.NEUSTAR",
- "*.NEW",
- "*.NEWHOLLAND",
- "*.NEWS",
- "*.NEXT",
- "*.NEXTDIRECT",
- "*.NEXUS",
- "*.NF",
- "*.NFL",
- "*.NG",
- "*.NGO",
- "*.NHK",
- "*.NI",
- "*.NICO",
- "*.NIKE",
- "*.NIKON",
- "*.NINJA",
- "*.NISSAN",
- "*.NISSAY",
- "*.NL",
- "*.NO",
- "*.NOKIA",
- "*.NORTHWESTERNMUTUAL",
- "*.NORTON",
- "*.NOW",
- "*.NOWRUZ",
- "*.NOWTV",
- "*.NP",
- "*.NR",
- "*.NRA",
- "*.NRW",
- "*.NTT",
- "*.NU",
- "*.NYC",
- "*.NZ",
- "*.OBI",
- "*.OBSERVER",
- "*.OFF",
- "*.OFFICE",
- "*.OKINAWA",
- "*.OLAYAN",
- "*.OLAYANGROUP",
- "*.OLDNAVY",
- "*.OLLO",
- "*.OM",
- "*.OMEGA",
- "*.ONE",
- "*.ONG",
- "*.ONL",
- "*.ONLINE",
- "*.ONYOURSIDE",
- "*.OOO",
- "*.OPEN",
- "*.ORACLE",
- "*.ORANGE",
- "*.ORG",
- "*.ORGANIC",
- "*.ORIGINS",
- "*.OSAKA",
- "*.OTSUKA",
- "*.OTT",
- "*.OVH",
- "*.PA",
- "*.PAGE",
- "*.PANASONIC",
- "*.PANERAI",
- "*.PARIS",
- "*.PARS",
- "*.PARTNERS",
- "*.PARTS",
- "*.PARTY",
- "*.PASSAGENS",
- "*.PAY",
- "*.PCCW",
- "*.PE",
- "*.PET",
- "*.PF",
- "*.PFIZER",
- "*.PG",
- "*.PH",
- "*.PHARMACY",
- "*.PHD",
- "*.PHILIPS",
- "*.PHONE",
- "*.PHOTO",
- "*.PHOTOGRAPHY",
- "*.PHOTOS",
- "*.PHYSIO",
- "*.PIAGET",
- "*.PICS",
- "*.PICTET",
- "*.PICTURES",
- "*.PID",
- "*.PIN",
- "*.PING",
- "*.PINK",
- "*.PIONEER",
- "*.PIZZA",
- "*.PK",
- "*.PL",
- "*.PLACE",
- "*.PLAY",
- "*.PLAYSTATION",
- "*.PLUMBING",
- "*.PLUS",
- "*.PM",
- "*.PN",
- "*.PNC",
- "*.POHL",
- "*.POKER",
- "*.POLITIE",
- "*.PORN",
- "*.POST",
- "*.PR",
- "*.PRAMERICA",
- "*.PRAXI",
- "*.PRESS",
- "*.PRIME",
- "*.PRO",
- "*.PROD",
- "*.PRODUCTIONS",
- "*.PROF",
- "*.PROGRESSIVE",
- "*.PROMO",
- "*.PROPERTIES",
- "*.PROPERTY",
- "*.PROTECTION",
- "*.PRU",
- "*.PRUDENTIAL",
- "*.PS",
- "*.PT",
- "*.PUB",
- "*.PW",
- "*.PWC",
- "*.PY",
- "*.QA",
- "*.QPON",
- "*.QUEBEC",
- "*.QUEST",
- "*.QVC",
- "*.RACING",
- "*.RADIO",
- "*.RAID",
- "*.RE",
- "*.READ",
- "*.REALESTATE",
- "*.REALTOR",
- "*.REALTY",
- "*.RECIPES",
- "*.RED",
- "*.REDSTONE",
- "*.REDUMBRELLA",
- "*.REHAB",
- "*.REISE",
- "*.REISEN",
- "*.REIT",
- "*.RELIANCE",
- "*.REN",
- "*.RENT",
- "*.RENTALS",
- "*.REPAIR",
- "*.REPORT",
- "*.REPUBLICAN",
- "*.REST",
- "*.RESTAURANT",
- "*.REVIEW",
- "*.REVIEWS",
- "*.REXROTH",
- "*.RICH",
- "*.RICHARDLI",
- "*.RICOH",
- "*.RIGHTATHOME",
- "*.RIL",
- "*.RIO",
- "*.RIP",
- "*.RMIT",
- "*.RO",
- "*.ROCHER",
- "*.ROCKS",
- "*.RODEO",
- "*.ROGERS",
- "*.ROOM",
- "*.RS",
- "*.RSVP",
- "*.RU",
- "*.RUGBY",
- "*.RUHR",
- "*.RUN",
- "*.RW",
- "*.RWE",
- "*.RYUKYU",
- "*.SA",
- "*.SAARLAND",
- "*.SAFE",
- "*.SAFETY",
- "*.SAKURA",
- "*.SALE",
- "*.SALON",
- "*.SAMSCLUB",
- "*.SAMSUNG",
- "*.SANDVIK",
- "*.SANDVIKCOROMANT",
- "*.SANOFI",
- "*.SAP",
- "*.SARL",
- "*.SAS",
- "*.SAVE",
- "*.SAXO",
- "*.SB",
- "*.SBI",
- "*.SBS",
- "*.SC",
- "*.SCA",
- "*.SCB",
- "*.SCHAEFFLER",
- "*.SCHMIDT",
- "*.SCHOLARSHIPS",
- "*.SCHOOL",
- "*.SCHULE",
- "*.SCHWARZ",
- "*.SCIENCE",
- "*.SCJOHNSON",
- "*.SCOR",
- "*.SCOT",
- "*.SD",
- "*.SE",
- "*.SEARCH",
- "*.SEAT",
- "*.SECURE",
- "*.SECURITY",
- "*.SEEK",
- "*.SELECT",
- "*.SENER",
- "*.SERVICES",
- "*.SES",
- "*.SEVEN",
- "*.SEW",
- "*.SEX",
- "*.SEXY",
- "*.SFR",
- "*.SG",
- "*.SH",
- "*.SHANGRILA",
- "*.SHARP",
- "*.SHAW",
- "*.SHELL",
- "*.SHIA",
- "*.SHIKSHA",
- "*.SHOES",
- "*.SHOP",
- "*.SHOPPING",
- "*.SHOUJI",
- "*.SHOW",
- "*.SHOWTIME",
- "*.SHRIRAM",
- "*.SI",
- "*.SILK",
- "*.SINA",
- "*.SINGLES",
- "*.SITE",
- "*.SJ",
- "*.SK",
- "*.SKI",
- "*.SKIN",
- "*.SKY",
- "*.SKYPE",
- "*.SL",
- "*.SLING",
- "*.SM",
- "*.SMART",
- "*.SMILE",
- "*.SN",
- "*.SNCF",
- "*.SO",
- "*.SOCCER",
- "*.SOCIAL",
- "*.SOFTBANK",
- "*.SOFTWARE",
- "*.SOHU",
- "*.SOLAR",
- "*.SOLUTIONS",
- "*.SONG",
- "*.SONY",
- "*.SOY",
- "*.SPACE",
- "*.SPIEGEL",
- "*.SPORT",
- "*.SPOT",
- "*.SPREADBETTING",
- "*.SR",
- "*.SRL",
- "*.SRT",
- "*.ST",
- "*.STADA",
- "*.STAPLES",
- "*.STAR",
- "*.STARHUB",
- "*.STATEBANK",
- "*.STATEFARM",
- "*.STATOIL",
- "*.STC",
- "*.STCGROUP",
- "*.STOCKHOLM",
- "*.STORAGE",
- "*.STORE",
- "*.STREAM",
- "*.STUDIO",
- "*.STUDY",
- "*.STYLE",
- "*.SU",
- "*.SUCKS",
- "*.SUPPLIES",
- "*.SUPPLY",
- "*.SUPPORT",
- "*.SURF",
- "*.SURGERY",
- "*.SUZUKI",
- "*.SV",
- "*.SWATCH",
- "*.SWIFTCOVER",
- "*.SWISS",
- "*.SX",
- "*.SY",
- "*.SYDNEY",
- "*.SYMANTEC",
- "*.SYSTEMS",
- "*.SZ",
- "*.TAB",
- "*.TAIPEI",
- "*.TALK",
- "*.TAOBAO",
- "*.TARGET",
- "*.TATAMOTORS",
- "*.TATAR",
- "*.TATTOO",
- "*.TAX",
- "*.TAXI",
- "*.TC",
- "*.TCI",
- "*.TD",
- "*.TDK",
- "*.TEAM",
- "*.TECH",
- "*.TECHNOLOGY",
- "*.TEL",
- "*.TELEFONICA",
- "*.TEMASEK",
- "*.TENNIS",
- "*.TEVA",
- "*.TF",
- "*.TG",
- "*.TH",
- "*.THD",
- "*.THEATER",
- "*.THEATRE",
- "*.TIAA",
- "*.TICKETS",
- "*.TIENDA",
- "*.TIFFANY",
- "*.TIPS",
- "*.TIRES",
- "*.TIROL",
- "*.TJ",
- "*.TJMAXX",
- "*.TJX",
- "*.TK",
- "*.TKMAXX",
- "*.TL",
- "*.TM",
- "*.TMALL",
- "*.TN",
- "*.TO",
- "*.TODAY",
- "*.TOKYO",
- "*.TOOLS",
- "*.TOP",
- "*.TORAY",
- "*.TOSHIBA",
- "*.TOTAL",
- "*.TOURS",
- "*.TOWN",
- "*.TOYOTA",
- "*.TOYS",
- "*.TR",
- "*.TRADE",
- "*.TRADING",
- "*.TRAINING",
- "*.TRAVEL",
- "*.TRAVELCHANNEL",
- "*.TRAVELERS",
- "*.TRAVELERSINSURANCE",
- "*.TRUST",
- "*.TRV",
- "*.TT",
- "*.TUBE",
- "*.TUI",
- "*.TUNES",
- "*.TUSHU",
- "*.TV",
- "*.TVS",
- "*.TW",
- "*.TZ",
- "*.UA",
- "*.UBANK",
- "*.UBS",
- "*.UCONNECT",
- "*.UG",
- "*.UK",
- "*.UNICOM",
- "*.UNIVERSITY",
- "*.UNO",
- "*.UOL",
- "*.UPS",
- "*.US",
- "*.UY",
- "*.UZ",
- "*.VA",
- "*.VACATIONS",
- "*.VANA",
- "*.VANGUARD",
- "*.VC",
- "*.VE",
- "*.VEGAS",
- "*.VENTURES",
- "*.VERISIGN",
- "*.VERSICHERUNG",
- "*.VET",
- "*.VG",
- "*.VI",
- "*.VIAJES",
- "*.VIDEO",
- "*.VIG",
- "*.VIKING",
- "*.VILLAS",
- "*.VIN",
- "*.VIP",
- "*.VIRGIN",
- "*.VISA",
- "*.VISION",
- "*.VISTAPRINT",
- "*.VIVA",
- "*.VIVO",
- "*.VLAANDEREN",
- "*.VN",
- "*.VODKA",
- "*.VOLKSWAGEN",
- "*.VOLVO",
- "*.VOTE",
- "*.VOTING",
- "*.VOTO",
- "*.VOYAGE",
- "*.VU",
- "*.VUELOS",
- "*.WALES",
- "*.WALMART",
- "*.WALTER",
- "*.WANG",
- "*.WANGGOU",
- "*.WARMAN",
- "*.WATCH",
- "*.WATCHES",
- "*.WEATHER",
- "*.WEATHERCHANNEL",
- "*.WEBCAM",
- "*.WEBER",
- "*.WEBSITE",
- "*.WED",
- "*.WEDDING",
- "*.WEIBO",
- "*.WEIR",
- "*.WF",
- "*.WHOSWHO",
- "*.WIEN",
- "*.WIKI",
- "*.WILLIAMHILL",
- "*.WIN",
- "*.WINDOWS",
- "*.WINE",
- "*.WINNERS",
- "*.WME",
- "*.WOLTERSKLUWER",
- "*.WOODSIDE",
- "*.WORK",
- "*.WORKS",
- "*.WORLD",
- "*.WOW",
- "*.WS",
- "*.WTC",
- "*.WTF",
- "*.XBOX",
- "*.XEROX",
- "*.XFINITY",
- "*.XIHUAN",
- "*.XIN",
- "*.XN--11B4C3D",
- "*.XN--1CK2E1B",
- "*.XN--1QQW23A",
- "*.XN--2SCRJ9C",
- "*.XN--30RR7Y",
- "*.XN--3BST00M",
- "*.XN--3DS443G",
- "*.XN--3E0B707E",
- "*.XN--3HCRJ9C",
- "*.XN--3OQ18VL8PN36A",
- "*.XN--3PXU8K",
- "*.XN--42C2D9A",
- "*.XN--45BR5CYL",
- "*.XN--45BRJ9C",
- "*.XN--45Q11C",
- "*.XN--4GBRIM",
- "*.XN--54B7FTA0CC",
- "*.XN--55QW42G",
- "*.XN--55QX5D",
- "*.XN--5SU34J936BGSG",
- "*.XN--5TZM5G",
- "*.XN--6FRZ82G",
- "*.XN--6QQ986B3XL",
- "*.XN--80ADXHKS",
- "*.XN--80AO21A",
- "*.XN--80AQECDR1A",
- "*.XN--80ASEHDB",
- "*.XN--80ASWG",
- "*.XN--8Y0A063A",
- "*.XN--90A3AC",
- "*.XN--90AE",
- "*.XN--90AIS",
- "*.XN--9DBQ2A",
- "*.XN--9ET52U",
- "*.XN--9KRT00A",
- "*.XN--B4W605FERD",
- "*.XN--BCK1B9A5DRE4C",
- "*.XN--C1AVG",
- "*.XN--C2BR7G",
- "*.XN--CCK2B3B",
- "*.XN--CG4BKI",
- "*.XN--CLCHC0EA0B2G2A9GCD",
- "*.XN--CZR694B",
- "*.XN--CZRS0T",
- "*.XN--CZRU2D",
- "*.XN--D1ACJ3B",
- "*.XN--D1ALF",
- "*.XN--E1A4C",
- "*.XN--ECKVDTC9D",
- "*.XN--EFVY88H",
- "*.XN--ESTV75G",
- "*.XN--FCT429K",
- "*.XN--FHBEI",
- "*.XN--FIQ228C5HS",
- "*.XN--FIQ64B",
- "*.XN--FIQS8S",
- "*.XN--FIQZ9S",
- "*.XN--FJQ720A",
- "*.XN--FLW351E",
- "*.XN--FPCRJ9C3D",
- "*.XN--FZC2C9E2C",
- "*.XN--FZYS8D69UVGM",
- "*.XN--G2XX48C",
- "*.XN--GCKR3F0F",
- "*.XN--GECRJ9C",
- "*.XN--GK3AT1E",
- "*.XN--H2BREG3EVE",
- "*.XN--H2BRJ9C",
- "*.XN--H2BRJ9C8C",
- "*.XN--HXT814E",
- "*.XN--I1B6B1A6A2E",
- "*.XN--IMR513N",
- "*.XN--IO0A7I",
- "*.XN--J1AEF",
- "*.XN--J1AMH",
- "*.XN--J6W193G",
- "*.XN--JLQ61U9W7B",
- "*.XN--JVR189M",
- "*.XN--KCRX77D1X4A",
- "*.XN--KPRW13D",
- "*.XN--KPRY57D",
- "*.XN--KPU716F",
- "*.XN--KPUT3I",
- "*.XN--L1ACC",
- "*.XN--LGBBAT1AD8J",
- "*.XN--MGB9AWBF",
- "*.XN--MGBA3A3EJT",
- "*.XN--MGBA3A4F16A",
- "*.XN--MGBA7C0BBN0A",
- "*.XN--MGBAAKC7DVF",
- "*.XN--MGBAAM7A8H",
- "*.XN--MGBAB2BD",
- "*.XN--MGBAI9AZGQP6J",
- "*.XN--MGBAYH7GPA",
- "*.XN--MGBB9FBPOB",
- "*.XN--MGBBH1A",
- "*.XN--MGBBH1A71E",
- "*.XN--MGBC0A9AZCG",
- "*.XN--MGBCA7DZDO",
- "*.XN--MGBERP4A5D4AR",
- "*.XN--MGBGU82A",
- "*.XN--MGBI4ECEXP",
- "*.XN--MGBPL2FH",
- "*.XN--MGBT3DHD",
- "*.XN--MGBTX2B",
- "*.XN--MGBX4CD0AB",
- "*.XN--MIX891F",
- "*.XN--MK1BU44C",
- "*.XN--MXTQ1M",
- "*.XN--NGBC5AZD",
- "*.XN--NGBE9E0A",
- "*.XN--NGBRX",
- "*.XN--NODE",
- "*.XN--NQV7F",
- "*.XN--NQV7FS00EMA",
- "*.XN--NYQY26A",
- "*.XN--O3CW4H",
- "*.XN--OGBPF8FL",
- "*.XN--OTU796D",
- "*.XN--P1ACF",
- "*.XN--P1AI",
- "*.XN--PBT977C",
- "*.XN--PGBS0DH",
- "*.XN--PSSY2U",
- "*.XN--Q9JYB4C",
- "*.XN--QCKA1PMC",
- "*.XN--QXAM",
- "*.XN--RHQV96G",
- "*.XN--ROVU88B",
- "*.XN--RVC1E0AM3E",
- "*.XN--S9BRJ9C",
- "*.XN--SES554G",
- "*.XN--T60B56A",
- "*.XN--TCKWE",
- "*.XN--TIQ49XQYJ",
- "*.XN--UNUP4Y",
- "*.XN--VERMGENSBERATER-CTB",
- "*.XN--VERMGENSBERATUNG-PWB",
- "*.XN--VHQUV",
- "*.XN--VUQ861B",
- "*.XN--W4R85EL8FHU5DNRA",
- "*.XN--W4RS40L",
- "*.XN--WGBH1C",
- "*.XN--WGBL6A",
- "*.XN--XHQ521B",
- "*.XN--XKC2AL3HYE2A",
- "*.XN--XKC2DL3A5EE0H",
- "*.XN--Y9A3AQ",
- "*.XN--YFRO4I67O",
- "*.XN--YGBI2AMMX",
- "*.XN--ZFR164B",
- "*.XXX",
- "*.XYZ",
- "*.YACHTS",
- "*.YAHOO",
- "*.YAMAXUN",
- "*.YANDEX",
- "*.YE",
- "*.YODOBASHI",
- "*.YOGA",
- "*.YOKOHAMA",
- "*.YOU",
- "*.YOUTUBE",
- "*.YT",
- "*.YUN",
- "*.ZA",
- "*.ZAPPOS",
- "*.ZARA",
- "*.ZERO",
- "*.ZIP",
- "*.ZIPPO",
- "*.ZM",
- "*.ZONE",
- "*.ZUERICH",
- "*.ZW"
- );
-}
diff --git a/security/crypto/src/main/java/androidx/security/config/TrustAnchorOptions.java b/security/crypto/src/main/java/androidx/security/config/TrustAnchorOptions.java
deleted file mode 100644
index 2136b76..0000000
--- a/security/crypto/src/main/java/androidx/security/config/TrustAnchorOptions.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package androidx.security.config;
-
-import androidx.annotation.NonNull;
-
-/**
- * Enum that enumerates valid trust anchors
- */
-public enum TrustAnchorOptions {
- USER_SYSTEM(0),
- SYSTEM_ONLY(1),
- USER_ONLY(2),
- LIMITED_SYSTEM(3);
-
- private final int mType;
-
- TrustAnchorOptions(int type) {
- this.mType = type;
- }
-
- /**
- * @return the mType value
- */
- public int getType() {
- return this.mType;
- }
-
- /**
- * @param id the id of the trust anchor
- * @return the mType
- */
- @NonNull
- public static TrustAnchorOptions fromId(int id) {
- switch (id) {
- case 0:
- return USER_SYSTEM;
- case 1:
- return SYSTEM_ONLY;
- case 2:
- return USER_ONLY;
- case 3:
- return LIMITED_SYSTEM;
- }
- return USER_SYSTEM;
- }
-
-}
diff --git a/security/crypto/src/main/java/androidx/security/context/SecureContextCompat.java b/security/crypto/src/main/java/androidx/security/context/SecureContextCompat.java
deleted file mode 100644
index 20888ee..0000000
--- a/security/crypto/src/main/java/androidx/security/context/SecureContextCompat.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package androidx.security.context;
-
-import android.annotation.TargetApi;
-import android.app.KeyguardManager;
-import android.content.Context;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.security.SecureConfig;
-import androidx.security.crypto.FileCipher;
-
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.concurrent.Executor;
-
-/**
- * Class that provides access to an encrypted file input and output streams, as well as encrypted
- * SharedPreferences.
- */
-public class SecureContextCompat {
-
- private static final String TAG = "SecureContextCompat";
-
- private Context mContext;
- private SecureConfig mSecureConfig;
-
- private static final String DEFAULT_FILE_ENCRYPTION_KEY = "default_encryption_key";
-
- /**
- * A listener for getting access to encrypted files. Required when the key requires a
- * Biometric Prompt.
- */
- public interface EncryptedFileInputStreamListener {
- /**
- * @param inputStream
- */
- void onEncryptedFileInput(@NonNull FileInputStream inputStream);
- }
-
- public SecureContextCompat(@NonNull Context context) {
- this(context, SecureConfig.getDefault());
- }
-
- public SecureContextCompat(@NonNull Context context, @NonNull SecureConfig secureConfig) {
- mContext = context;
- mSecureConfig = secureConfig;
- }
-
- /**
- * Open an encrypted private file associated with this Context's application
- * package for reading.
- *
- * @param name The name of the file to open; can not contain path separators.
- * @throws IOException
- */
- public void openEncryptedFileInput(@NonNull String name,
- @NonNull Executor executor,
- @NonNull EncryptedFileInputStreamListener listener) throws IOException {
- new FileCipher(name, mContext.openFileInput(name), mSecureConfig, executor, listener);
- }
-
- /**
- * Open a private encrypted file associated with this Context's application package for
- * writing. Creates the file if it doesn't already exist.
- * <p>
- * The written file will be encrypted with the default keyPairAlias.
- *
- * @param name The name of the file to open; can not contain path separators.
- * @param mode Operating mode.
- * @return The resulting {@link FileOutputStream}.
- * @throws IOException
- */
- @NonNull
- public FileOutputStream openEncryptedFileOutput(@NonNull String name, int mode)
- throws IOException {
- return openEncryptedFileOutput(name, mode, DEFAULT_FILE_ENCRYPTION_KEY);
- }
-
- /**
- * Open a private encrypted file associated with this Context's application package for
- * writing. Creates the file if it doesn't already exist.
- * <p>
- * The written file will be encrypted with the specified keyPairAlias.
- *
- * @param name The name of the file to open; can not contain path separators.
- * @param mode Operating mode.
- * @param keyPairAlias The alias of the KeyPair used for encryption, the KeyPair will be
- * created if it does not exist.
- * @return The resulting {@link FileOutputStream}.
- * @throws IOException
- */
- @NonNull
- public FileOutputStream openEncryptedFileOutput(@NonNull String name, int mode,
- @NonNull String keyPairAlias)
- throws IOException {
- FileCipher fileCipher = new FileCipher(keyPairAlias,
- mContext.openFileOutput(name, mode), mSecureConfig);
- return fileCipher.getFileOutputStream();
- }
-
-
- /**
- * Checks to see if the device is locked.
- *
- * @return true if the device is locked, false otherwise.
- */
- @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
- public boolean deviceLocked() {
- KeyguardManager keyGuardManager =
- (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
- return keyGuardManager.isDeviceLocked();
- }
-
-}
diff --git a/security/crypto/src/main/java/androidx/security/crypto/EncryptedFile.java b/security/crypto/src/main/java/androidx/security/crypto/EncryptedFile.java
new file mode 100644
index 0000000..0b4ab81
--- /dev/null
+++ b/security/crypto/src/main/java/androidx/security/crypto/EncryptedFile.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.security.crypto;
+
+import static androidx.security.crypto.MasterKeys.KEYSTORE_PATH_URI;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.StreamingAead;
+import com.google.crypto.tink.config.TinkConfig;
+import com.google.crypto.tink.integration.android.AndroidKeysetManager;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.streamingaead.StreamingAeadFactory;
+import com.google.crypto.tink.streamingaead.StreamingAeadKeyTemplates;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.FileChannel;
+import java.security.GeneralSecurityException;
+
+/**
+ * Class used to create and read encrypted files.
+ *
+ * @code {
+ * String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
+ *
+ * File file = new File(context.getFilesDir(), "secret_data");
+ * EncryptedFile encryptedFile = EncryptedFile.Builder(
+ * file,
+ * context,
+ * masterKeyAlias,
+ * EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
+ * ).build();
+ *
+ * // write to the encrypted file
+ * FileOutputStream encryptedOutputStream = encryptedFile.openFileOutput();
+ *
+ * // read the encrypted file
+ * FileInputStream encryptedInputStream = encryptedFile.openFileInput();
+ * }
+ *
+ */
+public final class EncryptedFile {
+
+ private static final String KEYSET_PREF_NAME =
+ "__androidx_security_crypto_encrypted_file_pref__";
+ private static final String KEYSET_ALIAS =
+ "__androidx_security_crypto_encrypted_file_keyset__";
+
+ final File mFile;
+ final Context mContext;
+ final String mMasterKeyAlias;
+ final StreamingAead mStreamingAead;
+
+
+ EncryptedFile(
+ @NonNull File file,
+ @NonNull String masterKeyAlias,
+ @NonNull StreamingAead streamingAead,
+ @NonNull Context context) {
+ mFile = file;
+ mContext = context;
+ mMasterKeyAlias = masterKeyAlias;
+ mStreamingAead = streamingAead;
+ }
+
+ /**
+ * The encryption scheme to encrypt files.
+ */
+ public enum FileEncryptionScheme {
+ /**
+ * The file content is encrypted using {@link StreamingAead} with AES-GCM, with the
+ * file name as associated data.
+ *
+ * For more information please see the Tink documentation:
+ *
+ * {@link StreamingAeadKeyTemplates}.AES256_GCM_HKDF_4KB
+ */
+ AES256_GCM_HKDF_4KB(StreamingAeadKeyTemplates.AES256_GCM_HKDF_4KB);
+
+ private KeyTemplate mStreamingAeadKeyTemplate;
+
+ FileEncryptionScheme(KeyTemplate keyTemplate) {
+ mStreamingAeadKeyTemplate = keyTemplate;
+ }
+
+ KeyTemplate getKeyTemplate() {
+ return mStreamingAeadKeyTemplate;
+ }
+ }
+
+ /**
+ * Builder class to configure EncryptedFile
+ */
+ public static final class Builder {
+
+ public Builder(@NonNull File file,
+ @NonNull Context context,
+ @NonNull String masterKeyAlias,
+ @NonNull FileEncryptionScheme fileEncryptionScheme) {
+ mFile = file;
+ mFileEncryptionScheme = fileEncryptionScheme;
+ mContext = context;
+ mMasterKeyAlias = masterKeyAlias;
+ }
+
+ // Required parameters
+ File mFile;
+ final FileEncryptionScheme mFileEncryptionScheme;
+ final Context mContext;
+ final String mMasterKeyAlias;
+
+ // Optional parameters
+ String mKeysetPrefName = KEYSET_PREF_NAME;
+ String mKeysetAlias = KEYSET_ALIAS;
+
+ /**
+ * @param keysetPrefName The SharedPreferences file to store the keyset.
+ * @return This Builder
+ */
+ @NonNull
+ public Builder setKeysetPrefName(@NonNull String keysetPrefName) {
+ mKeysetPrefName = keysetPrefName;
+ return this;
+ }
+
+ /**
+ * @param keysetAlias The alias in the SharedPreferences file to store the keyset.
+ * @return This Builder
+ */
+ @NonNull
+ public Builder setKeysetAlias(@NonNull String keysetAlias) {
+ mKeysetAlias = keysetAlias;
+ return this;
+ }
+
+ /**
+ * @return An EncryptedFile with the specified parameters.
+ */
+ @NonNull
+ public EncryptedFile build() throws GeneralSecurityException, IOException {
+ TinkConfig.register();
+
+ KeysetHandle streadmingAeadKeysetHandle = new AndroidKeysetManager.Builder()
+ .withKeyTemplate(mFileEncryptionScheme.getKeyTemplate())
+ .withSharedPref(mContext, mKeysetAlias, mKeysetPrefName)
+ .withMasterKeyUri(KEYSTORE_PATH_URI + mMasterKeyAlias)
+ .build().getKeysetHandle();
+
+ StreamingAead streamingAead = StreamingAeadFactory.getPrimitive(
+ streadmingAeadKeysetHandle);
+
+ EncryptedFile file = new EncryptedFile(mFile, mKeysetAlias, streamingAead,
+ mContext);
+ return file;
+ }
+ }
+
+ /**
+ * Opens a FileOutputStream for writing that automatically encrypts the data based on the
+ * provided settings.
+ *
+ * Please ensure that the same master key and keyset are used to decrypt or it
+ * will cause failures.
+ *
+ * @return The FileOutputStream that encrypts all data.
+ * @throws GeneralSecurityException when a bad master key or keyset has been used
+ * @throws IOException when the file already exists or is not available for writing
+ */
+ @NonNull
+ public FileOutputStream openFileOutput()
+ throws GeneralSecurityException, IOException {
+ if (mFile.exists()) {
+ throw new IOException("output file already exists, please use a new file: "
+ + mFile.getName());
+ }
+ FileOutputStream fileOutputStream = new FileOutputStream(mFile);
+ OutputStream encryptingStream = mStreamingAead.newEncryptingStream(fileOutputStream,
+ mFile.getName().getBytes(UTF_8));
+ return new EncryptedFileOutputStream(fileOutputStream.getFD(), encryptingStream);
+ }
+
+ /**
+ * Opens a FileInputStream that reads encrypted files based on the previous settings.
+ *
+ * Please ensure that the same master key and keyset are used to decrypt or it
+ * will cause failures.
+ *
+ * @return The input stream to read previously encrypted data.
+ * @throws GeneralSecurityException when a bad master key or keyset has been used
+ * @throws IOException when the file was not found
+ */
+ @NonNull
+ public FileInputStream openFileInput()
+ throws GeneralSecurityException, IOException {
+ if (!mFile.exists()) {
+ throw new IOException("file doesn't exist: " + mFile.getName());
+ }
+ FileInputStream fileInputStream = new FileInputStream(mFile);
+ InputStream decryptingStream = mStreamingAead.newDecryptingStream(fileInputStream,
+ mFile.getName().getBytes(UTF_8));
+ return new EncryptedFileInputStream(fileInputStream.getFD(), decryptingStream);
+ }
+
+ /**
+ * Encrypted file output stream
+ *
+ */
+ private static final class EncryptedFileOutputStream extends FileOutputStream {
+
+ private final OutputStream mEncryptedOutputStream;
+
+ EncryptedFileOutputStream(FileDescriptor descriptor, OutputStream encryptedOutputStream) {
+ super(descriptor);
+ mEncryptedOutputStream = encryptedOutputStream;
+ }
+
+ @Override
+ public void write(@NonNull byte[] b) throws IOException {
+ mEncryptedOutputStream.write(b);
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ mEncryptedOutputStream.write(b);
+ }
+
+ @Override
+ public void write(@NonNull byte[] b, int off, int len) throws IOException {
+ mEncryptedOutputStream.write(b, off, len);
+ }
+
+ @Override
+ public void close() throws IOException {
+ mEncryptedOutputStream.close();
+ }
+
+ @NonNull
+ @Override
+ public FileChannel getChannel() {
+ throw new UnsupportedOperationException("For encrypted files, please open the "
+ + "relevant FileInput/FileOutputStream.");
+ }
+
+ @Override
+ public void flush() throws IOException {
+ mEncryptedOutputStream.flush();
+ }
+
+ }
+
+ /**
+ * Encrypted file input stream
+ */
+ private static final class EncryptedFileInputStream extends FileInputStream {
+
+ private final InputStream mEncryptedInputStream;
+
+ EncryptedFileInputStream(FileDescriptor descriptor,
+ InputStream encryptedInputStream) {
+ super(descriptor);
+ mEncryptedInputStream = encryptedInputStream;
+ }
+
+ @Override
+ public int read() throws IOException {
+ return mEncryptedInputStream.read();
+ }
+
+ @Override
+ public int read(@NonNull byte[] b) throws IOException {
+ return mEncryptedInputStream.read(b);
+ }
+
+ @Override
+ public int read(@NonNull byte[] b, int off, int len) throws IOException {
+ return mEncryptedInputStream.read(b, off, len);
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ return mEncryptedInputStream.skip(n);
+ }
+
+ @Override
+ public int available() throws IOException {
+ return mEncryptedInputStream.available();
+ }
+
+ @Override
+ public void close() throws IOException {
+ mEncryptedInputStream.close();
+ }
+
+ @Override
+ public FileChannel getChannel() {
+ throw new UnsupportedOperationException("For encrypted files, please open the "
+ + "relevant FileInput/FileOutputStream.");
+ }
+
+ @Override
+ public synchronized void mark(int readlimit) {
+ mEncryptedInputStream.mark(readlimit);
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ mEncryptedInputStream.reset();
+ }
+
+ @Override
+ public boolean markSupported() {
+ return mEncryptedInputStream.markSupported();
+ }
+
+ }
+
+}
diff --git a/security/crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java b/security/crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java
new file mode 100644
index 0000000..a06eb1d
--- /dev/null
+++ b/security/crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java
@@ -0,0 +1,557 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.security.crypto;
+
+import static androidx.security.crypto.MasterKeys.KEYSTORE_PATH_URI;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.ArraySet;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.crypto.tink.Aead;
+import com.google.crypto.tink.DeterministicAead;
+import com.google.crypto.tink.KeysetHandle;
+import com.google.crypto.tink.aead.AeadFactory;
+import com.google.crypto.tink.aead.AeadKeyTemplates;
+import com.google.crypto.tink.config.TinkConfig;
+import com.google.crypto.tink.daead.DeterministicAeadFactory;
+import com.google.crypto.tink.daead.DeterministicAeadKeyTemplates;
+import com.google.crypto.tink.integration.android.AndroidKeysetManager;
+import com.google.crypto.tink.proto.KeyTemplate;
+import com.google.crypto.tink.subtle.Base64;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * An implementation of {@link SharedPreferences} that encrypts keys and values.
+ *
+ * @code {
+ * String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
+ *
+ * SharedPreferences sharedPreferences = EncryptedSharedPreferences.create(
+ * "secret_shared_prefs",
+ * masterKeyAlias,
+ * context,
+ * EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ * EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+ * );
+ *
+ * // use the shared preferences and editor as you normally would
+ * SharedPreferences.Editor editor = sharedPreferences.edit();
+ *
+ * }
+ */
+public final class EncryptedSharedPreferences implements SharedPreferences {
+
+ private static final String KEY_KEYSET_ALIAS =
+ "__androidx_security_crypto_encrypted_prefs_key_keyset__";
+ private static final String VALUE_KEYSET_ALIAS =
+ "__androidx_security_crypto_encrypted_prefs_value_keyset__";
+
+ private static final String NULL_VALUE = "__NULL__";
+
+ final SharedPreferences mSharedPreferences;
+ final List<OnSharedPreferenceChangeListener> mListeners;
+ final String mFileName;
+ final String mMasterKeyAlias;
+
+ final Aead mValueAead;
+ final DeterministicAead mKeyDeterministicAead;
+
+ EncryptedSharedPreferences(@NonNull String name,
+ @NonNull String masterKeyAlias,
+ @NonNull SharedPreferences sharedPreferences,
+ @NonNull Aead aead,
+ @NonNull DeterministicAead deterministicAead) {
+ mFileName = name;
+ mSharedPreferences = sharedPreferences;
+ mMasterKeyAlias = masterKeyAlias;
+ mValueAead = aead;
+ mKeyDeterministicAead = deterministicAead;
+ mListeners = new ArrayList<>();
+ }
+
+ /**
+ * Opens an instance of encrypted SharedPreferences
+ *
+ * @param fileName The name of the file to open; can not contain path separators.
+ * @return The SharedPreferences instance that encrypts all data.
+ * @throws GeneralSecurityException when a bad master key or keyset has been attempted
+ * @throws IOException when fileName can not be used
+ */
+ @NonNull
+ public static SharedPreferences create(@NonNull String fileName,
+ @NonNull String masterKeyAlias,
+ @NonNull Context context,
+ @NonNull PrefKeyEncryptionScheme prefKeyEncryptionScheme,
+ @NonNull PrefValueEncryptionScheme prefValueEncryptionScheme)
+ throws GeneralSecurityException, IOException {
+ TinkConfig.register();
+
+ KeysetHandle daeadKeysetHandle = new AndroidKeysetManager.Builder()
+ .withKeyTemplate(prefKeyEncryptionScheme.getKeyTemplate())
+ .withSharedPref(context, KEY_KEYSET_ALIAS, fileName)
+ .withMasterKeyUri(KEYSTORE_PATH_URI + masterKeyAlias)
+ .build().getKeysetHandle();
+ KeysetHandle aeadKeysetHandle = new AndroidKeysetManager.Builder()
+ .withKeyTemplate(prefValueEncryptionScheme.getKeyTemplate())
+ .withSharedPref(context, VALUE_KEYSET_ALIAS, fileName)
+ .withMasterKeyUri(KEYSTORE_PATH_URI + masterKeyAlias)
+ .build().getKeysetHandle();
+
+ DeterministicAead daead = DeterministicAeadFactory.getPrimitive(daeadKeysetHandle);
+ Aead aead = AeadFactory.getPrimitive(aeadKeysetHandle);
+
+ return new EncryptedSharedPreferences(fileName, masterKeyAlias,
+ context.getSharedPreferences(fileName, Context.MODE_PRIVATE), aead, daead);
+ }
+
+ /**
+ * The encryption scheme to encrypt keys.
+ */
+ public enum PrefKeyEncryptionScheme {
+ /**
+ * Pref keys are encrypted deterministically with AES256-SIV-CMAC (RFC 5297).
+ *
+ * For more information please see the Tink documentation:
+ *
+ * {@link DeterministicAeadKeyTemplates}.AES256_SIV
+ */
+ AES256_SIV(DeterministicAeadKeyTemplates.AES256_SIV);
+
+ private KeyTemplate mDeterministicAeadKeyTemplate;
+
+ PrefKeyEncryptionScheme(KeyTemplate keyTemplate) {
+ mDeterministicAeadKeyTemplate = keyTemplate;
+ }
+
+ KeyTemplate getKeyTemplate() {
+ return mDeterministicAeadKeyTemplate;
+ }
+ }
+
+ /**
+ * The encryption scheme to encrypt values.
+ */
+ public enum PrefValueEncryptionScheme {
+ /**
+ * Pref values are encrypted with AES256-GCM. The associated data is the encrypted pref key.
+ *
+ * For more information please see the Tink documentation:
+ *
+ * {@link AeadKeyTemplates}.AES256_GCM
+ */
+ AES256_GCM(AeadKeyTemplates.AES256_GCM);
+
+ private KeyTemplate mAeadKeyTemplate;
+
+ PrefValueEncryptionScheme(KeyTemplate keyTemplates) {
+ mAeadKeyTemplate = keyTemplates;
+ }
+
+ KeyTemplate getKeyTemplate() {
+ return mAeadKeyTemplate;
+ }
+ }
+
+ private static final class Editor implements SharedPreferences.Editor {
+ private final EncryptedSharedPreferences mEncryptedSharedPreferences;
+ private final SharedPreferences.Editor mEditor;
+ private final List<String> mKeysChanged;
+
+ Editor(EncryptedSharedPreferences encryptedSharedPreferences,
+ SharedPreferences.Editor editor) {
+ mEncryptedSharedPreferences = encryptedSharedPreferences;
+ mEditor = editor;
+ mKeysChanged = new CopyOnWriteArrayList<>();
+ }
+
+ @Override
+ @NonNull
+ public SharedPreferences.Editor putString(@Nullable String key, @Nullable String value) {
+ if (value == null) {
+ value = NULL_VALUE;
+ }
+ byte[] stringBytes = value.getBytes(UTF_8);
+ int stringByteLength = stringBytes.length;
+ ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + Integer.BYTES
+ + stringByteLength);
+ buffer.putInt(EncryptedType.STRING.getId());
+ buffer.putInt(stringByteLength);
+ buffer.put(stringBytes);
+ putEncryptedObject(key, buffer.array());
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public SharedPreferences.Editor putStringSet(@Nullable String key,
+ @Nullable Set<String> values) {
+ if (values == null) {
+ values = new ArraySet<>();
+ values.add(NULL_VALUE);
+ }
+ List<byte[]> byteValues = new ArrayList<>(values.size());
+ int totalBytes = values.size() * Integer.BYTES;
+ for (String strValue : values) {
+ byte[] byteValue = strValue.getBytes(UTF_8);
+ byteValues.add(byteValue);
+ totalBytes += byteValue.length;
+ }
+ totalBytes += Integer.BYTES;
+ ByteBuffer buffer = ByteBuffer.allocate(totalBytes);
+ buffer.putInt(EncryptedType.STRING_SET.getId());
+ for (byte[] bytes : byteValues) {
+ buffer.putInt(bytes.length);
+ buffer.put(bytes);
+ }
+ putEncryptedObject(key, buffer.array());
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public SharedPreferences.Editor putInt(@Nullable String key, int value) {
+ ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + Integer.BYTES);
+ buffer.putInt(EncryptedType.INT.getId());
+ buffer.putInt(value);
+ putEncryptedObject(key, buffer.array());
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public SharedPreferences.Editor putLong(@Nullable String key, long value) {
+ ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + Long.BYTES);
+ buffer.putInt(EncryptedType.LONG.getId());
+ buffer.putLong(value);
+ putEncryptedObject(key, buffer.array());
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public SharedPreferences.Editor putFloat(@Nullable String key, float value) {
+ ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + Float.BYTES);
+ buffer.putInt(EncryptedType.FLOAT.getId());
+ buffer.putFloat(value);
+ putEncryptedObject(key, buffer.array());
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public SharedPreferences.Editor putBoolean(@Nullable String key, boolean value) {
+ ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + Byte.BYTES);
+ buffer.putInt(EncryptedType.BOOLEAN.getId());
+ buffer.put(value ? (byte) 1 : (byte) 0);
+ putEncryptedObject(key, buffer.array());
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public SharedPreferences.Editor remove(@Nullable String key) {
+ mEditor.remove(mEncryptedSharedPreferences.encryptKey(key));
+ mKeysChanged.remove(key);
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public SharedPreferences.Editor clear() {
+ mEditor.clear();
+ mKeysChanged.clear();
+ return this;
+ }
+
+ @Override
+ public boolean commit() {
+ try {
+ return mEditor.commit();
+ } finally {
+ notifyListeners();
+ }
+ }
+
+ @Override
+ public void apply() {
+ mEditor.apply();
+ notifyListeners();
+ }
+
+ private void putEncryptedObject(String key, byte[] value) {
+ mKeysChanged.add(key);
+ if (key == null) {
+ key = NULL_VALUE;
+ }
+ try {
+ Pair<String, String> encryptedPair = mEncryptedSharedPreferences
+ .encryptKeyValuePair(key, value);
+ mEditor.putString(encryptedPair.first, encryptedPair.second);
+ } catch (GeneralSecurityException ex) {
+ throw new SecurityException("Could not encrypt data: " + ex.getMessage(), ex);
+ }
+ }
+
+ private void notifyListeners() {
+ for (OnSharedPreferenceChangeListener listener :
+ mEncryptedSharedPreferences.mListeners) {
+ for (String key : mKeysChanged) {
+ listener.onSharedPreferenceChanged(mEncryptedSharedPreferences, key);
+ }
+ }
+ }
+ }
+
+ // SharedPreferences methods
+
+ @Override
+ @NonNull
+ public Map<String, ?> getAll() {
+ Map<String, ? super Object> allEntries = new HashMap<>();
+ for (Map.Entry<String, ?> entry : mSharedPreferences.getAll().entrySet()) {
+ String decryptedKey = decryptKey(entry.getKey());
+
+ allEntries.put(decryptedKey,
+ getDecryptedObject(decryptedKey));
+ }
+ return allEntries;
+ }
+
+ @Nullable
+ @Override
+ public String getString(@Nullable String key, @Nullable String defValue) {
+ Object value = getDecryptedObject(key);
+ return (value != null && value instanceof String ? (String) value : defValue);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Nullable
+ @Override
+ public Set<String> getStringSet(@Nullable String key, @Nullable Set<String> defValues) {
+ Set<String> returnValues;
+ Object value = getDecryptedObject(key);
+ if (value instanceof Set) {
+ returnValues = (Set<String>) value;
+ } else {
+ returnValues = new ArraySet<>();
+ }
+ return returnValues.size() > 0 ? returnValues : defValues;
+ }
+
+ @Override
+ public int getInt(@Nullable String key, int defValue) {
+ Object value = getDecryptedObject(key);
+ return (value != null && value instanceof Integer ? (Integer) value : defValue);
+ }
+
+ @Override
+ public long getLong(@Nullable String key, long defValue) {
+ Object value = getDecryptedObject(key);
+ return (value != null && value instanceof Long ? (Long) value : defValue);
+ }
+
+ @Override
+ public float getFloat(@Nullable String key, float defValue) {
+ Object value = getDecryptedObject(key);
+ return (value != null && value instanceof Float ? (Float) value : defValue);
+ }
+
+ @Override
+ public boolean getBoolean(@Nullable String key, boolean defValue) {
+ Object value = getDecryptedObject(key);
+ return (value != null && value instanceof Boolean ? (Boolean) value : defValue);
+ }
+
+ @Override
+ public boolean contains(@Nullable String key) {
+ String encryptedKey = encryptKey(key);
+ return mSharedPreferences.contains(encryptedKey);
+ }
+
+ @Override
+ @NonNull
+ public SharedPreferences.Editor edit() {
+ return new Editor(this, mSharedPreferences.edit());
+ }
+
+ @Override
+ public void registerOnSharedPreferenceChangeListener(
+ @NonNull OnSharedPreferenceChangeListener listener) {
+ mListeners.add(listener);
+ }
+
+ @Override
+ public void unregisterOnSharedPreferenceChangeListener(
+ @NonNull OnSharedPreferenceChangeListener listener) {
+ mListeners.remove(listener);
+ }
+
+ /**
+ * Internal enum to set the type of encrypted data.
+ */
+ private enum EncryptedType {
+ STRING(0),
+ STRING_SET(1),
+ INT(2),
+ LONG(3),
+ FLOAT(4),
+ BOOLEAN(5);
+
+ int mId;
+
+ EncryptedType(int id) {
+ mId = id;
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public static EncryptedType fromId(int id) {
+ switch (id) {
+ case 0:
+ return STRING;
+ case 1:
+ return STRING_SET;
+ case 2:
+ return INT;
+ case 3:
+ return LONG;
+ case 4:
+ return FLOAT;
+ case 5:
+ return BOOLEAN;
+ }
+ return null;
+ }
+ }
+
+ private Object getDecryptedObject(String key) {
+ if (key == null) {
+ key = NULL_VALUE;
+ }
+ Object returnValue = null;
+ try {
+ String encryptedKey = encryptKey(key);
+ String encryptedValue = mSharedPreferences.getString(encryptedKey, null);
+ if (encryptedValue != null) {
+ byte[] cipherText = Base64.decode(encryptedValue, Base64.DEFAULT);
+ byte[] value = mValueAead.decrypt(cipherText, encryptedKey.getBytes(UTF_8));
+ ByteBuffer buffer = ByteBuffer.wrap(value);
+ buffer.position(0);
+ int typeId = buffer.getInt();
+ EncryptedType type = EncryptedType.fromId(typeId);
+ switch (type) {
+ case STRING:
+ int stringLength = buffer.getInt();
+ ByteBuffer stringSlice = buffer.slice();
+ buffer.limit(stringLength);
+ String stringValue = UTF_8.decode(stringSlice).toString();
+ if (stringValue.equals(NULL_VALUE)) {
+ returnValue = null;
+ } else {
+ returnValue = stringValue;
+ }
+ break;
+ case INT:
+ returnValue = buffer.getInt();
+ break;
+ case LONG:
+ returnValue = buffer.getLong();
+ break;
+ case FLOAT:
+ returnValue = buffer.getFloat();
+ break;
+ case BOOLEAN:
+ returnValue = buffer.get() != (byte) 0;
+ break;
+ case STRING_SET:
+ ArraySet<String> stringSet = new ArraySet<>();
+ while (buffer.hasRemaining()) {
+ int subStringLength = buffer.getInt();
+ ByteBuffer subStringSlice = buffer.slice();
+ subStringSlice.limit(subStringLength);
+ buffer.position(buffer.position() + subStringLength);
+ stringSet.add(UTF_8.decode(subStringSlice).toString());
+ }
+ if (stringSet.size() == 1 && NULL_VALUE.equals(stringSet.valueAt(0))) {
+ returnValue = null;
+ } else {
+ returnValue = stringSet;
+ }
+ break;
+ }
+ }
+ } catch (GeneralSecurityException ex) {
+ throw new SecurityException("Could not decrypt value. " + ex.getMessage(), ex);
+ }
+ return returnValue;
+ }
+
+ String encryptKey(String key) {
+ if (key == null) {
+ key = NULL_VALUE;
+ }
+ try {
+ byte[] encryptedKeyBytes = mKeyDeterministicAead.encryptDeterministically(
+ key.getBytes(UTF_8),
+ mFileName.getBytes());
+ return Base64.encode(encryptedKeyBytes);
+ } catch (GeneralSecurityException ex) {
+ throw new SecurityException("Could not encrypt key. " + ex.getMessage(), ex);
+ }
+ }
+
+ String decryptKey(String encryptedKey) {
+ try {
+ byte[] clearText = mKeyDeterministicAead.decryptDeterministically(
+ Base64.decode(encryptedKey, Base64.DEFAULT),
+ mFileName.getBytes());
+ String key = new String(clearText, UTF_8);
+ if (key.equals(NULL_VALUE)) {
+ key = null;
+ }
+ return key;
+ } catch (GeneralSecurityException ex) {
+ throw new SecurityException("Could not decrypt key. " + ex.getMessage(), ex);
+ }
+ }
+
+ Pair<String, String> encryptKeyValuePair(String key, byte[] value)
+ throws GeneralSecurityException {
+ String encryptedKey = encryptKey(key);
+ byte[] cipherText = mValueAead.encrypt(value, encryptedKey.getBytes(UTF_8));
+ return new Pair<>(encryptedKey, Base64.encode(cipherText));
+ }
+
+}
diff --git a/security/crypto/src/main/java/androidx/security/crypto/EphemeralSecretKey.java b/security/crypto/src/main/java/androidx/security/crypto/EphemeralSecretKey.java
deleted file mode 100644
index da2633a..0000000
--- a/security/crypto/src/main/java/androidx/security/crypto/EphemeralSecretKey.java
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package androidx.security.crypto;
-
-import android.security.keystore.KeyProperties;
-
-import androidx.annotation.NonNull;
-import androidx.security.SecureConfig;
-
-import java.security.GeneralSecurityException;
-import java.security.MessageDigest;
-import java.security.spec.KeySpec;
-import java.util.Arrays;
-import java.util.Locale;
-
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.DESKeySpec;
-import javax.crypto.spec.GCMParameterSpec;
-
-/**
- * A destroyable SecretKey. Can be completely evicted from memory on deletion.
- */
-public class EphemeralSecretKey implements KeySpec, SecretKey {
-
- private static final long serialVersionUID = 7177238317307289223L;
- private static final String TAG = "EphemeralSecretKey";
-
- private byte[] mKey;
-
- private String mAlgorithm;
-
- private boolean mKeyDestroyed;
-
- private SecureConfig mSecureConfig;
-
- public EphemeralSecretKey(@NonNull byte[] key) {
- this(key, KeyProperties.KEY_ALGORITHM_AES, SecureConfig.getDefault());
- }
-
- public EphemeralSecretKey(@NonNull byte[] key, @NonNull String algorithm) {
- this(key, algorithm, SecureConfig.getDefault());
- }
-
- public EphemeralSecretKey(@NonNull byte[] key, @NonNull SecureConfig secureConfig) {
- this(key, KeyProperties.KEY_ALGORITHM_AES, secureConfig);
- }
-
- /**
- * Constructs a secret mKey from the given byte array.
- *
- * <p>This constructor does not check if the given bytes indeed specify a
- * secret mKey of the specified mAlgorithm. For example, if the mAlgorithm is
- * DES, this constructor does not check if <code>mKey</code> is 8 bytes
- * long, and also does not check for weak or semi-weak keys.
- * In order for those checks to be performed, an mAlgorithm-specific
- * <i>mKey specification</i> class (in this case:
- * {@link DESKeySpec DESKeySpec})
- * should be used.
- *
- * @param key the mKey material of the secret mKey. The contents of
- * the array are copied to protect against subsequent modification.
- * @param algorithm the name of the secret-mKey mAlgorithm to be associated
- * with the given mKey material.
- * @throws IllegalArgumentException if <code>mAlgorithm</code>
- * is null or <code>mKey</code> is null or empty.
- */
- public EphemeralSecretKey(@NonNull byte[] key, @NonNull String algorithm,
- @NonNull SecureConfig secureConfig) {
- if (key == null || algorithm == null) {
- throw new IllegalArgumentException("Missing argument");
- }
- if (key.length == 0) {
- throw new IllegalArgumentException("Empty mKey");
- }
- this.mKey = key;
- this.mAlgorithm = algorithm;
- this.mKeyDestroyed = false;
- this.mSecureConfig = secureConfig;
- }
-
- /**
- * Constructs a secret mKey from the given byte array, using the first
- * <code>len</code> bytes of <code>mKey</code>, starting at
- * <code>offset</code> inclusive.
- *
- * <p> The bytes that constitute the secret mKey are
- * those between <code>mKey[offset]</code> and
- * <code>mKey[offset+len-1]</code> inclusive.
- *
- * <p>This constructor does not check if the given bytes indeed specify a
- * secret mKey of the specified mAlgorithm. For example, if the mAlgorithm is
- * DES, this constructor does not check if <code>mKey</code> is 8 bytes
- * long, and also does not check for weak or semi-weak keys.
- * In order for those checks to be performed, an mAlgorithm-specific mKey
- * specification class (in this case:
- * {@link DESKeySpec DESKeySpec})
- * must be used.
- *
- * @param key the mKey material of the secret mKey. The first
- * <code>len</code> bytes of the array beginning at
- * <code>offset</code> inclusive are copied to protect
- * against subsequent modification.
- * @param offset the offset in <code>mKey</code> where the mKey material
- * starts.
- * @param len the length of the mKey material.
- * @param algorithm the name of the secret-mKey mAlgorithm to be associated
- * with the given mKey material.
- * @throws IllegalArgumentException if <code>mAlgorithm</code>
- * is null or <code>mKey</code> is null,
- * empty, or too short,
- * i.e. {@code mKey.length-offset<len}.
- * @throws ArrayIndexOutOfBoundsException is thrown if
- * <code>offset</code> or <code>len</code>
- * index bytes outside the
- * <code>mKey</code>.
- */
- public EphemeralSecretKey(@NonNull byte[] key, int offset, int len,
- @NonNull String algorithm) {
- if (key == null || algorithm == null) {
- throw new IllegalArgumentException("Missing argument");
- }
- if (key.length == 0) {
- throw new IllegalArgumentException("Empty mKey");
- }
- if (key.length - offset < len) {
- throw new IllegalArgumentException("Invalid offset/length combination");
- }
- if (len < 0) {
- throw new ArrayIndexOutOfBoundsException("len is negative");
- }
- this.mKey = new byte[len];
- System.arraycopy(key, offset, this.mKey, 0, len);
- this.mAlgorithm = algorithm;
- this.mKeyDestroyed = false;
- }
-
- /**
- * Returns the name of the mAlgorithm associated with this secret mKey.
- *
- * @return the secret mKey mAlgorithm.
- */
- @NonNull
- public String getAlgorithm() {
- return this.mAlgorithm;
- }
-
- /**
- * Returns the name of the encoding format for this secret mKey.
- *
- * @return the string "RAW".
- */
- @NonNull
- public String getFormat() {
- return "RAW";
- }
-
- /**
- * Returns the mKey material of this secret mKey.
- *
- * @return the mKey material. Returns a new array
- * each time this method is called.
- */
- @NonNull
- public byte[] getEncoded() {
- return this.mKey;
- }
-
- /**
- * Calculates a hash code value for the object.
- * Objects that are equal will also have the same hashcode.
- */
- public int hashCode() {
- int retval = 0;
- for (int i = 1; i < this.mKey.length; i++) {
- retval += this.mKey[i] * i;
- }
- if (this.mAlgorithm.equalsIgnoreCase("TripleDES")) {
- return (retval ^= "desede".hashCode());
- } else {
- return (retval ^= this.mAlgorithm.toLowerCase(Locale.ENGLISH).hashCode());
- }
- }
-
- /**
- * Tests for equality between the specified object and this
- * object. Two SecretKeySpec objects are considered equal if
- * they are both SecretKey instances which have the
- * same case-insensitive mAlgorithm name and mKey encoding.
- *
- * @param obj the object to test for equality with this object.
- * @return true if the objects are considered equal, false if
- * <code>obj</code> is null or otherwise.
- */
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
-
- if (!(obj instanceof SecretKey)) {
- return false;
- }
-
- String thatAlg = ((SecretKey) obj).getAlgorithm();
- if (!(thatAlg.equalsIgnoreCase(this.mAlgorithm))) {
- if ((!(thatAlg.equalsIgnoreCase("DESede"))
- || !(this.mAlgorithm.equalsIgnoreCase("TripleDES")))
- && (!(thatAlg.equalsIgnoreCase("TripleDES"))
- || !(this.mAlgorithm.equalsIgnoreCase("DESede")))) {
- return false;
- }
- }
-
- byte[] thatKey = ((SecretKey) obj).getEncoded();
-
- return MessageDigest.isEqual(this.mKey, thatKey);
- }
-
- @Override
- public void destroy() {
- if (!mKeyDestroyed) {
- Arrays.fill(mKey, (byte) 0);
- this.mKey = null;
- mKeyDestroyed = true;
- }
- }
-
-
- /**
- * Manually destroy the cipher by zeroing out all instances of the mKey.
- *
- * @param cipher The Cipher used with this mKey.
- * @param opmode The opmode of the cipher.
- */
- public void destroyCipherKey(@NonNull Cipher cipher, int opmode) {
- try {
- byte[] blankKey = new byte[mSecureConfig.getSymmetricKeySize() / 8];
- byte[] iv = new byte[SecureConfig.AES_IV_SIZE_BYTES];
- Arrays.fill(blankKey, (byte) 0);
- Arrays.fill(iv, (byte) 0);
- EphemeralSecretKey blankSecretKey =
- new EphemeralSecretKey(blankKey, mSecureConfig.getSymmetricKeyAlgorithm());
- cipher.init(opmode, blankSecretKey,
- new GCMParameterSpec(mSecureConfig.getSymmetricGcmTagLength(), iv));
- } catch (GeneralSecurityException e) {
- throw new SecurityException("Could not destroy mKey.");
- }
- }
-
- @Override
- public boolean isDestroyed() {
- return mKeyDestroyed;
- }
-}
-
diff --git a/security/crypto/src/main/java/androidx/security/crypto/FileCipher.java b/security/crypto/src/main/java/androidx/security/crypto/FileCipher.java
deleted file mode 100644
index 19b36d3..0000000
--- a/security/crypto/src/main/java/androidx/security/crypto/FileCipher.java
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.security.crypto;
-
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.security.SecureConfig;
-import androidx.security.context.SecureContextCompat;
-
-import java.io.FileDescriptor;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.channels.FileChannel;
-import java.util.Arrays;
-import java.util.concurrent.Executor;
-
-/**
- * Class used to create and read encrypted files. Provides implementations
- * of EncryptedFileInput/Output Streams.
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class FileCipher {
-
- private String mFileName;
- private String mKeyPairAlias;
- private FileInputStream mFileInputStream;
- private FileOutputStream mFileOutputStream;
-
- private SecureContextCompat.EncryptedFileInputStreamListener mListener;
-
- SecureConfig mSecureConfig;
-
- public FileCipher(@NonNull String fileName, @NonNull FileInputStream fileInputStream,
- @NonNull SecureConfig secureConfig, @NonNull Executor executor,
- @NonNull SecureContextCompat.EncryptedFileInputStreamListener listener)
- throws IOException {
- mFileName = fileName;
- mFileInputStream = fileInputStream;
- mSecureConfig = secureConfig;
- EncryptedFileInputStream encryptedFileInputStream =
- new EncryptedFileInputStream(mFileInputStream);
- setEncryptedFileInputStreamListener(listener);
- encryptedFileInputStream.decrypt(listener);
- }
-
- public FileCipher(@NonNull String keyPairAlias, @NonNull FileOutputStream fileOutputStream,
- @NonNull SecureConfig secureConfig) {
- mKeyPairAlias = keyPairAlias;
- mFileOutputStream = new EncryptedFileOutputStream(mFileName, mKeyPairAlias,
- fileOutputStream);
- mSecureConfig = secureConfig;
- }
-
- /**
- * @param listener the listener to call back on
- */
- public void setEncryptedFileInputStreamListener(
- @NonNull SecureContextCompat.EncryptedFileInputStreamListener listener) {
- mListener = listener;
- }
-
- /**
- * @return the file output stream
- */
- @NonNull
- public FileOutputStream getFileOutputStream() {
- return mFileOutputStream;
- }
-
- /**
- * @return the file input stream
- */
- @NonNull
- public FileInputStream getFileInputStream() {
- return mFileInputStream;
- }
-
- /**
- * Encrypted file output stream
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- class EncryptedFileOutputStream extends FileOutputStream {
-
- private static final String TAG = "EncryptedFOS";
-
- FileOutputStream mFileOutputStream;
- private String mKeyPairAlias;
-
- EncryptedFileOutputStream(String name, String keyPairAlias,
- FileOutputStream fileOutputStream) {
- super(new FileDescriptor());
- this.mKeyPairAlias = keyPairAlias;
- this.mFileOutputStream = fileOutputStream;
- }
-
- String getAsymKeyPairAlias() {
- return this.mKeyPairAlias;
- }
-
- @Override
- public void write(@NonNull byte[] b) {
- SecureKeyStore secureKeyStore = SecureKeyStore.getDefault();
- if (!secureKeyStore.keyExists(getAsymKeyPairAlias())) {
- SecureKeyGenerator keyGenerator = SecureKeyGenerator.getDefault();
- keyGenerator.generateAsymmetricKeyPair(getAsymKeyPairAlias());
- }
- SecureKeyGenerator secureKeyGenerator = SecureKeyGenerator.getDefault();
- final EphemeralSecretKey secretKey = secureKeyGenerator.generateEphemeralDataKey();
- final SecureCipher secureCipher = SecureCipher
- .getDefault(mSecureConfig.getBiometricKeyAuthCallback());
- final Pair<byte[], byte[]> encryptedData =
- secureCipher.encryptEphemeralData(secretKey, b);
- secureCipher.encryptAsymmetric(getAsymKeyPairAlias(),
- secretKey.getEncoded(),
- new SecureCipher.SecureAsymmetricEncryptionListener() {
- public void encryptionComplete(byte[] encryptedEphemeralKey) {
- byte[] encodedData = secureCipher.encodeEphemeralData(
- getAsymKeyPairAlias().getBytes(), encryptedEphemeralKey,
- encryptedData.first, encryptedData.second);
- secretKey.destroy();
- try {
- mFileOutputStream.write(encodedData);
- } catch (IOException e) {
- Log.e(TAG, "Failed to write secure file.");
- e.printStackTrace();
- }
- }
- });
- }
-
- @Override
- public void write(int b) throws IOException {
- throw new UnsupportedOperationException("For encrypted files, you must write all data "
- + "simultaneously. Call #write(byte[]).");
- }
-
- @Override
- public void write(@NonNull byte[] b, int off, int len) throws IOException {
- throw new UnsupportedOperationException("For encrypted files, you must write all data "
- + "simultaneously. Call #write(byte[]).");
- }
-
- @Override
- public void close() throws IOException {
- mFileOutputStream.close();
- }
-
- @NonNull
- @Override
- public FileChannel getChannel() {
- throw new UnsupportedOperationException("For encrypted files, you must write all data "
- + "simultaneously. Call #write(byte[]).");
- }
-
- @Override
- protected void finalize() throws IOException {
- super.finalize();
- }
-
- @Override
- public void flush() throws IOException {
- mFileOutputStream.flush();
- }
- }
-
-
- /**
- * Encrypted file input stream
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- class EncryptedFileInputStream extends FileInputStream {
-
- // Was 25 characters, truncating to fix compile error
- private static final String TAG = "EncryptedFIS";
-
- private FileInputStream mFileInputStream;
- byte[] mDecryptedData;
- private int mReadStatus = 0;
-
- EncryptedFileInputStream(FileInputStream fileInputStream) {
- super(new FileDescriptor());
- this.mFileInputStream = fileInputStream;
- }
-
- @Override
- public int read() throws IOException {
- throw new UnsupportedOperationException("For encrypted files, you must read all data "
- + "simultaneously. Call #read(byte[]).");
- }
-
- void decrypt(final SecureContextCompat.EncryptedFileInputStreamListener listener)
- throws IOException {
- final EncryptedFileInputStream thisStream = this;
- if (this.mDecryptedData == null) {
- try {
- byte[] encodedData = new byte[mFileInputStream.available()];
- mReadStatus = mFileInputStream.read(encodedData);
- SecureCipher secureCipher = SecureCipher.getDefault(
- mSecureConfig.getBiometricKeyAuthCallback());
- secureCipher.decryptEncodedData(encodedData,
- new SecureCipher.SecureDecryptionListener() {
- public void decryptionComplete(byte[] clearText) {
- thisStream.mDecryptedData = clearText;
- //Binder.clearCallingIdentity();
- listener.onEncryptedFileInput(thisStream);
- }
- });
- } catch (IOException ex) {
- throw ex;
- }
- }
- }
-
- private void destroyCache() {
- if (mDecryptedData != null) {
- Arrays.fill(mDecryptedData, (byte) 0);
- mDecryptedData = null;
- }
- }
-
- @Override
- public int read(@NonNull byte[] b) {
- System.arraycopy(mDecryptedData, 0, b, 0, mDecryptedData.length);
- return mReadStatus;
- }
-
- @Override
- public int read(@NonNull byte[] b, int off, int len) throws IOException {
- throw new UnsupportedOperationException("For encrypted files, you must read all data "
- + "simultaneously. Call #read(byte[]).");
- }
-
- @Override
- public long skip(long n) throws IOException {
- throw new UnsupportedOperationException("For encrypted files, you must read all data "
- + "simultaneously. Call #read(byte[]).");
- }
-
- @Override
- public int available() {
- return mDecryptedData.length;
- }
-
- @Override
- public void close() throws IOException {
- destroyCache();
- mFileInputStream.close();
- }
-
- @Override
- public FileChannel getChannel() {
- throw new UnsupportedOperationException("For encrypted files, you must read all data "
- + "simultaneously. Call #read(byte[]).");
- }
-
- @Override
- protected void finalize() throws IOException {
- destroyCache();
- super.finalize();
- }
-
- @Override
- public synchronized void mark(int readlimit) {
- throw new UnsupportedOperationException("For encrypted files, you must read all data "
- + "simultaneously. Call #read(byte[]).");
- }
-
- @Override
- public synchronized void reset() throws IOException {
- throw new UnsupportedOperationException("For encrypted files, you must read all data "
- + "simultaneously. Call #read(byte[]).");
- }
-
- @Override
- public boolean markSupported() {
- return false;
- }
-
- }
-
-}
diff --git a/security/crypto/src/main/java/androidx/security/crypto/MasterKeys.java b/security/crypto/src/main/java/androidx/security/crypto/MasterKeys.java
new file mode 100644
index 0000000..8b04c5b
--- /dev/null
+++ b/security/crypto/src/main/java/androidx/security/crypto/MasterKeys.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.security.crypto;
+
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+
+import androidx.annotation.NonNull;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.Arrays;
+
+import javax.crypto.KeyGenerator;
+
+/**
+ * Convenient methods to create and obtain master keys in Android Keystore.
+ *
+ * <p>The master keys are used to encrypt data encryption keys for encrypting files and preferences.
+ *
+ */
+public final class MasterKeys {
+
+ private static final int KEY_SIZE = 256;
+
+ private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
+ static final String KEYSTORE_PATH_URI = "android-keystore://";
+ static final String MASTER_KEY_ALIAS = "_androidx_security_master_key_";
+
+ @NonNull
+ public static final KeyGenParameterSpec AES256_GCM_SPEC =
+ createAES256GCMKeyGenParameterSpec(MASTER_KEY_ALIAS);
+
+ /**
+ * Provides a safe and easy to use KenGenParameterSpec with the settings.
+ * Algorithm: AES
+ * Block Mode: GCM
+ * Padding: No Padding
+ * Key Size: 256
+ *
+ * @param keyAlias The alias for the master key
+ * @return The spec for the master key with the specified keyAlias
+ */
+ @NonNull
+ private static KeyGenParameterSpec createAES256GCMKeyGenParameterSpec(
+ @NonNull String keyAlias) {
+ KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
+ keyAlias,
+ KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+ .setKeySize(KEY_SIZE);
+ return builder.build();
+ }
+
+ /**
+ * Provides a safe and easy to use KenGenParameterSpec with the settings with a default
+ * key alias.
+ *
+ * Algorithm: AES
+ * Block Mode: GCM
+ * Padding: No Padding
+ * Key Size: 256
+ *
+ * @return The spec for the master key with the default key alias
+ */
+ @NonNull
+ private static KeyGenParameterSpec createAES256GCMKeyGenParameterSpec() {
+ return createAES256GCMKeyGenParameterSpec(MASTER_KEY_ALIAS);
+ }
+
+ /**
+ * Creates or gets the master key provided
+ *
+ * The encryption scheme is required fields to ensure that the type of
+ * encryption used is clear to developers.
+ *
+ * @param keyGenParameterSpec The key encryption scheme
+ * @return The key alias for the master key
+ */
+ @NonNull
+ public static String getOrCreate(
+ @NonNull KeyGenParameterSpec keyGenParameterSpec)
+ throws GeneralSecurityException, IOException {
+ validate(keyGenParameterSpec);
+ if (!MasterKeys.keyExists(keyGenParameterSpec.getKeystoreAlias())) {
+ generateKey(keyGenParameterSpec);
+ }
+ return keyGenParameterSpec.getKeystoreAlias();
+ }
+
+ private static void validate(KeyGenParameterSpec spec) {
+ if (spec.getKeySize() != KEY_SIZE) {
+ throw new IllegalArgumentException(
+ "invalid key size, want " + KEY_SIZE + " bits got " + spec.getKeySize()
+ + " bits");
+ }
+ if (spec.getBlockModes().equals(new String[] { KeyProperties.BLOCK_MODE_GCM })) {
+ throw new IllegalArgumentException(
+ "invalid block mode, want " + KeyProperties.BLOCK_MODE_GCM + " got "
+ + Arrays.toString(spec.getBlockModes()));
+ }
+ if (spec.getPurposes() != (KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)) {
+ throw new IllegalArgumentException(
+ "invalid purposes mode, want PURPOSE_ENCRYPT | PURPOSE_DECRYPT got "
+ + spec.getPurposes());
+ }
+ if (spec.getEncryptionPaddings().equals(new String[]
+ { KeyProperties.ENCRYPTION_PADDING_NONE })) {
+ throw new IllegalArgumentException(
+ "invalid padding mode, want " + KeyProperties.ENCRYPTION_PADDING_NONE + " got "
+ + Arrays.toString(spec.getEncryptionPaddings()));
+ }
+ }
+
+ private static void generateKey(@NonNull KeyGenParameterSpec keyGenParameterSpec)
+ throws GeneralSecurityException {
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(
+ KeyProperties.KEY_ALGORITHM_AES,
+ ANDROID_KEYSTORE);
+ keyGenerator.init(keyGenParameterSpec);
+ keyGenerator.generateKey();
+ }
+
+ private static boolean keyExists(@NonNull String keyAlias)
+ throws GeneralSecurityException, IOException {
+ KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
+ keyStore.load(null);
+ return keyStore.containsAlias(keyAlias);
+ }
+
+}
diff --git a/security/crypto/src/main/java/androidx/security/crypto/SecureCipher.java b/security/crypto/src/main/java/androidx/security/crypto/SecureCipher.java
deleted file mode 100644
index 5cd7af0..0000000
--- a/security/crypto/src/main/java/androidx/security/crypto/SecureCipher.java
+++ /dev/null
@@ -1,665 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.security.crypto;
-
-import android.os.Build;
-import android.security.keystore.KeyProperties;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.security.SecureConfig;
-import androidx.security.biometric.BiometricKeyAuthCallback;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.security.GeneralSecurityException;
-import java.security.Key;
-import java.security.KeyStore;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.SecureRandom;
-import java.security.Signature;
-import java.security.spec.MGF1ParameterSpec;
-
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.GCMParameterSpec;
-import javax.crypto.spec.OAEPParameterSpec;
-import javax.crypto.spec.PSource;
-
-/**
- * Class that helps encrypt and decrypt data.
- */
-public class SecureCipher {
-
- private static final String TAG = "SecureCipher";
- private static final int FILE_ENCODING_VERSION = 1;
-
- private SecureConfig mSecureConfig;
-
- /**
- * Listener for encryption that requires biometric prompt
- */
- public interface SecureAuthListener {
- /**
- * @param status the status of auth
- */
- void authComplete(@NonNull BiometricKeyAuthCallback.BiometricStatus status);
- }
-
- /**
- * Listener for encrypting symmetric data
- */
- public interface SecureSymmetricEncryptionListener {
- /**
- * @param cipherText the encrypted cipher text
- * @param iv the initialization vector used
- */
- void encryptionComplete(@NonNull byte[] cipherText, @NonNull byte[] iv);
- }
-
- /**
- * Listener for encrypting asymmetric data
- */
- public interface SecureAsymmetricEncryptionListener {
- /**
- * @param cipherText the encrypted cipher text
- */
- void encryptionComplete(@NonNull byte[] cipherText);
- }
-
- /**
- * The listener for decryption
- */
- public interface SecureDecryptionListener {
- /**
- * @param clearText the decrypted text
- */
- void decryptionComplete(@NonNull byte[] clearText);
- }
-
- /**
- * The listener for signing data
- */
- public interface SecureSignListener {
- /**
- * @param signature the signature
- */
- void signComplete(@NonNull byte[] signature);
- }
-
- /**
- * @return The secure cipher
- */
- @NonNull
- public static SecureCipher getDefault() {
- return new SecureCipher(SecureConfig.getDefault());
- }
-
- /**
- * @param biometricKeyAuthCallback The callback to use when authenticating a key
- * @return the cipher using biometric prompt
- */
- @NonNull
- public static SecureCipher getDefault(
- @NonNull BiometricKeyAuthCallback biometricKeyAuthCallback) {
- SecureConfig secureConfig = SecureConfig.getDefault();
- secureConfig.setSymmetricRequireUserAuth(true);
- secureConfig.setAsymmetricRequireUserAuth(true);
- secureConfig.setBiometricKeyAuthCallback(biometricKeyAuthCallback);
- return new SecureCipher(secureConfig);
- }
-
- /**
- * Gets an instance using the specified configuration
- *
- * @param secureConfig the config
- * @return the secure cipher
- */
- @NonNull
- public static SecureCipher getInstance(@NonNull SecureConfig secureConfig) {
- return new SecureCipher(secureConfig);
- }
-
-
- public SecureCipher(@NonNull SecureConfig secureConfig) {
- this.mSecureConfig = secureConfig;
- }
-
- /**
- * The encoding mType for files
- */
- public enum SecureFileEncodingType {
- SYMMETRIC(0),
- ASYMMETRIC(1),
- EPHEMERAL(2),
- NOT_ENCRYPTED(1000);
-
- private final int mType;
-
- SecureFileEncodingType(int type) {
- this.mType = type;
- }
-
- /**
- * @return the id
- */
- public int getType() {
- return this.mType;
- }
-
- /**
- * @param id the id
- * @return the encoding mType
- */
- @NonNull
- public static SecureFileEncodingType fromId(int id) {
- switch (id) {
- case 0:
- return SYMMETRIC;
- case 1:
- return ASYMMETRIC;
- case 2:
- return EPHEMERAL;
- }
- return NOT_ENCRYPTED;
- }
-
- }
-
-
- /**
- * Encrypts data with an existing key alias from the AndroidKeyStore.
- *
- * @param keyAlias The name of the existing SecretKey to retrieve from the AndroidKeyStore.
- * @param clearData The unencrypted data to encrypt
- */
- public void encrypt(@NonNull String keyAlias,
- @NonNull final byte[] clearData,
- @NonNull final SecureSymmetricEncryptionListener callback) {
- try {
- KeyStore keyStore = KeyStore.getInstance(mSecureConfig.getAndroidKeyStore());
- keyStore.load(null);
- SecretKey key = (SecretKey) keyStore.getKey(keyAlias, null);
- final Cipher cipher = Cipher.getInstance(
- mSecureConfig.getSymmetricCipherTransformation());
- cipher.init(Cipher.ENCRYPT_MODE, key);
- byte[] iv = cipher.getIV();
- if (mSecureConfig.getSymmetricRequireUserAuthEnabled()) {
- mSecureConfig.getBiometricKeyAuthCallback().authenticateKey(cipher,
- new SecureAuthListener() {
- public void authComplete(
- BiometricKeyAuthCallback.BiometricStatus status) {
-
- switch (status) {
- case SUCCESS:
- try {
- callback.encryptionComplete(cipher.doFinal(clearData),
- cipher.getIV());
- } catch (GeneralSecurityException e) {
- e.printStackTrace();
- }
- break;
- default:
- Log.i(TAG, "Failure");
- callback.encryptionComplete(null, null);
- }
- }
- });
- } else {
- callback.encryptionComplete(cipher.doFinal(clearData), cipher.getIV());
- }
- } catch (GeneralSecurityException ex) {
- throw new SecurityException(ex);
- } catch (IOException ex) {
- throw new SecurityException(ex);
- }
- }
-
- /**
- * Signs data based on the specific key that has been generated
- *
- * Uses SecureConfig.getSignatureAlgorithm for algorithm type
- *
- * @param keyAlias The key to use for signing
- * @param clearData The data to sign
- * @param callback The listener to call back with the signature
- */
- public void sign(@NonNull String keyAlias,
- @NonNull final byte[] clearData,
- @NonNull final SecureSignListener callback) {
- byte[] dataSignature = new byte[0];
- try {
- KeyStore keyStore = KeyStore.getInstance(mSecureConfig.getAndroidKeyStore());
- keyStore.load(null);
- PrivateKey key = (PrivateKey) keyStore.getKey(keyAlias, null);
- final Signature signature = Signature.getInstance(
- mSecureConfig.getSignatureAlgorithm());
- signature.initSign(key);
- signature.update(clearData);
- if (mSecureConfig.getAsymmetricRequireUserAuthEnabled()) {
- mSecureConfig.getBiometricKeyAuthCallback().authenticateKey(signature,
- new SecureAuthListener() {
- public void authComplete(
- BiometricKeyAuthCallback.BiometricStatus status) {
- Log.i(TAG, "Finished success auth!");
- switch (status) {
- case SUCCESS:
- try {
- byte[] sig = signature.sign();
- Log.i(TAG, "Signed " + clearData.length
- + " bytes");
- callback.signComplete(sig);
-
- } catch (GeneralSecurityException e) {
- e.printStackTrace();
- }
- break;
- default:
- Log.i(TAG, "Failure");
- callback.signComplete(null);
- }
- }
- });
- } else {
- dataSignature = signature.sign();
- callback.signComplete(dataSignature);
- }
- } catch (GeneralSecurityException ex) {
- ex.printStackTrace();
- //Log.e(TAG, ex.getMessage());
- } catch (IOException ex) {
- Log.e(TAG, ex.getMessage());
- ex.printStackTrace();
- }
- }
-
- /**
- * Verifies signed data
- *
- * Uses SecureConfig.getSignatureAlgorithm for algorithm type
- *
- * @param keyAlias The key to use for signing
- * @param clearData The signed data
- * @param signature The signature
- * @return true if the provided signature is valid, false otherwise
- */
- public boolean verify(@NonNull String keyAlias,
- @NonNull final byte[] clearData,
- @NonNull final byte[] signature) {
- try {
- KeyStore keyStore = KeyStore.getInstance(mSecureConfig.getAndroidKeyStore());
- keyStore.load(null);
- PublicKey publicKey = keyStore.getCertificate(keyAlias).getPublicKey();
- final Signature signatureObject = Signature.getInstance(
- mSecureConfig.getSignatureAlgorithm());
- signatureObject.initVerify(publicKey);
- signatureObject.update(clearData);
- return signatureObject.verify(signature);
- } catch (GeneralSecurityException ex) {
- ex.printStackTrace();
- //Log.e(TAG, ex.getMessage());
- } catch (IOException ex) {
- Log.e(TAG, ex.getMessage());
- ex.printStackTrace();
- }
- return false;
- }
-
- /**
- * Encrypts data with a public key from the cert in the AndroidKeyStore.
- *
- * @param keyAlias The name of the existing KeyPair to retrieve the
- * PublicKey from the AndroidKeyStore.
- * @param clearData The unencrypted data to encrypt
- */
- public void encryptAsymmetric(@NonNull String keyAlias,
- @NonNull byte[] clearData, @NonNull SecureAsymmetricEncryptionListener callback) {
- try {
- KeyStore keyStore = KeyStore.getInstance(mSecureConfig.getAndroidKeyStore());
- keyStore.load(null);
- PublicKey publicKey = keyStore.getCertificate(keyAlias).getPublicKey();
- Cipher cipher = Cipher.getInstance(
- mSecureConfig.getAsymmetricCipherTransformation());
- // Check to see if there are other padding types with a complex config.
- if (mSecureConfig.getAsymmetricPaddings().equals(
- KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)) {
- cipher.init(Cipher.ENCRYPT_MODE, publicKey,
- new OAEPParameterSpec("SHA-256",
- "MGF1", new MGF1ParameterSpec("SHA-1"),
- PSource.PSpecified.DEFAULT));
- } else {
- cipher.init(Cipher.ENCRYPT_MODE, publicKey);
- }
- byte[] clearText = cipher.doFinal(clearData);
- callback.encryptionComplete(clearText);
- } catch (GeneralSecurityException ex) {
- throw new SecurityException(ex);
- } catch (IOException ex) {
- throw new SecurityException(ex);
- }
- }
-
- /**
- * Encrypts data using an Ephemeral key, destroying any trace of the key from the Cipher used.
- *
- * @param ephemeralSecretKey The generated Ephemeral key
- * @param clearData The unencrypted data to encrypt
- * @return A Pair of byte[]'s, first is the encrypted data, second is the IV
- * (initialization vector)
- * used to encrypt which is required for decryption
- */
- @NonNull
- public Pair<byte[], byte[]> encryptEphemeralData(
- @NonNull EphemeralSecretKey ephemeralSecretKey, @NonNull byte[] clearData) {
- try {
- SecureRandom secureRandom = null;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- secureRandom = SecureRandom.getInstanceStrong();
- } else {
- secureRandom = new SecureRandom();
- }
- byte[] iv = new byte[SecureConfig.AES_IV_SIZE_BYTES];
- secureRandom.nextBytes(iv);
- GCMParameterSpec parameterSpec = new GCMParameterSpec(
- mSecureConfig.getSymmetricGcmTagLength(), iv);
- final Cipher cipher = Cipher.getInstance(
- mSecureConfig.getSymmetricCipherTransformation());
- cipher.init(Cipher.ENCRYPT_MODE, ephemeralSecretKey, parameterSpec);
- byte[] encryptedData = cipher.doFinal(clearData);
- ephemeralSecretKey.destroyCipherKey(cipher, Cipher.ENCRYPT_MODE);
- return new Pair<>(encryptedData, iv);
- } catch (GeneralSecurityException ex) {
- throw new SecurityException(ex);
- }
- }
-
- /**
- * Decrypts a previously encrypted byte[]
- * <p>
- * Destroys all traces of the key data in the Cipher.
- *
- * @param ephemeralSecretKey The generated Ephemeral key
- * @param encryptedData The byte[] of encrypted data
- * @param initializationVector The IV of which the encrypted data was encrypted with
- * @return The byte[] of data that has been decrypted
- */
- @NonNull
- public byte[] decryptEphemeralData(@NonNull EphemeralSecretKey ephemeralSecretKey,
- @NonNull byte[] encryptedData, @NonNull byte[] initializationVector) {
- try {
- final Cipher cipher = Cipher.getInstance(
- mSecureConfig.getSymmetricCipherTransformation());
- cipher.init(Cipher.DECRYPT_MODE, ephemeralSecretKey,
- new GCMParameterSpec(
- mSecureConfig.getSymmetricGcmTagLength(), initializationVector));
- byte[] decryptedData = cipher.doFinal(encryptedData);
- ephemeralSecretKey.destroyCipherKey(cipher, Cipher.DECRYPT_MODE);
- return decryptedData;
- } catch (GeneralSecurityException ex) {
- throw new SecurityException(ex);
- }
- }
-
- /**
- * Decrypts a previously encrypted byte[]
- *
- * @param keyAlias The name of the existing SecretKey to retrieve from the
- * AndroidKeyStore.
- * @param encryptedData The byte[] of encrypted data
- * @param initializationVector The IV of which the encrypted data was encrypted with
- */
- public void decrypt(@NonNull String keyAlias,
- @NonNull final byte[] encryptedData, @NonNull byte[] initializationVector,
- @NonNull final SecureDecryptionListener callback) {
- byte[] decryptedData = new byte[0];
- try {
- KeyStore keyStore = KeyStore.getInstance(mSecureConfig.getAndroidKeyStore());
- keyStore.load(null);
- Key key = keyStore.getKey(keyAlias, null);
- final Cipher cipher = Cipher.getInstance(
- mSecureConfig.getSymmetricCipherTransformation());
- GCMParameterSpec spec = new GCMParameterSpec(
- mSecureConfig.getSymmetricGcmTagLength(), initializationVector);
- cipher.init(Cipher.DECRYPT_MODE, key, spec);
- if (mSecureConfig.getSymmetricRequireUserAuthEnabled()) {
- mSecureConfig.getBiometricKeyAuthCallback().authenticateKey(cipher,
- new SecureAuthListener() {
- public void authComplete(
- BiometricKeyAuthCallback.BiometricStatus status) {
- switch (status) {
- case SUCCESS:
- try {
- callback.decryptionComplete(
- cipher.doFinal(encryptedData));
- } catch (GeneralSecurityException e) {
- e.printStackTrace();
- }
- break;
- default:
- Log.i(TAG, "Failure");
- callback.decryptionComplete(null);
- }
- }
- });
- } else {
- callback.decryptionComplete(cipher.doFinal(encryptedData));
- }
- } catch (GeneralSecurityException ex) {
- throw new SecurityException(ex);
- } catch (IOException ex) {
- throw new SecurityException(ex);
- }
- }
-
- /**
- * Decrypts a previously encrypted byte[] with the PrivateKey
- *
- * @param keyAlias The name of the existing KeyPair to retrieve from the AndroidKeyStore.
- * @param encryptedData The byte[] of encrypted data
- */
- public void decryptAsymmetric(@NonNull String keyAlias,
- @NonNull final byte[] encryptedData,
- @NonNull final SecureDecryptionListener callback) {
- byte[] decryptedData = new byte[0];
- try {
- KeyStore keyStore = KeyStore.getInstance(mSecureConfig.getAndroidKeyStore());
- keyStore.load(null);
- PrivateKey key = (PrivateKey) keyStore.getKey(keyAlias, null);
- final Cipher cipher = Cipher.getInstance(
- mSecureConfig.getAsymmetricCipherTransformation());
- if (mSecureConfig.getAsymmetricPaddings().equals(
- KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)) {
- cipher.init(Cipher.DECRYPT_MODE, key, new OAEPParameterSpec("SHA-256",
- "MGF1",
- new MGF1ParameterSpec("SHA-1"), PSource.PSpecified.DEFAULT));
- } else {
- cipher.init(Cipher.DECRYPT_MODE, key);
- }
- if (mSecureConfig.getAsymmetricRequireUserAuthEnabled()) {
- mSecureConfig.getBiometricKeyAuthCallback().authenticateKey(cipher,
- new SecureAuthListener() {
- public void authComplete(
- BiometricKeyAuthCallback.BiometricStatus status) {
- Log.i(TAG, "Finished success auth!");
- switch (status) {
- case SUCCESS:
- try {
- byte[] clearData = cipher.doFinal(encryptedData);
- Log.i(TAG, "Decrypted " + new String(clearData));
- callback.decryptionComplete(clearData);
-
- } catch (GeneralSecurityException e) {
- e.printStackTrace();
- }
- break;
- default:
- Log.i(TAG, "Failure");
- callback.decryptionComplete(null);
- }
- }
- });
- } else {
- decryptedData = cipher.doFinal(encryptedData);
- callback.decryptionComplete(decryptedData);
- }
- } catch (GeneralSecurityException ex) {
- ex.printStackTrace();
- //Log.e(TAG, ex.getMessage());
- } catch (IOException ex) {
- Log.e(TAG, ex.getMessage());
- ex.printStackTrace();
- }
- }
-
- /**
- * @param keyPairAlias
- * @param encryptedKey
- * @param cipherText
- * @param iv
- * @return
- */
- @NonNull
- public byte[] encodeEphemeralData(@NonNull byte[] keyPairAlias, @NonNull byte[] encryptedKey,
- @NonNull byte[] cipherText, @NonNull byte[] iv) {
- ByteBuffer byteBuffer = ByteBuffer.allocate(((Integer.SIZE / 8) * 5) + iv.length
- + keyPairAlias.length + encryptedKey.length + cipherText.length);
- byteBuffer.putInt(SecureFileEncodingType.EPHEMERAL.getType());
- byteBuffer.putInt(FILE_ENCODING_VERSION);
- byteBuffer.putInt(encryptedKey.length);
- byteBuffer.put(encryptedKey);
- byteBuffer.putInt(iv.length);
- byteBuffer.put(iv);
- byteBuffer.putInt(keyPairAlias.length);
- byteBuffer.put(keyPairAlias);
- byteBuffer.put(cipherText);
- return byteBuffer.array();
- }
-
- /**
- * @param keyAlias
- * @param cipherText
- * @param iv
- * @return
- */
- @NonNull
- public byte[] encodeSymmetricData(@NonNull byte[] keyAlias, @NonNull byte[] cipherText,
- @NonNull byte[] iv) {
- ByteBuffer byteBuffer = ByteBuffer.allocate(((Integer.SIZE / 8) * 4) + iv.length
- + keyAlias.length + cipherText.length);
- byteBuffer.putInt(SecureFileEncodingType.SYMMETRIC.getType());
- byteBuffer.putInt(FILE_ENCODING_VERSION);
- byteBuffer.putInt(iv.length);
- byteBuffer.put(iv);
- byteBuffer.putInt(keyAlias.length);
- byteBuffer.put(keyAlias);
- byteBuffer.put(cipherText);
- return byteBuffer.array();
- }
-
- /**
- * @param keyPairAlias
- * @param cipherText
- * @return
- */
- @NonNull
- public byte[] encodeAsymmetricData(@NonNull byte[] keyPairAlias,
- @NonNull byte[] cipherText) {
- ByteBuffer byteBuffer = ByteBuffer.allocate(((Integer.SIZE / 8) * 3)
- + keyPairAlias.length + cipherText.length);
- byteBuffer.putInt(SecureFileEncodingType.ASYMMETRIC.getType());
- byteBuffer.putInt(FILE_ENCODING_VERSION);
- byteBuffer.putInt(keyPairAlias.length);
- byteBuffer.put(keyPairAlias);
- byteBuffer.put(cipherText);
- return byteBuffer.array();
- }
-
- /**
- * @param encodedCipherText
- * @param callback
- */
- @SuppressWarnings("fallthrough")
- public void decryptEncodedData(@NonNull byte[] encodedCipherText,
- @NonNull final SecureDecryptionListener callback) {
- ByteBuffer byteBuffer = ByteBuffer.wrap(encodedCipherText);
- int encodingTypeVal = byteBuffer.getInt();
- SecureFileEncodingType encodingType = SecureFileEncodingType.fromId(encodingTypeVal);
- int encodingVersion = byteBuffer.getInt();
- byte[] encodedEphKey = null;
- byte[] iv = null;
- String keyAlias = null;
- byte[] cipherText = null;
-
- switch (encodingType) {
- case EPHEMERAL:
- int encodedEphKeyLength = byteBuffer.getInt();
- encodedEphKey = new byte[encodedEphKeyLength];
- byteBuffer.get(encodedEphKey);
- // fall through
- case SYMMETRIC:
- int ivLength = byteBuffer.getInt();
- iv = new byte[ivLength];
- byteBuffer.get(iv);
- // fall through
- case ASYMMETRIC:
- int keyAliasLength = byteBuffer.getInt();
- byte[] keyAliasBytes = new byte[keyAliasLength];
- byteBuffer.get(keyAliasBytes);
- keyAlias = new String(keyAliasBytes);
- cipherText = new byte[byteBuffer.remaining()];
- byteBuffer.get(cipherText);
- break;
- case NOT_ENCRYPTED:
- throw new SecurityException("Cannot determine file mType.");
- }
- switch (encodingType) {
- case EPHEMERAL:
- final byte[] ephemeralCipherText = cipherText;
- final byte[] ephemeralIv = iv;
- decryptAsymmetric(keyAlias, encodedEphKey,
- new SecureDecryptionListener() {
- @java.lang.Override
- public void decryptionComplete(byte[] clearText) {
- EphemeralSecretKey ephemeralSecretKey =
- new EphemeralSecretKey(clearText);
- byte[] decrypted = decryptEphemeralData(
- ephemeralSecretKey,
- ephemeralCipherText, ephemeralIv);
- callback.decryptionComplete(decrypted);
- ephemeralSecretKey.destroy();
- }
- });
-
- break;
- case SYMMETRIC:
- decrypt(
- keyAlias,
- cipherText, iv, callback);
- break;
- case ASYMMETRIC:
- decryptAsymmetric(
- keyAlias,
- cipherText, callback);
- break;
- case NOT_ENCRYPTED:
- throw new SecurityException("File not encrypted.");
- }
- }
-
-}
diff --git a/security/crypto/src/main/java/androidx/security/crypto/SecureKeyGenerator.java b/security/crypto/src/main/java/androidx/security/crypto/SecureKeyGenerator.java
deleted file mode 100644
index fa698e5..0000000
--- a/security/crypto/src/main/java/androidx/security/crypto/SecureKeyGenerator.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package androidx.security.crypto;
-
-import android.os.Build;
-import android.security.keystore.KeyGenParameterSpec;
-import android.security.keystore.KeyProperties;
-
-import androidx.annotation.NonNull;
-import androidx.security.SecureConfig;
-
-import java.security.GeneralSecurityException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.KeyPairGenerator;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.SecureRandom;
-
-import javax.crypto.KeyGenerator;
-
-/**
- * Class that provides easy and reliable key generation for both symmetric and asymmetric crypto.
- */
-public class SecureKeyGenerator {
-
- private SecureConfig mSecureConfig;
-
- @NonNull
- public static SecureKeyGenerator getDefault() {
- return new SecureKeyGenerator(SecureConfig.getDefault());
- }
-
- /**
- * @param secureConfig The config
- * @return The key generator
- */
- @NonNull
- public static SecureKeyGenerator getInstance(@NonNull SecureConfig secureConfig) {
- return new SecureKeyGenerator(secureConfig);
- }
-
- private SecureKeyGenerator(SecureConfig secureConfig) {
- this.mSecureConfig = secureConfig;
- }
-
- /**
- * <p>
- * Generates a sensitive data key and adds the SecretKey to the AndroidKeyStore.
- * Utilizes UnlockedDeviceProtection to ensure that the device must be unlocked in order to
- * use the generated key.
- * </p>
- *
- * @param keyAlias The name of the generated SecretKey to save into the AndroidKeyStore.
- * @return true if the key was generated, false otherwise
- */
- //@TargetApi(Build.VERSION_CODES.P)
- public boolean generateKey(@NonNull String keyAlias) {
- boolean created = false;
- try {
- KeyGenerator keyGenerator = KeyGenerator.getInstance(
- mSecureConfig.getSymmetricKeyAlgorithm(), mSecureConfig.getAndroidKeyStore());
- KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
- keyAlias, mSecureConfig.getSymmetricKeyPurposes())
- .setBlockModes(mSecureConfig.getSymmetricBlockModes())
- .setEncryptionPaddings(mSecureConfig.getSymmetricPaddings())
- .setKeySize(mSecureConfig.getSymmetricKeySize());
- builder = builder.setUserAuthenticationRequired(
- mSecureConfig.getSymmetricRequireUserAuthEnabled());
- builder = builder.setUserAuthenticationValidityDurationSeconds(
- mSecureConfig.getSymmetricRequireUserValiditySeconds());
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- builder = builder.setUnlockedDeviceRequired(
- mSecureConfig.getSymmetricSensitiveDataProtectionEnabled());
- }
- keyGenerator.init(builder.build());
- keyGenerator.generateKey();
- created = true;
- } catch (NoSuchAlgorithmException ex) {
- throw new SecurityException(ex);
- } catch (InvalidAlgorithmParameterException ex) {
- throw new SecurityException(ex);
- } catch (NoSuchProviderException ex) {
- throw new SecurityException(ex);
- }
- return created;
- }
-
- /**
- * <p>
- * Generates a sensitive data public/private key pair and adds the KeyPair to the
- * AndroidKeyStore. Utilizes UnlockedDeviceProtection to ensure that the device
- * must be unlocked in order to use the generated key.
- * </p>
- * <p>
- * ANDROID P ONLY (API LEVEL 28>)
- * </p>
- *
- * @param keyPairAlias The name of the generated SecretKey to save into the AndroidKeyStore.
- * @return true if the key was generated, false otherwise
- */
- public boolean generateAsymmetricKeyPair(@NonNull String keyPairAlias) {
- boolean created = false;
- try {
- KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance(
- mSecureConfig.getAsymmetricKeyPairAlgorithm(),
- mSecureConfig.getAndroidKeyStore());
- KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
- keyPairAlias, mSecureConfig.getAsymmetricKeyPurposes())
- .setEncryptionPaddings(mSecureConfig.getAsymmetricPaddings())
- .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
- .setBlockModes(mSecureConfig.getAsymmetricBlockModes())
- .setKeySize(mSecureConfig.getAsymmetricKeySize());
- builder = builder.setUserAuthenticationRequired(
- mSecureConfig.getAsymmetricRequireUserAuthEnabled());
- builder = builder.setUserAuthenticationValidityDurationSeconds(
- mSecureConfig.getAsymmetricRequireUserValiditySeconds());
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- builder = builder.setUnlockedDeviceRequired(
- mSecureConfig.getAsymmetricSensitiveDataProtectionEnabled());
- }
- keyGenerator.initialize(builder.build());
- keyGenerator.generateKeyPair();
- created = true;
- } catch (NoSuchProviderException | InvalidAlgorithmParameterException
- | NoSuchAlgorithmException ex) {
- throw new SecurityException(ex);
- }
- return created;
- }
-
- /**
- * <p>
- * Generates an Ephemeral symmetric key that can be fully destroyed and removed from memory.
- * </p>
- *
- * @return The EphemeralSecretKey generated
- */
- @NonNull
- public EphemeralSecretKey generateEphemeralDataKey() {
- try {
- SecureRandom secureRandom;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- secureRandom = SecureRandom.getInstanceStrong();
- } else {
- // Not best practices, TODO update this as per this SO thread
- // https://stackoverflow.com/questions/36813098/securerandom-provider-crypto-
- // unavailable-in-android-n-for-deterministially-gen
- secureRandom = new SecureRandom();
- }
- byte[] key = new byte[mSecureConfig.getSymmetricKeySize() / 8];
- secureRandom.nextBytes(key);
- return new EphemeralSecretKey(key, mSecureConfig.getSymmetricKeyAlgorithm());
- } catch (GeneralSecurityException ex) {
- throw new SecurityException(ex);
- }
- }
-
-}
diff --git a/security/crypto/src/main/java/androidx/security/crypto/SecureKeyStore.java b/security/crypto/src/main/java/androidx/security/crypto/SecureKeyStore.java
deleted file mode 100644
index b3a0681..0000000
--- a/security/crypto/src/main/java/androidx/security/crypto/SecureKeyStore.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.security.crypto;
-
-import android.security.keystore.KeyInfo;
-
-import androidx.annotation.NonNull;
-import androidx.security.SecureConfig;
-
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.security.KeyFactory;
-import java.security.KeyStore;
-import java.security.PrivateKey;
-
-import javax.crypto.SecretKey;
-import javax.crypto.SecretKeyFactory;
-
-/**
- * A class that provides simpler access to the AndroidKeyStore including commonly used utilities.
- */
-public class SecureKeyStore {
-
- private static final String TAG = "SecureKeyStore";
-
- private SecureConfig mSecureConfig;
-
- @NonNull
- public static SecureKeyStore getDefault() {
- return new SecureKeyStore(SecureConfig.getDefault());
- }
-
- /**
- * Gets an instance of SecureKeyStore based on the provided SecureConfig.
- *
- * @param secureConfig The SecureConfig to use the KeyStore.
- * @return A SecureKeyStore that has been configured.
- */
- @NonNull
- public static SecureKeyStore getInstance(@NonNull SecureConfig secureConfig) {
- return new SecureKeyStore(secureConfig);
- }
-
- private SecureKeyStore(@NonNull SecureConfig secureConfig) {
- this.mSecureConfig = secureConfig;
- }
-
- /**
- * Checks to see if the specified key exists in the AndroidKeyStore
- *
- * @param keyAlias The name of the generated SecretKey to save into the AndroidKeyStore.
- * @return true if the key is stored in secure hardware
- */
- public boolean keyExists(@NonNull String keyAlias) {
- boolean exists = false;
- try {
- KeyStore keyStore = KeyStore.getInstance(mSecureConfig.getAndroidKeyStore());
- keyStore.load(null);
- exists = keyStore.containsAlias(keyAlias);
- /*Certificate cert = keyStore.getCertificate(keyAlias);
- if (cert != null) {
- exists = cert.getPublicKey() != null;
- }*/
- } catch (GeneralSecurityException ex) {
- throw new SecurityException(ex);
- } catch (IOException ex) {
- throw new SecurityException(ex);
- }
- return exists;
- }
-
-
- /**
- * Delete a key from the specified keystore.
- *
- * @param keyAlias The key to delete from the KeyStore
- */
- public void deleteKey(@NonNull String keyAlias) {
- try {
- KeyStore keyStore = KeyStore.getInstance(mSecureConfig.getAndroidKeyStore());
- keyStore.load(null);
- keyStore.deleteEntry(keyAlias);
- } catch (GeneralSecurityException ex) {
- throw new SecurityException(ex);
- } catch (IOException ex) {
- throw new SecurityException(ex);
- }
- }
-
- /**
- * Checks to see if the specified key is stored in secure hardware
- *
- * @param keyAlias The name of the generated SecretKey to save into the AndroidKeyStore.
- * @return true if the key is stored in secure hardware
- */
- public boolean checkKeyInsideSecureHardware(@NonNull String keyAlias) {
- boolean inHardware = false;
- try {
- KeyStore keyStore = KeyStore.getInstance(mSecureConfig.getAndroidKeyStore());
- keyStore.load(null);
- SecretKey key = (SecretKey) keyStore.getKey(keyAlias, null);
- SecretKeyFactory factory = SecretKeyFactory.getInstance(key.getAlgorithm(),
- mSecureConfig.getAndroidKeyStore());
- KeyInfo keyInfo;
- keyInfo = (KeyInfo) factory.getKeySpec(key, KeyInfo.class);
- inHardware = keyInfo.isInsideSecureHardware();
- return inHardware;
- } catch (GeneralSecurityException e) {
- return inHardware;
- } catch (IOException e) {
- return inHardware;
- }
- }
-
- /**
- * Checks to see if the specified private key is stored in secure hardware
- *
- * @param keyAlias The name of the generated SecretKey to save into the AndroidKeyStore.
- * @return true if the key is stored in secure hardware
- */
- public boolean checkKeyInsideSecureHardwareAsymmetric(@NonNull String keyAlias) {
- boolean inHardware = false;
- try {
- KeyStore keyStore = KeyStore.getInstance(mSecureConfig.getAndroidKeyStore());
- keyStore.load(null);
- PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, null);
-
- KeyFactory factory = KeyFactory.getInstance(privateKey.getAlgorithm(),
- mSecureConfig.getAndroidKeyStore());
- KeyInfo keyInfo;
-
- keyInfo = factory.getKeySpec(privateKey, KeyInfo.class);
- inHardware = keyInfo.isInsideSecureHardware();
- return inHardware;
-
- } catch (GeneralSecurityException e) {
- return inHardware;
- } catch (IOException e) {
- return inHardware;
- }
- }
-
-}
diff --git a/security/crypto/src/main/java/androidx/security/net/SecureKeyManager.java b/security/crypto/src/main/java/androidx/security/net/SecureKeyManager.java
deleted file mode 100644
index 77c10b2..0000000
--- a/security/crypto/src/main/java/androidx/security/net/SecureKeyManager.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package androidx.security.net;
-
-
-import android.app.Activity;
-import android.content.Intent;
-import android.security.KeyChain;
-import android.security.KeyChainAliasCallback;
-import android.security.KeyChainException;
-
-import androidx.annotation.NonNull;
-import androidx.security.SecureConfig;
-
-import java.net.Socket;
-import java.security.Principal;
-import java.security.PrivateKey;
-import java.security.cert.X509Certificate;
-
-import javax.net.ssl.X509KeyManager;
-
-/**
- * Class that helps generate and manage crypto keys
- */
-public class SecureKeyManager implements X509KeyManager, KeyChainAliasCallback {
- private static final String TAG = "SecureKeyManager";
-
- private final String mAlias;
- private X509Certificate[] mCertChain;
- private PrivateKey mPrivateKey;
- private static Activity sActivity;
- private SecureConfig mSecureConfig;
-
- public static void setContext(@NonNull Activity activity) {
- sActivity = activity;
- }
-
- public enum CertType {
- X509(0),
- PKCS12(1),
- NOT_SUPPORTED(1000);
-
- private final int mType;
-
- CertType(int type) {
- this.mType = type;
- }
-
- /**
- * @return the type as an int
- */
- public int getType() {
- return this.mType;
- }
-
- /**
- * @param id the id of the cert type
- * @return the cert type
- */
- @NonNull
- public static CertType fromId(int id) {
- switch (id) {
- case 0:
- return X509;
- case 1:
- return PKCS12;
- }
- return NOT_SUPPORTED;
- }
- }
-
-
- /**
- * @param alias the key alias
- * @return the key manager
- */
- @NonNull
- public static SecureKeyManager getDefault(@NonNull String alias) {
- return getDefault(alias, SecureConfig.getDefault());
- }
-
- /**
- * @param alias the key alias
- * @param secureConfig the configuration
- * @return the key manager
- */
- @NonNull
- public static SecureKeyManager getDefault(@NonNull String alias,
- @NonNull SecureConfig secureConfig) {
- SecureKeyManager keyManager = new SecureKeyManager(alias, secureConfig);
- try {
- KeyChain.choosePrivateKeyAlias(sActivity, keyManager,
- secureConfig.getClientCertAlgorithms(),
- null, null, -1, alias);
- } catch (Exception ex) {
- ex.printStackTrace();
- }
- return keyManager;
- }
-
- /**
- * @param certType cert mType to install
- * @param certData the cert data in byte[] format
- * @param keyAlias the alias of they key to use
- * @param secureConfig the crypto config
- * @return the secure key manager instance
- */
- @NonNull
- public static SecureKeyManager installCertManually(@NonNull CertType certType,
- @NonNull byte[] certData, @NonNull String keyAlias,
- @NonNull SecureConfig secureConfig) {
- SecureKeyManager keyManager = new SecureKeyManager(keyAlias, secureConfig);
- Intent intent = KeyChain.createInstallIntent();
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- switch (certType) {
- case X509:
- intent.putExtra(KeyChain.EXTRA_CERTIFICATE, certData);
- break;
- case PKCS12:
- intent.putExtra(KeyChain.EXTRA_PKCS12, certData);
- break;
- default:
- throw new SecurityException("Cert mType not supported.");
- }
- sActivity.startActivity(intent);
- return keyManager;
- }
-
- public SecureKeyManager(@NonNull String alias, @NonNull SecureConfig secureConfig) {
- this.mAlias = alias;
- this.mSecureConfig = secureConfig;
- }
-
- @Override
- @NonNull
- public String chooseClientAlias(@NonNull String[] arg0,
- @NonNull Principal[] arg1, @NonNull Socket arg2) {
- return mAlias;
- }
-
- @Override
- @NonNull
- public X509Certificate[] getCertificateChain(@NonNull String alias) {
- if (this.mAlias.equals(alias)) return mCertChain;
- return null;
- }
-
- public void setCertChain(@NonNull X509Certificate[] certChain) {
- this.mCertChain = certChain;
- }
-
- @Override
- @NonNull
- public PrivateKey getPrivateKey(@NonNull String alias) {
- if (this.mAlias.equals(alias)) return mPrivateKey;
- return null;
- }
-
- public void setPrivateKey(@NonNull PrivateKey privateKey) {
- this.mPrivateKey = privateKey;
- }
-
- @Override
- @NonNull
- public final String chooseServerAlias(@NonNull String keyType,
- @NonNull Principal[] issuers, @NonNull Socket socket) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- @NonNull
- public final String[] getClientAliases(@NonNull String keyType, @NonNull Principal[] issuers) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- @NonNull
- public final String[] getServerAliases(@NonNull String keyType, @NonNull Principal[] issuers) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void alias(@NonNull String alias) {
- try {
- mCertChain = KeyChain.getCertificateChain(sActivity.getApplicationContext(), alias);
- mPrivateKey = KeyChain.getPrivateKey(sActivity.getApplicationContext(), alias);
- if (mCertChain == null || mPrivateKey == null) {
- throw new SecurityException("Could not retrieve the cert chain and private key"
- + " from client cert.");
- }
- this.setCertChain(mCertChain);
- this.setPrivateKey(mPrivateKey);
- } catch (KeyChainException ex) {
- throw new SecurityException("Could not retrieve the cert chain and private key from"
- + " client cert.");
- } catch (InterruptedException ex) {
- throw new SecurityException("Could not retrieve the cert chain and private key from"
- + " client cert.");
- }
- }
-}
diff --git a/security/crypto/src/main/java/androidx/security/net/SecureURL.java b/security/crypto/src/main/java/androidx/security/net/SecureURL.java
deleted file mode 100644
index f93c8a8..0000000
--- a/security/crypto/src/main/java/androidx/security/net/SecureURL.java
+++ /dev/null
@@ -1,310 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package androidx.security.net;
-
-
-import android.annotation.TargetApi;
-import android.os.Build;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.security.SecureConfig;
-import androidx.security.config.TldConstants;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLConnection;
-import java.security.GeneralSecurityException;
-import java.security.KeyStore;
-import java.security.cert.CertPath;
-import java.security.cert.CertPathValidator;
-import java.security.cert.CertPathValidatorException;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateFactory;
-import java.security.cert.CertificateParsingException;
-import java.security.cert.PKIXParameters;
-import java.security.cert.PKIXRevocationChecker;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Map;
-
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSocket;
-
-/**
- * A URL that provides TLS, certification validity checks, and TLD verification automatically.
- */
-@TargetApi(Build.VERSION_CODES.N)
-public class SecureURL {
-
- private static final String TAG = "SecureURL";
-
- private URL mUrl;
- private SecureConfig mSecureConfig;
- private String mClientCertAlias;
-
- public SecureURL(@NonNull String spec)
- throws MalformedURLException {
- this(spec, null, SecureConfig.getDefault());
- }
-
- public SecureURL(@NonNull String spec, @NonNull String clientCertAlias)
- throws MalformedURLException {
- this(spec, clientCertAlias, SecureConfig.getDefault());
- }
-
- public SecureURL(@NonNull String spec, @NonNull String clientCertAlias,
- @NonNull SecureConfig secureConfig)
- throws MalformedURLException {
- this.mUrl = new URL(addProtocol(spec));
- this.mClientCertAlias = clientCertAlias;
- this.mSecureConfig = secureConfig;
- }
-
-
- /**
- * Gets the hostname used to construct the underlying URL.
- *
- * @return the hostname associated with the Url.
- */
- @NonNull
- public String getHostname() {
- return this.mUrl.getHost();
- }
-
- /**
- * Gets the port used to construct the underlying URL.
- *
- * @return the port associated with the Url.
- */
- public int getPort() {
- int port = this.mUrl.getPort();
- if (port == -1) {
- port = this.mUrl.getDefaultPort();
- }
- return port;
- }
-
- private String addProtocol(@NonNull String spec) {
- if (!spec.toLowerCase().startsWith("http://")
- && !spec.toLowerCase().startsWith("https://")) {
- return "https://" + spec;
- }
- return spec;
- }
-
-
- /**
- * Gets the client cert alias.
- *
- * @return The client cert alias.
- */
- @NonNull
- public String getClientCertAlias() {
- return this.mClientCertAlias;
- }
-
-
- /**
- * Opens a connection using default certs with a custom SSLSocketFactory.
- *
- * @return the UrlConnection of the newly opened connection.
- * @throws IOException
- */
- @NonNull
- public URLConnection openConnection() throws IOException {
- HttpsURLConnection urlConnection = (HttpsURLConnection) this.mUrl.openConnection();
- urlConnection.setSSLSocketFactory(new ValidatableSSLSocketFactory(this));
- return urlConnection;
- }
-
- /**
- * Opens a connection using the provided trusted list of CAs.
- *
- * @param trustedCAs list of CAs to be trusted by this connection
- * @return The opened connection
- * @throws IOException
- */
- @NonNull
- public URLConnection openUserTrustedCertConnection(
- @NonNull Map<String, InputStream> trustedCAs)
- throws IOException {
- HttpsURLConnection urlConnection = (HttpsURLConnection) this.mUrl.openConnection();
- urlConnection.setSSLSocketFactory(new ValidatableSSLSocketFactory(this,
- trustedCAs, mSecureConfig));
- return urlConnection;
- }
-
- /**
- * Checks the hostname against an open SSLSocket connect to the hostname for validity for certs
- * and hostname validity. Only used internally by ValidatableSSLSocket.
- * <p>
- * Example Code:
- * SSLSocketFactory sf = (SSLSocketFactory) SSLSocketFactory.getDefault();
- * SSLSocket socket = (SSLSocket) sf.createSocket("https://"+hostname, 443);
- * socket.startHandshake();
- * boolean valid = SecurityExt.isValid(hostname, socket);
- * </p>
- *
- * @param hostname The host name to check
- * @param socket The SSLSocket that is open to the URL of the host to check
- * @return true if the SSLSocket has a valid cert and if the hostname is valid, false otherwise.
- */
- boolean isValid(@NonNull String hostname, @NonNull SSLSocket socket) {
- try {
- Log.i(TAG, "Hostname verifier: " + HttpsURLConnection
- .getDefaultHostnameVerifier().verify(hostname, socket.getSession()));
- Log.i(TAG, "isValid Peer Certs: "
- + isValid(Arrays.asList(socket.getSession().getPeerCertificates())));
- return HttpsURLConnection.getDefaultHostnameVerifier()
- .verify(hostname, socket.getSession())
- && isValid(Arrays.asList(socket.getSession().getPeerCertificates()))
- && validTldWildcards(Arrays.asList(socket.getSession().getPeerCertificates()));
- } catch (SSLPeerUnverifiedException e) {
- Log.i(TAG, "Valid Check failed: " + e.getMessage());
- e.printStackTrace();
- return false;
- }
- }
-
- /**
- * Checks the HttpsUrlConnection certificates for validity.
- * <p>
- * Example Code:
- * SecureURL mUrl = new SecureURL("https://" + host);
- * conn = (HttpsURLConnection) mUrl.openConnection();
- * boolean valid = SecurityExt.isValid(conn);
- * </p>
- *
- * @param conn The connection to check the certificates of
- * @return true if the certificates for the HttpsUrlConnection are valid, false otherwise
- */
- public boolean isValid(@NonNull HttpsURLConnection conn) {
- try {
- return isValid(Arrays.asList(conn.getServerCertificates()))
- && validTldWildcards(Arrays.asList(conn.getServerCertificates()));
- } catch (SSLPeerUnverifiedException e) {
- Log.i(TAG, "Valid Check failed: " + e.getMessage());
- e.printStackTrace();
- return false;
- }
- }
-
-
- /**
- * Internal method to check a list of certificates for validity.
- *
- * @param certs list of certs to check
- * @return true if the certs are valid, false otherwise
- */
- private boolean isValid(@NonNull List<? extends Certificate> certs) {
- try {
- List<Certificate> leafCerts = new ArrayList<>();
- for (Certificate cert : certs) {
- if (!isRootCA(cert)) {
- leafCerts.add(cert);
- }
- }
- CertPath path = CertificateFactory.getInstance(mSecureConfig.getCertPath())
- .generateCertPath(leafCerts);
- KeyStore ks = KeyStore.getInstance(mSecureConfig.getAndroidCAStore());
- try {
- ks.load(null, null);
- } catch (IOException e) {
- e.printStackTrace();
- throw new AssertionError(e);
- }
- CertPathValidator cpv = CertPathValidator.getInstance(mSecureConfig
- .getCertPathValidator());
- PKIXParameters params = new PKIXParameters(ks);
- PKIXRevocationChecker checker = (PKIXRevocationChecker) cpv.getRevocationChecker();
- checker.setOptions(EnumSet.of(PKIXRevocationChecker.Option.NO_FALLBACK));
- params.addCertPathChecker(checker);
- cpv.validate(path, params);
- return true;
- } catch (CertPathValidatorException e) {
- // If this message prints out "Unable to determine revocation status due to
- // network error"
- // Make sure your network security config allows for clear text access of the relevant
- // OCSP mUrl.
- e.printStackTrace();
- return false;
- } catch (GeneralSecurityException e) {
- e.printStackTrace();
- return false;
- }
- }
-
- /**
- * Internal method to check if a cert is a CA.
- *
- * @param cert The cert to check
- * @return true if the cert is a RootCA, false otherwise
- */
- private boolean isRootCA(@NonNull Certificate cert) {
- boolean rootCA = false;
- if (cert instanceof X509Certificate) {
- X509Certificate x509Certificate = (X509Certificate) cert;
- if (x509Certificate.getSubjectDN().getName().equals(
- x509Certificate.getIssuerDN().getName())) {
- rootCA = true;
- }
- }
- return rootCA;
- }
-
-
- private boolean validTldWildcards(@NonNull List<? extends Certificate> certs) {
- // For a more complete list https://publicsuffix.org/list/public_suffix_list.dat
- for (Certificate cert : certs) {
- if (cert instanceof X509Certificate) {
- X509Certificate x509Cert = (X509Certificate) cert;
- try {
- Collection<List<?>> subAltNames = x509Cert.getSubjectAlternativeNames();
- if (subAltNames != null) {
- List<String> dnsNames = new ArrayList<>();
- for (List<?> tldList : subAltNames) {
- if (tldList.size() >= 2) {
- dnsNames.add(tldList.get(1).toString().toUpperCase());
- }
- }
- // Populate DNS NAMES, make sure they are lower case
- for (String dnsName : dnsNames) {
- if (TldConstants.VALID_TLDS.contains(dnsName)) {
- Log.i(TAG, "FAILED WILDCARD TldConstants CHECK: " + dnsName);
- return false;
- }
- }
- }
- } catch (CertificateParsingException ex) {
- Log.i(TAG, "Cert Parsing Issue: " + ex.getMessage());
- return false;
- }
- }
- }
- return true;
- }
-
-}
diff --git a/security/crypto/src/main/java/androidx/security/net/ValidatableSSLSocket.java b/security/crypto/src/main/java/androidx/security/net/ValidatableSSLSocket.java
deleted file mode 100644
index e7be287..0000000
--- a/security/crypto/src/main/java/androidx/security/net/ValidatableSSLSocket.java
+++ /dev/null
@@ -1,397 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package androidx.security.net;
-
-import android.annotation.TargetApi;
-import android.os.Build;
-
-import androidx.annotation.RestrictTo;
-import androidx.security.SecureConfig;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.InetAddress;
-import java.net.Socket;
-import java.net.SocketAddress;
-import java.net.SocketException;
-import java.nio.channels.SocketChannel;
-
-import javax.net.ssl.HandshakeCompletedListener;
-import javax.net.ssl.SSLParameters;
-import javax.net.ssl.SSLSession;
-import javax.net.ssl.SSLSocket;
-
-/**
- * A custom implementation of SSLSocket which forces TLS, and handles automatically doing
- * certificate validity checks.
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-class ValidatableSSLSocket extends SSLSocket {
-
- private static final String TAG = "ValidatableSSLSocket";
-
- private SSLSocket mSslSocket;
- private String mHostname;
- private SecureURL mSecureURL;
- private boolean mHandshakeStarted = false;
- private SecureConfig mSecureConfig;
-
- ValidatableSSLSocket(SecureURL secureURL, Socket sslSocket, SecureConfig secureConfig)
- throws IOException {
- this.mSecureURL = secureURL;
- this.mHostname = secureURL.getHostname();
- this.mSslSocket = (SSLSocket) sslSocket;
- this.mSecureConfig = secureConfig;
- setSecureCiphers();
- isValid();
- }
-
- private void setSecureCiphers() {
- if (mSecureConfig.getUseStrongSSLCiphersEnabled()) {
- this.mSslSocket.setEnabledCipherSuites(mSecureConfig.getStrongSSLCiphers());
- }
- }
-
- private void isValid() throws IOException {
- startHandshake();
- try {
- if (!mSecureURL.isValid(this.mHostname, this.mSslSocket)) {
- throw new IOException("Found invalid certificate");
- }
- } catch (IOException ex) {
- ex.printStackTrace();
- throw new IOException("Found invalid certificate");
- }
- }
-
- @Override
- public void startHandshake() throws IOException {
- if (!mHandshakeStarted) {
- mSslSocket.startHandshake();
- mHandshakeStarted = true;
- }
- }
-
- @Override
- public String[] getSupportedCipherSuites() {
- return mSslSocket.getSupportedCipherSuites();
- }
-
- @Override
- public String[] getEnabledCipherSuites() {
- return mSslSocket.getEnabledCipherSuites();
- }
-
- @Override
- public void setEnabledCipherSuites(String[] suites) {
- mSslSocket.setEnabledCipherSuites(suites);
- }
-
- @Override
- public String[] getSupportedProtocols() {
- return mSslSocket.getSupportedProtocols();
- }
-
- @Override
- public String[] getEnabledProtocols() {
- return mSslSocket.getEnabledProtocols();
- }
-
- @Override
- public void setEnabledProtocols(String[] protocols) {
- mSslSocket.setEnabledProtocols(protocols);
- }
-
- @Override
- public SSLSession getSession() {
- return mSslSocket.getSession();
- }
-
- @Override
- public void addHandshakeCompletedListener(HandshakeCompletedListener listener) {
- mSslSocket.addHandshakeCompletedListener(listener);
- }
-
- @Override
- public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) {
- mSslSocket.removeHandshakeCompletedListener(listener);
- }
-
- @Override
- public void setUseClientMode(boolean mode) {
- mSslSocket.setUseClientMode(mode);
- }
-
- @Override
- public boolean getUseClientMode() {
- return mSslSocket.getUseClientMode();
- }
-
- @Override
- public void setNeedClientAuth(boolean need) {
- mSslSocket.setNeedClientAuth(need);
- }
-
- @Override
- public boolean getNeedClientAuth() {
- return mSslSocket.getNeedClientAuth();
- }
-
- @Override
- public void setWantClientAuth(boolean want) {
- mSslSocket.setWantClientAuth(want);
- }
-
- @Override
- public boolean getWantClientAuth() {
- return mSslSocket.getWantClientAuth();
- }
-
- @Override
- public void setEnableSessionCreation(boolean flag) {
- mSslSocket.setEnableSessionCreation(flag);
- }
-
- @Override
- public boolean getEnableSessionCreation() {
- return mSslSocket.getEnableSessionCreation();
- }
-
- @Override
- @TargetApi(Build.VERSION_CODES.N)
- public SSLSession getHandshakeSession() {
- return mSslSocket.getHandshakeSession();
- }
-
- @Override
- public SSLParameters getSSLParameters() {
- return mSslSocket.getSSLParameters();
- }
-
- @Override
- public void setSSLParameters(SSLParameters params) {
- mSslSocket.setSSLParameters(params);
- }
-
- @Override
- public String toString() {
- return mSslSocket.toString();
- }
-
- @Override
- public void connect(SocketAddress endpoint) throws IOException {
- mSslSocket.connect(endpoint);
- }
-
- @Override
- public void connect(SocketAddress endpoint, int timeout) throws IOException {
- mSslSocket.connect(endpoint, timeout);
- }
-
- @Override
- public void bind(SocketAddress bindpoint) throws IOException {
- mSslSocket.bind(bindpoint);
- }
-
- @Override
- public InetAddress getInetAddress() {
- return mSslSocket.getInetAddress();
- }
-
- @Override
- public InetAddress getLocalAddress() {
- return mSslSocket.getLocalAddress();
- }
-
- @Override
- public int getPort() {
- return mSslSocket.getPort();
- }
-
- @Override
- public int getLocalPort() {
- return mSslSocket.getLocalPort();
- }
-
- @Override
- public SocketAddress getRemoteSocketAddress() {
- return mSslSocket.getRemoteSocketAddress();
- }
-
- @Override
- public SocketAddress getLocalSocketAddress() {
- return mSslSocket.getLocalSocketAddress();
- }
-
- @Override
- public SocketChannel getChannel() {
- return mSslSocket.getChannel();
- }
-
- @Override
- public InputStream getInputStream() throws IOException {
- return mSslSocket.getInputStream();
- }
-
- @Override
- public OutputStream getOutputStream() throws IOException {
- return mSslSocket.getOutputStream();
- }
-
- @Override
- public void setTcpNoDelay(boolean on) throws SocketException {
- mSslSocket.setTcpNoDelay(on);
- }
-
- @Override
- public boolean getTcpNoDelay() throws SocketException {
- return mSslSocket.getTcpNoDelay();
- }
-
- @Override
- public void setSoLinger(boolean on, int linger) throws SocketException {
- mSslSocket.setSoLinger(on, linger);
- }
-
- @Override
- public int getSoLinger() throws SocketException {
- return mSslSocket.getSoLinger();
- }
-
- @Override
- public void sendUrgentData(int data) throws IOException {
- mSslSocket.sendUrgentData(data);
- }
-
- @Override
- public void setOOBInline(boolean on) throws SocketException {
- mSslSocket.setOOBInline(on);
- }
-
- @Override
- public boolean getOOBInline() throws SocketException {
- return mSslSocket.getOOBInline();
- }
-
- @Override
- public synchronized void setSoTimeout(int timeout) throws SocketException {
- mSslSocket.setSoTimeout(timeout);
- }
-
- @Override
- public synchronized int getSoTimeout() throws SocketException {
- return mSslSocket.getSoTimeout();
- }
-
- @Override
- public synchronized void setSendBufferSize(int size) throws SocketException {
- mSslSocket.setSendBufferSize(size);
- }
-
- @Override
- public synchronized int getSendBufferSize() throws SocketException {
- return mSslSocket.getSendBufferSize();
- }
-
- @Override
- public synchronized void setReceiveBufferSize(int size) throws SocketException {
- mSslSocket.setReceiveBufferSize(size);
- }
-
- @Override
- public synchronized int getReceiveBufferSize() throws SocketException {
- return mSslSocket.getReceiveBufferSize();
- }
-
- @Override
- public void setKeepAlive(boolean on) throws SocketException {
- mSslSocket.setKeepAlive(on);
- }
-
- @Override
- public boolean getKeepAlive() throws SocketException {
- return mSslSocket.getKeepAlive();
- }
-
- @Override
- public void setTrafficClass(int tc) throws SocketException {
- mSslSocket.setTrafficClass(tc);
- }
-
- @Override
- public int getTrafficClass() throws SocketException {
- return mSslSocket.getTrafficClass();
- }
-
- @Override
- public void setReuseAddress(boolean on) throws SocketException {
- mSslSocket.setReuseAddress(on);
- }
-
- @Override
- public boolean getReuseAddress() throws SocketException {
- return mSslSocket.getReuseAddress();
- }
-
- @Override
- public synchronized void close() throws IOException {
- mSslSocket.close();
- }
-
- @Override
- public void shutdownInput() throws IOException {
- mSslSocket.shutdownInput();
- }
-
- @Override
- public void shutdownOutput() throws IOException {
- mSslSocket.shutdownOutput();
- }
-
- @Override
- public boolean isConnected() {
- return mSslSocket.isConnected();
- }
-
- @Override
- public boolean isBound() {
- return mSslSocket.isBound();
- }
-
- @Override
- public boolean isClosed() {
- return mSslSocket.isClosed();
- }
-
- @Override
- public boolean isInputShutdown() {
- return mSslSocket.isInputShutdown();
- }
-
- @Override
- public boolean isOutputShutdown() {
- return mSslSocket.isOutputShutdown();
- }
-
- @Override
- public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) {
- mSslSocket.setPerformancePreferences(connectionTime, latency, bandwidth);
- }
-}
diff --git a/security/crypto/src/main/java/androidx/security/net/ValidatableSSLSocketFactory.java b/security/crypto/src/main/java/androidx/security/net/ValidatableSSLSocketFactory.java
deleted file mode 100644
index 0c678b2..0000000
--- a/security/crypto/src/main/java/androidx/security/net/ValidatableSSLSocketFactory.java
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package androidx.security.net;
-
-import static androidx.security.SecureConfig.SSL_TLS;
-
-import androidx.annotation.RestrictTo;
-import androidx.security.SecureConfig;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.InetAddress;
-import java.net.Socket;
-import java.net.UnknownHostException;
-import java.security.GeneralSecurityException;
-import java.security.KeyStore;
-import java.security.SecureRandom;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateFactory;
-import java.util.Enumeration;
-import java.util.Map;
-
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSocketFactory;
-import javax.net.ssl.TrustManagerFactory;
-
-/**
- * A custom implementation of SSLSocketFactory which handles the creation of custom SSLSockets
- * that handle extra functionality and do validity checking.
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-class ValidatableSSLSocketFactory extends SSLSocketFactory {
-
- private static final String TAG = "ValidatableSSLSocketFactory";
-
- private SSLSocketFactory mSslSocketFactory;
- private SecureURL mSecureURL;
- private Socket mSocket;
- private SecureConfig mSecureConfig;
-
- ValidatableSSLSocketFactory(SecureURL secureURL, SSLSocketFactory sslSocketFactory,
- SecureConfig secureConfig) throws IOException {
- this.mSecureURL = secureURL;
- this.mSslSocketFactory = sslSocketFactory;
- this.mSecureConfig = secureConfig;
- this.mSocket = new ValidatableSSLSocket(secureURL,
- mSslSocketFactory.createSocket(mSecureURL.getHostname(), mSecureURL.getPort()),
- mSecureConfig);
- }
-
- ValidatableSSLSocketFactory(SecureURL secureURL, SSLSocketFactory sslSocketFactory)
- throws IOException {
- this(secureURL, sslSocketFactory, SecureConfig.getDefault());
- }
-
- ValidatableSSLSocketFactory(SecureURL secureURL) throws IOException {
- this(secureURL, (SSLSocketFactory) SSLSocketFactory.getDefault(),
- SecureConfig.getDefault());
- }
-
- ValidatableSSLSocketFactory(SecureURL secureURL,
- Map<String, InputStream> trustedCAs, SecureConfig secureConfig) throws IOException {
- this(secureURL, createUserTrustSSLSocketFactory(trustedCAs, secureConfig, secureURL),
- secureConfig);
- }
-
- @Override
- public String[] getDefaultCipherSuites() {
- return mSslSocketFactory.getDefaultCipherSuites();
- }
-
- @Override
- public String[] getSupportedCipherSuites() {
- return mSslSocketFactory.getSupportedCipherSuites();
- }
-
- @Override
- public Socket createSocket(Socket s, String host, int port, boolean autoClose)
- throws IOException {
- if (mSocket == null) {
- mSocket = new ValidatableSSLSocket(
- mSecureURL, mSslSocketFactory.createSocket(s, host, port, autoClose),
- mSecureConfig);
- }
- return mSocket;
- }
-
- @Override
- public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
- if (mSocket == null) {
- mSocket = new ValidatableSSLSocket(mSecureURL,
- mSslSocketFactory.createSocket(host, port),
- mSecureConfig);
- }
- return mSocket;
- }
-
- @Override
- public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
- throws IOException, UnknownHostException {
- if (mSocket == null) {
- mSocket = new ValidatableSSLSocket(
- mSecureURL, mSslSocketFactory.createSocket(host, port, localHost, localPort),
- mSecureConfig);
- }
- return mSocket;
- }
-
- @Override
- public Socket createSocket(InetAddress host, int port) throws IOException {
- if (mSocket == null) {
- mSocket = new ValidatableSSLSocket(mSecureURL, mSslSocketFactory
- .createSocket(host, port),
- mSecureConfig);
- }
- return mSocket;
- }
-
- @Override
- public Socket createSocket(InetAddress address, int port, InetAddress localAddress,
- int localPort) throws IOException {
- if (mSocket == null) {
- mSocket = new ValidatableSSLSocket(
- mSecureURL, mSslSocketFactory.createSocket(address, port, localAddress,
- localPort),
- mSecureConfig);
- }
- return mSocket;
- }
-
- // TODO Evaluate the need for all of these options
- private static SSLSocketFactory createUserTrustSSLSocketFactory(Map<String, InputStream>
- trustAnchors, SecureConfig secureConfig, SecureURL secureURL) {
- try {
- TrustManagerFactory tmf = TrustManagerFactory.getInstance(
- TrustManagerFactory.getDefaultAlgorithm());
- KeyStore clientStore = KeyStore.getInstance(secureConfig.getKeystoreType());
- clientStore.load(null, null);
-
- KeyStore trustStore = null;
- switch (secureConfig.getTrustAnchorOptions()) {
- case USER_ONLY:
- case USER_SYSTEM:
- case LIMITED_SYSTEM:
- trustStore = KeyStore.getInstance(secureConfig.getKeystoreType());
- trustStore.load(null, null);
- break;
- }
-
- switch (secureConfig.getTrustAnchorOptions()) {
- case USER_SYSTEM:
- KeyStore caStore = KeyStore.getInstance(secureConfig.getAndroidCAStore());
- caStore.load(null, null);
- Enumeration<String> caAliases = caStore.aliases();
- while (caAliases.hasMoreElements()) {
- String alias = caAliases.nextElement();
- trustStore.setCertificateEntry(alias, caStore.getCertificate(alias));
- }
- break;
- case USER_ONLY:
- case LIMITED_SYSTEM:
- for (Map.Entry<String, InputStream> ca : trustAnchors.entrySet()) {
- CertificateFactory cf = CertificateFactory
- .getInstance(secureConfig.getCertPath());
- Certificate userCert = cf.generateCertificate(ca.getValue());
- trustStore.setCertificateEntry(ca.getKey(), userCert);
- }
- break;
- }
-
- tmf.init(trustStore);
- SSLContext sslContext = SSLContext.getInstance(SSL_TLS);
-
- KeyManager[] keyManagersArray = new KeyManager[1];
- keyManagersArray[0] = SecureKeyManager.getDefault(
- secureURL.getClientCertAlias(), secureConfig);
- sslContext.init(keyManagersArray, tmf.getTrustManagers(), new SecureRandom());
- return sslContext.getSocketFactory();
- } catch (GeneralSecurityException ex) {
- throw new SecurityException("Issue creating User SSLSocketFactory.");
- } catch (IOException ex) {
- throw new SecurityException("Issue creating User SSLSocketFactory.");
- }
- }
-
-}
diff --git a/studiow b/studiow
index 87fe5ff..9d1c4c6 100755
--- a/studiow
+++ b/studiow
@@ -134,6 +134,16 @@
fi
}
+function ensureLocalPropertiesUpdated() {
+ testPath="${projectDir}/local.properties"
+ populaterCommand="./gradlew help"
+ if [ ! -f "${testPath}" ]; then
+ cd "$scriptDir"
+ echo "Creating $testPath by running '$populaterCommand'"
+ eval $populaterCommand
+ fi
+}
+
function runStudioLinux() {
studioPath="${studioUnzippedPath}/android-studio/bin/studio.sh"
echo "$studioPath &"
@@ -161,6 +171,7 @@
function main() {
updateStudio
checkLicenseAgreement
+ ensureLocalPropertiesUpdated
runStudio
}
diff --git a/transition/src/main/java/androidx/transition/Transition.java b/transition/src/main/java/androidx/transition/Transition.java
index 7fe90fe..21895ca 100644
--- a/transition/src/main/java/androidx/transition/Transition.java
+++ b/transition/src/main/java/androidx/transition/Transition.java
@@ -1987,16 +1987,21 @@
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
void forceToEnd(ViewGroup sceneRoot) {
- ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+ final ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
int numOldAnims = runningAnimators.size();
- if (sceneRoot != null) {
- WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
- for (int i = numOldAnims - 1; i >= 0; i--) {
- AnimationInfo info = runningAnimators.valueAt(i);
- if (info.mView != null && windowId != null && windowId.equals(info.mWindowId)) {
- Animator anim = runningAnimators.keyAt(i);
- anim.end();
- }
+ if (sceneRoot == null || numOldAnims == 0) {
+ return;
+ }
+
+ WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
+ final ArrayMap<Animator, AnimationInfo> oldAnimators = new ArrayMap(runningAnimators);
+ runningAnimators.clear();
+
+ for (int i = numOldAnims - 1; i >= 0; i--) {
+ AnimationInfo info = oldAnimators.valueAt(i);
+ if (info.mView != null && windowId != null && windowId.equals(info.mWindowId)) {
+ Animator anim = oldAnimators.keyAt(i);
+ anim.end();
}
}
}
diff --git a/work/workmanager-gcm/src/androidTest/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcherTest.kt b/work/workmanager-gcm/src/androidTest/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcherTest.kt
index b04c246..1948994 100644
--- a/work/workmanager-gcm/src/androidTest/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcherTest.kt
+++ b/work/workmanager-gcm/src/androidTest/java/androidx/work/impl/background/gcm/WorkManagerGcmDispatcherTest.kt
@@ -22,7 +22,7 @@
import androidx.arch.core.executor.TaskExecutor
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.work.Configuration
import androidx.work.impl.WorkManagerImpl
import androidx.work.impl.utils.SynchronousExecutor
@@ -36,7 +36,7 @@
import java.util.concurrent.Executor
@RunWith(AndroidJUnit4::class)
-@SmallTest
+@MediumTest
class WorkManagerGcmDispatcherTest {
lateinit var mContext: Context
lateinit var mExecutor: Executor