Merge "Implementing a check for verifying that equals() and hashCode() are implemented by the key of a multimap query result." into androidx-main
diff --git a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/ActivityResultRegistryTest.kt b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/ActivityResultRegistryTest.kt
index 9da389c..68e1f87 100644
--- a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/ActivityResultRegistryTest.kt
+++ b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/ActivityResultRegistryTest.kt
@@ -18,6 +18,7 @@
import android.app.Activity.RESULT_OK
import android.content.Intent
+import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
@@ -155,9 +156,39 @@
}
}
+ val savedState = Bundle()
+ registryOwner.activityResultRegistry.onSaveInstanceState(savedState)
+
activityScenario.recreate()
- activityScenario.onActivity {
+ val restoredOwner = ActivityResultRegistryOwner {
+ object : ActivityResultRegistry() {
+ override fun <I : Any?, O : Any?> onLaunch(
+ requestCode: Int,
+ contract: ActivityResultContract<I, O>,
+ input: I,
+ options: ActivityOptionsCompat?
+ ) {
+ launchCount++
+ }
+ }
+ }
+
+ restoredOwner.activityResultRegistry.onRestoreInstanceState(savedState)
+
+ activityScenario.onActivity { activity ->
+ (activity as ComponentActivity).setContent {
+ CompositionLocalProvider(
+ LocalActivityResultRegistryOwner provides restoredOwner
+ ) {
+ launcher = rememberLauncherForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {}
+ }
+ }
+ }
+
+ composeTestRule.runOnIdle {
launcher?.launch(Intent()) ?: fail("launcher was not composed")
assertWithMessage("the registry was not invoked")
.that(launchCount)
diff --git a/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt b/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
index 3c8c327..03474b9 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
@@ -447,6 +447,24 @@
}
@Test
+ fun testLaunchUnregistered() {
+ val contract = StartActivityForResult()
+ val activityResult = registry.register("key", contract) { }
+
+ activityResult.unregister()
+
+ try {
+ activityResult.launch(null)
+ } catch (e: IllegalStateException) {
+ assertThat(e).hasMessageThat().contains(
+ "Attempting to launch an unregistered ActivityResultLauncher with contract " +
+ contract + " and input null. You must ensure the ActivityResultLauncher is " +
+ "registered before calling launch()."
+ )
+ }
+ }
+
+ @Test
fun testSavePendingOnRestore() {
var code = 0
val noDispatchRegistry = object : ActivityResultRegistry() {
diff --git a/activity/activity/src/main/java/androidx/activity/result/ActivityResultCaller.java b/activity/activity/src/main/java/androidx/activity/result/ActivityResultCaller.java
index b60d58e..65d3aa2 100644
--- a/activity/activity/src/main/java/androidx/activity/result/ActivityResultCaller.java
+++ b/activity/activity/src/main/java/androidx/activity/result/ActivityResultCaller.java
@@ -33,7 +33,7 @@
* Register a request to {@link Activity#startActivityForResult start an activity for result},
* designated by the given {@link ActivityResultContract contract}.
*
- * This creates a record in the {@link ActivityResultRegistry registry} associated wit this
+ * This creates a record in the {@link ActivityResultRegistry registry} associated with this
* caller, managing request code, as well as conversions to/from {@link Intent} under the hood.
*
* This *must* be called unconditionally, as part of initialization path, typically as a field
diff --git a/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java b/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
index d7fb56a..f91f4cc 100644
--- a/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
+++ b/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
@@ -124,7 +124,7 @@
+ "they are STARTED.");
}
- final int requestCode = registerKey(key);
+ registerKey(key);
LifecycleContainer lifecycleContainer = mKeyToLifecycleContainers.get(key);
if (lifecycleContainer == null) {
lifecycleContainer = new LifecycleContainer(lifecycle);
@@ -162,9 +162,15 @@
return new ActivityResultLauncher<I>() {
@Override
public void launch(I input, @Nullable ActivityOptionsCompat options) {
- mLaunchedKeys.add(key);
Integer innerCode = mKeyToRc.get(key);
- onLaunch((innerCode != null) ? innerCode : requestCode, contract, input, options);
+ if (innerCode == null) {
+ throw new IllegalStateException("Attempting to launch an unregistered "
+ + "ActivityResultLauncher with contract " + contract + " and input "
+ + input + ". You must ensure the ActivityResultLauncher is registered "
+ + "before calling launch().");
+ }
+ mLaunchedKeys.add(key);
+ onLaunch(innerCode, contract, input, options);
}
@Override
@@ -201,7 +207,7 @@
@NonNull final String key,
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {
- final int requestCode = registerKey(key);
+ registerKey(key);
mKeyToCallback.put(key, new CallbackAndContract<>(callback, contract));
if (mParsedPendingResults.containsKey(key)) {
@@ -221,9 +227,15 @@
return new ActivityResultLauncher<I>() {
@Override
public void launch(I input, @Nullable ActivityOptionsCompat options) {
- mLaunchedKeys.add(key);
Integer innerCode = mKeyToRc.get(key);
- onLaunch((innerCode != null) ? innerCode : requestCode, contract, input, options);
+ if (innerCode == null) {
+ throw new IllegalStateException("Attempting to launch an unregistered "
+ + "ActivityResultLauncher with contract " + contract + " and input "
+ + input + ". You must ensure the ActivityResultLauncher is registered "
+ + "before calling launch().");
+ }
+ mLaunchedKeys.add(key);
+ onLaunch(innerCode, contract, input, options);
}
@Override
@@ -398,14 +410,13 @@
}
}
- private int registerKey(String key) {
+ private void registerKey(String key) {
Integer existing = mKeyToRc.get(key);
if (existing != null) {
- return existing;
+ return;
}
int rc = generateRandomNumber();
bindRcKey(rc, key);
- return rc;
}
/**
diff --git a/benchmark/integration-tests/crystalball-experiment/build.gradle b/benchmark/integration-tests/crystalball-experiment/build.gradle
deleted file mode 100644
index 068b678..0000000
--- a/benchmark/integration-tests/crystalball-experiment/build.gradle
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import androidx.build.LibraryGroups
-import androidx.build.Publish
-
-plugins {
- id("AndroidXPlugin")
- id("com.android.library")
- id("kotlin-android")
-}
-
-android {
- defaultConfig {
- minSdkVersion 18
- multiDexEnabled true
- // testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
- // Listener List
- testInstrumentationRunnerArgument "listener",
- "android.device.collectors.CpuUsageListener," +
- "android.device.collectors.ProcLoadListener," +
- "android.device.collectors.PerfettoListener," +
- "android.device.collectors.AppStartupListener," +
- "android.device.collectors.JankListener," +
- "android.device.collectors.CrashListener," +
- "android.device.collectors.ScreenshotOnFailureCollector," +
- "android.device.collectors.LogcatOnFailureCollector," +
- "android.device.collectors.IncidentReportListener," +
- "android.device.collectors.TotalPssMetricListener"
-
- // ProcLoadListener
- testInstrumentationRunnerArgument "procload-collector:proc-loadavg-interval", "2000"
- testInstrumentationRunnerArgument "procload-collector:proc-loadavg-threshold", "0.5"
- testInstrumentationRunnerArgument "procload-collector:proc-loadavg-timeout", "90000"
-
- // CpuUsageListener
- testInstrumentationRunnerArgument "cpuusage-collector:disable_per_freq", "true"
- testInstrumentationRunnerArgument "cpuusage-collector:disable_per_pkg", "true"
-
- // TotalPssMetricListener
- testInstrumentationRunnerArgument "totalpss-collector:process-names", "androidx.compose.integration.demos"
-
- // JankListener (disable)
- testInstrumentationRunnerArgument "jank-listener:log", "true"
-
- // Microbenchmark runner configuration
- testInstrumentationRunnerArgument "iterations", "1"
- }
-}
-
-dependencies {
- api(libs.junit)
- api(libs.kotlinStdlib)
- api("androidx.annotation:annotation:1.1.0")
- // TODO: remove, once we remove the minor usages in CrystalBall
- implementation(libs.guavaAndroid)
- androidTestImplementation("com.android:collector-device-lib:0.1.0")
- androidTestImplementation("com.android:collector-device-lib-platform:0.1.0")
- androidTestImplementation("com.android:collector-helper-utilities:0.1.0")
- androidTestImplementation("com.android:jank-helper:0.1.0")
- androidTestImplementation("com.android:memory-helper:0.1.0")
- androidTestImplementation("com.android:perfetto-helper:0.1.0")
- androidTestImplementation("com.android:platform-test-composers:0.1.0")
- androidTestImplementation("com.android:power-helper:0.1.0")
- androidTestImplementation("com.android:simpleperf-helper:0.1.0")
- androidTestImplementation("com.android:statsd-helper:0.1.0")
- androidTestImplementation("com.android:system-metric-helper:0.1.0")
- androidTestImplementation("com.android:test-composers:0.1.0")
- androidTestImplementation("com.android:platform-test-rules:0.1.0")
- androidTestImplementation("com.android:microbenchmark-device-lib:0.1.0")
- androidTestImplementation("androidx.test:rules:1.3.0")
- androidTestImplementation("androidx.test:runner:1.3.0")
- implementation(libs.testExtJunit)
- implementation(libs.testUiautomator)
-}
-
-androidx {
- name = "Android Benchmark - Crystalball experiment"
- publish = Publish.NONE
- mavenGroup = LibraryGroups.BENCHMARK
- inceptionYear = "2020"
- description = "Android Benchmark - Crystalball experiment"
-}
diff --git a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/JankCollectionHelperTest.kt b/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/JankCollectionHelperTest.kt
deleted file mode 100644
index af49322..0000000
--- a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/JankCollectionHelperTest.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.benchmark.macro
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import androidx.test.filters.SdkSuppress
-import com.android.helpers.JankCollectionHelper
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class JankCollectionHelperTest {
- // Setting a minSdkVersion of 27 because JankHelper fails with an error on API 26.
- // Needs a fix in JankHelper and re-import of prebults.
- // https://android-build.googleplex.com/builds/tests/view?invocationId=I00300005943166534&redirect=http://sponge2/025964b6-d278-44a7-805c-56d8010935a8
- @Test
- @SdkSuppress(minSdkVersion = 27)
- fun trivialTest() {
- JankCollectionHelper()
- }
-}
diff --git a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/OpenAppMicrobenchmark.kt b/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/OpenAppMicrobenchmark.kt
deleted file mode 100644
index d975384..0000000
--- a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/OpenAppMicrobenchmark.kt
+++ /dev/null
@@ -1,86 +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.benchmark.macro
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.content.Intent
-import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
-import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
-import android.platform.test.microbenchmark.Microbenchmark
-import android.platform.test.rule.CompilationFilterRule
-import android.platform.test.rule.DropCachesRule
-import android.platform.test.rule.KillAppsRule
-import android.platform.test.rule.NaturalOrientationRule
-import android.platform.test.rule.PressHomeRule
-import androidx.test.filters.LargeTest
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.UiDevice
-import androidx.test.uiautomator.Until
-import org.junit.AfterClass
-import org.junit.ClassRule
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-import org.junit.rules.RuleChain
-import org.junit.runner.RunWith
-
-@SuppressLint("UnsupportedTestRunner")
-@RunWith(Microbenchmark::class)
-class OpenAppMicrobenchmark {
-
- @Test
- @LargeTest
- @Ignore("Gradle arguments are not passed to the TestRunner")
- fun open() {
- // Launch the app
- val context: Context = InstrumentationRegistry.getInstrumentation().context
- val intent: Intent = context.packageManager.getLaunchIntentForPackage(thePackage)!!
- // Clear out any previous instances
- intent.flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK
- context.startActivity(intent)
- device.wait(
- Until.hasObject(By.pkg(thePackage).depth(0)),
- 5000
- )
- }
-
- // Method-level rules
- @get:Rule
- var rules: RuleChain = RuleChain.outerRule(KillAppsRule(thePackage))
- .around(DropCachesRule())
- .around(CompilationFilterRule(thePackage))
- .around(PressHomeRule())
-
- companion object {
- // Pixel Settings App
- const val thePackage = "com.android.settings"
- private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
-
- @JvmStatic
- @AfterClass
- fun close() {
- device.pressHome()
- }
-
- // Class-level rules
- @JvmField
- @ClassRule
- var orientationRule = NaturalOrientationRule()
- }
-}
diff --git a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/TrivialTest.kt b/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/TrivialTest.kt
deleted file mode 100644
index 4c660b6..0000000
--- a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/TrivialTest.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.benchmark.macro
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class TrivialTest {
- @Test
- fun foo() {
- BenchmarkClass()
- }
-}
diff --git a/benchmark/integration-tests/crystalball-experiment/src/main/AndroidManifest.xml b/benchmark/integration-tests/crystalball-experiment/src/main/AndroidManifest.xml
deleted file mode 100644
index a599d46..0000000
--- a/benchmark/integration-tests/crystalball-experiment/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2020 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<manifest package="androidx.benchmark.macro"/>
diff --git a/benchmark/integration-tests/crystalball-experiment/src/scripts/copy_crystalball_prebuilts.py b/benchmark/integration-tests/crystalball-experiment/src/scripts/copy_crystalball_prebuilts.py
deleted file mode 100755
index e3880a7..0000000
--- a/benchmark/integration-tests/crystalball-experiment/src/scripts/copy_crystalball_prebuilts.py
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/usr/bin/python3
-
-import argparse
-import os
-import shutil
-
-
-
-SOONG_SUFFIX = '/android_common/javac'
-VERSION = '0.1.0'
-
-
-def main():
- parser = argparse.ArgumentParser(description='Copy Crystalball Prebuilts')
- parser.add_argument('soong_path', action="store", help='Looks like "/usr/local/google/home/rahulrav/Workspace/internal_android/out/soong/.intermediates"')
- parser.add_argument('prebuilts_directory', action="store", help='Looks like "/mnt/Android/Flatfoot/androidx_main/prebuilts/androidx/external/com/android/"')
- parser.add_argument('--verbose', action="store_true", default=False)
- parse_result = parser.parse_args()
-
- soong_path = parse_result.soong_path
- soong_output_directory = f'{soong_path}/platform_testing/libraries/'
- prebuilts_directory = parse_result.prebuilts_directory
-
- mapping = dict([
- # Device Collectors
- (f'{soong_output_directory}/device-collectors/src/main/collector-device-lib/{SOONG_SUFFIX}/collector-device-lib.jar', f'{prebuilts_directory}/collector-device-lib/{VERSION}/collector-device-lib-{VERSION}.jar'),
- # Collectors Device Lib Platform
- (f'{soong_output_directory}/device-collectors/src/main/platform-collectors/collector-device-lib-platform/{SOONG_SUFFIX}/collector-device-lib-platform.jar', f'{prebuilts_directory}/collector-device-lib-platform/{VERSION}/collector-device-lib-platform-{VERSION}.jar'),
- # Collector Helpers
- (f'{soong_output_directory}/collectors-helper/utilities/collector-helper-utilities/{SOONG_SUFFIX}/collector-helper-utilities.jar', f'{prebuilts_directory}/collector-helper-utilities/{VERSION}/collector-helper-utilities-{VERSION}.jar'),
- (f'{soong_output_directory}/collectors-helper/jank/jank-helper/{SOONG_SUFFIX}/jank-helper.jar', f'{prebuilts_directory}/jank-helper/{VERSION}/jank-helper-{VERSION}.jar'),
- (f'{soong_output_directory}/collectors-helper/memory/memory-helper/{SOONG_SUFFIX}/memory-helper.jar', f'{prebuilts_directory}/memory-helper/{VERSION}/memory-helper-{VERSION}.jar'),
- # Microbenchmarks
- (f'{soong_output_directory}/health/runners/microbenchmark/microbenchmark-device-lib/{SOONG_SUFFIX}/microbenchmark-device-lib.jar', f'{prebuilts_directory}/microbenchmark-device-lib/{VERSION}/microbenchmark-device-lib-{VERSION}.jar'),
- # Perfetto Helper
- (f'{soong_output_directory}/collectors-helper/perfetto/perfetto-helper/{SOONG_SUFFIX}/perfetto-helper.jar', f'{prebuilts_directory}/perfetto-helper/{VERSION}/perfetto-helper-{VERSION}.jar'),
- # Platform Protos
- (f'{soong_path}/frameworks/base/platformprotoslite/{SOONG_SUFFIX}/platformprotoslite.jar', f'{prebuilts_directory}/platformprotoslite/{VERSION}/platformprotoslite-{VERSION}.jar'),
- (f'{soong_path}/frameworks/base/platformprotosnano/{SOONG_SUFFIX}/platformprotosnano.jar', f'{prebuilts_directory}/platformprotosnano/{VERSION}/platformprotosnano-{VERSION}.jar'),
- # Platform Test Composers
- (f'{soong_output_directory}/health/composers/platform/platform-test-composers/{SOONG_SUFFIX}/platform-test-composers.jar', f'{prebuilts_directory}/platform-test-composers/{VERSION}/platform-test-composers-{VERSION}.jar'),
- # Platform Test Rules
- (f'{soong_output_directory}/health/rules/platform-test-rules/{SOONG_SUFFIX}/platform-test-rules.jar', f'{prebuilts_directory}/platform-test-rules/{VERSION}/platform-test-rules-{VERSION}.jar'),
- # Power Helper
- (f'{soong_output_directory}/collectors-helper/power/power-helper/{SOONG_SUFFIX}/power-helper.jar', f'{prebuilts_directory}/power-helper/{VERSION}/power-helper-{VERSION}.jar'),
- # Simple Perf Helper
- (f'{soong_output_directory}/collectors-helper/simpleperf/simpleperf-helper/{SOONG_SUFFIX}/simpleperf-helper.jar', f'{prebuilts_directory}/simpleperf-helper/{VERSION}/simpleperf-helper-{VERSION}.jar'),
- # Statsd Helper
- (f'{soong_output_directory}/collectors-helper/statsd/statsd-helper/{SOONG_SUFFIX}/statsd-helper.jar', f'{prebuilts_directory}/statsd-helper/{VERSION}/statsd-helper-{VERSION}.jar'),
- # Statsd Protos
- (f'{soong_path}/frameworks/base/cmds/statsd/statsdprotonano/{SOONG_SUFFIX}/statsdprotonano.jar', f'{prebuilts_directory}/statsdprotonano/{VERSION}/statsdprotonano-{VERSION}.jar'),
- # System Metric Helpers
- (f'{soong_output_directory}/collectors-helper/system/system-metric-helper/{SOONG_SUFFIX}/system-metric-helper.jar', f'{prebuilts_directory}/system-metric-helper/{VERSION}/system-metric-helper-{VERSION}.jar'),
- # Test Composers
- (f'{soong_output_directory}/health/composers/host/test-composers/{SOONG_SUFFIX}/test-composers.jar', f'{prebuilts_directory}/test-composers/{VERSION}/test-composers-{VERSION}.jar'),
- ])
-
- size = len(mapping)
- if parse_result.verbose:
- print(f'Total number of entries = {size}')
-
- for key in mapping:
- if not os.path.exists(key):
- print(f'Artifact at {key} does not exist. You may have forgotten to build the necessary artifacts.')
-
- if os.path.exists(mapping[key]) and parse_result.verbose:
- print(f'Artifact at {mapping[key]} will be overwritten')
-
- shutil.copyfile(key, mapping[key])
-
- if parse_result.verbose:
- print(f'Copied from {key} to {mapping[key]}\n')
-
- print('All done.')
-
-if __name__ == "__main__":
- main()
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/JankCollectionHelper.java b/benchmark/macro/src/main/java/androidx/benchmark/macro/JankCollectionHelper.java
index 47db363..d0b11e8 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/JankCollectionHelper.java
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/JankCollectionHelper.java
@@ -26,7 +26,7 @@
import androidx.test.uiautomator.UiDevice;
import java.io.IOException;
-import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@@ -37,7 +37,7 @@
/** Collects jank metrics for all or a list of processes. */
class JankCollectionHelper {
- private static final String LOG_TAG = "JankCollectionHelper";
+ private static final String LOG_TAG = JankCollectionHelper.class.getSimpleName();
// Prefix for all output metrics that come from the gfxinfo dump.
@VisibleForTesting static final String GFXINFO_METRICS_PREFIX = "gfxinfo";
@@ -59,34 +59,50 @@
"total_frames"),
// Example: "Janky frames: 785 (3.85%)"
JANKY_FRAMES_COUNT(
- Pattern.compile(".*Janky frames: (\\d+) \\((.+)\\%\\).*", Pattern.DOTALL),
+ Pattern.compile(
+ ".*Janky frames: (\\d+) \\(([0-9]+[\\.]?[0-9]+)\\%\\).*", Pattern.DOTALL),
1,
"janky_frames_count"),
// Example: "Janky frames: 785 (3.85%)"
JANKY_FRAMES_PRCNT(
- Pattern.compile(".*Janky frames: (\\d+) \\((.+)\\%\\).*", Pattern.DOTALL),
+ Pattern.compile(
+ ".*Janky frames: (\\d+) \\(([0-9]+[\\.]?[0-9]+)\\%\\).*", Pattern.DOTALL),
2,
"janky_frames_percent"),
+ // Example: "Janky frames (legacy): 785 (3.85%)"
+ JANKY_FRAMES_LEGACY_COUNT(
+ Pattern.compile(
+ ".*Janky frames \\(legacy\\): (\\d+) \\(([0-9]+[\\.]?[0-9]+)\\%\\).*",
+ Pattern.DOTALL),
+ 1,
+ "janky_frames_legacy_count"),
+ // Example: "Janky frames (legacy): 785 (3.85%)"
+ JANKY_FRAMES_LEGACY_PRCNT(
+ Pattern.compile(
+ ".*Janky frames \\(legacy\\): (\\d+) \\(([0-9]+[\\.]?[0-9]+)\\%\\).*",
+ Pattern.DOTALL),
+ 2,
+ "janky_frames_legacy_percent"),
// Example: "50th percentile: 9ms"
FRAME_TIME_50TH(
Pattern.compile(".*50th percentile: (\\d+)ms.*", Pattern.DOTALL),
1,
- "jank_percentile_50"),
+ "frame_render_time_percentile_50"),
// Example: "90th percentile: 9ms"
FRAME_TIME_90TH(
Pattern.compile(".*90th percentile: (\\d+)ms.*", Pattern.DOTALL),
1,
- "jank_percentile_90"),
+ "frame_render_time_percentile_90"),
// Example: "95th percentile: 9ms"
FRAME_TIME_95TH(
Pattern.compile(".*95th percentile: (\\d+)ms.*", Pattern.DOTALL),
1,
- "jank_percentile_95"),
+ "frame_render_time_percentile_95"),
// Example: "99th percentile: 9ms"
FRAME_TIME_99TH(
Pattern.compile(".*99th percentile: (\\d+)ms.*", Pattern.DOTALL),
1,
- "jank_percentile_99"),
+ "frame_render_time_percentile_99"),
// Example: "Number Missed Vsync: 0"
NUM_MISSED_VSYNC(
Pattern.compile(".*Number Missed Vsync: (\\d+).*", Pattern.DOTALL),
@@ -117,26 +133,32 @@
Pattern.compile(".*Number Frame deadline missed: (\\d+).*", Pattern.DOTALL),
1,
"deadline_missed"),
+ // Number Frame deadline missed (legacy): 0
+ NUM_FRAME_DEADLINE_MISSED_LEGACY(
+ Pattern.compile(
+ ".*Number Frame deadline missed \\(legacy\\): (\\d+).*", Pattern.DOTALL),
+ 1,
+ "deadline_missed_legacy"),
// Example: "50th gpu percentile: 9ms"
GPU_FRAME_TIME_50TH(
Pattern.compile(".*50th gpu percentile: (\\d+)ms.*", Pattern.DOTALL),
1,
- "gpu_jank_percentile_50"),
+ "gpu_frame_render_time_percentile_50"),
// Example: "90th gpu percentile: 9ms"
GPU_FRAME_TIME_90TH(
Pattern.compile(".*90th gpu percentile: (\\d+)ms.*", Pattern.DOTALL),
1,
- "gpu_jank_percentile_90"),
+ "gpu_frame_render_time_percentile_90"),
// Example: "95th gpu percentile: 9ms"
GPU_FRAME_TIME_95TH(
Pattern.compile(".*95th gpu percentile: (\\d+)ms.*", Pattern.DOTALL),
1,
- "gpu_jank_percentile_95"),
+ "gpu_frame_render_time_percentile_95"),
// Example: "99th gpu percentile: 9ms"
GPU_FRAME_TIME_99TH(
Pattern.compile(".*99th gpu percentile: (\\d+)ms.*", Pattern.DOTALL),
1,
- "gpu_jank_percentile_99");
+ "gpu_frame_render_time_percentile_99");
private final Pattern mPattern;
private final int mGroupIndex;
@@ -237,7 +259,7 @@
/** Add a package or list of packages to be tracked. */
public void addTrackedPackages(@NonNull String... packages) {
- mTrackedPackages.addAll(Arrays.asList(packages));
+ Collections.addAll(mTrackedPackages, packages);
}
/** Clear the {@code gfxinfo} for all packages. */
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/Metric.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/Metric.kt
index 8a23f8a..8657fea 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/Metric.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/Metric.kt
@@ -84,23 +84,26 @@
* via [androidx.benchmark.Stats.putInBundle].
*/
private val keyRenameMap = mapOf(
- "jank_percentile_50" to "frameTime50thPercentileMs",
- "jank_percentile_90" to "frameTime90thPercentileMs",
- "jank_percentile_95" to "frameTime95thPercentileMs",
- "jank_percentile_99" to "frameTime99thPercentileMs",
- "gpu_jank_percentile_50" to "gpuFrameTime50thPercentileMs",
- "gpu_jank_percentile_90" to "gpuFrameTime90thPercentileMs",
- "gpu_jank_percentile_95" to "gpuFrameTime95thPercentileMs",
- "gpu_jank_percentile_99" to "gpuFrameTime99thPercentileMs",
+ "frame_render_time_percentile_50" to "frameTime50thPercentileMs",
+ "frame_render_time_percentile_90" to "frameTime90thPercentileMs",
+ "frame_render_time_percentile_95" to "frameTime95thPercentileMs",
+ "frame_render_time_percentile_99" to "frameTime99thPercentileMs",
+ "gpu_frame_render_time_percentile_50" to "gpuFrameTime50thPercentileMs",
+ "gpu_frame_render_time_percentile_90" to "gpuFrameTime90thPercentileMs",
+ "gpu_frame_render_time_percentile_95" to "gpuFrameTime95thPercentileMs",
+ "gpu_frame_render_time_percentile_99" to "gpuFrameTime99thPercentileMs",
"missed_vsync" to "vsyncMissedFrameCount",
"deadline_missed" to "deadlineMissedFrameCount",
+ "deadline_missed_legacy" to "deadlineMissedFrameCountLegacy",
"janky_frames_count" to "jankyFrameCount",
+ "janky_frames_legacy_count" to "jankyFrameCountLegacy",
"high_input_latency" to "highInputLatencyFrameCount",
"slow_ui_thread" to "slowUiThreadFrameCount",
"slow_bmp_upload" to "slowBitmapUploadFrameCount",
"slow_issue_draw_cmds" to "slowIssueDrawCommandsFrameCount",
"total_frames" to "totalFrameCount",
- "janky_frames_percent" to "jankyFramePercent"
+ "janky_frames_percent" to "jankyFramePercent",
+ "janky_frames_legacy_percent" to "jankyFramePercentLegacy"
)
/**
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
index b7e9cf1..3e175c1 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
@@ -25,7 +25,8 @@
import androidx.build.studio.StudioTask.Companion.registerStudioTask
import androidx.build.testConfiguration.registerOwnersServiceTasks
import androidx.build.uptodatedness.TaskUpToDateValidator
-import com.android.build.gradle.api.AndroidBasePlugin
+import com.android.build.gradle.AppPlugin
+import com.android.build.gradle.LibraryPlugin
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
@@ -114,10 +115,13 @@
}
)
)
- project.plugins.withType(AndroidBasePlugin::class.java) {
- buildOnServerTask.dependsOn("${project.path}:assembleRelease")
- if (!project.usingMaxDepVersions()) {
- project.afterEvaluate {
+ project.afterEvaluate {
+ if (project.plugins.hasPlugin(LibraryPlugin::class.java) ||
+ project.plugins.hasPlugin(AppPlugin::class.java)
+ ) {
+
+ buildOnServerTask.dependsOn("${project.path}:assembleRelease")
+ if (!project.usingMaxDepVersions()) {
project.agpVariants.all { variant ->
// in AndroidX, release and debug variants are essentially the same,
// so we don't run the lintRelease task on the build server
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index c25342a..556ee60 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -56,7 +56,7 @@
val CORE_ROLE = Version("1.1.0-alpha02")
val CURSORADAPTER = Version("1.1.0-alpha01")
val CUSTOMVIEW = Version("1.2.0-alpha01")
- val DATASTORE = Version("1.0.0-rc01")
+ val DATASTORE = Version("1.0.0-rc02")
val DOCUMENTFILE = Version("1.1.0-alpha01")
val DRAWERLAYOUT = Version("1.2.0-alpha01")
val DYNAMICANIMATION = Version("1.1.0-alpha04")
@@ -65,7 +65,7 @@
val EMOJI2 = Version("1.0.0-alpha03")
val ENTERPRISE = Version("1.1.0-rc01")
val EXIFINTERFACE = Version("1.4.0-alpha01")
- val FRAGMENT = Version("1.4.0-alpha04")
+ val FRAGMENT = Version("1.4.0-alpha05")
val FUTURES = Version("1.2.0-alpha01")
val GRIDLAYOUT = Version("1.1.0-alpha01")
val HEALTH_SERVICES_CLIENT = Version("1.0.0-alpha02")
@@ -88,8 +88,8 @@
val MEDIA = Version("1.5.0-alpha01")
val MEDIA2 = Version("1.2.0-beta01")
val MEDIAROUTER = Version("1.3.0-alpha01")
- val NAVIGATION = Version("2.4.0-alpha04")
- val PAGING = Version("3.1.0-alpha02")
+ val NAVIGATION = Version("2.4.0-alpha05")
+ val PAGING = Version("3.1.0-alpha03")
val PAGING_COMPOSE = Version("1.0.0-alpha12")
val PALETTE = Version("1.1.0-alpha01")
val PRINT = Version("1.1.0-beta01")
@@ -130,24 +130,24 @@
val VERSIONED_PARCELABLE = Version("1.2.0-alpha01")
val VIEWPAGER = Version("1.1.0-alpha01")
val VIEWPAGER2 = Version("1.1.0-alpha02")
- val WEAR = Version("1.2.0-alpha11")
- val WEAR_COMPLICATIONS_DATA = Version("1.0.0-alpha17")
- val WEAR_COMPLICATIONS_DATA_SOURCE = Version("1.0.0-alpha17")
- val WEAR_COMPOSE = Version("1.0.0-alpha01")
- val WEAR_INPUT = Version("1.1.0-alpha03")
+ val WEAR = Version("1.2.0-alpha12")
+ val WEAR_COMPLICATIONS_DATA = Version("1.0.0-alpha18")
+ val WEAR_COMPLICATIONS_DATA_SOURCE = Version("1.0.0-alpha18")
+ val WEAR_COMPOSE = Version("1.0.0-alpha02")
+ val WEAR_INPUT = Version("1.1.0-beta01")
val WEAR_INPUT_TESTING = WEAR_INPUT
- val WEAR_ONGOING = Version("1.0.0-alpha08")
- val WEAR_PHONE_INTERACTIONS = Version("1.0.0-alpha05")
- val WEAR_REMOTE_INTERACTIONS = Version("1.0.0-alpha04")
- val WEAR_TILES = Version("1.0.0-alpha08")
- val WEAR_WATCHFACE = Version("1.0.0-alpha17")
- val WEAR_WATCHFACE_CLIENT = Version("1.0.0-alpha17")
+ val WEAR_ONGOING = Version("1.0.0-beta01")
+ val WEAR_PHONE_INTERACTIONS = Version("1.0.0-alpha06")
+ val WEAR_REMOTE_INTERACTIONS = Version("1.0.0-alpha05")
+ val WEAR_TILES = Version("1.0.0-alpha09")
+ val WEAR_WATCHFACE = Version("1.0.0-alpha18")
+ val WEAR_WATCHFACE_CLIENT = Version("1.0.0-alpha18")
val WEAR_WATCHFACE_CLIENT_GUAVA = WEAR_WATCHFACE_CLIENT
- val WEAR_WATCHFACE_COMPLICATIONS_RENDERING = Version("1.0.0-alpha17")
- val WEAR_WATCHFACE_DATA = Version("1.0.0-alpha17")
- val WEAR_WATCHFACE_EDITOR = Version("1.0.0-alpha17")
+ val WEAR_WATCHFACE_COMPLICATIONS_RENDERING = Version("1.0.0-alpha18")
+ val WEAR_WATCHFACE_DATA = Version("1.0.0-alpha18")
+ val WEAR_WATCHFACE_EDITOR = Version("1.0.0-alpha18")
val WEAR_WATCHFACE_EDITOR_GUAVA = WEAR_WATCHFACE_EDITOR
- val WEAR_WATCHFACE_STYLE = Version("1.0.0-alpha17")
+ val WEAR_WATCHFACE_STYLE = Version("1.0.0-alpha18")
val WEBKIT = Version("1.5.0-alpha01")
val WINDOW = Version("1.0.0-alpha09")
val WINDOW_EXTENSIONS = Version("1.0.0-alpha01")
diff --git a/buildSrc/src/main/kotlin/androidx/build/dackka/DackkaTask.kt b/buildSrc/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
index 0166db9..9b12bef 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
@@ -103,6 +103,10 @@
"-globalLinks",
linksConfiguration,
+ // Set logging level to only show warnings and errors
+ "-loggingLevel",
+ "WARN",
+
// Configuration of sources. The generated string looks like this:
// "-sourceSet -src /path/to/src -samples /path/to/samples ..."
"-sourceSet",
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt b/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
index 99e2728..e993e52 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
@@ -508,6 +508,10 @@
":emoji2:integration-tests:init-enabled-macrobenchmark-target",
),
setOf(
+ ":wear:benchmark:integration-tests:macrobenchmark",
+ ":wear:benchmark:integration-tests:macrobenchmark-target"
+ ),
+ setOf(
":wear:compose:integration-tests:macrobenchmark",
":wear:compose:integration-tests:macrobenchmark-target"
),
diff --git a/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt
index 1947caa..5b3177d 100644
--- a/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt
@@ -560,7 +560,7 @@
}
}
-private const val DACKKA_DEPENDENCY = "com.google.devsite:dackka:0.0.7"
+private const val DACKKA_DEPENDENCY = "com.google.devsite:dackka:0.0.8"
private const val DOCLAVA_DEPENDENCY = "com.android:doclava:1.0.6"
// Allowlist for directories that should be processed by Dackka
@@ -576,13 +576,13 @@
"androidx/benchmark/**",
"androidx/biometric/**",
"androidx/browser/**",
-// "androidx/camera/**",
+ "androidx/camera/**",
"androidx/car/**",
"androidx/cardview/**",
"androidx/collection/**",
"androidx/compose/**",
"androidx/concurrent/**",
-// "androidx/contentpager/**",
+ "androidx/contentpager/**",
"androidx/coordinatorlayout/**",
// "androidx/core/**",
"androidx/cursoradapter/**",
@@ -599,7 +599,7 @@
"androidx/gridlayout/**",
"androidx/health/**",
"androidx/heifwriter/**",
-// "androidx/hilt/**",
+ "androidx/hilt/**",
"androidx/interpolator/**",
// "androidx/leanback/**",
"androidx/legacy/**",
@@ -607,7 +607,7 @@
"androidx/loader/**",
"androidx/localbroadcastmanager/**",
"androidx/media/**",
-// "androidx/media2/**",
+ "androidx/media2/**",
"androidx/mediarouter/**",
"androidx/navigation/**",
"androidx/paging/**",
@@ -632,12 +632,12 @@
"androidx/tracing/**",
"androidx/transition/**",
"androidx/tvprovider/**",
-// "androidx/vectordrawable/**",
+ "androidx/vectordrawable/**",
"androidx/versionedparcelable/**",
"androidx/viewpager/**",
"androidx/viewpager2/**",
"androidx/wear/**",
-// "androidx/webkit/**",
+ "androidx/webkit/**",
"androidx/window/**",
"androidx/work/**"
)
diff --git a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index f723ca4..0fb51b6 100644
--- a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -119,18 +119,24 @@
* alternative project. Default is for the project to register the new config task to itself
*/
fun Project.addAppApkToTestConfigGeneration(overrideProject: Project = this) {
+ if (project.isMacrobenchmarkTarget()) {
+ return
+ }
// TODO(aurimas): migrate away from this when upgrading to AGP 7.1.0-alpha03 or newer
@Suppress("DEPRECATION")
extensions.getByType<
com.android.build.api.extension.ApplicationAndroidComponentsExtension
>().apply {
- onVariants(selector().withBuildType("debug")) { debugVariant ->
- overrideProject.tasks.withType(GenerateTestConfigurationTask::class.java)
- .configureEach {
- it.appFolder.set(debugVariant.artifacts.get(SingleArtifact.APK))
- it.appLoader.set(debugVariant.artifacts.getBuiltArtifactsLoader())
- it.appProjectPath.set(overrideProject.path)
- }
+ onVariants(selector().withBuildType("debug")) { appVariant ->
+ overrideProject.tasks.named(
+ "${AndroidXPlugin.GENERATE_TEST_CONFIGURATION_TASK}" +
+ "${appVariant.name}AndroidTest"
+ ) { configTask ->
+ configTask as GenerateTestConfigurationTask
+ configTask.appFolder.set(appVariant.artifacts.get(SingleArtifact.APK))
+ configTask.appLoader.set(appVariant.artifacts.getBuiltArtifactsLoader())
+ configTask.appProjectPath.set(overrideProject.path)
+ }
}
}
}
@@ -340,7 +346,7 @@
this.rootProject.tasks.findByName(
AndroidXPlugin.ZIP_CONSTRAINED_TEST_CONFIGS_WITH_APKS_TASK
)!!.dependsOn(configTask)
- } else if (path.endsWith("macrobenchmark-target")) {
+ } else if (isMacrobenchmarkTarget()) {
configTask.configure { task ->
task.appFolder.set(artifacts.get(SingleArtifact.APK))
task.appLoader.set(artifacts.getBuiltArtifactsLoader())
@@ -349,6 +355,13 @@
}
}
+/**
+ * Tells whether this project is the macrobenchmark-target project
+ */
+fun Project.isMacrobenchmarkTarget(): Boolean {
+ return path.endsWith("macrobenchmark-target")
+}
+
fun Project.configureTestConfigGeneration(baseExtension: BaseExtension) {
// TODO(aurimas): migrate away from this when upgrading to AGP 7.1.0-alpha03 or newer
@Suppress("DEPRECATION")
@@ -381,7 +394,7 @@
)
}
path.endsWith("macrobenchmark") ||
- path.endsWith("macrobenchmark-target") -> {
+ isMacrobenchmarkTarget() -> {
configureMacrobenchmarkConfigTask(
androidTest.name,
androidTest.artifacts,
diff --git a/busytown/androidx-studio-integration.sh b/busytown/androidx-studio-integration.sh
index 14f8dc9..4ba41d1 100755
--- a/busytown/androidx-studio-integration.sh
+++ b/busytown/androidx-studio-integration.sh
@@ -47,7 +47,8 @@
function buildAndroidx() {
LOG_PROCESSOR="$SCRIPT_DIR/../development/build_log_processor.sh"
properties="-Pandroidx.summarizeStderr --no-daemon -Pandroidx.allWarningsAsErrors"
- "$LOG_PROCESSOR" $gw $properties -p frameworks/support listTaskOutputs bOS -x verifyDependencyVersions --stacktrace -PverifyUpToDate --profile
+ # runErrorProne is disabled due to I77d9800990e2a46648f7ed2713c54398cd798a0d in AGP
+ "$LOG_PROCESSOR" $gw $properties -p frameworks/support listTaskOutputs bOS -x verifyDependencyVersions -x runErrorProne --stacktrace -PverifyUpToDate --profile
$SCRIPT_DIR/impl/parse_profile_htmls.sh
}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.java
index 929a264..fdfd9dc 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.java
@@ -225,8 +225,8 @@
final CameraManagerCompat cameraManagerCompat = CameraManagerCompat.from(cameraManagerImpl);
// Build camera info
- final Camera2CameraInfoImpl camera2CameraInfo = new Camera2CameraInfoImpl(mCameraId,
- cameraManagerCompat.getCameraCharacteristicsCompat(mCameraId));
+ final Camera2CameraInfoImpl camera2CameraInfo =
+ new Camera2CameraInfoImpl(mCameraId, cameraManagerCompat);
// Initialize camera instance
mCamera2CameraImpl = new Camera2CameraImpl(cameraManagerCompat, mCameraId,
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplForceOpenCameraTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplForceOpenCameraTest.kt
index d30d2f2..debe2f1 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplForceOpenCameraTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplForceOpenCameraTest.kt
@@ -167,7 +167,7 @@
// Build camera info from cameraId
val camera2CameraInfo = Camera2CameraInfoImpl(
cameraId,
- cameraManagerCompat.getCameraCharacteristicsCompat(cameraId)
+ cameraManagerCompat
)
// Initialize camera instance
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplStateTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplStateTest.kt
index 49a77ea..edf597d 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplStateTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplStateTest.kt
@@ -333,7 +333,7 @@
// Build camera info
val camera2CameraInfo = Camera2CameraInfoImpl(
cameraId,
- cameraManagerCompat.getCameraCharacteristicsCompat(cameraId)
+ cameraManagerCompat
)
// Initialize camera state registry and only allow 1 open camera at most inside CameraX
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index 16f48a1..13b1c6a 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -155,7 +155,7 @@
CameraManagerCompat cameraManagerCompat =
CameraManagerCompat.from((Context) ApplicationProvider.getApplicationContext());
Camera2CameraInfoImpl camera2CameraInfo = new Camera2CameraInfoImpl(
- mCameraId, cameraManagerCompat.getCameraCharacteristicsCompat(mCameraId));
+ mCameraId, cameraManagerCompat);
mCamera2CameraImpl = new Camera2CameraImpl(cameraManagerCompat, mCameraId,
camera2CameraInfo,
mCameraStateRegistry, sCameraExecutor, sCameraHandler);
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
index 46ab8cd..0453b3f 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
@@ -147,7 +147,7 @@
CameraManagerCompat cameraManagerCompat =
CameraManagerCompat.from((Context) ApplicationProvider.getApplicationContext());
Camera2CameraInfoImpl camera2CameraInfo = new Camera2CameraInfoImpl(
- mCameraId, cameraManagerCompat.getCameraCharacteristicsCompat(mCameraId));
+ mCameraId, cameraManagerCompat);
mCamera2CameraImpl = new Camera2CameraImpl(
CameraManagerCompat.from((Context) ApplicationProvider.getApplicationContext()),
mCameraId,
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java
index 61c4069..51454e5 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java
@@ -80,7 +80,7 @@
Camera2CameraInfoImpl camera2CameraInfoImpl = mCameraInfos.get(cameraId);
if (camera2CameraInfoImpl == null) {
camera2CameraInfoImpl = new Camera2CameraInfoImpl(
- cameraId, mCameraManager.getCameraCharacteristicsCompat(cameraId));
+ cameraId, mCameraManager);
mCameraInfos.put(cameraId, camera2CameraInfoImpl);
}
return camera2CameraInfoImpl;
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index c0f9b4a..178f4ee 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -25,7 +25,9 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
+import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.CameraManagerCompat;
import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
import androidx.camera.camera2.interop.Camera2CameraInfo;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
@@ -48,7 +50,10 @@
import java.util.ArrayList;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.Executor;
/**
@@ -90,19 +95,23 @@
private final Quirks mCameraQuirks;
@NonNull
private final CamcorderProfileProvider mCamera2CamcorderProfileProvider;
+ @NonNull
+ private final CameraManagerCompat mCameraManager;
/**
* Constructs an instance. Before {@link #linkWithCameraControl(Camera2CameraControlImpl)} is
* called, camera control related API (torch/exposure/zoom) will return default values.
*/
Camera2CameraInfoImpl(@NonNull String cameraId,
- @NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat) {
+ @NonNull CameraManagerCompat cameraManager) throws CameraAccessExceptionCompat {
mCameraId = Preconditions.checkNotNull(cameraId);
- mCameraCharacteristicsCompat = cameraCharacteristicsCompat;
+ mCameraManager = cameraManager;
+
+ mCameraCharacteristicsCompat = cameraManager.getCameraCharacteristicsCompat(mCameraId);
mCamera2CameraInfo = new Camera2CameraInfo(this);
- mCameraQuirks = CameraQuirks.get(cameraId, cameraCharacteristicsCompat);
+ mCameraQuirks = CameraQuirks.get(cameraId, mCameraCharacteristicsCompat);
mCamera2CamcorderProfileProvider = new Camera2CamcorderProfileProvider(cameraId,
- cameraCharacteristicsCompat);
+ mCameraCharacteristicsCompat);
mCameraStateLiveData = new RedirectableLiveData<>(
CameraState.create(CameraState.Type.CLOSED));
}
@@ -399,6 +408,36 @@
}
/**
+ * Returns a map consisting of the camera ids and the {@link CameraCharacteristics}s.
+ *
+ * <p>For every camera, the map contains at least the CameraCharacteristics for the camera id.
+ * If the camera is logical camera, it will also contain associated physical camera ids and
+ * their CameraCharacteristics.
+ *
+ */
+ @NonNull
+ public Map<String, CameraCharacteristics> getCameraCharacteristicsMap() {
+ LinkedHashMap<String, CameraCharacteristics> map = new LinkedHashMap<>();
+
+ map.put(mCameraId, mCameraCharacteristicsCompat.toCameraCharacteristics());
+
+ for (String physicalCameraId : mCameraCharacteristicsCompat.getPhysicalCameraIds()) {
+ if (Objects.equals(physicalCameraId, mCameraId)) {
+ continue;
+ }
+ try {
+ map.put(physicalCameraId,
+ mCameraManager.getCameraCharacteristicsCompat(physicalCameraId)
+ .toCameraCharacteristics());
+ } catch (CameraAccessExceptionCompat e) {
+ Logger.e(TAG,
+ "Failed to get CameraCharacteristics for cameraId " + physicalCameraId, e);
+ }
+ }
+ return map;
+ }
+
+ /**
* A {@link LiveData} which can be redirected to another {@link LiveData}. If no redirection
* is set, initial value will be used.
*/
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsApi28Impl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsApi28Impl.java
new file mode 100644
index 0000000..6e9ed1d
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsApi28Impl.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal.compat;
+
+import android.hardware.camera2.CameraCharacteristics;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.util.Set;
+
+@RequiresApi(28)
+class CameraCharacteristicsApi28Impl extends CameraCharacteristicsBaseImpl{
+
+ CameraCharacteristicsApi28Impl(@NonNull CameraCharacteristics cameraCharacteristics) {
+ super(cameraCharacteristics);
+ }
+
+ @NonNull
+ @Override
+ public Set<String> getPhysicalCameraIds() {
+ return mCameraCharacteristics.getPhysicalCameraIds();
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsBaseImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsBaseImpl.java
new file mode 100644
index 0000000..8fd289e
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsBaseImpl.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal.compat;
+
+import android.hardware.camera2.CameraCharacteristics;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.util.Collections;
+import java.util.Set;
+
+@RequiresApi(21)
+class CameraCharacteristicsBaseImpl
+ implements CameraCharacteristicsCompat.CameraCharacteristicsCompatImpl {
+ @NonNull
+ protected final CameraCharacteristics mCameraCharacteristics;
+ CameraCharacteristicsBaseImpl(@NonNull CameraCharacteristics cameraCharacteristics) {
+ mCameraCharacteristics = cameraCharacteristics;
+ }
+
+ @Nullable
+ @Override
+ public <T> T get(@NonNull CameraCharacteristics.Key<T> key) {
+ return mCameraCharacteristics.get(key);
+ }
+
+ @NonNull
+ @Override
+ public Set<String> getPhysicalCameraIds() {
+ return Collections.emptySet();
+ }
+
+ @NonNull
+ @Override
+ public CameraCharacteristics unwrap() {
+ return mCameraCharacteristics;
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
index efc77115..107c0d6 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
@@ -17,6 +17,7 @@
package androidx.camera.camera2.internal.compat;
import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
@@ -25,6 +26,7 @@
import java.util.HashMap;
import java.util.Map;
+import java.util.Set;
/**
* A wrapper for {@link CameraCharacteristics} which caches the retrieved values to optimize
@@ -35,10 +37,14 @@
@GuardedBy("this")
private final Map<CameraCharacteristics.Key<?>, Object> mValuesCache = new HashMap<>();
@NonNull
- private final CameraCharacteristics mCameraCharacteristics;
+ private final CameraCharacteristicsCompatImpl mCameraCharacteristicsImpl;
private CameraCharacteristicsCompat(@NonNull CameraCharacteristics cameraCharacteristics) {
- mCameraCharacteristics = cameraCharacteristics;
+ if (Build.VERSION.SDK_INT >= 28) {
+ mCameraCharacteristicsImpl = new CameraCharacteristicsApi28Impl(cameraCharacteristics);
+ } else {
+ mCameraCharacteristicsImpl = new CameraCharacteristicsBaseImpl(cameraCharacteristics);
+ }
}
/**
@@ -70,7 +76,7 @@
return value;
}
- value = mCameraCharacteristics.get(key);
+ value = mCameraCharacteristicsImpl.get(key);
if (value != null) {
mValuesCache.put(key, value);
}
@@ -79,10 +85,42 @@
}
/**
+ * Returns the physical camera Ids if it is a logical camera. Otherwise it would
+ * return an empty set.
+ */
+ @NonNull
+ public Set<String> getPhysicalCameraIds() {
+ return mCameraCharacteristicsImpl.getPhysicalCameraIds();
+ }
+
+ /**
* Returns the {@link CameraCharacteristics} represented by this object.
*/
@NonNull
public CameraCharacteristics toCameraCharacteristics() {
- return mCameraCharacteristics;
+ return mCameraCharacteristicsImpl.unwrap();
}
-}
+
+ /**
+ * CameraCharacteristic Implementation Interface
+ */
+ public interface CameraCharacteristicsCompatImpl {
+ /**
+ * Gets the key/values from the CameraCharacteristics .
+ */
+ @Nullable
+ <T> T get(@NonNull CameraCharacteristics.Key<T> key);
+
+ /**
+ * Get physical camera ids.
+ */
+ @NonNull
+ Set<String> getPhysicalCameraIds();
+
+ /**
+ * Returns the underlying {@link CameraCharacteristics} instance.
+ */
+ @NonNull
+ CameraCharacteristics unwrap();
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CameraNoResponseWhenEnablingFlashQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CameraNoResponseWhenEnablingFlashQuirk.java
new file mode 100644
index 0000000..0a5a550
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CameraNoResponseWhenEnablingFlashQuirk.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal.compat.quirk;
+
+import static androidx.camera.core.CameraSelector.LENS_FACING_BACK;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.core.internal.compat.quirk.UseTorchAsFlashQuirk;
+
+import java.util.Locale;
+
+/**
+ * Camera gets stuck when taking pictures with flash ON or AUTO in dark environment.
+ *
+ * <p>See b/193336562 for details.
+ */
+class CameraNoResponseWhenEnablingFlashQuirk implements UseTorchAsFlashQuirk {
+ static boolean load(@NonNull CameraCharacteristicsCompat characteristics) {
+ return "SAMSUNG".equals(Build.BRAND.toUpperCase(Locale.US))
+ // Enables on all Samsung Galaxy Note 5 devices.
+ && Build.MODEL.toUpperCase(Locale.US).startsWith("SM-N920")
+ && characteristics.get(CameraCharacteristics.LENS_FACING) == LENS_FACING_BACK;
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CameraQuirks.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CameraQuirks.java
index c76c712..5e4cf17 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CameraQuirks.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CameraQuirks.java
@@ -59,6 +59,13 @@
if (CamcorderProfileResolutionQuirk.load(cameraCharacteristicsCompat)) {
quirks.add(new CamcorderProfileResolutionQuirk(cameraCharacteristicsCompat));
}
+ if (ImageCaptureWashedOutImageQuirk.load(cameraCharacteristicsCompat)) {
+ quirks.add(new ImageCaptureWashedOutImageQuirk());
+ }
+ if (CameraNoResponseWhenEnablingFlashQuirk.load(cameraCharacteristicsCompat)) {
+ quirks.add(new CameraNoResponseWhenEnablingFlashQuirk());
+ }
+
return new Quirks(quirks);
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/ImageCaptureWashedOutImageQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ImageCaptureWashedOutImageQuirk.java
similarity index 70%
rename from camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/ImageCaptureWashedOutImageQuirk.java
rename to camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ImageCaptureWashedOutImageQuirk.java
index 62fa918..0ca7ff3 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/ImageCaptureWashedOutImageQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ImageCaptureWashedOutImageQuirk.java
@@ -14,11 +14,16 @@
* limitations under the License.
*/
-package androidx.camera.core.internal.compat.quirk;
+package androidx.camera.camera2.internal.compat.quirk;
+import static androidx.camera.core.CameraSelector.LENS_FACING_BACK;
+
+import android.hardware.camera2.CameraCharacteristics;
import android.os.Build;
-import androidx.camera.core.impl.Quirk;
+import androidx.annotation.NonNull;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.core.internal.compat.quirk.UseTorchAsFlashQuirk;
import java.util.Arrays;
import java.util.List;
@@ -29,7 +34,7 @@
*
* <p>See b/176399765 and b/181966663.
*/
-public class ImageCaptureWashedOutImageQuirk implements Quirk {
+public class ImageCaptureWashedOutImageQuirk implements UseTorchAsFlashQuirk {
// List of devices with the issue. See b/181966663.
private static final List<String> DEVICE_MODELS = Arrays.asList(
@@ -54,8 +59,9 @@
"SM-G935P"
);
- static boolean load() {
+ static boolean load(@NonNull CameraCharacteristicsCompat cameraCharacteristics) {
return "SAMSUNG".equals(Build.BRAND.toUpperCase(Locale.US))
- && DEVICE_MODELS.contains(Build.MODEL.toUpperCase(Locale.US));
+ && DEVICE_MODELS.contains(Build.MODEL.toUpperCase(Locale.US))
+ && cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == LENS_FACING_BACK;
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraInfo.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraInfo.java
index eaff42a..80fdc10 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraInfo.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/Camera2CameraInfo.java
@@ -26,12 +26,14 @@
import androidx.camera.core.CameraInfo;
import androidx.core.util.Preconditions;
+import java.util.Map;
+
/**
* An interface for retrieving Camera2-related camera information.
*/
@ExperimentalCamera2Interop
public final class Camera2CameraInfo {
-
+ private static final String TAG = "Camera2CameraInfo";
private final Camera2CameraInfoImpl mCamera2CameraInfoImpl;
/**
@@ -50,8 +52,8 @@
* @param cameraInfo The {@link CameraInfo} to get from.
* @return The camera information with Camera2 implementation.
* @throws IllegalArgumentException if the camera info does not contain the camera2 information
- * (e.g., if CameraX was not initialized with a
- * {@link androidx.camera.camera2.Camera2Config}).
+ * (e.g., if CameraX was not initialized with a
+ * {@link androidx.camera.camera2.Camera2Config}).
*/
@NonNull
public static Camera2CameraInfo from(@NonNull CameraInfo cameraInfo) {
@@ -73,7 +75,6 @@
* one time. When the CameraDevice changes then the camera id will change.
*
* @return the camera ID.
-
* @throws IllegalStateException if the camera info does not contain the camera 2 camera ID
* (e.g., if CameraX was not initialized with a
* {@link androidx.camera.camera2.Camera2Config}).
@@ -90,8 +91,8 @@
* that would be obtained from
* {@link android.hardware.camera2.CameraManager#getCameraCharacteristics(String)}.
*
- * @param <T> The type of the characteristic value.
- * @param key The {@link CameraCharacteristics.Key} of the characteristic.
+ * @param <T> The type of the characteristic value.
+ * @param key The {@link CameraCharacteristics.Key} of the characteristic.
* @return the value of the characteristic.
*/
@Nullable
@@ -111,7 +112,6 @@
* @throws IllegalStateException if the camera info does not contain the camera 2
* characteristics(e.g., if CameraX was not initialized with a
* {@link androidx.camera.camera2.Camera2Config}).
- *
* @hide
*/
// TODO: Hidden until new extensions API released.
@@ -124,4 +124,19 @@
Camera2CameraInfoImpl impl = (Camera2CameraInfoImpl) cameraInfo;
return impl.getCameraCharacteristicsCompat().toCameraCharacteristics();
}
+
+ /**
+ * Returns a map consisting of the camera ids and the {@link CameraCharacteristics}s.
+ *
+ * <p>For every camera, the map contains at least the CameraCharacteristics for the camera id.
+ * If the camera is logical camera, it will also contain associated physical camera ids and
+ * their CameraCharacteristics.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public Map<String, CameraCharacteristics> getCameraCharacteristicsMap() {
+ return mCamera2CameraInfoImpl.getCameraCharacteristicsMap();
+ }
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index b054f4f..106223f 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -25,13 +25,18 @@
import static org.mockito.Mockito.when;
import android.content.Context;
-import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.os.Build;
+import android.util.Pair;
import android.view.Surface;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.CameraManagerCompat;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ExposureState;
@@ -56,13 +61,17 @@
import org.robolectric.shadows.ShadowCameraCharacteristics;
import org.robolectric.shadows.ShadowCameraManager;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
import java.util.concurrent.Executor;
@RunWith(RobolectricTestRunner.class)
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
public class Camera2CameraInfoImplTest {
-
private static final String CAMERA0_ID = "0";
private static final int CAMERA0_SUPPORTED_HARDWARE_LEVEL =
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
@@ -81,6 +90,7 @@
private CameraCharacteristicsCompat mCameraCharacteristics0;
private CameraCharacteristicsCompat mCameraCharacteristics1;
+ private CameraManagerCompat mCameraManagerCompat;
private ZoomControl mMockZoomControl;
private TorchControl mMockTorchControl;
private ExposureControl mExposureControl;
@@ -88,19 +98,13 @@
private Camera2CameraControlImpl mMockCameraControl;
@Before
- public void setUp() throws CameraAccessException {
+ public void setUp() throws CameraAccessExceptionCompat {
initCameras();
- CameraManager cameraManager =
- (CameraManager) ApplicationProvider.getApplicationContext().getSystemService(
- Context.CAMERA_SERVICE);
-
- mCameraCharacteristics0 =
- CameraCharacteristicsCompat.toCameraCharacteristicsCompat(
- cameraManager.getCameraCharacteristics(CAMERA0_ID));
- mCameraCharacteristics1 =
- CameraCharacteristicsCompat.toCameraCharacteristicsCompat(
- cameraManager.getCameraCharacteristics(CAMERA1_ID));
+ mCameraManagerCompat =
+ CameraManagerCompat.from((Context) ApplicationProvider.getApplicationContext());
+ mCameraCharacteristics0 = mCameraManagerCompat.getCameraCharacteristicsCompat(CAMERA0_ID);
+ mCameraCharacteristics1 = mCameraManagerCompat.getCameraCharacteristicsCompat(CAMERA1_ID);
mMockZoomControl = mock(ZoomControl.class);
mMockTorchControl = mock(TorchControl.class);
@@ -115,25 +119,26 @@
}
@Test
- public void canCreateCameraInfo() {
+ public void canCreateCameraInfo() throws CameraAccessExceptionCompat {
CameraInfoInternal cameraInfoInternal =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
assertThat(cameraInfoInternal).isNotNull();
}
@Test
- public void cameraInfo_canReturnSensorOrientation() {
+ public void cameraInfo_canReturnSensorOrientation() throws CameraAccessExceptionCompat {
CameraInfoInternal cameraInfoInternal =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
assertThat(cameraInfoInternal.getSensorRotationDegrees()).isEqualTo(
CAMERA0_SENSOR_ORIENTATION);
}
@Test
- public void cameraInfo_canCalculateCorrectRelativeRotation_forBackCamera() {
+ public void cameraInfo_canCalculateCorrectRelativeRotation_forBackCamera()
+ throws CameraAccessExceptionCompat {
CameraInfoInternal cameraInfoInternal =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
// Note: these numbers depend on the camera being a back-facing camera.
assertThat(cameraInfoInternal.getSensorRotationDegrees(Surface.ROTATION_0))
@@ -147,9 +152,10 @@
}
@Test
- public void cameraInfo_canCalculateCorrectRelativeRotation_forFrontCamera() {
+ public void cameraInfo_canCalculateCorrectRelativeRotation_forFrontCamera()
+ throws CameraAccessExceptionCompat {
CameraInfoInternal cameraInfoInternal =
- new Camera2CameraInfoImpl(CAMERA1_ID, mCameraCharacteristics1);
+ new Camera2CameraInfoImpl(CAMERA1_ID, mCameraManagerCompat);
// Note: these numbers depend on the camera being a front-facing camera.
assertThat(cameraInfoInternal.getSensorRotationDegrees(Surface.ROTATION_0))
@@ -163,47 +169,52 @@
}
@Test
- public void cameraInfo_canReturnLensFacing() {
+ public void cameraInfo_canReturnLensFacing() throws CameraAccessExceptionCompat {
CameraInfoInternal cameraInfoInternal =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
assertThat(cameraInfoInternal.getLensFacing()).isEqualTo(CAMERA0_LENS_FACING_ENUM);
}
@Test
- public void cameraInfo_canReturnHasFlashUnit_forBackCamera() {
+ public void cameraInfo_canReturnHasFlashUnit_forBackCamera()
+ throws CameraAccessExceptionCompat {
CameraInfoInternal cameraInfoInternal =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
assertThat(cameraInfoInternal.hasFlashUnit()).isEqualTo(CAMERA0_FLASH_INFO_BOOLEAN);
}
@Test
- public void cameraInfo_canReturnHasFlashUnit_forFrontCamera() {
+ public void cameraInfo_canReturnHasFlashUnit_forFrontCamera()
+ throws CameraAccessExceptionCompat {
CameraInfoInternal cameraInfoInternal =
- new Camera2CameraInfoImpl(CAMERA1_ID, mCameraCharacteristics1);
+ new Camera2CameraInfoImpl(CAMERA1_ID, mCameraManagerCompat);
assertThat(cameraInfoInternal.hasFlashUnit()).isEqualTo(CAMERA1_FLASH_INFO_BOOLEAN);
}
@Test
- public void cameraInfoWithoutCameraControl_canReturnDefaultTorchState() {
+ public void cameraInfoWithoutCameraControl_canReturnDefaultTorchState()
+ throws CameraAccessExceptionCompat {
Camera2CameraInfoImpl camera2CameraInfoImpl =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
assertThat(camera2CameraInfoImpl.getTorchState().getValue())
.isEqualTo(TorchControl.DEFAULT_TORCH_STATE);
}
@Test
- public void cameraInfoWithCameraControl_canReturnTorchState() {
+ public void cameraInfoWithCameraControl_canReturnTorchState()
+ throws CameraAccessExceptionCompat {
when(mMockTorchControl.getTorchState()).thenReturn(new MutableLiveData<>(TorchState.ON));
Camera2CameraInfoImpl camera2CameraInfoImpl =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
assertThat(camera2CameraInfoImpl.getTorchState().getValue()).isEqualTo(TorchState.ON);
}
@Test
- public void torchStateLiveData_SameInstanceBeforeAndAfterCameraControlLink() {
+ public void torchStateLiveData_SameInstanceBeforeAndAfterCameraControlLink()
+ throws CameraAccessExceptionCompat {
Camera2CameraInfoImpl camera2CameraInfoImpl =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
// Calls getTorchState() to trigger RedirectableLiveData
LiveData<Integer> torchStateLiveData = camera2CameraInfoImpl.getTorchState();
@@ -219,29 +230,32 @@
// zoom related tests just ensure it uses ZoomControl to get the value
// Full tests are performed at ZoomControlDeviceTest / ZoomControlTest.
@Test
- public void cameraInfoWithCameraControl_getZoom_valueIsCorrect() {
+ public void cameraInfoWithCameraControl_getZoom_valueIsCorrect()
+ throws CameraAccessExceptionCompat {
ZoomState zoomState = ImmutableZoomState.create(3.0f, 8.0f, 1.0f, 0.2f);
when(mMockZoomControl.getZoomState()).thenReturn(new MutableLiveData<>(zoomState));
Camera2CameraInfoImpl camera2CameraInfoImpl =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
assertThat(camera2CameraInfoImpl.getZoomState().getValue()).isEqualTo(zoomState);
}
@Test
- public void cameraInfoWithoutCameraControl_getDetaultZoomState() {
+ public void cameraInfoWithoutCameraControl_getDetaultZoomState()
+ throws CameraAccessExceptionCompat {
Camera2CameraInfoImpl camera2CameraInfoImpl =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
assertThat(camera2CameraInfoImpl.getZoomState().getValue())
.isEqualTo(ZoomControl.getDefaultZoomState(mCameraCharacteristics0));
}
@Test
- public void zoomStateLiveData_SameInstanceBeforeAndAfterCameraControlLink() {
+ public void zoomStateLiveData_SameInstanceBeforeAndAfterCameraControlLink()
+ throws CameraAccessExceptionCompat {
Camera2CameraInfoImpl camera2CameraInfoImpl =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
// Calls getZoomState() to trigger RedirectableLiveData
LiveData<ZoomState> zoomStateLiveData = camera2CameraInfoImpl.getZoomState();
@@ -256,21 +270,23 @@
}
@Test
- public void cameraInfoWithCameraControl_canReturnExposureState() {
+ public void cameraInfoWithCameraControl_canReturnExposureState()
+ throws CameraAccessExceptionCompat {
ExposureState exposureState = new ExposureStateImpl(mCameraCharacteristics0, 2);
when(mExposureControl.getExposureState()).thenReturn(exposureState);
Camera2CameraInfoImpl camera2CameraInfoImpl =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
assertThat(camera2CameraInfoImpl.getExposureState()).isEqualTo(exposureState);
}
@Test
- public void cameraInfoWithoutCameraControl_canReturnDefaultExposureState() {
+ public void cameraInfoWithoutCameraControl_canReturnDefaultExposureState()
+ throws CameraAccessExceptionCompat {
Camera2CameraInfoImpl camera2CameraInfoImpl =
- new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
ExposureState defaultState =
ExposureControl.getDefaultExposureState(mCameraCharacteristics0);
@@ -286,25 +302,26 @@
}
@Test
- public void cameraInfo_getImplementationType_legacy() {
- final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID,
- mCameraCharacteristics0);
+ public void cameraInfo_getImplementationType_legacy() throws CameraAccessExceptionCompat {
+ final CameraInfoInternal cameraInfo =
+ new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
assertThat(cameraInfo.getImplementationType()).isEqualTo(
CameraInfo.IMPLEMENTATION_TYPE_CAMERA2_LEGACY);
}
@Test
- public void cameraInfo_getImplementationType_noneLegacy() {
- final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
- mCameraCharacteristics1);
+ public void cameraInfo_getImplementationType_noneLegacy() throws CameraAccessExceptionCompat {
+ final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(
+ CAMERA1_ID, mCameraManagerCompat);
assertThat(cameraInfo.getImplementationType()).isEqualTo(
CameraInfo.IMPLEMENTATION_TYPE_CAMERA2);
}
@Test
- public void addSessionCameraCaptureCallback_isCalledToCameraControl() {
- final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
- mCameraCharacteristics1);
+ public void addSessionCameraCaptureCallback_isCalledToCameraControl()
+ throws CameraAccessExceptionCompat {
+ final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
+ CAMERA1_ID, mCameraManagerCompat);
cameraInfo.linkWithCameraControl(mMockCameraControl);
Executor executor = mock(Executor.class);
@@ -315,9 +332,10 @@
}
@Test
- public void removeSessionCameraCaptureCallback_isCalledToCameraControl() {
- final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
- mCameraCharacteristics1);
+ public void removeSessionCameraCaptureCallback_isCalledToCameraControl()
+ throws CameraAccessExceptionCompat {
+ final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
+ CAMERA1_ID, mCameraManagerCompat);
cameraInfo.linkWithCameraControl(mMockCameraControl);
CameraCaptureCallback callback = mock(CameraCaptureCallback.class);
@@ -327,9 +345,10 @@
}
@Test
- public void addSessionCameraCaptureCallbackWithoutCameraControl_attachedToCameraControlLater() {
- final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
- mCameraCharacteristics1);
+ public void addSessionCameraCaptureCallbackWithoutCameraControl_attachedToCameraControlLater()
+ throws CameraAccessExceptionCompat {
+ final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
+ CAMERA1_ID, mCameraManagerCompat);
Executor executor = mock(Executor.class);
CameraCaptureCallback callback = mock(CameraCaptureCallback.class);
cameraInfo.addSessionCaptureCallback(executor, callback);
@@ -340,9 +359,10 @@
}
@Test
- public void removeSessionCameraCaptureCallbackWithoutCameraControl_callbackIsRemoved() {
- final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
- mCameraCharacteristics1);
+ public void removeSessionCameraCaptureCallbackWithoutCameraControl_callbackIsRemoved()
+ throws CameraAccessExceptionCompat {
+ final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
+ CAMERA1_ID, mCameraManagerCompat);
// Add two callbacks
Executor executor1 = mock(Executor.class);
CameraCaptureCallback callback1 = mock(CameraCaptureCallback.class);
@@ -361,9 +381,10 @@
}
@Test
- public void cameraInfoWithCameraControl_canReturnIsFocusMeteringSupported() {
- final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID,
- mCameraCharacteristics0);
+ public void cameraInfoWithCameraControl_canReturnIsFocusMeteringSupported()
+ throws CameraAccessExceptionCompat {
+ final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
+ CAMERA0_ID, mCameraManagerCompat);
cameraInfo.linkWithCameraControl(mMockCameraControl);
@@ -377,6 +398,54 @@
assertThat(cameraInfo.isFocusMeteringSupported(action)).isTrue();
}
+ @Config(minSdk = 28)
+ @RequiresApi(28)
+ @Test
+ public void canReturnCameraCharacteristicsMapWithPhysicalCameras()
+ throws CameraAccessExceptionCompat {
+ CameraCharacteristics characteristics0 = mock(CameraCharacteristics.class);
+ CameraCharacteristics characteristicsPhysical2 = mock(CameraCharacteristics.class);
+ CameraCharacteristics characteristicsPhysical3 = mock(CameraCharacteristics.class);
+ when(characteristics0.getPhysicalCameraIds())
+ .thenReturn(new HashSet<>(Arrays.asList("0", "2", "3")));
+ CameraManagerCompat cameraManagerCompat = initCameraManagerWithPhysicalIds(
+ Arrays.asList(
+ new Pair<>("0", characteristics0),
+ new Pair<>("2", characteristicsPhysical2),
+ new Pair<>("3", characteristicsPhysical3)));
+ Camera2CameraInfoImpl impl = new Camera2CameraInfoImpl("0", cameraManagerCompat);
+
+ Map<String, CameraCharacteristics> map = impl.getCameraCharacteristicsMap();
+ assertThat(map.size()).isEqualTo(3);
+ assertThat(map.get("0")).isSameInstanceAs(characteristics0);
+ assertThat(map.get("2")).isSameInstanceAs(characteristicsPhysical2);
+ assertThat(map.get("3")).isSameInstanceAs(characteristicsPhysical3);
+ }
+
+ @Config(maxSdk = 27)
+ @Test
+ public void canReturnCameraCharacteristicsMapWithMainCamera()
+ throws CameraAccessExceptionCompat {
+ Camera2CameraInfoImpl impl = new Camera2CameraInfoImpl("0", mCameraManagerCompat);
+ Map<String, CameraCharacteristics> map = impl.getCameraCharacteristicsMap();
+ assertThat(map.size()).isEqualTo(1);
+ assertThat(map.get("0"))
+ .isSameInstanceAs(mCameraCharacteristics0.toCameraCharacteristics());
+ }
+
+ private CameraManagerCompat initCameraManagerWithPhysicalIds(
+ List<Pair<String, CameraCharacteristics>> cameraIdsAndCharacteristicsList) {
+ FakeCameraManagerImpl cameraManagerImpl = new FakeCameraManagerImpl();
+ for (Pair<String, CameraCharacteristics> pair : cameraIdsAndCharacteristicsList) {
+ String cameraId = pair.first;
+ CameraCharacteristics cameraCharacteristics = pair.second;
+ cameraManagerImpl.addCamera(cameraId, cameraCharacteristics);
+ }
+ return CameraManagerCompat.from(cameraManagerImpl);
+ }
+
+
+
private void initCameras() {
// **** Camera 0 characteristics ****//
CameraCharacteristics characteristics0 =
@@ -432,4 +501,49 @@
.getSystemService(Context.CAMERA_SERVICE)))
.addCamera(CAMERA1_ID, characteristics1);
}
+
+ private static class FakeCameraManagerImpl
+ implements CameraManagerCompat.CameraManagerCompatImpl {
+ private final HashMap<String, CameraCharacteristics> mCameraIdCharacteristics =
+ new HashMap<>();
+
+ public void addCamera(@NonNull String cameraId,
+ @NonNull CameraCharacteristics cameraCharacteristics) {
+ mCameraIdCharacteristics.put(cameraId, cameraCharacteristics);
+ }
+ @NonNull
+ @Override
+ public String[] getCameraIdList() throws CameraAccessExceptionCompat {
+ return mCameraIdCharacteristics.keySet().toArray(new String[0]);
+ }
+
+ @Override
+ public void registerAvailabilityCallback(@NonNull Executor executor,
+ @NonNull CameraManager.AvailabilityCallback callback) {
+ }
+
+ @Override
+ public void unregisterAvailabilityCallback(
+ @NonNull CameraManager.AvailabilityCallback callback) {
+ }
+
+ @NonNull
+ @Override
+ public CameraCharacteristics getCameraCharacteristics(@NonNull String cameraId)
+ throws CameraAccessExceptionCompat {
+ return mCameraIdCharacteristics.get(cameraId);
+ }
+
+ @Override
+ public void openCamera(@NonNull String cameraId, @NonNull Executor executor,
+ @NonNull CameraDevice.StateCallback callback) throws CameraAccessExceptionCompat {
+
+ }
+
+ @NonNull
+ @Override
+ public CameraManager getCameraManager() {
+ return null;
+ }
+ }
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
index 61a23c0..b039939f7 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
@@ -569,6 +569,8 @@
((ShadowCameraManager) Shadow.extract(cameraManager))
.addCamera(cameraId, characteristics);
+ CameraManagerCompat cameraManagerCompat =
+ CameraManagerCompat.from((Context) ApplicationProvider.getApplicationContext());
StreamConfigurationMap mockMap = mock(StreamConfigurationMap.class);
when(mockMap.getOutputSizes(anyInt())).thenReturn(mSupportedSizes);
// ImageFormat.PRIVATE was supported since API level 23. Before that, the supported
@@ -579,7 +581,7 @@
@CameraSelector.LensFacing int lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(
lensFacing);
mCameraFactory.insertCamera(lensFacingEnum, cameraId, () -> new FakeCamera(cameraId, null,
- new Camera2CameraInfoImpl(cameraId, getCameraCharacteristicsCompat(cameraId))));
+ new Camera2CameraInfoImpl(cameraId, cameraManagerCompat)));
}
private void initCameraX() {
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
index c830dc8..5c8beda 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
@@ -25,7 +25,6 @@
import android.content.Context;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
-import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
@@ -36,7 +35,6 @@
import androidx.annotation.NonNull;
import androidx.camera.camera2.Camera2Config;
-import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
import androidx.camera.camera2.internal.compat.workaround.ExcludedSupportedSizesContainer;
import androidx.camera.core.CameraUnavailableException;
@@ -106,7 +104,7 @@
};
private final Context mContext = ApplicationProvider.getApplicationContext();
- private final CameraManagerCompat mCameraManager = CameraManagerCompat.from(mContext);
+ private final CameraManagerCompat mCameraManagerCompat = CameraManagerCompat.from(mContext);
@Before
public void setUp() throws IllegalAccessException {
@@ -144,7 +142,7 @@
setupCamera();
final SupportedSurfaceCombination supportedSurfaceCombination =
- new SupportedSurfaceCombination(mContext, BACK_CAMERA_ID, mCameraManager,
+ new SupportedSurfaceCombination(mContext, BACK_CAMERA_ID, mCameraManagerCompat,
mMockCamcorderProfileHelper);
List<Size> excludedSizes = Arrays.asList(
@@ -183,8 +181,8 @@
shadowCharacteristics.set(
CameraCharacteristics.SENSOR_ORIENTATION, DEFAULT_SENSOR_ORIENTATION);
- CameraManager cameraManager = (CameraManager) ApplicationProvider.getApplicationContext()
- .getSystemService(Context.CAMERA_SERVICE);
+ CameraManager cameraManager =
+ (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
((ShadowCameraManager) Shadow.extract(cameraManager)).addCamera(BACK_CAMERA_ID,
characteristics);
@@ -202,8 +200,7 @@
final FakeCameraFactory cameraFactory = new FakeCameraFactory();
cameraFactory.insertCamera(lensFacingEnum, BACK_CAMERA_ID,
() -> new FakeCamera(BACK_CAMERA_ID, null,
- new Camera2CameraInfoImpl(BACK_CAMERA_ID,
- getCameraCharacteristicsCompat(BACK_CAMERA_ID))));
+ new Camera2CameraInfoImpl(BACK_CAMERA_ID, mCameraManagerCompat)));
initCameraX(cameraFactory);
}
@@ -227,14 +224,4 @@
.build();
CameraX.initialize(mContext, cameraXConfig);
}
-
- private CameraCharacteristicsCompat getCameraCharacteristicsCompat(String cameraId)
- throws CameraAccessException {
- CameraManager cameraManager =
- (CameraManager) ApplicationProvider.getApplicationContext().getSystemService(
- Context.CAMERA_SERVICE);
-
- return CameraCharacteristicsCompat.toCameraCharacteristicsCompat(
- cameraManager.getCameraCharacteristics(cameraId));
- }
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
index bda1bc4..e4d4710 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
@@ -2222,8 +2222,7 @@
mCameraManagerCompat = CameraManagerCompat.from(mContext);
mCameraFactory.insertCamera(lensFacingEnum, cameraId, () -> new FakeCamera(cameraId, null,
- new Camera2CameraInfoImpl(cameraId,
- mCameraManagerCompat.getCameraCharacteristicsCompat(cameraId))));
+ new Camera2CameraInfoImpl(cameraId, mCameraManagerCompat)));
initCameraX();
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompatTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompatTest.java
index 80bad45..1fbe51f 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompatTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompatTest.java
@@ -18,9 +18,14 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
import android.hardware.camera2.CameraCharacteristics;
import android.os.Build;
+import androidx.annotation.RequiresApi;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -85,4 +90,24 @@
assertThat(characteristicsCompat.get(CameraCharacteristics.SENSOR_ORIENTATION))
.isNull();
}
+
+ @Config(minSdk = 28)
+ @RequiresApi(28)
+ @Test
+ public void getPhysicalCameraIds_invokeCameraCharacteristics_api28() {
+ CameraCharacteristics cameraCharacteristics = mock(CameraCharacteristics.class);
+ CameraCharacteristicsCompat characteristicsCompat =
+ CameraCharacteristicsCompat.toCameraCharacteristicsCompat(cameraCharacteristics);
+
+ characteristicsCompat.getPhysicalCameraIds();
+ verify(cameraCharacteristics).getPhysicalCameraIds();
+ }
+
+ @Config(maxSdk = 27)
+ @Test
+ public void getPhysicalCameraIds_returnEmptyList_below28() {
+ CameraCharacteristicsCompat characteristicsCompat =
+ CameraCharacteristicsCompat.toCameraCharacteristicsCompat(mCharacteristics);
+ assertThat(characteristicsCompat.getPhysicalCameraIds()).isEmpty();
+ }
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/interop/Camera2CameraInfoTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/interop/Camera2CameraInfoTest.java
index d6883c0..992be21 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/interop/Camera2CameraInfoTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/interop/Camera2CameraInfoTest.java
@@ -25,6 +25,7 @@
import android.os.Build;
import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
import androidx.camera.camera2.internal.Camera2CameraInfoImpl;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.core.impl.CameraInfoInternal;
@@ -35,6 +36,9 @@
import org.robolectric.annotation.Config;
import org.robolectric.annotation.internal.DoNotInstrument;
+import java.util.HashMap;
+import java.util.Map;
+
@RunWith(RobolectricTestRunner.class)
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
@@ -85,4 +89,18 @@
CameraInfoInternal wrongCameraInfo = mock(CameraInfoInternal.class);
Camera2CameraInfo.from(wrongCameraInfo);
}
+
+ @Config(minSdk = 28)
+ @RequiresApi(28)
+ @Test
+ public void canGetCameraCharacteristicsMap_fromCamera2CameraInfo() {
+ Camera2CameraInfoImpl impl = mock(Camera2CameraInfoImpl.class);
+ Map<String, CameraCharacteristics> characteristicsMap = new HashMap<>();
+ when(impl.getCameraCharacteristicsMap()).thenReturn(characteristicsMap);
+ Camera2CameraInfo camera2CameraInfo = new Camera2CameraInfo(impl);
+
+ Map<String, CameraCharacteristics> map = camera2CameraInfo.getCameraCharacteristicsMap();
+ assertThat(map).isSameInstanceAs(characteristicsMap);
+ }
+
}
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingImageReaderDeviceTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingImageReaderDeviceTest.kt
index e915887d..7d1c8fd 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingImageReaderDeviceTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingImageReaderDeviceTest.kt
@@ -83,12 +83,14 @@
@Test
fun processesImage_whenImageInBundleEnqueued() = runBlocking {
- val processingImageReader = ProcessingImageReader(
- 640, 480,
- ImageFormat.YUV_420_888, 2,
- CameraXExecutors.mainThreadExecutor(), mCaptureBundle,
+ val processingImageReader = ProcessingImageReader.Builder(
+ 640,
+ 480,
+ ImageFormat.YUV_420_888,
+ 2,
+ mCaptureBundle,
mProcessor
- )
+ ).build()
val job = async {
suspendCoroutine<ImageProxy?> { cont ->
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 71ae5c9..4ad53be 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -113,9 +113,8 @@
import androidx.camera.core.internal.IoConfig;
import androidx.camera.core.internal.TargetConfig;
import androidx.camera.core.internal.YuvToJpegProcessor;
-import androidx.camera.core.internal.compat.quirk.DeviceQuirks;
-import androidx.camera.core.internal.compat.quirk.ImageCaptureWashedOutImageQuirk;
import androidx.camera.core.internal.compat.quirk.SoftwareJpegEncodingPreferredQuirk;
+import androidx.camera.core.internal.compat.quirk.UseTorchAsFlashQuirk;
import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability;
import androidx.camera.core.internal.utils.ImageUtil;
import androidx.concurrent.futures.CallbackToFutureAdapter;
@@ -312,7 +311,7 @@
* <p>When the flag is set, torch will be opened and closed to replace the flash fired by flash
* mode.
*/
- private final boolean mUseTorchFlash;
+ private boolean mUseTorchFlash = false;
////////////////////////////////////////////////////////////////////////////////////////////
// [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
@@ -362,11 +361,6 @@
} else {
mEnableCheck3AConverged = false; // skip 3A convergence in MIN_LATENCY mode
}
-
- mUseTorchFlash = DeviceQuirks.get(ImageCaptureWashedOutImageQuirk.class) != null;
- if (mUseTorchFlash) {
- Logger.d(TAG, "Open and close torch to replace the flash fired by flash mode.");
- }
}
@UiThread
@@ -417,16 +411,12 @@
}
// TODO: To allow user to use an Executor for the image processing.
- mProcessingImageReader =
- new ProcessingImageReader(
- resolution.getWidth(),
- resolution.getHeight(),
- inputFormat,
- mMaxCaptureStages,
- /* postProcessExecutor */mExecutor,
- getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle()),
- captureProcessor,
- outputFormat);
+ mProcessingImageReader = new ProcessingImageReader.Builder(resolution.getWidth(),
+ resolution.getHeight(), inputFormat, mMaxCaptureStages,
+ getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle()),
+ captureProcessor).setPostProcessExecutor(mExecutor).setOutputFormat(
+ outputFormat).build();
+
mMetadataMatchingCaptureCallback = mProcessingImageReader.getCameraCaptureCallback();
mImageReader = new SafeCloseImageReaderProxy(mProcessingImageReader);
if (softwareJpegProcessor != null) {
@@ -1348,6 +1338,14 @@
// enforceSoftwareJpegConstraints() hasn't removed the request.
mUseSoftwareJpeg = useCaseConfig.isSoftwareJpegEncoderRequested();
+ CameraInternal camera = getCamera();
+ Preconditions.checkNotNull(camera, "Attached camera cannot be null");
+ mUseTorchFlash = camera.getCameraInfoInternal().getCameraQuirks()
+ .contains(UseTorchAsFlashQuirk.class);
+ if (mUseTorchFlash) {
+ Logger.d(TAG, "Open and close torch to replace the flash fired by flash mode.");
+ }
+
mExecutor =
Executors.newFixedThreadPool(
1,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingImageReader.java b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingImageReader.java
index 25e687d..3afb75c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingImageReader.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingImageReader.java
@@ -40,6 +40,7 @@
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
/**
* An {@link ImageReaderProxy} which takes one or more {@link android.media.Image}, processes it,
@@ -53,6 +54,12 @@
*/
class ProcessingImageReader implements ImageReaderProxy {
private static final String TAG = "ProcessingImageReader";
+
+ // Exif metadata are restricted in size to 64 kB in JPEG images because according to
+ // the specification this information must be contained within a single JPEG APP1
+ // segment. (See: https://en.wikipedia.org/wiki/Exif)
+ private static final int EXIF_MAX_SIZE_BYTES = 64000;
+
final Object mLock = new Object();
// Callback when Image is ready from InputImageReader.
@@ -169,75 +176,41 @@
private final List<Integer> mCaptureIdList = new ArrayList<>();
- ProcessingImageReader(int width, int height, int format, int maxImages,
- @NonNull Executor postProcessExecutor,
- @NonNull CaptureBundle captureBundle, @NonNull CaptureProcessor captureProcessor) {
- this(width, height, format, maxImages, postProcessExecutor, captureBundle,
- captureProcessor, format);
- }
-
- /**
- * Create a {@link ProcessingImageReader} with specific configurations.
- *
- * @param width Width of the ImageReader
- * @param height Height of the ImageReader
- * @param inputFormat Input image format
- * @param maxImages Maximum Image number the ImageReader can hold. The capacity should
- * be greater than the captureBundle size in order to hold all the
- * Images needed with this processing.
- * @param postProcessExecutor The Executor to execute the post-process of the image result.
- * @param captureBundle The {@link CaptureBundle} includes the processing information
- * @param captureProcessor The {@link CaptureProcessor} to be invoked when the Images are
- * ready
- * @param outputFormat Output image format
- */
- ProcessingImageReader(int width, int height, int inputFormat, int maxImages,
- @NonNull Executor postProcessExecutor,
- @NonNull CaptureBundle captureBundle, @NonNull CaptureProcessor captureProcessor,
- int outputFormat) {
- this(new MetadataImageReader(width, height, inputFormat, maxImages), postProcessExecutor,
- captureBundle, captureProcessor, outputFormat);
- }
-
- ProcessingImageReader(@NonNull MetadataImageReader imageReader,
- @NonNull Executor postProcExecutor,
- @NonNull CaptureBundle capBundle,
- @NonNull CaptureProcessor capProcessor) {
- this(imageReader, postProcExecutor, capBundle, capProcessor, imageReader.getImageFormat());
- }
-
- ProcessingImageReader(@NonNull MetadataImageReader imageReader,
- @NonNull Executor postProcessExecutor,
- @NonNull CaptureBundle captureBundle,
- @NonNull CaptureProcessor captureProcessor,
- int outputFormat) {
- if (imageReader.getMaxImages() < captureBundle.getCaptureStages().size()) {
+ ProcessingImageReader(@NonNull Builder builder) {
+ if (builder.mInputImageReader.getMaxImages()
+ < builder.mCaptureBundle.getCaptureStages().size()) {
throw new IllegalArgumentException(
"MetadataImageReader is smaller than CaptureBundle.");
}
- mInputImageReader = imageReader;
+ mInputImageReader = builder.mInputImageReader;
// For JPEG ImageReaders, the Surface that is created will have format BLOB which can
// only be allocated with a height of 1. The output Image from the image reader will read
// its dimensions from the JPEG data's EXIF in order to set the final dimensions.
- int outputWidth = imageReader.getWidth();
- int outputHeight = imageReader.getHeight();
- if (outputFormat == ImageFormat.JPEG) {
- outputWidth = imageReader.getWidth() * imageReader.getHeight();
+ int outputWidth = mInputImageReader.getWidth();
+ int outputHeight = mInputImageReader.getHeight();
+
+ if (builder.mOutputFormat == ImageFormat.JPEG) {
+ // The output JPEG compression quality is 100 when taking a picture in MAX_QUALITY
+ // mode. It might cause the compressed data size exceeds image's width * height.
+ // YUV_420_888 should be 1.5 times of image's width * height. The compressed data
+ // size shouldn't exceed it. Therefore, scales the output image reader byte buffer to
+ // 1.5 times when the JPEG compression quality setting is 100.
+ outputWidth = (int) (outputWidth * outputHeight * 1.5f) + EXIF_MAX_SIZE_BYTES;
outputHeight = 1;
}
mOutputImageReader = new AndroidImageReaderProxy(
- ImageReader.newInstance(outputWidth, outputHeight, outputFormat,
- imageReader.getMaxImages()));
+ ImageReader.newInstance(outputWidth, outputHeight, builder.mOutputFormat,
+ mInputImageReader.getMaxImages()));
- mPostProcessExecutor = postProcessExecutor;
- mCaptureProcessor = captureProcessor;
- mCaptureProcessor.onOutputSurface(mOutputImageReader.getSurface(), outputFormat);
+ mPostProcessExecutor = builder.mPostProcessExecutor;
+ mCaptureProcessor = builder.mCaptureProcessor;
+ mCaptureProcessor.onOutputSurface(mOutputImageReader.getSurface(), builder.mOutputFormat);
mCaptureProcessor.onResolutionUpdate(
new Size(mInputImageReader.getWidth(), mInputImageReader.getHeight()));
- setCaptureBundle(captureBundle);
+ setCaptureBundle(builder.mCaptureBundle);
}
@Override
@@ -450,4 +423,81 @@
}
}
}
+
+ /**
+ * The builder to create a {@link ProcessingImageReader} object.
+ */
+ static final class Builder {
+ @NonNull
+ protected final MetadataImageReader mInputImageReader;
+ @NonNull
+ protected final CaptureBundle mCaptureBundle;
+ @NonNull
+ protected final CaptureProcessor mCaptureProcessor;
+
+ protected int mOutputFormat;
+
+ @NonNull
+ protected Executor mPostProcessExecutor = Executors.newSingleThreadExecutor();
+
+ /**
+ * Create a {@link Builder} with specific configurations.
+ *
+ * @param imageReader The input image reader.
+ * @param captureBundle The {@link CaptureBundle} includes the processing information
+ * @param captureProcessor The {@link CaptureProcessor} to be invoked when the Images are
+ * ready
+ */
+ Builder(@NonNull MetadataImageReader imageReader, @NonNull CaptureBundle captureBundle,
+ @NonNull CaptureProcessor captureProcessor) {
+ mInputImageReader = imageReader;
+ mCaptureBundle = captureBundle;
+ mCaptureProcessor = captureProcessor;
+ mOutputFormat = imageReader.getImageFormat();
+ }
+
+ /**
+ * Create a {@link Builder} with specific configurations.
+ *
+ * @param width Width of the ImageReader
+ * @param height Height of the ImageReader
+ * @param inputFormat Input image format
+ * @param maxImages Maximum Image number the ImageReader can hold. The capacity
+ * should be greater than the captureBundle size in order to hold
+ * all the Images needed with this processing.
+ * @param captureBundle The {@link CaptureBundle} includes the processing information
+ * @param captureProcessor The {@link CaptureProcessor} to be invoked when the Images are
+ * ready
+ */
+ Builder(int width, int height, int inputFormat, int maxImages,
+ @NonNull CaptureBundle captureBundle, @NonNull CaptureProcessor captureProcessor) {
+ this(new MetadataImageReader(width, height, inputFormat, maxImages), captureBundle,
+ captureProcessor);
+ }
+
+ /**
+ * Sets an Executor to execute the post-process of the image result.
+ */
+ @NonNull
+ Builder setPostProcessExecutor(@NonNull Executor postProcessExecutor) {
+ mPostProcessExecutor = postProcessExecutor;
+ return this;
+ }
+
+ /**
+ * Sets the output image format.
+ */
+ @NonNull
+ Builder setOutputFormat(int outputFormat) {
+ mOutputFormat = outputFormat;
+ return this;
+ }
+
+ /**
+ * Builds an {@link ProcessingImageReader} from current configurations.
+ */
+ ProcessingImageReader build() {
+ return new ProcessingImageReader(this);
+ }
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/YuvToJpegProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/YuvToJpegProcessor.java
index 40c4d50..5cdf970 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/YuvToJpegProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/YuvToJpegProcessor.java
@@ -45,7 +45,6 @@
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.List;
-import java.util.concurrent.ExecutionException;
/**
* A CaptureProcessor which produces JPEGs from input YUV images.
@@ -151,10 +150,15 @@
// Enqueue the completed jpeg image
imageWriter.queueInputImage(jpegImage);
jpegImage = null;
- } catch (InterruptedException | ExecutionException e) {
+ } catch (Exception e) {
+ // InterruptedException, ExecutionException and EOFException might be caught here.
+ //
// InterruptedException should not be possible here since
// imageProxyListenableFuture.isDone() returned true, but we have to handle the
// exception case so bundle it with ExecutionException.
+ //
+ // EOFException might happen if the compressed JPEG data size exceeds the byte buffer
+ // size of the output image reader.
if (processing) {
Logger.e(TAG, "Failed to process YUV -> JPEG", e);
// Something went wrong attempting to retrieve ImageProxy. Enqueue an invalid buffer
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
index 1dc5105..b43c532 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
@@ -42,10 +42,6 @@
quirks.add(new ImageCaptureRotationOptionQuirk());
}
- if (ImageCaptureWashedOutImageQuirk.load()) {
- quirks.add(new ImageCaptureWashedOutImageQuirk());
- }
-
return quirks;
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/UseTorchAsFlashQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/UseTorchAsFlashQuirk.java
new file mode 100644
index 0000000..b8facd4
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/UseTorchAsFlashQuirk.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat.quirk;
+
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * A quirk interface which denotes CameraX should use torch for flash when flashMode is
+ * ON or AUTO.
+ *
+ * <p>Subclasses of this interface can denote the reason why torch is required instead of AE
+ * pre-capture.
+ */
+public interface UseTorchAsFlashQuirk extends Quirk {
+}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ProcessingImageReaderTest.java b/camera/camera-core/src/test/java/androidx/camera/core/ProcessingImageReaderTest.java
index d1dd263..09d9d63 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ProcessingImageReaderTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ProcessingImageReaderTest.java
@@ -130,9 +130,9 @@
throws InterruptedException, TimeoutException, ExecutionException {
// Sets the callback from ProcessingImageReader to start processing
CaptureProcessor captureProcessor = mock(CaptureProcessor.class);
- ProcessingImageReader processingImageReader = new ProcessingImageReader(
- mMetadataImageReader, sPausedExecutor, mCaptureBundle,
- captureProcessor);
+ ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(
+ mMetadataImageReader, mCaptureBundle, captureProcessor).setPostProcessExecutor(
+ sPausedExecutor).build();
processingImageReader.setOnImageAvailableListener(mock(
ImageReaderProxy.OnImageAvailableListener.class),
CameraXExecutors.mainThreadExecutor());
@@ -193,9 +193,8 @@
throws InterruptedException {
// Sets the callback from ProcessingImageReader to start processing
WaitingCaptureProcessor waitingCaptureProcessor = new WaitingCaptureProcessor();
- ProcessingImageReader processingImageReader = new ProcessingImageReader(
- mMetadataImageReader, android.os.AsyncTask.THREAD_POOL_EXECUTOR, mCaptureBundle,
- waitingCaptureProcessor);
+ ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(
+ mMetadataImageReader, mCaptureBundle, waitingCaptureProcessor).build();
processingImageReader.setOnImageAvailableListener(mock(
ImageReaderProxy.OnImageAvailableListener.class),
CameraXExecutors.mainThreadExecutor());
@@ -234,9 +233,9 @@
@Test
public void closeImageHalfway() throws InterruptedException {
// Sets the callback from ProcessingImageReader to start processing
- ProcessingImageReader processingImageReader = new ProcessingImageReader(
- mMetadataImageReader, sPausedExecutor, mCaptureBundle,
- NOOP_PROCESSOR);
+ ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(
+ mMetadataImageReader, mCaptureBundle, NOOP_PROCESSOR).setPostProcessExecutor(
+ sPausedExecutor).build();
processingImageReader.setOnImageAvailableListener(mock(
ImageReaderProxy.OnImageAvailableListener.class),
CameraXExecutors.mainThreadExecutor());
@@ -263,17 +262,15 @@
MetadataImageReader metadataImageReader = new MetadataImageReader(imageReaderProxy);
// Expects to throw exception when creating ProcessingImageReader.
- new ProcessingImageReader(metadataImageReader, android.os.AsyncTask.THREAD_POOL_EXECUTOR,
- mCaptureBundle,
- NOOP_PROCESSOR);
+ new ProcessingImageReader.Builder(metadataImageReader, mCaptureBundle,
+ NOOP_PROCESSOR).build();
}
@Test(expected = IllegalArgumentException.class)
public void captureStageExceedMaxCaptureStage_setCaptureBundleThrowsException() {
// Creates a ProcessingImageReader with maximum Image number.
- ProcessingImageReader processingImageReader = new ProcessingImageReader(100, 100,
- ImageFormat.YUV_420_888, 2, android.os.AsyncTask.THREAD_POOL_EXECUTOR,
- mCaptureBundle, mock(CaptureProcessor.class));
+ ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(100, 100,
+ ImageFormat.YUV_420_888, 2, mCaptureBundle, mock(CaptureProcessor.class)).build();
// Expects to throw exception when invoke the setCaptureBundle method with a
// CaptureBundle size greater than maximum image number.
@@ -284,9 +281,9 @@
@Test
public void imageReaderFormatIsOutputFormat() {
// Creates a ProcessingImageReader with input format YUV_420_888 and output JPEG
- ProcessingImageReader processingImageReader = new ProcessingImageReader(100, 100,
- ImageFormat.YUV_420_888, 2, android.os.AsyncTask.THREAD_POOL_EXECUTOR,
- mCaptureBundle, mock(CaptureProcessor.class), ImageFormat.JPEG);
+ ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(100, 100,
+ ImageFormat.YUV_420_888, 2, mCaptureBundle,
+ mock(CaptureProcessor.class)).setOutputFormat(ImageFormat.JPEG).build();
assertThat(processingImageReader.getImageFormat()).isEqualTo(ImageFormat.JPEG);
}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
index 190550f..7d12101 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
@@ -72,6 +72,8 @@
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
+private const val FINALIZE_TIMEOUT = 5000L
+
@LargeTest
@RunWith(AndroidJUnit4::class)
class RecorderTest {
@@ -187,7 +189,7 @@
activeRecording.stop()
- inOrder.verify(videoRecordEventListener, timeout(1000L))
+ inOrder.verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
.accept(any(VideoRecordEvent.Finalize::class.java))
checkFileHasAudioAndVideo(Uri.fromFile(file))
@@ -234,7 +236,7 @@
activeRecording.stop()
// Wait for the recording to complete.
- assertThat(finalizeSemaphore.tryAcquire(1000L, TimeUnit.MILLISECONDS)).isTrue()
+ assertThat(finalizeSemaphore.tryAcquire(FINALIZE_TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
assertThat(uri).isNotEqualTo(Uri.EMPTY)
@@ -273,7 +275,7 @@
activeRecording.stop()
- inOrder.verify(videoRecordEventListener, timeout(1000L))
+ inOrder.verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
.accept(any(VideoRecordEvent.Finalize::class.java))
checkFileHasAudioAndVideo(Uri.fromFile(file))
@@ -336,7 +338,7 @@
activeRecording.stop()
// Wait for the recording to be finalized.
- inOrder.verify(videoRecordEventListener, timeout(1000L))
+ inOrder.verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
.accept(any(VideoRecordEvent.Finalize::class.java))
checkFileHasAudioAndVideo(Uri.fromFile(file))
@@ -387,7 +389,7 @@
// Stop
activeRecording.stop()
- inOrder.verify(videoRecordEventListener, timeout(1000L))
+ inOrder.verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
.accept(any(VideoRecordEvent.Finalize::class.java))
val captor = ArgumentCaptor.forClass(VideoRecordEvent::class.java)
@@ -519,7 +521,7 @@
activeRecording.stop()
// Wait for the recording to be finalized.
- inOrder.verify(videoRecordEventListener, timeout(1000L))
+ inOrder.verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
.accept(any(VideoRecordEvent.Finalize::class.java))
file.delete()
}
@@ -554,7 +556,7 @@
activeRecording.stop()
// Wait for the recording to be finalized.
- inOrder.verify(videoRecordEventListener, timeout(1000L))
+ inOrder.verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
.accept(any(VideoRecordEvent.Finalize::class.java))
file.delete()
}
@@ -577,7 +579,7 @@
invokeSurfaceRequest()
- verify(videoRecordEventListener, timeout(1000L))
+ verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
.accept(any(VideoRecordEvent.Finalize::class.java))
file.delete()
@@ -605,7 +607,38 @@
.accept(any(VideoRecordEvent.Status::class.java))
}
+ inOrder.verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
+ .accept(any(VideoRecordEvent.Finalize::class.java))
+
+ file.delete()
+ }
+
+ @Test
+ fun stop_WhenUseCaseDetached() {
+ clearInvocations(videoRecordEventListener)
+ invokeSurfaceRequest()
+ val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ val outputOptions = FileOutputOptions.builder().setFile(file).build()
+
+ val pendingRecording = recorder.prepareRecording(outputOptions)
+ pendingRecording.withEventListener(
+ CameraXExecutors.directExecutor(),
+ videoRecordEventListener
+ ).withAudioEnabled()
+
+ pendingRecording.start()
+
+ val inOrder = inOrder(videoRecordEventListener)
inOrder.verify(videoRecordEventListener, timeout(1000L))
+ .accept(any(VideoRecordEvent.Start::class.java))
+ inOrder.verify(videoRecordEventListener, timeout(15000L).atLeast(5))
+ .accept(any(VideoRecordEvent.Status::class.java))
+
+ instrumentation.runOnMainSync {
+ cameraUseCaseAdapter.removeUseCases(listOf(preview))
+ }
+
+ inOrder.verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
.accept(any(VideoRecordEvent.Finalize::class.java))
file.delete()
@@ -661,7 +694,7 @@
activeRecording.stop()
- verify(videoRecordEventListener, timeout(1000L))
+ verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
.accept(any(VideoRecordEvent.Finalize::class.java))
checkFileAudio(Uri.fromFile(file), false)
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureRotationTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureRotationTest.kt
deleted file mode 100644
index 6aed56b..0000000
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureRotationTest.kt
+++ /dev/null
@@ -1,200 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.camera.video
-
-import android.Manifest
-import android.content.ContentResolver
-import android.content.Context
-import android.media.MediaMetadataRetriever
-import android.media.MediaRecorder
-import android.net.Uri
-import android.os.Build
-import android.view.Surface
-import androidx.camera.camera2.Camera2Config
-import androidx.camera.core.CameraSelector
-import androidx.camera.core.CameraX
-import androidx.camera.core.VideoCapture
-import androidx.camera.core.impl.utils.CameraOrientationUtil
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.internal.CameraUseCaseAdapter
-import androidx.camera.testing.AudioUtil
-import androidx.camera.testing.CameraUtil
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.filters.LargeTest
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.rule.GrantPermissionRule
-import com.google.common.truth.Truth.assertThat
-import com.nhaarman.mockitokotlin2.any
-import org.junit.After
-import org.junit.Assume
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.mockito.ArgumentCaptor
-import org.mockito.Mockito
-import java.io.File
-import java.io.IOException
-import java.util.ArrayList
-import java.util.concurrent.ExecutionException
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.TimeoutException
-
-@LargeTest
-@RunWith(Parameterized::class)
-class VideoCaptureRotationTest(
- private var cameraSelector: CameraSelector,
- private var targetRotation: Int
-) {
- companion object {
- @JvmStatic
- @Parameterized.Parameters
- fun data(): Collection<Array<Any>> {
- val result: MutableList<Array<Any>> = ArrayList()
- result.add(arrayOf(CameraSelector.DEFAULT_BACK_CAMERA, Surface.ROTATION_90))
- result.add(arrayOf(CameraSelector.DEFAULT_BACK_CAMERA, Surface.ROTATION_180))
- result.add(arrayOf(CameraSelector.DEFAULT_FRONT_CAMERA, Surface.ROTATION_90))
- result.add(arrayOf(CameraSelector.DEFAULT_FRONT_CAMERA, Surface.ROTATION_180))
- return result
- }
- }
-
- private val instrumentation = InstrumentationRegistry.getInstrumentation()
-
- @get:Rule
- val cameraRule = CameraUtil.grantCameraPermissionAndPreTest()
-
- @get:Rule
- val runtimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
- Manifest.permission.WRITE_EXTERNAL_STORAGE,
- Manifest.permission.RECORD_AUDIO
- )
-
- private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
- private lateinit var context: Context
- private lateinit var contentResolver: ContentResolver
- private lateinit var videoUseCase: VideoCapture
- private lateinit var callback: VideoCapture.OnVideoSavedCallback
- private lateinit var outputFileResultsArgumentCaptor:
- ArgumentCaptor<VideoCapture.OutputFileResults>
-
- @Before
- fun setUp() {
- // TODO(b/168175357): Fix VideoCaptureTest problems on CuttleFish API 29
- Assume.assumeFalse(
- "Cuttlefish has MediaCodec dequeueInput/Output buffer fails issue. Unable to test.",
- Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
- )
-
- // TODO(b/168187087): Video: Unable to record Video on Pixel 1 API 26,27 when only
- // VideoCapture is bound
- Assume.assumeFalse(
- "Pixel running API 26 has CameraDevice.onError when set repeating request",
- Build.DEVICE == "sailfish" &&
- (Build.VERSION.SDK_INT == 26 || Build.VERSION.SDK_INT == 27)
- )
- Assume.assumeTrue(AudioUtil.canStartAudioRecord(MediaRecorder.AudioSource.CAMCORDER))
-
- context = ApplicationProvider.getApplicationContext()
- CameraX.initialize(context, Camera2Config.defaultConfig())
- Assume.assumeTrue(
- CameraUtil.hasCameraWithLensFacing(
- cameraSelector.lensFacing!!
- )
- )
- cameraUseCaseAdapter = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector)
- contentResolver = context.contentResolver
-
- callback = Mockito.mock(
- VideoCapture.OnVideoSavedCallback::class.java
- )
- outputFileResultsArgumentCaptor = ArgumentCaptor.forClass(
- VideoCapture.OutputFileResults::class.java
- )
- }
-
- @After
- @Throws(InterruptedException::class, ExecutionException::class, TimeoutException::class)
- fun tearDown() {
- instrumentation.runOnMainSync {
- if (this::cameraUseCaseAdapter.isInitialized) {
- cameraUseCaseAdapter.removeUseCases(cameraUseCaseAdapter.useCases)
- }
- }
- CameraX.shutdown()[10000, TimeUnit.MILLISECONDS]
- }
-
- @Test
- @Throws(IOException::class)
- fun metadataGetCorrectRotation_afterVideoCaptureRecording() {
- // Sets the device rotation.
- videoUseCase = VideoCapture.Builder()
- .setTargetRotation(targetRotation)
- .build()
- val savedFile = File.createTempFile("CameraX", ".tmp")
- savedFile.deleteOnExit()
-
- instrumentation.runOnMainSync {
- try {
- cameraUseCaseAdapter.addUseCases(setOf(videoUseCase))
- } catch (e: CameraUseCaseAdapter.CameraException) {
- e.printStackTrace()
- }
- }
-
- // Start recording
- videoUseCase.startRecording(
- VideoCapture.OutputFileOptions.Builder(savedFile).build(),
- CameraXExecutors.mainThreadExecutor(), callback
- )
- // The way to control recording length might not be applicable in the new VideoCapture.
- try {
- Thread.sleep(3000)
- } catch (e: InterruptedException) {
- e.printStackTrace()
- }
-
- // Assert.
- // Checks the target rotation is correct when the use case is bound.
- videoUseCase.stopRecording()
-
- Mockito.verify(callback, Mockito.timeout(2000)).onVideoSaved(any())
-
- val targetRotationDegree = CameraOrientationUtil.surfaceRotationToDegrees(targetRotation)
- val videoRotation: Int
- val mediaRetriever = MediaMetadataRetriever()
-
- mediaRetriever.apply {
- setDataSource(context, Uri.fromFile(savedFile))
- videoRotation = extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION
- )?.toInt()!!
- }
-
- val sensorRotation = CameraUtil.getSensorOrientation(cameraSelector.lensFacing!!)
- // Whether the camera lens and display are facing opposite directions.
- val isOpposite = cameraSelector.lensFacing == CameraSelector.LENS_FACING_BACK
- val relativeRotation = CameraOrientationUtil.getRelativeImageRotation(
- targetRotationDegree,
- sensorRotation!!,
- isOpposite
- )
-
- // Checks the rotation from video file's metadata is matched with the relative rotation.
- assertThat(videoRotation).isEqualTo(relativeRotation)
- }
-}
\ No newline at end of file
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
new file mode 100644
index 0000000..c482c6d
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -0,0 +1,305 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video
+
+import android.content.Context
+import android.graphics.SurfaceTexture
+import android.media.MediaMetadataRetriever
+import android.net.Uri
+import android.os.Build
+import android.util.Log
+import android.util.Size
+import android.view.Surface
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.core.Preview
+import androidx.camera.core.impl.utils.CameraOrientationUtil
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.SurfaceTextureProvider
+import androidx.core.util.Consumer
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.io.File
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+@LargeTest
+@RunWith(Parameterized::class)
+class VideoRecordingTest(
+ private var cameraSelector: CameraSelector
+) {
+
+ @get:Rule
+ val cameraRule = CameraUtil.grantCameraPermissionAndPreTest()
+
+ companion object {
+ private const val VIDEO_TIMEOUT = 10_000L
+ private const val TAG = "VideoRecordingTest"
+ @JvmStatic
+ @Parameterized.Parameters
+ fun data(): Collection<Array<Any>> {
+ return listOf(
+ arrayOf(CameraSelector.DEFAULT_BACK_CAMERA),
+ arrayOf(CameraSelector.DEFAULT_FRONT_CAMERA),
+ )
+ }
+ }
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val context: Context = ApplicationProvider.getApplicationContext()
+ private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
+ private lateinit var preview: Preview
+ private lateinit var cameraInfo: CameraInfo
+
+ private lateinit var latchForVideoSaved: CountDownLatch
+ private lateinit var latchForVideoRecording: CountDownLatch
+
+ private lateinit var finalize: VideoRecordEvent.Finalize
+
+ private val videoRecordEventListener = Consumer<VideoRecordEvent> {
+ when (it.eventType) {
+ VideoRecordEvent.EVENT_TYPE_START -> {
+ // Recording start.
+ Log.d(TAG, "Recording start")
+ }
+ VideoRecordEvent.EVENT_TYPE_FINALIZE -> {
+ // Recording stop.
+ Log.d(TAG, "Recording finalize")
+ finalize = it as VideoRecordEvent.Finalize
+ latchForVideoSaved.countDown()
+ }
+ VideoRecordEvent.EVENT_TYPE_STATUS -> {
+ // Make sure the recording proceed for a while.
+ latchForVideoRecording.countDown()
+ }
+ VideoRecordEvent.EVENT_TYPE_PAUSE, VideoRecordEvent.EVENT_TYPE_RESUME -> {
+ // no op for this test, skip these event now.
+ }
+ else -> {
+ throw IllegalStateException()
+ }
+ }
+ }
+
+ @Before
+ fun setUp() {
+ Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
+ // Skip for b/168175357
+ Assume.assumeFalse(
+ "Cuttlefish has MediaCodec dequeueInput/Output buffer fails issue. Unable to test.",
+ Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
+ )
+
+ CameraX.initialize(context, Camera2Config.defaultConfig()).get()
+ cameraUseCaseAdapter = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector)
+ cameraInfo = cameraUseCaseAdapter.cameraInfo
+
+ // Add extra Preview to provide an additional surface for b/168187087.
+ preview = Preview.Builder().build()
+ // Sets surface provider to preview
+ instrumentation.runOnMainSync {
+ preview.setSurfaceProvider(
+ getSurfaceProvider()
+ )
+ }
+ }
+
+ @After
+ fun tearDown() {
+ if (this::cameraUseCaseAdapter.isInitialized) {
+ instrumentation.runOnMainSync {
+ cameraUseCaseAdapter.apply {
+ removeUseCases(useCases)
+ }
+ }
+ }
+ CameraX.shutdown().get(10, TimeUnit.SECONDS)
+ }
+
+ @Test
+ fun getMetadataRotation_when_setTargetRotation() {
+ // Arrange.
+ val videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
+ // Just set one Surface.ROTATION_90 to verify the function work or not.
+ val targetRotation = Surface.ROTATION_90
+ videoCapture.targetRotation = targetRotation
+
+ val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ latchForVideoSaved = CountDownLatch(1)
+ latchForVideoRecording = CountDownLatch(5)
+
+ instrumentation.runOnMainSync {
+ cameraUseCaseAdapter.addUseCases(listOf(preview, videoCapture))
+ }
+
+ // Act.
+ completeVideoRecording(videoCapture, file)
+
+ // Verify.
+ verifyMetadataRotation(targetRotation, file)
+ file.delete()
+ }
+
+ // TODO: Add other metadata info check, e.g. location, after Recorder add more metadata.
+
+ @Test
+ fun getCorrectResolution_when_setSupportedQuality() {
+ Assume.assumeTrue(QualitySelector.getSupportedQualities(cameraInfo).isNotEmpty())
+
+ val qualityList = QualitySelector.getSupportedQualities(cameraInfo)
+ instrumentation.runOnMainSync {
+ cameraUseCaseAdapter.addUseCases(listOf(preview))
+ }
+
+ Log.d(TAG, "CameraSelector: ${cameraSelector.lensFacing}, QualityList: $qualityList ")
+ qualityList.forEach loop@{ quality ->
+ val targetResolution = QualitySelector.getResolution(cameraInfo, quality)
+ if (targetResolution == null) {
+ // If targetResolution is null, try next one
+ Log.e(TAG, "Unable to get resolution for the quality: $quality")
+ return@loop
+ }
+
+ val recorder = Recorder.Builder()
+ .setQualitySelector(QualitySelector.of(quality)).build()
+
+ val videoCapture = VideoCapture.withOutput(recorder)
+ val file = File.createTempFile("video_$targetResolution", ".tmp")
+ .apply { deleteOnExit() }
+
+ latchForVideoSaved = CountDownLatch(1)
+ latchForVideoRecording = CountDownLatch(5)
+
+ instrumentation.runOnMainSync {
+ cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
+ }
+
+ // Act.
+ completeVideoRecording(videoCapture, file)
+
+ // Verify.
+ verifyVideoResolution(targetResolution, file)
+
+ // Cleanup.
+ file.delete()
+ instrumentation.runOnMainSync {
+ cameraUseCaseAdapter.apply {
+ removeUseCases(listOf(videoCapture))
+ }
+ }
+ }
+ }
+
+ private fun completeVideoRecording(videoCapture: VideoCapture<Recorder>, file: File) {
+ val outputOptions = FileOutputOptions.builder().setFile(file).build()
+
+ val activeRecording = videoCapture.output
+ .prepareRecording(outputOptions)
+ .withEventListener(
+ CameraXExecutors.directExecutor(),
+ videoRecordEventListener
+ )
+ .start()
+
+ // Wait for status event to proceed recording for a while.
+ assertThat(latchForVideoRecording.await(VIDEO_TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+
+ activeRecording.stop()
+ // Wait for finalize event to saved file.
+ assertThat(latchForVideoSaved.await(VIDEO_TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+
+ // Check if any error after recording finalized
+ assertWithMessage(TAG + "Finalize with error: ${finalize.error}, ${finalize.cause}.")
+ .that(finalize.hasError()).isFalse()
+ }
+
+ private fun verifyMetadataRotation(targetRotation: Int, file: File) {
+ // Whether the camera lens and display are facing opposite directions.
+ val isOpposite = cameraSelector.lensFacing == CameraSelector.LENS_FACING_BACK
+ val relativeRotation = CameraOrientationUtil.getRelativeImageRotation(
+ CameraOrientationUtil.surfaceRotationToDegrees(targetRotation),
+ CameraUtil.getSensorOrientation(cameraSelector.lensFacing!!)!!,
+ isOpposite
+ )
+ val videoRotation = getRotationInMetadata(Uri.fromFile(file))
+
+ // Checks the rotation from video file's metadata is matched with the relative rotation.
+ assertWithMessage(
+ TAG + ", $targetRotation rotation test failure:" +
+ ", videoRotation: $videoRotation" +
+ ", relativeRotation: $relativeRotation"
+ ).that(videoRotation).isEqualTo(relativeRotation)
+ }
+
+ private fun verifyVideoResolution(targetResolution: Size, file: File) {
+ val mediaRetriever = MediaMetadataRetriever()
+ lateinit var resolution: Size
+ mediaRetriever.apply {
+ setDataSource(context, Uri.fromFile(file))
+ val height = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)!!
+ .toInt()
+ val width = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)!!
+ .toInt()
+ resolution = Size(width, height)
+ }
+
+ // Compare with the resolution of video and the targetResolution in QualitySelector
+ assertWithMessage(
+ TAG + ", verifyVideoResolution failure:" +
+ ", videoResolution: $resolution" +
+ ", targetResolution: $targetResolution"
+ ).that(resolution).isEqualTo(targetResolution)
+ }
+
+ private fun getRotationInMetadata(uri: Uri): Int {
+ val mediaRetriever = MediaMetadataRetriever()
+ return mediaRetriever.let {
+ it.setDataSource(context, uri)
+ it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toInt()!!
+ }
+ }
+
+ private fun getSurfaceProvider(): Preview.SurfaceProvider {
+ return SurfaceTextureProvider.createSurfaceTextureProvider(
+ object : SurfaceTextureProvider.SurfaceTextureCallback {
+ override fun onSurfaceTextureReady(
+ surfaceTexture: SurfaceTexture,
+ resolution: Size
+ ) {
+ // No-op
+ }
+ override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
+ surfaceTexture.release()
+ }
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/AudioSpec.java b/camera/camera-video/src/main/java/androidx/camera/video/AudioSpec.java
index 703d831..95d2746 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/AudioSpec.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/AudioSpec.java
@@ -23,6 +23,7 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
import com.google.auto.value.AutoValue;
@@ -31,7 +32,9 @@
/**
* Audio specification that is options to config audio source and encoding.
+ * @hide
*/
+@RestrictTo(Scope.LIBRARY)
@AutoValue
public abstract class AudioSpec {
@@ -158,7 +161,11 @@
@NonNull
public abstract Builder toBuilder();
- /** The builder of the {@link AudioSpec}. */
+ /**
+ * The builder of the {@link AudioSpec}.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY)
@SuppressWarnings("StaticFinalBuilder")
@AutoValue.Builder
public abstract static class Builder {
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/MediaSpec.java b/camera/camera-video/src/main/java/androidx/camera/video/MediaSpec.java
index f27d8c6..66a78ee 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/MediaSpec.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/MediaSpec.java
@@ -22,6 +22,7 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
import androidx.core.util.Consumer;
import com.google.auto.value.AutoValue;
@@ -32,7 +33,9 @@
/**
* MediaSpec communicates the encoding type and encoder-specific options for both the
* video and audio inputs to the VideoOutput.
+ * @hide
*/
+@RestrictTo(Scope.LIBRARY)
@AutoValue
public abstract class MediaSpec {
@@ -120,7 +123,9 @@
/**
* The builder for {@link MediaSpec}.
+ * @hide
*/
+ @RestrictTo(Scope.LIBRARY)
@SuppressWarnings("StaticFinalBuilder")
@AutoValue.Builder
public abstract static class Builder {
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/QualitySelector.java b/camera/camera-video/src/main/java/androidx/camera/video/QualitySelector.java
index c570ce8..2f7ad9a 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/QualitySelector.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/QualitySelector.java
@@ -39,21 +39,20 @@
/**
* QualitySelector defines the desired quality setting.
*
- * <p>There are several defined quality constants such as {@link #QUALITY_SD},
- * {@link #QUALITY_HD}, {@link #QUALITY_FHD} and {@link #QUALITY_FHD}, but not all of them
- * are supported on every device since each device has its own capabilities.
+ * <p>There are pre-defined quality constants that are universally used for video, such as
+ * {@link #QUALITY_SD}, {@link #QUALITY_HD}, {@link #QUALITY_FHD} and {@link #QUALITY_UHD}, but
+ * not all of them are supported on every device since each device has its own capabilities.
* {@link #isQualitySupported(CameraInfo, int)} can be used to check whether a quality is
* supported on the device or not and {@link #getResolution(CameraInfo, int)} can be used to get
- * the actual resolution defined in the device. However, checking qualities one by one is not
- * so inconvenient for the quality setting. QualitySelector is designed to facilitate the quality
- * setting. The typical usage is
+ * the actual resolution defined in the device. Aside from checking the qualities one by one,
+ * QualitySelector provides a more convenient way to select a quality. The typical usage of
+ * selecting a single desired quality is:
* <pre>
* <code>
* QualitySelector qualitySelector = QualitySelector.of(QualitySelector.QUALITY_FHD)
* </code>
* </pre>
- * if there is only one desired quality, or a series of quality constants can be set by desired
- * order
+ * Or the usage of selecting a series of qualities by desired order:
* <pre>
* <code>
* QualitySelector qualitySelector = QualitySelector
@@ -62,12 +61,12 @@
* .finallyTry(QualitySelector.QUALITY_SHD)
* </code>
* </pre>
- * A recommended way to set the {@link Procedure#finallyTry(int)} is giving guaranteed supported
+ * The recommended way to set the {@link Procedure#finallyTry(int)} is giving guaranteed supported
* qualities such as {@link #QUALITY_LOWEST} and {@link #QUALITY_HIGHEST}, which ensures the
- * QualitySelector can always choose a supported quality. Another way to ensure a quality is
+ * QualitySelector can always choose a supported quality. Another way to ensure a quality will be
* selected when none of the desired qualities are supported is to use
* {@link Procedure#finallyTry(int, int)} with an open-ended fallback strategy such as
- * {@link #FALLBACK_STRATEGY_LOWER}.
+ * {@link #FALLBACK_STRATEGY_LOWER}:
* <pre>
* <code>
* QualitySelector qualitySelector = QualitySelector
@@ -75,30 +74,26 @@
* .finallyTry(QualitySelector.QUALITY_FHD, FALLBACK_STRATEGY_LOWER)
* </code>
* </pre>
- * If QUALITY_UHD and QUALITY_FHD are not supported on the device, the next lower supported
- * quality than QUALITY_FHD will be attempted. If no lower quality is supported, the next higher
- * supported quality will be selected. {@link #select(CameraInfo)} can obtain the final result
- * quality based on the desired qualities and fallback strategy, {@link #QUALITY_NONE} will be
- * returned if all desired qualities are not supported and fallback strategy also cannot find a
- * supported one.
+ * If QUALITY_UHD and QUALITY_FHD are not supported on the device, QualitySelector will select
+ * the quality that is closest to and lower than QUALITY_FHD. If no lower quality is supported,
+ * the quality that is closest to and higher than QUALITY_FHD will be selected.
*/
public class QualitySelector {
private static final String TAG = "QualitySelector";
/**
- * Indicates no quality.
+ * A non-applicable quality.
*
* <p>Check QUALITY_NONE via {@link #isQualitySupported(CameraInfo, int)} will return
- * {@code false}. {@link #select(CameraInfo)} will return QUALITY_NONE if all desired
- * qualities are not supported and fallback strategy is not able to find a supported one.
+ * {@code false}.
*/
public static final int QUALITY_NONE = -1;
/**
- * Choose the lowest video quality supported by the video frame producer.
+ * The lowest video quality supported by the video frame producer.
*/
public static final int QUALITY_LOWEST = CamcorderProfile.QUALITY_LOW;
/**
- * Choose the highest video quality supported by the video frame producer.
+ * The highest video quality supported by the video frame producer.
*/
public static final int QUALITY_HIGHEST = CamcorderProfile.QUALITY_HIGH;
/**
@@ -144,38 +139,31 @@
QUALITY_FHD, QUALITY_HD, QUALITY_SD);
/**
- * No fallback strategy.
- *
- * <p>When using this fallback strategy, if {@link #select(CameraInfo)} fails to find a
- * supported quality, it will return {@link #QUALITY_NONE}.
+ * The strategy that no fallback strategy will be applied.
*/
public static final int FALLBACK_STRATEGY_NONE = 0;
/**
- * Choose a higher quality if the desired quality isn't supported. Choose a lower quality if
- * no higher quality is supported.
+ * Choose the quality that is closest to and higher than the desired quality. If that can not
+ * result in a supported quality, choose the quality that is closest to and lower than the
+ * desired quality.
*/
public static final int FALLBACK_STRATEGY_HIGHER = 1;
/**
- * Choose a higher quality if the desired quality isn't supported.
- *
- * <p>When a higher quality can't be found, {@link #select(CameraInfo)} will return
- * {@link #QUALITY_NONE}.
+ * Choose the quality that is closest to and higher than the desired quality.
*/
public static final int FALLBACK_STRATEGY_STRICTLY_HIGHER = 2;
/**
- * Choose a lower quality if the desired quality isn't supported. Choose a higher quality if
- * no lower quality is supported.
+ * Choose the quality that is closest to and lower than the desired quality. If that can not
+ * result in a supported quality, choose the quality that is closest to and higher than the
+ * desired quality.
*/
public static final int FALLBACK_STRATEGY_LOWER = 3;
/**
- * Choose a lower quality if the desired quality isn't supported.
- *
- * <p>When a lower quality can't be found, {@link #select(CameraInfo)} will return
- * {@link #QUALITY_NONE}.
+ * Choose the quality that is closest to and lower than the desired quality.
*/
public static final int FALLBACK_STRATEGY_STRICTLY_LOWER = 4;
@@ -199,7 +187,7 @@
}
/**
- * Check if the input quality is one of video quality constants.
+ * Checks if the input quality is one of video quality constants.
*
* @hide
*/
@@ -209,7 +197,7 @@
}
/**
- * Get all video quality constants with clearly defined size sorted from large to small.
+ * Gets all video quality constants with clearly defined size sorted from largest to smallest.
*
* <p>{@link #QUALITY_NONE}, {@link #QUALITY_HIGHEST} and {@link #QUALITY_LOWEST} are not
* included.
@@ -225,7 +213,7 @@
/**
* Gets all supported qualities on the device.
*
- * <p>The returned list is sorted by quality size from large to small. For the qualities in
+ * <p>The returned list is sorted by quality size from largest to smallest. For the qualities in
* the returned list, with the same input cameraInfo,
* {@link #isQualitySupported(CameraInfo, int)} will return {@code true} and
* {@link #getResolution(CameraInfo, int)} will return the corresponding resolution.
@@ -243,13 +231,20 @@
/**
* Checks if the quality is supported.
*
- * <p>For the qualities in the list of {@link #getSupportedQualities}, calling this method with
- * these qualities will return {@code true}.
+ * <p>Calling this method with one of the qualities contained in the returned list of
+ * {@link #getSupportedQualities} will return {@code true}.
*
- * @param cameraInfo the cameraInfo
- * @param quality one of the quality constants. Possible values include
- * {@link #QUALITY_LOWEST}, {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD},
- * {@link #QUALITY_FHD}, or {@link #QUALITY_UHD}.
+ * <p>Possible values for {@code quality} include {@link #QUALITY_LOWEST},
+ * {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD}, {@link #QUALITY_FHD},
+ * {@link #QUALITY_UHD} and {@link #QUALITY_NONE}.
+ *
+ * <p>If this method is called with {@link #QUALITY_LOWEST} or {@link #QUALITY_HIGHEST}, it
+ * will return {@code true} except the case that none of the qualities can be supported.
+ *
+ * <p>If this method is called with {@link #QUALITY_NONE}, it will always return {@code false}.
+ *
+ * @param cameraInfo the cameraInfo for checking the quality.
+ * @param quality one of the quality constants.
* @return {@code true} if the quality is supported; {@code false} otherwise.
* @see #getSupportedQualities(CameraInfo)
*/
@@ -261,11 +256,14 @@
/**
* Gets the corresponding resolution from the input quality.
*
- * @param cameraInfo the cameraInfo
- * @param quality one of the quality constants. Possible values include
- * {@link QualitySelector#QUALITY_LOWEST}, {@link QualitySelector#QUALITY_HIGHEST},
- * {@link QualitySelector#QUALITY_SD}, {@link QualitySelector#QUALITY_HD},
- * {@link QualitySelector#QUALITY_FHD}, or {@link QualitySelector#QUALITY_UHD}.
+ * <p>Possible values for {@code quality} include {@link #QUALITY_LOWEST},
+ * {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD}, {@link #QUALITY_FHD},
+ * {@link #QUALITY_UHD} and {@link #QUALITY_NONE}.
+ *
+ * <p>If this method is called with {@link #QUALITY_NONE}, it will always return {@code null}.
+ *
+ * @param cameraInfo the cameraInfo for checking the quality.
+ * @param quality one of the quality constants.
* @return the corresponding resolution from the input quality, or {@code null} if the
* quality is not supported on the device. {@link #isQualitySupported(CameraInfo, int)} can
* be used to check if the input quality is supported.
@@ -296,13 +294,17 @@
}
/**
- * Sets the first desired quality.
+ * Sets the desired quality with the highest priority.
+ *
+ * <p>This method initiates a procedure for specifying the requirements of selecting
+ * qualities. Other requirements can be further added with {@link Procedure} methods.
*
* @param quality the quality constant. Possible values include {@link #QUALITY_LOWEST},
* {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD}, {@link #QUALITY_FHD},
* or {@link #QUALITY_UHD}.
- * @return the procedure that can continue to be set
- * @throws IllegalArgumentException if not a quality constant.
+ * @return the {@link Procedure} for specifying quality selection requirements.
+ * @throws IllegalArgumentException if the given quality is not a quality constant.
+ * @see Procedure
*/
@NonNull
public static Procedure firstTry(@VideoQuality int quality) {
@@ -318,7 +320,7 @@
* {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD}, {@link #QUALITY_FHD},
* or {@link #QUALITY_UHD}.
* @return the QualitySelector instance.
- * @throws IllegalArgumentException if not a quality constant.
+ * @throws IllegalArgumentException if the given quality is not a quality constant.
*/
@NonNull
public static QualitySelector of(@VideoQuality int quality) {
@@ -349,24 +351,28 @@
}
/**
- * Find a quality match to the desired quality settings.
+ * Finds a quality that matches the desired quality settings.
*
- * <p>The method bases on the desired qualities and the fallback strategy to find out a
- * supported quality on this device. The desired qualities can be set by a series of try
- * methods such as {@link #firstTry(int)}, {@link #of(int)},
- * {@link Procedure#thenTry(int)} and {@link Procedure#finallyTry(int)}. The fallback strategy
- * can be set via {@link #of(int, int)} and {@link Procedure#finallyTry(int, int)}. If no
- * fallback strategy is specified, {@link #FALLBACK_STRATEGY_NONE} will be applied by default.
+ * <p>The method bases on the desired qualities and the fallback strategy to find a supported
+ * quality on this device. The desired qualities can be set by a series of try methods such
+ * as {@link #firstTry(int)}, {@link #of(int)}, {@link Procedure#thenTry(int)} and
+ * {@link Procedure#finallyTry(int)}. The fallback strategy can be set via
+ * {@link #of(int, int)} and {@link Procedure#finallyTry(int, int)}. If no fallback strategy
+ * is specified, {@link #FALLBACK_STRATEGY_NONE} will be applied by default.
*
* <p>The search algorithm first checks which desired quality is supported according to the
* set sequence. If no desired quality is supported, the fallback strategy will be applied to
* the quality set with it. If there is still no quality can be found, {@link #QUALITY_NONE}
* will be returned.
*
- * @param cameraInfo the cameraInfo
+ * @param cameraInfo the cameraInfo for checking the quality.
* @return the first supported quality of the desired qualities, or a supported quality
* searched by fallback strategy, or {@link #QUALITY_NONE} when no quality is found.
+ * @see Procedure
+ *
+ * @hide
*/
+ @RestrictTo(Scope.LIBRARY)
@VideoQuality
public int select(@NonNull CameraInfo cameraInfo) {
VideoCapabilities videoCapabilities = VideoCapabilities.from(cameraInfo);
@@ -474,7 +480,7 @@
}
/**
- * The procedure can continue to set the desired quality and fallback strategy.
+ * The procedure can be used to set desired qualities and fallback strategy.
*/
public static class Procedure {
private final List<Integer> mPreferredQualityList = new ArrayList<>();
@@ -484,13 +490,13 @@
}
/**
- * Sets the next desired quality.
+ * Adds a quality candidate.
*
* @param quality the quality constant. Possible values include {@link #QUALITY_LOWEST},
* {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD},
* {@link #QUALITY_FHD} or {@link #QUALITY_UHD}.
* @return the procedure that can continue to be set
- * @throws IllegalArgumentException if not a quality constant
+ * @throws IllegalArgumentException if the given quality is not a quality constant
*/
@NonNull
public Procedure thenTry(@VideoQuality int quality) {
@@ -503,11 +509,14 @@
*
* <p>The returned QualitySelector will adopt {@link #FALLBACK_STRATEGY_NONE}.
*
+ * <p>This method finishes the setting procedure and generates a {@link QualitySelector}
+ * with the requirements set to the procedure.
+ *
* @param quality the quality constant. Possible values include {@link #QUALITY_LOWEST},
* {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD},
* {@link #QUALITY_FHD} or {@link #QUALITY_UHD}.
- * @return the QualitySelector.
- * @throws IllegalArgumentException if not a quality constant
+ * @return the {@link QualitySelector}.
+ * @throws IllegalArgumentException if the given quality is not a quality constant
*/
@NonNull
public QualitySelector finallyTry(@VideoQuality int quality) {
@@ -520,6 +529,9 @@
* <p>The fallback strategy will be applied on this quality when all desired qualities are
* not supported.
*
+ * <p>This method finishes the setting procedure and generates a {@link QualitySelector}
+ * with the requirements set to the procedure.
+ *
* @param quality the quality constant. Possible values include {@link #QUALITY_LOWEST},
* {@link #QUALITY_HIGHEST}, {@link #QUALITY_SD}, {@link #QUALITY_HD},
* {@link #QUALITY_FHD} or {@link #QUALITY_UHD}.
@@ -527,7 +539,7 @@
* {@link #FALLBACK_STRATEGY_NONE}, {@link #FALLBACK_STRATEGY_HIGHER},
* {@link #FALLBACK_STRATEGY_STRICTLY_HIGHER}, {@link #FALLBACK_STRATEGY_LOWER} and
* {@link #FALLBACK_STRATEGY_STRICTLY_LOWER}.
- * @return the QualitySelector.
+ * @return the {@link QualitySelector}.
* @throws IllegalArgumentException if {@code quality} is not a quality constant or
* {@code fallbackStrategy} is not a fallback strategy constant.
*/
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
index caaa794..869bcd9b 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
@@ -16,6 +16,11 @@
package androidx.camera.video;
+import static androidx.camera.video.QualitySelector.FALLBACK_STRATEGY_HIGHER;
+import static androidx.camera.video.QualitySelector.QUALITY_FHD;
+import static androidx.camera.video.QualitySelector.QUALITY_HD;
+import static androidx.camera.video.QualitySelector.QUALITY_SD;
+
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ContentValues;
@@ -142,14 +147,9 @@
*/
PAUSED,
/**
- * There's a running recording and the Recorder is being released.
+ * There's a running recording and the Recorder is being reset.
*/
- RELEASING,
- /**
- * The Recorder has been released and any operation attempt will throw an
- * {@link IllegalStateException}.
- */
- RELEASED,
+ RESETING,
/**
* The Recorder encountered errors and any operation will attempt will throw an
* {@link IllegalStateException}. Users can handle the error by monitoring
@@ -185,6 +185,24 @@
ENCODER_ERROR
}
+ /**
+ * Default quality selector for recordings.
+ *
+ * <p>The default quality selector chooses a video quality suitable for recordings based on
+ * device and compatibility constraints. It is equivalent to:
+ * <pre>{@code
+ * QualitySelector.firstTry(QUALITY_FHD)
+ * .thenTry(QUALITY_HD)
+ * .thenTry(QUALITY_SD)
+ * .finallyTry(QUALITY_FHD, FALLBACK_STRATEGY_HIGHER);
+ * }</pre>
+ */
+ public static final QualitySelector DEFAULT_QUALITY_SELECTOR =
+ QualitySelector.firstTry(QUALITY_FHD)
+ .thenTry(QUALITY_HD)
+ .thenTry(QUALITY_SD)
+ .finallyTry(QUALITY_FHD, FALLBACK_STRATEGY_HIGHER);
+
private static final AudioSpec AUDIO_SPEC_DEFAULT =
AudioSpec.builder()
.setSourceFormat(
@@ -196,6 +214,7 @@
.build();
private static final VideoSpec VIDEO_SPEC_DEFAULT =
VideoSpec.builder()
+ .setQualitySelector(DEFAULT_QUALITY_SELECTOR)
.setAspectRatio(VideoSpec.ASPECT_RATIO_16_9)
.build();
private static final MediaSpec MEDIA_SPEC_DEFAULT =
@@ -285,9 +304,11 @@
@SuppressLint("MissingPermission")
@Override
- public void onSurfaceRequested(@NonNull SurfaceRequest surfaceRequest) {
+ public void onSurfaceRequested(@NonNull SurfaceRequest request) {
synchronized (mLock) {
switch (getObservableData(mState)) {
+ case RESETING:
+ // Fall-through
case PENDING_RECORDING:
// Fall-through
case PENDING_PAUSED:
@@ -295,7 +316,7 @@
case INITIALIZING:
// The recorder should be initialized only once until it is released.
if (mSurfaceRequested.compareAndSet(false, true)) {
- mSequentialExecutor.execute(() -> initializeInternal(surfaceRequest));
+ mSequentialExecutor.execute(() -> initializeInternal(request));
}
break;
case IDLING:
@@ -306,16 +327,12 @@
// Fall-through
case ERROR:
throw new IllegalStateException("The Recorder has been initialized.");
- case RELEASING:
- // Fall-through
- case RELEASED:
- surfaceRequest.willNotProvideSurface();
- Logger.w(TAG, "A surface is requested while the Recorder is released.");
- break;
}
}
}
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
@NonNull
public Observable<MediaSpec> getMediaSpec() {
@@ -401,30 +418,7 @@
@NonNull
private PendingRecording prepareRecordingInternal(@NonNull OutputOptions options) {
Preconditions.checkNotNull(options, "The OutputOptions cannot be null.");
- synchronized (mLock) {
- switch (getObservableData(mState)) {
- case INITIALIZING:
- // Fall-through
- case PENDING_RECORDING:
- // Fall-through
- case PENDING_PAUSED:
- // Fall-through
- case ERROR:
- // Fall-through, create PendingRecording as usual, but it will be instantly
- // finalized at start().
- case IDLING:
- // Fall-through
- case PAUSED:
- // Fall-through
- case RECORDING:
- break;
- case RELEASING:
- // Fall-through
- case RELEASED:
- throw new IllegalStateException("The Recorder has been released.");
- }
- return new PendingRecording(this, options);
- }
+ return new PendingRecording(this, options);
}
/**
@@ -432,7 +426,7 @@
*
* @return the {@link QualitySelector} provided to
* {@link Builder#setQualitySelector(QualitySelector)} on the builder used to create this
- * recorder, or the default value of {@link VideoSpec#QUALITY_SELECTOR_AUTO} if no quality
+ * recorder, or the default value of {@link Recorder#DEFAULT_QUALITY_SELECTOR} if no quality
* selector was provided.
*/
@NonNull
@@ -492,6 +486,8 @@
ActiveRecording activeRecording = ActiveRecording.from(pendingRecording);
mRunningRecording = activeRecording;
switch (getObservableData(mState)) {
+ case RESETING:
+ // Fall-through
case INITIALIZING:
// The recording will automatically start once the initialization completes.
setState(State.PENDING_RECORDING);
@@ -508,10 +504,6 @@
// Fall-through
case RECORDING:
throw new IllegalStateException("There's an active recording.");
- case RELEASING:
- // Fall-through
- case RELEASED:
- throw new IllegalStateException("The Recorder has been released.");
case ERROR:
finalizeRecording(VideoRecordEvent.ERROR_RECORDER_ERROR, mErrorCause);
break;
@@ -526,6 +518,8 @@
switch (getObservableData(mState)) {
case PENDING_RECORDING:
// Fall-through
+ case RESETING:
+ // Fall-through
case INITIALIZING:
// The recording will automatically pause once the initialization completes.
setState(State.PENDING_PAUSED);
@@ -541,10 +535,6 @@
case PAUSED:
// No-op when the recording is already paused.
break;
- case RELEASING:
- // Fall-through
- case RELEASED:
- throw new IllegalStateException("The Recorder has been released.");
case ERROR:
finalizeRecording(VideoRecordEvent.ERROR_RECORDER_ERROR, mErrorCause);
break;
@@ -557,6 +547,8 @@
switch (getObservableData(mState)) {
case PENDING_PAUSED:
// Fall-through
+ case RESETING:
+ // Fall-through
case INITIALIZING:
// The recording will automatically start once the initialization completes.
setState(State.PENDING_RECORDING);
@@ -572,10 +564,6 @@
mSequentialExecutor.execute(this::resumeInternal);
setState(State.RECORDING);
break;
- case RELEASING:
- // Fall-through
- case RELEASED:
- throw new IllegalStateException("The Recorder has been released.");
case ERROR:
finalizeRecording(VideoRecordEvent.ERROR_RECORDER_ERROR, mErrorCause);
break;
@@ -590,6 +578,8 @@
// Fall-through
case PENDING_PAUSED:
// Fall-through
+ case RESETING:
+ // Fall-through
case INITIALIZING:
finalizeRecording(VideoRecordEvent.ERROR_RECORDER_UNINITIALIZED,
new IllegalStateException("The Recorder hasn't been initialized."));
@@ -602,10 +592,6 @@
case RECORDING:
mSequentialExecutor.execute(() -> stopInternal(VideoRecordEvent.ERROR_NONE));
break;
- case RELEASING:
- // Fall-through
- case RELEASED:
- throw new IllegalStateException("The Recorder has been released.");
case ERROR:
finalizeRecording(VideoRecordEvent.ERROR_RECORDER_ERROR, mErrorCause);
break;
@@ -621,7 +607,7 @@
* is released will get {@link IllegalStateException}.
*/
@ExecutedBy("mSequentialExecutor")
- void release() {
+ void reset() {
synchronized (mLock) {
switch (getObservableData(mState)) {
case PENDING_RECORDING:
@@ -633,20 +619,18 @@
case ERROR:
// Fall-through
case IDLING:
- releaseInternal();
+ resetInternal();
break;
case PAUSED:
// Fall-through
case RECORDING:
- setState(State.RELEASING);
+ setState(State.RESETING);
// If there's an active recording, stop it first then release the resources
// at finalizeRecording().
mSequentialExecutor.execute(() -> stopInternal(VideoRecordEvent.ERROR_NONE));
break;
- case RELEASING:
- // Fall-through
- case RELEASED:
- // No-Op, the Recorder is already released.
+ case RESETING:
+ // No-Op, the Recorder is being reset.
break;
}
}
@@ -669,8 +653,7 @@
resultSurface.release();
}
mSurface = null;
- release();
- setState(State.INITIALIZING);
+ reset();
});
onInitialized();
} else {
@@ -692,9 +675,7 @@
// Fall-through
case PAUSED:
// Fall-through
- case RELEASING:
- // Fall-through
- case RELEASED:
+ case RESETING:
throw new IllegalStateException(
"Incorrectly invoke onInitialized() in state " + state);
case INITIALIZING:
@@ -990,8 +971,7 @@
resultSurface.release();
}
mSurface = null;
- release();
- setState(State.INITIALIZING);
+ reset();
});
onInitialized();
});
@@ -1253,7 +1233,7 @@
}
@ExecutedBy("mSequentialExecutor")
- private void releaseInternal() {
+ private void resetInternal() {
if (mAudioEncoder != null) {
mAudioEncoder.release();
mAudioSource = null;
@@ -1268,7 +1248,7 @@
}
mSurfaceRequested.set(false);
- setState(State.RELEASED);
+ setState(State.INITIALIZING);
}
private int internalAudioStateToEventAudioState(AudioState audioState) {
@@ -1349,8 +1329,8 @@
setAudioState(AudioState.INITIALIZING);
}
synchronized (mLock) {
- if (getObservableData(mState) == State.RELEASING) {
- releaseInternal();
+ if (getObservableData(mState) == State.RESETING) {
+ resetInternal();
} else {
setState(State.IDLING);
}
@@ -1460,7 +1440,7 @@
* depending on the resolutions supported by the camera and codec capabilities.
*
* <p>If no quality selector is provided, the default is
- * {@link VideoSpec#QUALITY_SELECTOR_AUTO}.
+ * {@link #DEFAULT_QUALITY_SELECTOR}.
* @see QualitySelector
*/
@NonNull
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index b8791541..acb2ae7 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -90,19 +90,32 @@
/**
* A use case that provides camera stream suitable for video application.
*
- * <p>VideoCapture is used to create a camera stream suitable for video application. This stream
- * is used by the implementation of {@link VideoOutput}. Calling {@link #withOutput(VideoOutput)}
- * can generate a VideoCapture use case binding to the given VideoOutput.
+ * <p>VideoCapture is used to create a camera stream suitable for video application. The camera
+ * stream is used by the extended classes of {@link VideoOutput}.
+ * {@link #withOutput(VideoOutput)} can be used to create a VideoCapture instance associated with
+ * the given VideoOutput.
*
- * <p>When binding VideoCapture, VideoCapture will initialize the camera stream according to the
- * resolution found by the {@link QualitySelector} in VideoOutput. Then VideoCapture will invoke
- * {@link VideoOutput#onSurfaceRequested(SurfaceRequest)} to request VideoOutput to provide a
- * {@link Surface} via {@link SurfaceRequest#provideSurface} to complete the initialization
- * process. After VideoCapture is bound, updating the QualitySelector in VideoOutput will have no
- * effect. If it needs to change the resolution of the camera stream after VideoCapture is bound,
- * it has to unbind the original VideoCapture, update the QualitySelector in VideoOutput and then
- * re-bind the VideoCapture. If the implementation of VideoOutput does not support modifying the
- * QualitySelector afterwards, it has to create a new VideoOutput and VideoCapture for re-bind.
+ * <p>When {@linkplain androidx.camera.lifecycle.ProcessCameraProvider#bindToLifecycle binding}
+ * VideoCapture to lifecycle, VideoCapture will create a camera stream with the resolution
+ * selected by the {@link QualitySelector} in VideoOutput. Example:
+ * <pre>
+ * <code>
+ * Recorder recorder = new Recorder.Builder()
+ * .setQualitySelector(QualitySelector.of(QualitySelector.QUALITY_FHD))
+ * .build();
+ * VideoCapture<Recorder> videoCapture = VideoCapture.withOutput(recorder);
+ * </code>
+ * </pre>
+ *
+ * <p>Then VideoCapture will {@link VideoOutput#onSurfaceRequested(SurfaceRequest) ask}
+ * VideoOutput to {@link SurfaceRequest#provideSurface provide} a {@link Surface} for setting up
+ * the camera stream. After VideoCapture is bound, update QualitySelector in VideoOutput will not
+ * have any effect. If it needs to change the resolution of the camera stream after VideoCapture
+ * is bound, it has to update QualitySelector of VideoOutput and rebind
+ * ({@linkplain androidx.camera.lifecycle.ProcessCameraProvider#unbind unbind} and
+ * {@linkplain androidx.camera.lifecycle.ProcessCameraProvider#bindToLifecycle bind}) the
+ * VideoCapture. If the extended class of VideoOutput does not have API to modify the
+ * QualitySelector, it has to create new VideoOutput and VideoCapture for rebinding.
*
* @param <T> the type of VideoOutput
*/
@@ -118,14 +131,13 @@
private SurfaceRequest mSurfaceRequest;
/**
- * Create a VideoCapture builder with a {@link VideoOutput}.
+ * Create a VideoCapture associated with the given {@link VideoOutput}.
*
- * @param videoOutput the associated VideoOutput.
- * @return the new Builder
+ * @throws NullPointerException if {@code videoOutput} is null.
*/
@NonNull
public static <T extends VideoOutput> VideoCapture<T> withOutput(@NonNull T videoOutput) {
- return new VideoCapture.Builder<T>(videoOutput).build();
+ return new VideoCapture.Builder<T>(Preconditions.checkNotNull(videoOutput)).build();
}
/**
@@ -138,7 +150,10 @@
}
/**
- * Gets the {@link VideoOutput} associated to this VideoCapture.
+ * Gets the {@link VideoOutput} associated with this VideoCapture.
+ *
+ * @return the value provided to {@link #withOutput(VideoOutput)} used to create this
+ * VideoCapture.
*/
@SuppressWarnings("unchecked")
@NonNull
@@ -155,7 +170,10 @@
* has been attached to a camera.
*
* @return The rotation of the intended target.
+ *
+ * @hide
*/
+ @RestrictTo(Scope.LIBRARY_GROUP)
@RotationValue
public int getTargetRotation() {
return getTargetRotationInternal();
@@ -173,7 +191,10 @@
* created. The use case is fully created once it has been attached to a camera.
*
* @param rotation Desired rotation of the output video.
+ *
+ * @hide
*/
+ @RestrictTo(Scope.LIBRARY_GROUP)
@OptIn(markerClass = ExperimentalUseCaseGroup.class)
public void setTargetRotation(@RotationValue int rotation) {
if (setTargetRotationInternal(rotation)) {
@@ -181,6 +202,12 @@
}
}
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
@Override
public void onAttached() {
getOutput().getStreamState().addObserver(CameraXExecutors.mainThreadExecutor(),
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoOutput.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoOutput.java
index f75556b..4a1b310 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoOutput.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoOutput.java
@@ -122,7 +122,9 @@
* changes may not come for free and may require the video frame producer to re-initialize,
* which could cause a new {@link SurfaceRequest} to be sent to
* {@link #onSurfaceRequested(SurfaceRequest)}.
+ * @hide
*/
+ @RestrictTo(Scope.LIBRARY)
@NonNull
default Observable<MediaSpec> getMediaSpec() {
return ConstantObservable.withValue(null);
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoSpec.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoSpec.java
index bb14990..e286cbd 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoSpec.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoSpec.java
@@ -25,6 +25,8 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
import com.google.auto.value.AutoValue;
@@ -33,7 +35,9 @@
/**
* Video specification that is options to config video encoding.
+ * @hide
*/
+@RestrictTo(Scope.LIBRARY)
@AutoValue
public abstract class VideoSpec {
@@ -124,7 +128,11 @@
@NonNull
public abstract Builder toBuilder();
- /** The builder of the {@link VideoSpec}. */
+ /**
+ * The builder of the {@link VideoSpec}.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY)
@SuppressWarnings("StaticFinalBuilder")
@AutoValue.Builder
public abstract static class Builder {
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
index 6561438..4382484 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
@@ -53,6 +53,7 @@
import androidx.camera.core.ViewPort;
import androidx.camera.view.internal.compat.quirk.DeviceQuirks;
import androidx.camera.view.internal.compat.quirk.PreviewOneThirdWiderQuirk;
+import androidx.camera.view.internal.compat.quirk.TextureViewRotationQuirk;
import androidx.core.util.Preconditions;
/**
@@ -152,8 +153,14 @@
Matrix getTextureViewCorrectionMatrix() {
Preconditions.checkState(isTransformationInfoReady());
RectF surfaceRect = new RectF(0, 0, mResolution.getWidth(), mResolution.getHeight());
- return getRectToRect(surfaceRect, surfaceRect,
- -surfaceRotationToRotationDegrees(mTargetRotation));
+ int rotationDegrees = -surfaceRotationToRotationDegrees(mTargetRotation);
+
+ TextureViewRotationQuirk textureViewRotationQuirk =
+ DeviceQuirks.get(TextureViewRotationQuirk.class);
+ if (textureViewRotationQuirk != null) {
+ rotationDegrees += textureViewRotationQuirk.getCorrectionRotation(mIsFrontCamera);
+ }
+ return getRectToRect(surfaceRect, surfaceRect, rotationDegrees);
}
/**
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
index d73a851..00641c7 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
@@ -47,6 +47,10 @@
quirks.add(new SurfaceViewStretchedQuirk());
}
+ if (TextureViewRotationQuirk.load()) {
+ quirks.add(new TextureViewRotationQuirk());
+ }
+
return quirks;
}
}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/TextureViewRotationQuirk.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/TextureViewRotationQuirk.java
new file mode 100644
index 0000000..d7e1880
--- /dev/null
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/TextureViewRotationQuirk.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view.internal.compat.quirk;
+
+import android.os.Build;
+import android.view.TextureView;
+
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * A quirk that requires applying extra rotation on {@link TextureView}
+ *
+ * <p> On certain devices, the rotation of the output is incorrect. One example is b/177561470.
+ * In which case, the extra rotation is needed to correct the output on {@link TextureView}.
+ */
+public class TextureViewRotationQuirk implements Quirk {
+
+ private static final String FAIRPHONE = "Fairphone";
+ private static final String FAIRPHONE_2_MODEL = "FP2";
+
+ static boolean load() {
+ return isFairphone2();
+ }
+
+ /**
+ * Gets correction needed for the given camera.
+ */
+ public int getCorrectionRotation(boolean isFrontCamera) {
+ if (isFairphone2() && isFrontCamera) {
+ // On Fairphone2, the front camera output on TextureView is rotated 180°.
+ // See: b/177561470.
+ return 180;
+ }
+ return 0;
+ }
+
+ private static boolean isFairphone2() {
+ return FAIRPHONE.equalsIgnoreCase(Build.MANUFACTURER)
+ && FAIRPHONE_2_MODEL.equalsIgnoreCase(Build.MODEL);
+ }
+}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt
index f37ef2d0e..bc3c951 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt
@@ -35,6 +35,7 @@
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
import kotlin.math.roundToInt
// Size of the PreviewView. Aspect ratio 2:1.
@@ -71,19 +72,19 @@
@RunWith(RobolectricTestRunner::class)
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-public class PreviewTransformationTest {
+class PreviewTransformationTest {
private lateinit var mPreviewTransform: PreviewTransformation
private lateinit var mView: View
@Before
- public fun setUp() {
+ fun setUp() {
mPreviewTransform = PreviewTransformation()
mView = View(ApplicationProvider.getApplicationContext())
}
@Test
- public fun withPreviewStretchedQuirk_cropRectIsAdjusted() {
+ fun withPreviewStretchedQuirk_cropRectIsAdjusted() {
// Arrange.
QuirkInjector.inject(PreviewOneThirdWiderQuirk())
@@ -100,7 +101,7 @@
}
@Test
- public fun cropRectWidthOffByOnePixel_match() {
+ fun cropRectWidthOffByOnePixel_match() {
assertThat(
isCropRectAspectRatioMatchPreviewView(
Rect(
@@ -114,7 +115,7 @@
}
@Test
- public fun cropRectWidthOffByTwoPixels_mismatch() {
+ fun cropRectWidthOffByTwoPixels_mismatch() {
assertThat(
isCropRectAspectRatioMatchPreviewView(
Rect(
@@ -138,7 +139,43 @@
}
@Test
- public fun correctTextureViewWith0Rotation() {
+ fun fairphone2BackCamera_noCorrection() {
+ ReflectionHelpers.setStaticField(Build::class.java, "MANUFACTURER", "Fairphone")
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "FP2")
+ assertThat(getTextureViewCorrection(Surface.ROTATION_0, BACK_CAMERA)).isEqualTo(
+ intArrayOf(
+ 0,
+ 0,
+ SURFACE_SIZE.width,
+ 0,
+ SURFACE_SIZE.width,
+ SURFACE_SIZE.height,
+ 0,
+ SURFACE_SIZE.height
+ )
+ )
+ }
+
+ @Test
+ fun fairphone2BackCamera_corrected() {
+ ReflectionHelpers.setStaticField(Build::class.java, "MANUFACTURER", "Fairphone")
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "FP2")
+ assertThat(getTextureViewCorrection(Surface.ROTATION_0, FRONT_CAMERA)).isEqualTo(
+ intArrayOf(
+ SURFACE_SIZE.width,
+ SURFACE_SIZE.height,
+ 0,
+ SURFACE_SIZE.height,
+ 0,
+ 0,
+ SURFACE_SIZE.width,
+ 0
+ )
+ )
+ }
+
+ @Test
+ fun correctTextureViewWith0Rotation() {
assertThat(getTextureViewCorrection(Surface.ROTATION_0)).isEqualTo(
intArrayOf(
0,
@@ -154,7 +191,7 @@
}
@Test
- public fun correctTextureViewWith90Rotation() {
+ fun correctTextureViewWith90Rotation() {
assertThat(getTextureViewCorrection(Surface.ROTATION_90)).isEqualTo(
intArrayOf(
0,
@@ -170,7 +207,7 @@
}
@Test
- public fun correctTextureViewWith180Rotation() {
+ fun correctTextureViewWith180Rotation() {
assertThat(getTextureViewCorrection(Surface.ROTATION_180)).isEqualTo(
intArrayOf(
SURFACE_SIZE.width,
@@ -186,7 +223,7 @@
}
@Test
- public fun correctTextureViewWith270Rotation() {
+ fun correctTextureViewWith270Rotation() {
assertThat(getTextureViewCorrection(Surface.ROTATION_270)).isEqualTo(
intArrayOf(
SURFACE_SIZE.width,
@@ -201,15 +238,22 @@
)
}
+ private fun getTextureViewCorrection(@RotationValue rotation: Int): IntArray {
+ return getTextureViewCorrection(rotation, BACK_CAMERA)
+ }
+
/**
* Corrects TextureView based on target rotation and return the corrected vertices.
*/
- private fun getTextureViewCorrection(@RotationValue rotation: Int): IntArray {
+ private fun getTextureViewCorrection(
+ @RotationValue rotation: Int,
+ isFrontCamera: Boolean
+ ): IntArray {
// Arrange.
mPreviewTransform.setTransformationInfo(
SurfaceRequest.TransformationInfo.of(CROP_RECT, 90, rotation),
SURFACE_SIZE,
- BACK_CAMERA
+ isFrontCamera
)
// Act.
@@ -220,18 +264,16 @@
private fun convertToIntArray(elements: FloatArray): IntArray {
var result = IntArray(elements.size)
- var index = 0
- for (element in elements) {
- result.set(index, element.roundToInt())
- index++
+ for ((index, element) in elements.withIndex()) {
+ result[index] = element.roundToInt()
}
return result
}
@Test
- public fun ratioMatch_surfaceIsScaledToFillPreviewView() {
+ fun ratioMatch_surfaceIsScaledToFillPreviewView() {
// Arrange.
mPreviewTransform.setTransformationInfo(
SurfaceRequest.TransformationInfo.of(
@@ -260,7 +302,7 @@
}
@Test
- public fun mismatchedCropRect_fitStart() {
+ fun mismatchedCropRect_fitStart() {
assertForMismatchedCropRect(
PreviewView.ScaleType.FIT_START,
LayoutDirection.LTR,
@@ -272,7 +314,7 @@
}
@Test
- public fun mismatchedCropRect_fitCenter() {
+ fun mismatchedCropRect_fitCenter() {
assertForMismatchedCropRect(
PreviewView.ScaleType.FIT_CENTER,
LayoutDirection.LTR,
@@ -284,7 +326,7 @@
}
@Test
- public fun mismatchedCropRect_fitEnd() {
+ fun mismatchedCropRect_fitEnd() {
assertForMismatchedCropRect(
PreviewView.ScaleType.FIT_END,
LayoutDirection.LTR,
@@ -296,7 +338,7 @@
}
@Test
- public fun mismatchedCropRectFrontCamera_fitStart() {
+ fun mismatchedCropRectFrontCamera_fitStart() {
assertForMismatchedCropRect(
PreviewView.ScaleType.FIT_START,
LayoutDirection.LTR,
@@ -308,7 +350,7 @@
}
@Test
- public fun mismatchedCropRect_fillStart() {
+ fun mismatchedCropRect_fillStart() {
assertForMismatchedCropRect(
PreviewView.ScaleType.FILL_START,
LayoutDirection.LTR,
@@ -320,7 +362,7 @@
}
@Test
- public fun mismatchedCropRect_fillCenter() {
+ fun mismatchedCropRect_fillCenter() {
assertForMismatchedCropRect(
PreviewView.ScaleType.FILL_CENTER,
LayoutDirection.LTR,
@@ -332,7 +374,7 @@
}
@Test
- public fun mismatchedCropRect_fillEnd() {
+ fun mismatchedCropRect_fillEnd() {
assertForMismatchedCropRect(
PreviewView.ScaleType.FILL_END,
LayoutDirection.LTR,
@@ -344,7 +386,7 @@
}
@Test
- public fun mismatchedCropRect_fitStartWithRtl_actsLikeFitEnd() {
+ fun mismatchedCropRect_fitStartWithRtl_actsLikeFitEnd() {
assertForMismatchedCropRect(
PreviewView.ScaleType.FIT_START,
LayoutDirection.RTL,
@@ -382,7 +424,7 @@
}
@Test
- public fun frontCamera0_transformationIsMirrored() {
+ fun frontCamera0_transformationIsMirrored() {
testOffCenterCropRectMirroring(FRONT_CAMERA, CROP_RECT_0, PREVIEW_VIEW_SIZE, 0)
// Assert:
@@ -393,7 +435,7 @@
}
@Test
- public fun backCamera0_transformationIsNotMirrored() {
+ fun backCamera0_transformationIsNotMirrored() {
testOffCenterCropRectMirroring(BACK_CAMERA, CROP_RECT_0, PREVIEW_VIEW_SIZE, 0)
// Assert:
@@ -404,7 +446,7 @@
}
@Test
- public fun frontCameraRotated90_transformationIsMirrored() {
+ fun frontCameraRotated90_transformationIsMirrored() {
testOffCenterCropRectMirroring(
FRONT_CAMERA, CROP_RECT_90, PIVOTED_PREVIEW_VIEW_SIZE, 90
)
@@ -417,7 +459,7 @@
}
@Test
- public fun previewViewSizeIs0_noOps() {
+ fun previewViewSizeIs0_noOps() {
testOffCenterCropRectMirroring(
FRONT_CAMERA, CROP_RECT_90, Size(0, 0), 90
)
@@ -430,7 +472,7 @@
}
@Test
- public fun backCameraRotated90_transformationIsNotMirrored() {
+ fun backCameraRotated90_transformationIsNotMirrored() {
testOffCenterCropRectMirroring(BACK_CAMERA, CROP_RECT_90, PIVOTED_PREVIEW_VIEW_SIZE, 90)
// Assert:
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index 811daab..06b2eff 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -21,6 +21,7 @@
import static androidx.camera.core.ImageCapture.FLASH_MODE_ON;
import android.Manifest;
+import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
@@ -351,6 +352,7 @@
});
}
+ @SuppressLint("MissingPermission")
private void setUpRecordButton() {
mRecordUi.getButtonRecord().setOnClickListener((view) -> {
RecordUi.State state = mRecordUi.getState();
@@ -359,6 +361,7 @@
createDefaultVideoFolderIfNotExist();
mActiveRecording = getVideoCapture().getOutput()
.prepareRecording(getNewVideoOutputFileOptions())
+ .withAudioEnabled()
.withEventListener(ContextCompat.getMainExecutor(CameraXActivity.this),
mVideoRecordEventListener)
.start();
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java
index 264be0c..9d70074 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java
@@ -205,6 +205,7 @@
serviceComponentName);
mViewModel = new ViewModelProvider(this, factory).get(CarAppViewModel.class);
mViewModel.setActivity(this);
+ mViewModel.resetState();
mViewModel.getError().observe(this, this::onErrorChanged);
mViewModel.getState().observe(this, this::onStateChanged);
@@ -239,43 +240,50 @@
}
private void onErrorChanged(@Nullable ErrorHandler.ErrorType errorType) {
- mErrorMessageView.setError(errorType);
+ ThreadUtils.runOnMain(() -> {
+ mErrorMessageView.setError(errorType);
+ });
}
private void onStateChanged(@NonNull CarAppViewModel.State state) {
- requireNonNull(mSurfaceView);
- requireNonNull(mSurfaceHolderListener);
+ ThreadUtils.runOnMain(() -> {
+ requireNonNull(mSurfaceView);
+ requireNonNull(mSurfaceHolderListener);
- switch (state) {
- case IDLE:
- mSurfaceView.setVisibility(View.GONE);
- mSurfaceHolderListener.setSurfaceListener(null);
- mErrorMessageView.setVisibility(View.GONE);
- mLoadingView.setVisibility(View.GONE);
- break;
- case ERROR:
- mSurfaceView.setVisibility(View.GONE);
- mSurfaceHolderListener.setSurfaceListener(null);
- mErrorMessageView.setVisibility(View.VISIBLE);
- mLoadingView.setVisibility(View.GONE);
- break;
- case CONNECTING:
- mSurfaceView.setVisibility(View.GONE);
- mSurfaceHolderListener.setSurfaceListener(null);
- mErrorMessageView.setVisibility(View.GONE);
- mLoadingView.setVisibility(View.VISIBLE);
- break;
- case CONNECTED:
- mSurfaceView.setVisibility(View.VISIBLE);
- mErrorMessageView.setVisibility(View.GONE);
- mLoadingView.setVisibility(View.GONE);
- break;
- }
+ switch (state) {
+ case IDLE:
+ mSurfaceView.setVisibility(View.GONE);
+ mSurfaceHolderListener.setSurfaceListener(null);
+ mErrorMessageView.setVisibility(View.GONE);
+ mLoadingView.setVisibility(View.GONE);
+ break;
+ case ERROR:
+ mSurfaceView.setVisibility(View.GONE);
+ mSurfaceHolderListener.setSurfaceListener(null);
+ mErrorMessageView.setVisibility(View.VISIBLE);
+ mLoadingView.setVisibility(View.GONE);
+ break;
+ case CONNECTING:
+ mSurfaceView.setVisibility(View.GONE);
+ mErrorMessageView.setVisibility(View.GONE);
+ mLoadingView.setVisibility(View.VISIBLE);
+ break;
+ case CONNECTED:
+ mSurfaceView.setVisibility(View.VISIBLE);
+ mErrorMessageView.setVisibility(View.GONE);
+ mLoadingView.setVisibility(View.GONE);
+ break;
+ }
+ });
}
@Override
protected void onNewIntent(@NonNull Intent intent) {
super.onNewIntent(intent);
+
+ requireNonNull(mSurfaceHolderListener).setSurfaceListener(null);
+ requireNonNull(mActivityLifecycleDelegate).registerRendererCallback(null);
+
requireNonNull(mViewModel).bind(intent, mCarActivity, getDisplayId());
}
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModel.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModel.java
index ad3f433..7011f44 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModel.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModel.java
@@ -93,11 +93,18 @@
mIRendererCallback = rendererCallback;
}
- /** Updates the activity hosting this view model */
+ /** Updates the activity hosting this view model. */
void setActivity(@Nullable Activity activity) {
sActivity = new WeakReference<>(activity);
}
+ /** Resets the internal state of this view model. */
+ @SuppressWarnings("NullAway")
+ void resetState() {
+ mState.setValue(State.IDLE);
+ mError.setValue(null);
+ }
+
/**
* Binds to the renderer service and initializes the service if not bound already.
*
@@ -107,8 +114,8 @@
@SuppressWarnings("NullAway")
void bind(@NonNull Intent intent, @NonNull ICarAppActivity iCarAppActivity,
int displayId) {
- mState.postValue(State.CONNECTING);
- mError.postValue(null);
+ mState.setValue(State.CONNECTING);
+ mError.setValue(null);
mServiceConnectionManager.bind(intent, iCarAppActivity, displayId);
}
@@ -123,7 +130,7 @@
if (mIRendererCallback != null) {
getServiceDispatcher().dispatch("onDestroyed", mIRendererCallback::onDestroyed);
}
- mState.postValue(State.IDLE);
+ mState.setValue(State.IDLE);
unbind();
}
@@ -158,9 +165,9 @@
// displayed to the user.
return;
}
- mError.postValue(errorCode);
+ mState.setValue(State.ERROR);
+ mError.setValue(errorCode);
});
- mState.postValue(State.ERROR);
unbind();
}
@@ -170,16 +177,15 @@
@SuppressWarnings("NullAway")
@Override
public void onConnect() {
- mState.postValue(State.CONNECTED);
- mError.postValue(null);
+ mState.setValue(State.CONNECTED);
+ mError.setValue(null);
}
/** Attempts to rebind to the host service */
@SuppressWarnings("NullAway")
public void retryBinding() {
Activity activity = requireNonNull(sActivity.get());
- mState.postValue(State.CONNECTING);
- mError.postValue(null);
+ mError.setValue(null);
activity.recreate();
}
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/info/AutomotiveCarInfo.java b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/info/AutomotiveCarInfo.java
index 1d13783..d0013db 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/info/AutomotiveCarInfo.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/info/AutomotiveCarInfo.java
@@ -34,10 +34,13 @@
import static java.util.Objects.requireNonNull;
import android.car.VehiclePropertyIds;
+import android.os.Build;
import android.util.Log;
+import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.car.app.hardware.common.CarPropertyResponse;
@@ -83,13 +86,12 @@
public static final int TOLL_CARD_STATUS_ID = 289410874;
// VEHICLE_SPEED_DISPLAY_UNIT in VehiclePropertyIds. The property is added after Android Q.
- // TODO(192383417) check the compile SDK
public static final int SPEED_DISPLAY_UNIT_ID = 289408516;
private static final float UNKNOWN_CAPACITY = Float.NEGATIVE_INFINITY;
private static final List<Integer> MILEAGE_REQUEST =
Arrays.asList(PERF_ODOMETER, DISTANCE_DISPLAY_UNITS);
- private static final List<Integer> TOLL_REQUEST =
+ static final List<Integer> TOLL_REQUEST =
Collections.singletonList(TOLL_CARD_STATUS_ID);
private static final List<Integer> SPEED_REQUEST =
Arrays.asList(VehiclePropertyIds.PERF_VEHICLE_SPEED,
@@ -140,15 +142,25 @@
@Override
public void addTollListener(@NonNull Executor executor,
@NonNull OnCarDataAvailableListener<TollCard> listener) {
- TollListener tollListener = new TollListener(listener, executor);
- mPropertyManager.submitRegisterListenerRequest(TOLL_REQUEST, DEFAULT_SAMPLE_RATE,
- tollListener, executor);
- mListenerMap.put(listener, tollListener);
+ if (Build.VERSION.SDK_INT > 30) {
+ Api31Impl.addTollListener(executor, listener, mPropertyManager, mListenerMap);
+ } else {
+ TollCard unimplementedTollCard = new TollCard.Builder()
+ .setCardState(CarValue.UNIMPLEMENTED_INTEGER).build();
+ executor.execute(() -> listener.onCarDataAvailable(unimplementedTollCard));
+ }
}
@Override
public void removeTollListener(@NonNull OnCarDataAvailableListener<TollCard> listener) {
- removeListenerImpl(listener);
+ OnCarPropertyResponseListener responseListener = mListenerMap.remove(listener);
+ if (responseListener == null) {
+ Log.d(LogTags.TAG_CAR_HARDWARE, "Listener is not registered yet");
+ return;
+ }
+ if (Build.VERSION.SDK_INT > 30) {
+ Api31Impl.removeTollListener(responseListener, mPropertyManager);
+ }
}
@Override
@@ -332,6 +344,25 @@
}, executor);
}
+ @RequiresApi(31)
+ private static class Api31Impl {
+ @DoNotInline
+ static void addTollListener(Executor executor,
+ OnCarDataAvailableListener<TollCard> listener, PropertyManager propertyManager,
+ Map<OnCarDataAvailableListener<?>, OnCarPropertyResponseListener> listenerMap) {
+ TollListener tollListener = new TollListener(listener, executor);
+ propertyManager.submitRegisterListenerRequest(TOLL_REQUEST, DEFAULT_SAMPLE_RATE,
+ tollListener, executor);
+ listenerMap.put(listener, tollListener);
+ }
+
+ @DoNotInline
+ static void removeTollListener(OnCarPropertyResponseListener listener,
+ PropertyManager propertyManager) {
+ propertyManager.submitUnregisterListenerRequest(listener);
+ }
+ }
+
private void removeListenerImpl(OnCarDataAvailableListener<?> listener) {
OnCarPropertyResponseListener responseListener = mListenerMap.remove(listener);
if (responseListener != null) {
@@ -515,14 +546,14 @@
case EV_BATTERY_LEVEL:
if (mEvBatteryCapacity != Float.NEGATIVE_INFINITY) {
batteryPercentValue = new CarValue<>(
- (Float) response.getValue() / mEvBatteryCapacity,
+ (Float) response.getValue() / mEvBatteryCapacity * 100,
response.getTimestampMillis(), response.getStatus());
}
break;
case FUEL_LEVEL:
if (mFuelCapacity != Float.NEGATIVE_INFINITY) {
fuelPercentValue = new CarValue<>(
- (Float) response.getValue() / mFuelCapacity,
+ (Float) response.getValue() / mFuelCapacity * 100,
response.getTimestampMillis(), response.getStatus());
}
break;
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java
index 1435075..a633647 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java
@@ -262,10 +262,10 @@
verify(rendererCallback, times(1)).onBackPressed();
// Verify focus request sent to host.
- activity.mSurfaceView.requestFocus();
- verify(callback, times(1)).onWindowFocusChanged(true, false);
activity.mSurfaceView.clearFocus();
verify(callback, times(1)).onWindowFocusChanged(false, false);
+ activity.mSurfaceView.requestFocus();
+ verify(callback, times(1)).onWindowFocusChanged(true, false);
long downTime = SystemClock.uptimeMillis();
long eventTime = SystemClock.uptimeMillis();
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelTest.java
index 7c80e2c..9a2a52e 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelTest.java
@@ -131,7 +131,7 @@
mMainLooper.idle();
assertThat(mCarAppViewModel.getState().getValue())
- .isEqualTo(CarAppViewModel.State.CONNECTING);
+ .isEqualTo(CarAppViewModel.State.IDLE);
assertThat(mCarAppViewModel.getError().getValue()).isNull();
}
}
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java
index aeb1972..800c110 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java
@@ -52,6 +52,7 @@
import androidx.car.app.hardware.common.CarPropertyResponse;
import androidx.car.app.hardware.common.CarUnit;
+import androidx.car.app.hardware.common.CarValue;
import androidx.car.app.hardware.common.OnCarDataAvailableListener;
import androidx.car.app.hardware.common.OnCarPropertyResponseListener;
import androidx.car.app.hardware.common.PropertyManager;
@@ -197,8 +198,9 @@
assertThat(mileage.getDistanceDisplayUnit().getValue()).isEqualTo(2);
}
+ @Config(minSdk = 31)
@Test
- public void getTollCard_verifyResponse() throws InterruptedException {
+ public void getTollCard_verifyResponseApi31() throws InterruptedException {
AtomicReference<TollCard> loadedResult = new AtomicReference<>();
OnCarDataAvailableListener<TollCard> listener = (data) -> {
loadedResult.set(data);
@@ -222,6 +224,20 @@
assertThat(tollCard.getCardState().getValue()).isEqualTo(TollCard.TOLLCARD_STATE_VALID);
}
+ @Config(minSdk = 30)
+ @Test
+ public void getTollCard_verifyResponseApi30() {
+ AtomicReference<TollCard> loadedResult = new AtomicReference<>();
+ OnCarDataAvailableListener<TollCard> listener = (data) -> {
+ loadedResult.set(data);
+ mCountDownLatch.countDown();
+ };
+ mAutomotiveCarInfo.addTollListener(mExecutor, listener);
+
+ TollCard tollCard = loadedResult.get();
+ assertThat(tollCard.getCardState().getStatus()).isEqualTo(CarValue.STATUS_UNIMPLEMENTED);
+ }
+
@Test
public void getSpeed_verifyResponse() throws InterruptedException {
float defaultSpeed = 20f;
@@ -263,12 +279,15 @@
public void getEnergyLevel_verifyResponse() throws InterruptedException {
ArgumentCaptor<OnCarPropertyResponseListener> captor = ArgumentCaptor.forClass(
OnCarPropertyResponseListener.class);
-
+ float evBatteryCapacity = 100f;
+ float evBatteryLevelValue = 50f;
+ float fuelCapacity = 120f;
+ float fuelLevelValue = 50f;
List<CarPropertyResponse<?>> capacities = new ArrayList<>();
capacities.add(CarPropertyResponse.create(INFO_EV_BATTERY_CAPACITY,
- STATUS_SUCCESS, 1, 2f));
+ STATUS_SUCCESS, 1, evBatteryCapacity));
capacities.add(CarPropertyResponse.create(INFO_FUEL_CAPACITY,
- STATUS_SUCCESS, 1, 3f));
+ STATUS_SUCCESS, 1, fuelCapacity));
ListenableFuture<List<CarPropertyResponse<?>>> future =
Futures.immediateFuture(capacities);
when(mPropertyManager.submitGetPropertyRequest(any(), any())).thenReturn(future);
@@ -286,9 +305,9 @@
eq(DEFAULT_SAMPLE_RATE), captor.capture(), any());
mResponse.add(CarPropertyResponse.create(EV_BATTERY_LEVEL,
- STATUS_SUCCESS, 1, 4f));
+ STATUS_SUCCESS, 1, evBatteryLevelValue));
mResponse.add(CarPropertyResponse.create(FUEL_LEVEL,
- STATUS_SUCCESS, 1, 6f));
+ STATUS_SUCCESS, 1, fuelLevelValue));
mResponse.add(CarPropertyResponse.create(FUEL_LEVEL_LOW,
STATUS_SUCCESS, 1, true));
mResponse.add(CarPropertyResponse.create(RANGE_REMAINING,
@@ -300,9 +319,9 @@
EnergyLevel energyLevel = loadedResult.get();
assertThat(energyLevel.getBatteryPercent().getValue()).isEqualTo(
- 2f);
+ evBatteryLevelValue / evBatteryCapacity * 100);
assertThat(energyLevel.getFuelPercent().getValue()).isEqualTo(
- 2f);
+ fuelLevelValue / fuelCapacity * 100);
assertThat(energyLevel.getEnergyIsLow().getValue()).isEqualTo(
true);
assertThat(energyLevel.getRangeRemainingMeters().getValue()).isEqualTo(
diff --git a/car/app/app-samples/helloworld/automotive/src/main/AndroidManifest.xml b/car/app/app-samples/helloworld/automotive/src/main/AndroidManifest.xml
index 1c79b24..1a2c9bb 100644
--- a/car/app/app-samples/helloworld/automotive/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/helloworld/automotive/src/main/AndroidManifest.xml
@@ -20,7 +20,22 @@
android:versionCode="1"
android:versionName="1.0">
- <uses-feature android:name="android.software.car.templates_host" />
+ <!-- Various required feature settings for an automotive app. -->
+ <uses-feature
+ android:name="android.hardware.type.automotive"
+ android:required="true" />
+ <uses-feature
+ android:name="android.software.car.templates_host"
+ android:required="true" />
+ <uses-feature
+ android:name="android.hardware.wifi"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.screen.portrait"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.screen.landscape"
+ android:required="false" />
<application
android:label="@string/app_name"
diff --git a/car/app/app-samples/helloworld/common/src/test/java/androidx/car/app/sample/helloworld/common/HelloWorldSessionTest.java b/car/app/app-samples/helloworld/common/src/test/java/androidx/car/app/sample/helloworld/common/HelloWorldSessionTest.java
index 9679e5c..f8aaac63f 100644
--- a/car/app/app-samples/helloworld/common/src/test/java/androidx/car/app/sample/helloworld/common/HelloWorldSessionTest.java
+++ b/car/app/app-samples/helloworld/common/src/test/java/androidx/car/app/sample/helloworld/common/HelloWorldSessionTest.java
@@ -50,7 +50,7 @@
public void onCreateScreen_returnsExpectedScreen() {
HelloWorldService service = Robolectric.setupService(HelloWorldService.class);
Session session = service.onCreateSession();
- SessionController controller = SessionController.of(session, mTestCarContext);
+ SessionController controller = new SessionController(session, mTestCarContext);
controller.create(new Intent().setComponent(
new ComponentName(mTestCarContext, HelloWorldService.class)));
diff --git a/car/app/app-samples/navigation/automotive/src/main/AndroidManifest.xml b/car/app/app-samples/navigation/automotive/src/main/AndroidManifest.xml
index ca1f203..725f8d1 100644
--- a/car/app/app-samples/navigation/automotive/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/navigation/automotive/src/main/AndroidManifest.xml
@@ -28,7 +28,22 @@
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
- <uses-feature android:name="android.software.car.templates_host" />
+ <!-- Various required feature settings for an automotive app. -->
+ <uses-feature
+ android:name="android.hardware.type.automotive"
+ android:required="true" />
+ <uses-feature
+ android:name="android.software.car.templates_host"
+ android:required="true" />
+ <uses-feature
+ android:name="android.hardware.wifi"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.screen.portrait"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.screen.landscape"
+ android:required="false" />
<application
android:label="@string/app_name"
diff --git a/car/app/app-samples/places/automotive/src/main/AndroidManifest.xml b/car/app/app-samples/places/automotive/src/main/AndroidManifest.xml
index dd07ac6..5add5d2 100644
--- a/car/app/app-samples/places/automotive/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/places/automotive/src/main/AndroidManifest.xml
@@ -27,7 +27,22 @@
<!-- For PlaceListMapTemplate -->
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES"/>
- <uses-feature android:name="android.software.car.templates_host" />
+ <!-- Various required feature settings for an automotive app. -->
+ <uses-feature
+ android:name="android.hardware.type.automotive"
+ android:required="true" />
+ <uses-feature
+ android:name="android.software.car.templates_host"
+ android:required="true" />
+ <uses-feature
+ android:name="android.hardware.wifi"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.screen.portrait"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.screen.landscape"
+ android:required="false" />
<application
android:label="@string/app_name"
diff --git a/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
index 3fd2ff87..6c2e940 100644
--- a/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
@@ -41,7 +41,22 @@
<uses-permission android:name="android.car.permission.READ_CAR_DISPLAY_UNITS"/>
<uses-permission android:name="android.car.permission.CAR_ENERGY_PORTS"/>
- <uses-feature android:name="android.software.car.templates_host" />
+ <!-- Various required feature settings for an automotive app. -->
+ <uses-feature
+ android:name="android.hardware.type.automotive"
+ android:required="true" />
+ <uses-feature
+ android:name="android.software.car.templates_host"
+ android:required="true" />
+ <uses-feature
+ android:name="android.hardware.wifi"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.screen.portrait"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.screen.landscape"
+ android:required="false" />
<application
android:label="@string/app_name"
diff --git a/car/app/app-testing/api/current.txt b/car/app/app-testing/api/current.txt
index 7c69ce0..fa317ae 100644
--- a/car/app/app-testing/api/current.txt
+++ b/car/app/app-testing/api/current.txt
@@ -6,12 +6,12 @@
}
public class ScreenController {
+ ctor public ScreenController(androidx.car.app.testing.TestCarContext, androidx.car.app.Screen);
method public androidx.car.app.testing.ScreenController create();
method public androidx.car.app.testing.ScreenController destroy();
- method public androidx.car.app.Screen get();
+ method public androidx.car.app.Screen getScreen();
method public Object? getScreenResult();
method public java.util.List<androidx.car.app.model.Template!> getTemplatesReturned();
- method public static androidx.car.app.testing.ScreenController of(androidx.car.app.testing.TestCarContext, androidx.car.app.Screen);
method public androidx.car.app.testing.ScreenController pause();
method public void reset();
method public androidx.car.app.testing.ScreenController resume();
@@ -20,10 +20,10 @@
}
public class SessionController {
+ ctor public SessionController(androidx.car.app.Session, androidx.car.app.testing.TestCarContext);
method public androidx.car.app.testing.SessionController create(android.content.Intent);
method public androidx.car.app.testing.SessionController destroy();
- method public androidx.car.app.Session get();
- method public static androidx.car.app.testing.SessionController of(androidx.car.app.Session, androidx.car.app.testing.TestCarContext);
+ method public androidx.car.app.Session getSession();
method public androidx.car.app.testing.SessionController pause();
method public androidx.car.app.testing.SessionController resume();
method public androidx.car.app.testing.SessionController start();
diff --git a/car/app/app-testing/api/public_plus_experimental_current.txt b/car/app/app-testing/api/public_plus_experimental_current.txt
index 7c69ce0..fa317ae 100644
--- a/car/app/app-testing/api/public_plus_experimental_current.txt
+++ b/car/app/app-testing/api/public_plus_experimental_current.txt
@@ -6,12 +6,12 @@
}
public class ScreenController {
+ ctor public ScreenController(androidx.car.app.testing.TestCarContext, androidx.car.app.Screen);
method public androidx.car.app.testing.ScreenController create();
method public androidx.car.app.testing.ScreenController destroy();
- method public androidx.car.app.Screen get();
+ method public androidx.car.app.Screen getScreen();
method public Object? getScreenResult();
method public java.util.List<androidx.car.app.model.Template!> getTemplatesReturned();
- method public static androidx.car.app.testing.ScreenController of(androidx.car.app.testing.TestCarContext, androidx.car.app.Screen);
method public androidx.car.app.testing.ScreenController pause();
method public void reset();
method public androidx.car.app.testing.ScreenController resume();
@@ -20,10 +20,10 @@
}
public class SessionController {
+ ctor public SessionController(androidx.car.app.Session, androidx.car.app.testing.TestCarContext);
method public androidx.car.app.testing.SessionController create(android.content.Intent);
method public androidx.car.app.testing.SessionController destroy();
- method public androidx.car.app.Session get();
- method public static androidx.car.app.testing.SessionController of(androidx.car.app.Session, androidx.car.app.testing.TestCarContext);
+ method public androidx.car.app.Session getSession();
method public androidx.car.app.testing.SessionController pause();
method public androidx.car.app.testing.SessionController resume();
method public androidx.car.app.testing.SessionController start();
diff --git a/car/app/app-testing/api/restricted_current.txt b/car/app/app-testing/api/restricted_current.txt
index 7c69ce0..fa317ae 100644
--- a/car/app/app-testing/api/restricted_current.txt
+++ b/car/app/app-testing/api/restricted_current.txt
@@ -6,12 +6,12 @@
}
public class ScreenController {
+ ctor public ScreenController(androidx.car.app.testing.TestCarContext, androidx.car.app.Screen);
method public androidx.car.app.testing.ScreenController create();
method public androidx.car.app.testing.ScreenController destroy();
- method public androidx.car.app.Screen get();
+ method public androidx.car.app.Screen getScreen();
method public Object? getScreenResult();
method public java.util.List<androidx.car.app.model.Template!> getTemplatesReturned();
- method public static androidx.car.app.testing.ScreenController of(androidx.car.app.testing.TestCarContext, androidx.car.app.Screen);
method public androidx.car.app.testing.ScreenController pause();
method public void reset();
method public androidx.car.app.testing.ScreenController resume();
@@ -20,10 +20,10 @@
}
public class SessionController {
+ ctor public SessionController(androidx.car.app.Session, androidx.car.app.testing.TestCarContext);
method public androidx.car.app.testing.SessionController create(android.content.Intent);
method public androidx.car.app.testing.SessionController destroy();
- method public androidx.car.app.Session get();
- method public static androidx.car.app.testing.SessionController of(androidx.car.app.Session, androidx.car.app.testing.TestCarContext);
+ method public androidx.car.app.Session getSession();
method public androidx.car.app.testing.SessionController pause();
method public androidx.car.app.testing.SessionController resume();
method public androidx.car.app.testing.SessionController start();
diff --git a/car/app/app-testing/src/main/java/androidx/car/app/testing/ScreenController.java b/car/app/app-testing/src/main/java/androidx/car/app/testing/ScreenController.java
index 9812453..8ac67da 100644
--- a/car/app/app-testing/src/main/java/androidx/car/app/testing/ScreenController.java
+++ b/car/app/app-testing/src/main/java/androidx/car/app/testing/ScreenController.java
@@ -54,15 +54,30 @@
*
* @throws NullPointerException if either {@code testCarContext} or {@code screen} are null
*/
+ public ScreenController(@NonNull TestCarContext testCarContext, @NonNull Screen screen) {
+ mScreen = requireNonNull(screen);
+ mTestCarContext = requireNonNull(testCarContext);
+
+ // Use reflection to inject the TestCarContext into the Screen.
+ try {
+ Field field = Screen.class.getDeclaredField("mCarContext");
+ field.setAccessible(true);
+ field.set(screen, testCarContext);
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException("Failed to set a test car context for testing", e);
+ }
+ }
+
+ /** Returns the {@link Screen} being controlled. */
@NonNull
- public static ScreenController of(
- @NonNull TestCarContext testCarContext, @NonNull Screen screen) {
- return new ScreenController(requireNonNull(screen), requireNonNull(testCarContext));
+ public Screen getScreen() {
+ return mScreen;
}
/** Resets values tracked by this {@link ScreenController}. */
public void reset() {
- mTestCarContext.getCarService(TestAppManager.class).resetTemplatesStoredForScreen(get());
+ mTestCarContext.getCarService(TestAppManager.class).resetTemplatesStoredForScreen(
+ getScreen());
}
/**
@@ -80,7 +95,7 @@
List<Template> templates = new ArrayList<>();
for (Pair<Screen, Template> pair :
mTestCarContext.getCarService(TestAppManager.class).getTemplatesReturned()) {
- if (pair.first == get()) {
+ if (pair.first == getScreen()) {
templates.add(pair.second);
}
}
@@ -182,12 +197,6 @@
return this;
}
- /** Returns the {@link Screen} being controlled. */
- @NonNull
- public Screen get() {
- return mScreen;
- }
-
private void putScreenOnStackIfNotTop() {
TestScreenManager testScreenManager = mTestCarContext.getCarService(
TestScreenManager.class);
@@ -196,20 +205,6 @@
}
}
- private ScreenController(Screen screen, TestCarContext testCarContext) {
- this.mScreen = screen;
- this.mTestCarContext = testCarContext;
-
- // Use reflection to inject the TestCarContext into the Screen.
- try {
- Field field = Screen.class.getDeclaredField("mCarContext");
- field.setAccessible(true);
- field.set(screen, testCarContext);
- } catch (ReflectiveOperationException e) {
- throw new IllegalStateException("Failed to set a test car context for testing", e);
- }
- }
-
@SuppressLint("BanUncheckedReflection")
private void dispatchLifecycleEvent(Event event) {
// Use reflection to call internal APIs for testing purposes.
diff --git a/car/app/app-testing/src/main/java/androidx/car/app/testing/SessionController.java b/car/app/app-testing/src/main/java/androidx/car/app/testing/SessionController.java
index 153290f..fc4d3d8 100644
--- a/car/app/app-testing/src/main/java/androidx/car/app/testing/SessionController.java
+++ b/car/app/app-testing/src/main/java/androidx/car/app/testing/SessionController.java
@@ -50,9 +50,29 @@
* @param context the {@link TestCarContext} that the {@code session} should use.
* @throws NullPointerException if {@code session} or {@code context} is {@code null}
*/
+ public SessionController(@NonNull Session session, @NonNull TestCarContext context) {
+ mSession = requireNonNull(session);
+ mTestCarContext = requireNonNull(context);
+
+ // Use reflection to inject the TestCarContext into the Session.
+ try {
+ Field registry = Session.class.getDeclaredField("mRegistry");
+ registry.setAccessible(true);
+ registry.set(session, mTestCarContext.getLifecycleOwner().mRegistry);
+
+ Field carContext = Session.class.getDeclaredField("mCarContext");
+ carContext.setAccessible(true);
+ carContext.set(session, mTestCarContext);
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException(
+ "Failed to set internal Session values for testing", e);
+ }
+ }
+
+ /** Returns the {@link Session} that is being controlled. */
@NonNull
- public static SessionController of(@NonNull Session session, @NonNull TestCarContext context) {
- return new SessionController(requireNonNull(session), requireNonNull(context));
+ public Session getSession() {
+ return mSession;
}
/**
@@ -147,29 +167,4 @@
return this;
}
-
- /** Returns the {@link Session} that is being controlled. */
- @NonNull
- public Session get() {
- return mSession;
- }
-
- private SessionController(Session session, TestCarContext context) {
- mSession = session;
- mTestCarContext = context;
-
- // Use reflection to inject the TestCarContext into the Session.
- try {
- Field registry = Session.class.getDeclaredField("mRegistry");
- registry.setAccessible(true);
- registry.set(session, mTestCarContext.getLifecycleOwner().mRegistry);
-
- Field carContext = Session.class.getDeclaredField("mCarContext");
- carContext.setAccessible(true);
- carContext.set(session, mTestCarContext);
- } catch (ReflectiveOperationException e) {
- throw new IllegalStateException(
- "Failed to set internal Session values for testing", e);
- }
- }
}
diff --git a/car/app/app-testing/src/test/java/androidx/car/app/testing/ScreenControllerTest.java b/car/app/app-testing/src/test/java/androidx/car/app/testing/ScreenControllerTest.java
index 76f4bc0..b8dfc87 100644
--- a/car/app/app-testing/src/test/java/androidx/car/app/testing/ScreenControllerTest.java
+++ b/car/app/app-testing/src/test/java/androidx/car/app/testing/ScreenControllerTest.java
@@ -64,7 +64,7 @@
MockitoAnnotations.initMocks(this);
mCarContext = TestCarContext.createCarContext(ApplicationProvider.getApplicationContext());
- mScreenController = ScreenController.of(mCarContext, mTestScreen);
+ mScreenController = new ScreenController(mCarContext, mTestScreen);
mTestScreen.getLifecycle().addObserver(mMockObserver);
}
@@ -160,7 +160,7 @@
@Test
public void getScreenResult() {
- Screen screen = mScreenController.get();
+ Screen screen = mScreenController.getScreen();
String result = "this is the result";
screen.setResult(result);
diff --git a/car/app/app-testing/src/test/java/androidx/car/app/testing/SessionControllerTest.java b/car/app/app-testing/src/test/java/androidx/car/app/testing/SessionControllerTest.java
index e2df7e1..ff403f5 100644
--- a/car/app/app-testing/src/test/java/androidx/car/app/testing/SessionControllerTest.java
+++ b/car/app/app-testing/src/test/java/androidx/car/app/testing/SessionControllerTest.java
@@ -67,7 +67,7 @@
};
- mSessionController = SessionController.of(session, mCarContext);
+ mSessionController = new SessionController(session, mCarContext);
session.getLifecycle().addObserver(mMockObserver);
}
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 73954f0..c252929 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -294,7 +294,6 @@
method public androidx.car.app.hardware.common.CarValue<java.lang.Integer!> getDistanceDisplayUnit();
method public androidx.car.app.hardware.common.CarValue<java.lang.Boolean!> getEnergyIsLow();
method public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getFuelPercent();
- method @Deprecated public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getRangeRemaining();
method public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getRangeRemainingMeters();
}
@@ -305,7 +304,6 @@
method public androidx.car.app.hardware.info.EnergyLevel.Builder setDistanceDisplayUnit(androidx.car.app.hardware.common.CarValue<java.lang.Integer!>);
method public androidx.car.app.hardware.info.EnergyLevel.Builder setEnergyIsLow(androidx.car.app.hardware.common.CarValue<java.lang.Boolean!>);
method public androidx.car.app.hardware.info.EnergyLevel.Builder setFuelPercent(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
- method @Deprecated public androidx.car.app.hardware.info.EnergyLevel.Builder setRangeRemaining(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
method public androidx.car.app.hardware.info.EnergyLevel.Builder setRangeRemainingMeters(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
}
@@ -354,7 +352,6 @@
@androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.RequiresCarApi(3) public final class Mileage {
method public androidx.car.app.hardware.common.CarValue<java.lang.Integer!> getDistanceDisplayUnit();
- method @Deprecated public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getOdometer();
method public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getOdometerMeters();
}
@@ -362,7 +359,6 @@
ctor public Mileage.Builder();
method public androidx.car.app.hardware.info.Mileage build();
method public androidx.car.app.hardware.info.Mileage.Builder setDistanceDisplayUnit(androidx.car.app.hardware.common.CarValue<java.lang.Integer!>);
- method @Deprecated public androidx.car.app.hardware.info.Mileage.Builder setOdometer(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
method public androidx.car.app.hardware.info.Mileage.Builder setOdometerMeters(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
}
@@ -381,9 +377,7 @@
}
@androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.RequiresCarApi(3) public final class Speed {
- method @Deprecated public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getDisplaySpeed();
method public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getDisplaySpeedMetersPerSecond();
- method @Deprecated public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getRawSpeed();
method public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getRawSpeedMetersPerSecond();
method public androidx.car.app.hardware.common.CarValue<java.lang.Integer!> getSpeedDisplayUnit();
}
@@ -391,9 +385,7 @@
public static final class Speed.Builder {
ctor public Speed.Builder();
method public androidx.car.app.hardware.info.Speed build();
- method @Deprecated public androidx.car.app.hardware.info.Speed.Builder setDisplaySpeed(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
method public androidx.car.app.hardware.info.Speed.Builder setDisplaySpeedMetersPerSecond(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
- method @Deprecated public androidx.car.app.hardware.info.Speed.Builder setRawSpeed(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
method public androidx.car.app.hardware.info.Speed.Builder setRawSpeedMetersPerSecond(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
method public androidx.car.app.hardware.info.Speed.Builder setSpeedDisplayUnit(androidx.car.app.hardware.common.CarValue<java.lang.Integer!>);
}
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index d10044b..8136625 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -297,7 +297,6 @@
method public androidx.car.app.hardware.common.CarValue<java.lang.Integer!> getDistanceDisplayUnit();
method public androidx.car.app.hardware.common.CarValue<java.lang.Boolean!> getEnergyIsLow();
method public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getFuelPercent();
- method @Deprecated public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getRangeRemaining();
method public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getRangeRemainingMeters();
}
@@ -308,7 +307,6 @@
method public androidx.car.app.hardware.info.EnergyLevel.Builder setDistanceDisplayUnit(androidx.car.app.hardware.common.CarValue<java.lang.Integer!>);
method public androidx.car.app.hardware.info.EnergyLevel.Builder setEnergyIsLow(androidx.car.app.hardware.common.CarValue<java.lang.Boolean!>);
method public androidx.car.app.hardware.info.EnergyLevel.Builder setFuelPercent(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
- method @Deprecated public androidx.car.app.hardware.info.EnergyLevel.Builder setRangeRemaining(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
method public androidx.car.app.hardware.info.EnergyLevel.Builder setRangeRemainingMeters(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
}
@@ -357,7 +355,6 @@
@androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.RequiresCarApi(3) public final class Mileage {
method public androidx.car.app.hardware.common.CarValue<java.lang.Integer!> getDistanceDisplayUnit();
- method @Deprecated public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getOdometer();
method public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getOdometerMeters();
}
@@ -365,7 +362,6 @@
ctor public Mileage.Builder();
method public androidx.car.app.hardware.info.Mileage build();
method public androidx.car.app.hardware.info.Mileage.Builder setDistanceDisplayUnit(androidx.car.app.hardware.common.CarValue<java.lang.Integer!>);
- method @Deprecated public androidx.car.app.hardware.info.Mileage.Builder setOdometer(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
method public androidx.car.app.hardware.info.Mileage.Builder setOdometerMeters(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
}
@@ -384,9 +380,7 @@
}
@androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.RequiresCarApi(3) public final class Speed {
- method @Deprecated public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getDisplaySpeed();
method public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getDisplaySpeedMetersPerSecond();
- method @Deprecated public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getRawSpeed();
method public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getRawSpeedMetersPerSecond();
method public androidx.car.app.hardware.common.CarValue<java.lang.Integer!> getSpeedDisplayUnit();
}
@@ -394,9 +388,7 @@
public static final class Speed.Builder {
ctor public Speed.Builder();
method public androidx.car.app.hardware.info.Speed build();
- method @Deprecated public androidx.car.app.hardware.info.Speed.Builder setDisplaySpeed(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
method public androidx.car.app.hardware.info.Speed.Builder setDisplaySpeedMetersPerSecond(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
- method @Deprecated public androidx.car.app.hardware.info.Speed.Builder setRawSpeed(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
method public androidx.car.app.hardware.info.Speed.Builder setRawSpeedMetersPerSecond(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
method public androidx.car.app.hardware.info.Speed.Builder setSpeedDisplayUnit(androidx.car.app.hardware.common.CarValue<java.lang.Integer!>);
}
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 73954f0..c252929 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -294,7 +294,6 @@
method public androidx.car.app.hardware.common.CarValue<java.lang.Integer!> getDistanceDisplayUnit();
method public androidx.car.app.hardware.common.CarValue<java.lang.Boolean!> getEnergyIsLow();
method public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getFuelPercent();
- method @Deprecated public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getRangeRemaining();
method public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getRangeRemainingMeters();
}
@@ -305,7 +304,6 @@
method public androidx.car.app.hardware.info.EnergyLevel.Builder setDistanceDisplayUnit(androidx.car.app.hardware.common.CarValue<java.lang.Integer!>);
method public androidx.car.app.hardware.info.EnergyLevel.Builder setEnergyIsLow(androidx.car.app.hardware.common.CarValue<java.lang.Boolean!>);
method public androidx.car.app.hardware.info.EnergyLevel.Builder setFuelPercent(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
- method @Deprecated public androidx.car.app.hardware.info.EnergyLevel.Builder setRangeRemaining(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
method public androidx.car.app.hardware.info.EnergyLevel.Builder setRangeRemainingMeters(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
}
@@ -354,7 +352,6 @@
@androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.RequiresCarApi(3) public final class Mileage {
method public androidx.car.app.hardware.common.CarValue<java.lang.Integer!> getDistanceDisplayUnit();
- method @Deprecated public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getOdometer();
method public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getOdometerMeters();
}
@@ -362,7 +359,6 @@
ctor public Mileage.Builder();
method public androidx.car.app.hardware.info.Mileage build();
method public androidx.car.app.hardware.info.Mileage.Builder setDistanceDisplayUnit(androidx.car.app.hardware.common.CarValue<java.lang.Integer!>);
- method @Deprecated public androidx.car.app.hardware.info.Mileage.Builder setOdometer(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
method public androidx.car.app.hardware.info.Mileage.Builder setOdometerMeters(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
}
@@ -381,9 +377,7 @@
}
@androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.RequiresCarApi(3) public final class Speed {
- method @Deprecated public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getDisplaySpeed();
method public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getDisplaySpeedMetersPerSecond();
- method @Deprecated public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getRawSpeed();
method public androidx.car.app.hardware.common.CarValue<java.lang.Float!> getRawSpeedMetersPerSecond();
method public androidx.car.app.hardware.common.CarValue<java.lang.Integer!> getSpeedDisplayUnit();
}
@@ -391,9 +385,7 @@
public static final class Speed.Builder {
ctor public Speed.Builder();
method public androidx.car.app.hardware.info.Speed build();
- method @Deprecated public androidx.car.app.hardware.info.Speed.Builder setDisplaySpeed(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
method public androidx.car.app.hardware.info.Speed.Builder setDisplaySpeedMetersPerSecond(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
- method @Deprecated public androidx.car.app.hardware.info.Speed.Builder setRawSpeed(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
method public androidx.car.app.hardware.info.Speed.Builder setRawSpeedMetersPerSecond(androidx.car.app.hardware.common.CarValue<java.lang.Float!>);
method public androidx.car.app.hardware.info.Speed.Builder setSpeedDisplayUnit(androidx.car.app.hardware.common.CarValue<java.lang.Integer!>);
}
diff --git a/car/app/app/build.gradle b/car/app/app/build.gradle
index a017570..b6e0f42 100644
--- a/car/app/app/build.gradle
+++ b/car/app/app/build.gradle
@@ -16,6 +16,22 @@
import androidx.build.LibraryGroups
import androidx.build.LibraryType
+import androidx.build.Release
+import androidx.build.checkapi.LibraryApiTaskConfig
+import androidx.build.metalava.MetalavaRunnerKt
+import androidx.build.uptodatedness.EnableCachingKt
+import androidx.build.Version
+
+import com.android.build.gradle.LibraryExtension
+import com.android.build.gradle.api.AndroidSourceDirectorySet
+import com.android.build.gradle.api.SourceKind
+import com.google.common.io.Files
+import org.apache.commons.io.FileUtils
+
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+import static androidx.build.dependencies.DependenciesKt.*
plugins {
id("AndroidXPlugin")
@@ -47,6 +63,10 @@
testImplementation project(path: ':car:app:app-testing')
}
+project.ext {
+ latestCarAppApiLevel = "3"
+}
+
android {
defaultConfig {
minSdkVersion 23
@@ -74,3 +94,353 @@
inceptionYear = "2020"
description = "Build navigation, parking, and charging apps for Android Auto"
}
+
+// Use MetalavaRunnerKt to execute Metalava operations. MetalavaRunnerKt is defined in the buildSrc
+// project and provides convience methods for interacting with Metalava. Configruation required
+// for MetalavaRunnerKt is taken from buildSrc/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt.
+class ProtocolApiTask extends DefaultTask {
+ private final WorkerExecutor workerExecutor
+
+ @Inject
+ ProtocolApiTask(WorkerExecutor workerExecutor) {
+ this.workerExecutor = workerExecutor
+ }
+
+ @Internal
+ def getLibraryVariant() {
+ LibraryExtension extension = project.extensions.getByType(LibraryExtension.class)
+ LibraryApiTaskConfig config = new LibraryApiTaskConfig(extension)
+ return config.library.libraryVariants.find({
+ it.name == Release.DEFAULT_PUBLISH_CONFIG
+ })
+ }
+
+ @Internal
+ def getLibraryExtension() {
+ return project.extensions.getByType(LibraryExtension.class)
+ }
+
+ @Internal
+ def getSourceDirs() {
+ List<File> sourceDirs = new ArrayList<File>()
+ for (ConfigurableFileTree fileTree : getLibraryVariant().getSourceFolders(SourceKind
+ .JAVA)) {
+ if (fileTree.getDir().exists()) {
+ sourceDirs.add(fileTree.getDir())
+ }
+ }
+ return sourceDirs
+ }
+
+ @Internal
+ def runMetalava(List<String> additionalArgs) {
+ FileCollection metalavaClasspath = MetalavaRunnerKt.getMetalavaClasspath(project)
+ FileCollection dependencyClasspath = getLibraryVariant().getCompileClasspath(null).filter {
+ it.exists()
+ }
+
+ List<File> classpath = new ArrayList<File>()
+ classpath.addAll(getLibraryExtension().bootClasspath)
+ classpath.addAll(dependencyClasspath)
+
+ List<String> standardArgs = [
+ "--classpath",
+ classpath.join(File.pathSeparator),
+ '--source-path',
+ sourceDirs.join(File.pathSeparator),
+ '--format=v4',
+ '--output-kotlin-nulls=yes',
+ '--quiet'
+ ]
+ standardArgs.addAll(additionalArgs)
+
+ MetalavaRunnerKt.runMetalavaWithArgs(
+ metalavaClasspath,
+ standardArgs,
+ workerExecutor,
+ )
+ }
+}
+
+// Use Metalava to generate an API signature file that only includes public API that is annotated
+// with @androidx.car.app.annotations.CarProtocol
+class GenerateProtocolApiTask extends ProtocolApiTask {
+ @Inject
+ GenerateProtocolApiTask(WorkerExecutor workerExecutor) {
+ super(workerExecutor)
+ }
+
+ @InputFiles
+ @PathSensitive(PathSensitivity.RELATIVE)
+ FileCollection inputSourceDirs = project.files() // Re-run on source changes
+
+ @OutputFile
+ File generatedApi
+
+ @TaskAction
+ def exec() {
+ List<String> args = [
+ '--api',
+ generatedApi.toString(),
+ "--show-annotation",
+ "@androidx.car.app.annotations.CarProtocol",
+ "--hide",
+ "UnhiddenSystemApi"
+ ]
+
+ runMetalava(args)
+ }
+}
+
+// Compare two files and throw an exception if they are not equivalent. This task is used to check
+// for diffs to generated Metalava API signature files, which would indicate a protocol API change.
+class CheckProtocolApiTask extends DefaultTask {
+ @InputFile
+ @Optional
+ File currentApi
+
+ @InputFile
+ File generatedApi
+
+ def summarizeDiff(File a, File b) {
+ if (!a.exists()) {
+ return "$a does not exist"
+ }
+ if (!b.exists()) {
+ return "$b does not exist"
+ }
+ Process process = new ProcessBuilder(Arrays.asList("diff", a.toString(), b.toString()))
+ .redirectOutput(ProcessBuilder.Redirect.PIPE)
+ .start()
+ process.waitFor(5, TimeUnit.SECONDS)
+ List<String> diffLines = process.inputStream.newReader().readLines()
+ int maxSummaryLines = 50
+ if (diffLines.size() > maxSummaryLines) {
+ diffLines = diffLines.subList(0, maxSummaryLines)
+ diffLines.add("[long diff was truncated]")
+ }
+ return String.join("\n", diffLines)
+ }
+
+ @TaskAction
+ def exec() {
+ if (currentApi == null) {
+ return
+ }
+
+ if (!FileUtils.contentEquals(currentApi, generatedApi)) {
+ String diff = summarizeDiff(currentApi, generatedApi)
+ String message = """API definition has changed
+
+ Declared definition is $currentApi
+ True definition is $generatedApi
+
+ Please run `./gradlew updateProtocolApi` to confirm these changes are
+ intentional by updating the API definition.
+
+ Difference between these files:
+ $diff"""
+
+ throw new GradleException("Protocol changes detected!\n$message")
+ }
+ }
+}
+
+// Check for compatibility breaking changes between two Metalava API signature files. This task is
+// used to detect backward-compatibility breaking changes to protocol API.
+class CheckProtocolApiCompatibilityTask extends ProtocolApiTask {
+ @Inject
+ CheckProtocolApiCompatibilityTask(WorkerExecutor workerExecutor) {
+ super(workerExecutor)
+ }
+
+ @InputFile
+ @Optional
+ File previousApi
+
+ @InputFile
+ @Optional
+ File generatedApi
+
+ @TaskAction
+ def exec() {
+ if (previousApi == null || generatedApi == null) {
+ return
+ }
+
+ List<String> args = [
+ '--source-files',
+ generatedApi.toString(),
+ "--check-compatibility:api:released",
+ previousApi.toString()
+ ]
+ runMetalava(args)
+ }
+}
+
+// Update protocol API signature file for current Car API level to reflect the state of current
+// protocol API in the project.
+class UpdateProtocolApiTask extends DefaultTask {
+ @InputDirectory
+ File protocolDir
+
+ @InputFile
+ File generatedApi
+
+ @InputFile
+ @Optional
+ File currentApi
+
+ @TaskAction
+ def exec() {
+ // The expected Car protocol API signature file for the current Car API level and project
+ // version
+ File updatedApi = new File(protocolDir, String.format(
+ "protocol-%s_%s.txt", project.latestCarAppApiLevel, project.version))
+
+ // Determine whether this API level was previously released by checking whether the project
+ // version matches
+ boolean alreadyReleased = currentApi != updatedApi
+
+ // Determine whether this API level is final (Only snapshot, dev, alpha releases are
+ // non-final)
+ boolean isCurrentApiFinal = false
+ if (currentApi != null) {
+ String currentApiFileName = currentApi.name
+ int versionStart = currentApiFileName.indexOf("_")
+ int versionEnd = currentApiFileName.indexOf(".txt")
+
+ String parsedCurrentVersion = currentApiFileName.substring(versionStart + 1, versionEnd)
+ Version currentVersion = new Version(parsedCurrentVersion)
+ isCurrentApiFinal = currentVersion.isFinalApi()
+ }
+
+ if (currentApi != null && alreadyReleased && isCurrentApiFinal) {
+ throw new GradleException("Version has changed for current Car API level. Increment " +
+ "Car API level before making protocol API changes")
+ }
+
+ // Overwrite protocol API signature file for current Car API level
+ Files.copy(generatedApi, updatedApi)
+ }
+}
+
+class ApiLevelFileWriterTask extends DefaultTask {
+ @Input
+ String carApiLevel = project.latestCarAppApiLevel
+
+ @OutputFile
+ File apiLevelFile
+
+ @TaskAction
+ def exec() {
+ PrintWriter writer = new PrintWriter(apiLevelFile)
+ writer.println(carApiLevel)
+ writer.close()
+ }
+}
+
+// Paths and file locations required for protocol API operations
+class ProtocolLocation {
+ File buildDir
+ File protocolDir
+ File generatedApi
+ File currentApi
+ File previousApi
+
+ def getProtocolApiFile(int carApiLevel) {
+ File[] apiFiles = protocolDir.listFiles(new FilenameFilter() {
+ boolean accept(File dir, String name) {
+ return name.startsWith(String.format("protocol-%d_", carApiLevel))
+ }
+ })
+
+ if (apiFiles == null || apiFiles.size() == 0) {
+ return null
+ } else if (apiFiles.size() > 1) {
+ StringBuilder builder = new StringBuilder()
+ for (File file : currentApis) {
+ builder.append(file.path)
+ builder.append("\n")
+ }
+
+ throw new GradleException(
+ String.format("Multiple API signature files found for Car API level %s\n%s",
+ carApiLevel, builder.toString()))
+ }
+
+ return apiFiles[0]
+ }
+
+ ProtocolLocation(Project project) {
+ buildDir = new File(project.buildDir, "/protocol/")
+ generatedApi = new File(buildDir, "/generated.txt")
+ protocolDir = new File(project.projectDir, "/protocol/")
+ int currentApiLevel = Integer.parseInt(project.latestCarAppApiLevel)
+ currentApi = getProtocolApiFile(currentApiLevel)
+ previousApi = getProtocolApiFile(currentApiLevel - 1)
+ }
+}
+
+def RESOURCE_DIRECTORY = "generatedResources"
+def API_LEVEL_FILE_PATH = "$RESOURCE_DIRECTORY/car-app-api.level"
+
+LibraryExtension library = project.extensions.getByType(LibraryExtension.class)
+
+// afterEvaluate required to read extension properties
+afterEvaluate {
+ task writeCarApiLevelFile(type: ApiLevelFileWriterTask) {
+ File artifactName = new File(buildDir, API_LEVEL_FILE_PATH)
+ apiLevelFile = artifactName
+ }
+
+ AndroidSourceDirectorySet resources = library.sourceSets.getByName("main").resources
+ Set<File> resFiles = new HashSet<>()
+ resFiles.add(resources.srcDirs)
+ resFiles.add(new File(buildDir, RESOURCE_DIRECTORY))
+ resources.srcDirs(resFiles)
+ Set<String> includes = resources.includes
+ if (!includes.isEmpty()) {
+ includes.add("*.level")
+ resources.setIncludes(includes)
+ }
+
+ ProtocolLocation projectProtocolLocation = new ProtocolLocation(project)
+ task generateProtocolApi(type: GenerateProtocolApiTask) {
+ description = "Generate an API signature file for the classes annotated with @CarProtocol"
+ generatedApi = projectProtocolLocation.generatedApi
+ dependsOn(assemble)
+ }
+ task checkProtocolApiCompat(type: CheckProtocolApiCompatibilityTask) {
+ description = "Check for BREAKING changes to the protocol API"
+ if (projectProtocolLocation.previousApi != null) {
+ previousApi = projectProtocolLocation.previousApi
+ }
+ generatedApi = projectProtocolLocation.generatedApi
+ dependsOn(generateProtocolApi)
+ }
+ task checkProtocolApi(type: CheckProtocolApiTask) {
+ description = "Check for changes to the protocol API"
+ generatedApi = projectProtocolLocation.generatedApi
+ currentApi = projectProtocolLocation.currentApi
+ dependsOn(checkProtocolApiCompat)
+ }
+ task updateProtocolApi(type: UpdateProtocolApiTask) {
+ description = "Update protocol API signature file for current Car API level to reflect" +
+ "the current state of the protocol API in the source tree."
+ protocolDir = projectProtocolLocation.protocolDir
+ generatedApi = projectProtocolLocation.generatedApi
+ currentApi = projectProtocolLocation.currentApi
+ dependsOn(checkProtocolApiCompat)
+ }
+ EnableCachingKt.cacheEvenIfNoOutputs(checkProtocolApi)
+ EnableCachingKt.cacheEvenIfNoOutputs(checkProtocolApiCompat)
+ checkApi.dependsOn(checkProtocolApi)
+}
+
+library.libraryVariants.all { variant ->
+ variant.processJavaResourcesProvider.configure {
+ it.dependsOn(writeCarApiLevelFile)
+ }
+}
+
diff --git a/car/app/app/src/main/java/androidx/car/app/CarContext.java b/car/app/app/src/main/java/androidx/car/app/CarContext.java
index 84b6922..1107d0c 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarContext.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarContext.java
@@ -277,8 +277,7 @@
* <dt>An {@link Intent} to start this app in the car.
* <dd>The component name of the intent must be the one for the {@link CarAppService} that
* contains this {@link CarContext}. If the component name is for a different
- * component, the
- * method will throw a {@link SecurityException}.
+ * component, the method will throw a {@link SecurityException}.
* </dl>
*
* <p><b>This method should not be called until the {@link Lifecycle.State} of the context's
diff --git a/car/app/app/src/main/java/androidx/car/app/hardware/info/EnergyLevel.java b/car/app/app/src/main/java/androidx/car/app/hardware/info/EnergyLevel.java
index 480eae7..67633bb 100644
--- a/car/app/app/src/main/java/androidx/car/app/hardware/info/EnergyLevel.java
+++ b/car/app/app/src/main/java/androidx/car/app/hardware/info/EnergyLevel.java
@@ -46,11 +46,6 @@
@NonNull
private final CarValue<Boolean> mEnergyIsLow;
- // TODO(b/192106888): Remove when new values fully supported by Android Auto Host.
- @Keep
- @Nullable
- private final CarValue<Float> mRangeRemaining;
-
@Keep
@Nullable
private final CarValue<Float> mRangeRemainingMeters;
@@ -80,23 +75,7 @@
/** Returns the range remaining from the car hardware in meters. */
@NonNull
public CarValue<Float> getRangeRemainingMeters() {
- if (mRangeRemainingMeters != null) {
- return requireNonNull(mRangeRemainingMeters);
- }
- return requireNonNull(mRangeRemaining);
- }
-
- // TODO(b/192106888): Remove when new values fully supported by Android Auto Host.
-
- /**
- * Returns the range remaining from the car hardware in meters.
- *
- * @deprecated use {@link #getRangeRemainingMeters()}
- */
- @NonNull
- @Deprecated
- public CarValue<Float> getRangeRemaining() {
- return getRangeRemainingMeters();
+ return requireNonNull(mRangeRemainingMeters);
}
/**
@@ -153,7 +132,6 @@
mBatteryPercent = requireNonNull(builder.mBatteryPercent);
mFuelPercent = requireNonNull(builder.mFuelPercent);
mEnergyIsLow = requireNonNull(builder.mEnergyIsLow);
- mRangeRemaining = null;
mRangeRemainingMeters = requireNonNull(builder.mRangeRemainingMeters);
mDistanceDisplayUnit = requireNonNull(builder.mDistanceDisplayUnit);
}
@@ -163,7 +141,6 @@
mBatteryPercent = CarValue.UNIMPLEMENTED_FLOAT;
mFuelPercent = CarValue.UNIMPLEMENTED_FLOAT;
mEnergyIsLow = CarValue.UNIMPLEMENTED_BOOLEAN;
- mRangeRemaining = null;
mRangeRemainingMeters = CarValue.UNIMPLEMENTED_FLOAT;
mDistanceDisplayUnit = CarValue.UNIMPLEMENTED_INTEGER;
}
@@ -206,21 +183,6 @@
return this;
}
- // TODO(b/192106888): Remove when new values fully supported by Android Auto Host.
-
- /**
- * Sets the range of the remaining fuel in meters.
- *
- * @throws NullPointerException if {@code rangeRemaining} is {@code null}
- * @deprecated use {@link #setRangeRemainingMeters}
- */
- @NonNull
- @Deprecated
- public Builder setRangeRemaining(@NonNull CarValue<Float> rangeRemainingMeters) {
- mRangeRemainingMeters = requireNonNull(rangeRemainingMeters);
- return this;
- }
-
/**
* Sets the range of the remaining fuel in meters.
*
diff --git a/car/app/app/src/main/java/androidx/car/app/hardware/info/Mileage.java b/car/app/app/src/main/java/androidx/car/app/hardware/info/Mileage.java
index 68df7a9..ae7111a 100644
--- a/car/app/app/src/main/java/androidx/car/app/hardware/info/Mileage.java
+++ b/car/app/app/src/main/java/androidx/car/app/hardware/info/Mileage.java
@@ -33,11 +33,6 @@
@CarProtocol
@RequiresCarApi(3)
public final class Mileage {
- // TODO(b/192106888): Remove when new values fully supported by Android Auto Host.
- @Keep
- @Nullable
- private final CarValue<Float> mOdometer;
-
@Keep
@Nullable
private final CarValue<Float> mOdometerMeters;
@@ -49,23 +44,7 @@
/** Returns the value of the odometer from the car hardware in meters. */
@NonNull
public CarValue<Float> getOdometerMeters() {
- if (mOdometerMeters != null) {
- return requireNonNull(mOdometerMeters);
- }
- return requireNonNull(mOdometer);
- }
-
- // TODO(b/192106888): Remove when new values fully supported by Android Auto Host.
-
- /**
- * Returns the value of the odometer from the car hardware in meters.
- *
- * @deprecated use {@link #getOdometerMeters()}
- */
- @NonNull
- @Deprecated
- public CarValue<Float> getOdometer() {
- return getOdometerMeters();
+ return requireNonNull(mOdometerMeters);
}
/**
@@ -108,14 +87,12 @@
}
Mileage(Builder builder) {
- mOdometer = null;
mOdometerMeters = requireNonNull(builder.mOdometerMeters);
mDistanceDisplayUnit = requireNonNull(builder.mDistanceDisplayUnit);
}
/** Constructs an empty instance, used by serialization code. */
private Mileage() {
- mOdometer = CarValue.UNIMPLEMENTED_FLOAT;
mOdometerMeters = CarValue.UNIMPLEMENTED_FLOAT;
mDistanceDisplayUnit = CarValue.UNIMPLEMENTED_INTEGER;
}
@@ -126,21 +103,6 @@
CarValue<@CarDistanceUnit Integer> mDistanceDisplayUnit =
CarValue.UNIMPLEMENTED_INTEGER;
- // TODO(b/192106888): Remove when new values fully supported by Android Auto Host.
-
- /**
- * Sets the odometer value in meters.
- *
- * @throws NullPointerException if {@code odometer} is {@code null}
- * @deprecated use {@link #setOdometerMeters}
- */
- @NonNull
- @Deprecated
- public Builder setOdometer(@NonNull CarValue<Float> odometer) {
- mOdometerMeters = requireNonNull(odometer);
- return this;
- }
-
/**
* Sets the odometer value in meters.
*
diff --git a/car/app/app/src/main/java/androidx/car/app/hardware/info/Speed.java b/car/app/app/src/main/java/androidx/car/app/hardware/info/Speed.java
index 9290f84..9a4424c 100644
--- a/car/app/app/src/main/java/androidx/car/app/hardware/info/Speed.java
+++ b/car/app/app/src/main/java/androidx/car/app/hardware/info/Speed.java
@@ -35,20 +35,10 @@
@CarProtocol
@RequiresCarApi(3)
public final class Speed {
- // TODO(b/192106888): Remove when new values fully supported by Android Auto Host.
- @Keep
- @Nullable
- private final CarValue<Float> mRawSpeed;
-
@Keep
@Nullable
private final CarValue<Float> mRawSpeedMetersPerSecond;
- // TODO(b/192106888): Remove when new values fully supported by Android Auto Host.
- @Keep
- @Nullable
- private final CarValue<Float> mDisplaySpeed;
-
@Keep
@Nullable
private final CarValue<Float> mDisplaySpeedMetersPerSecond;
@@ -65,26 +55,7 @@
*/
@NonNull
public CarValue<Float> getRawSpeedMetersPerSecond() {
- if (mRawSpeedMetersPerSecond != null) {
- return requireNonNull(mRawSpeedMetersPerSecond);
- }
- return requireNonNull(mRawSpeed);
- }
-
- // TODO(b/192106888): Remove when new values fully supported by Android Auto Host.
-
- /**
- * Returns the raw speed of the car in meters/second.
- *
- * <p>The value is positive when the vehicle is moving forward, negative when moving
- * backwards and zero when stopped.
- *
- * @deprecated use {@link #getRawSpeedMetersPerSecond()}
- */
- @NonNull
- @Deprecated
- public CarValue<Float> getRawSpeed() {
- return getRawSpeedMetersPerSecond();
+ return requireNonNull(mRawSpeedMetersPerSecond);
}
/**
@@ -98,29 +69,7 @@
*/
@NonNull
public CarValue<Float> getDisplaySpeedMetersPerSecond() {
- if (mRawSpeedMetersPerSecond != null) {
- return requireNonNull(mDisplaySpeedMetersPerSecond);
- }
- return requireNonNull(mDisplaySpeed);
- }
-
- // TODO(b/192106888): Remove when new values fully supported by Android Auto Host.
-
- /**
- * Returns the display speed of the car in meters/second.
- *
- * <p>Some cars display a slightly slower speed than the actual speed. This is usually
- * displayed on the speedometer.
- *
- * <p>The value is positive when the vehicle is moving forward, negative when moving
- * backwards and zero when stopped.
- *
- * @deprecated use {@link #getDisplaySpeedMetersPerSecond()}
- */
- @NonNull
- @Deprecated
- public CarValue<Float> getDisplaySpeed() {
- return getDisplaySpeedMetersPerSecond();
+ return requireNonNull(mDisplaySpeedMetersPerSecond);
}
/**
@@ -168,18 +117,14 @@
}
Speed(Builder builder) {
- mRawSpeed = null;
mRawSpeedMetersPerSecond = requireNonNull(builder.mRawSpeedMetersPerSecond);
- mDisplaySpeed = null;
mDisplaySpeedMetersPerSecond = requireNonNull(builder.mDisplaySpeedMetersPerSecond);
mSpeedDisplayUnit = requireNonNull(builder.mSpeedDisplayUnit);
}
/** Constructs an empty instance, used by serialization code. */
private Speed() {
- mRawSpeed = null;
mRawSpeedMetersPerSecond = CarValue.UNIMPLEMENTED_FLOAT;
- mDisplaySpeed = null;
mDisplaySpeedMetersPerSecond = CarValue.UNIMPLEMENTED_FLOAT;
mSpeedDisplayUnit = CarValue.UNIMPLEMENTED_INTEGER;
}
@@ -190,21 +135,6 @@
CarValue<Float> mDisplaySpeedMetersPerSecond = CarValue.UNIMPLEMENTED_FLOAT;
CarValue<@CarSpeedUnit Integer> mSpeedDisplayUnit = CarValue.UNIMPLEMENTED_INTEGER;
- // TODO(b/192106888): Remove when new values fully supported by Android Auto Host.
-
- /**
- * Sets the raw speed in meters per second.
- *
- * @throws NullPointerException if {@code rawSpeedMetersPerSecond} is {@code null}
- * @deprecated use {@link #setRawSpeedMetersPerSecond}
- */
- @Deprecated
- @NonNull
- public Builder setRawSpeed(@NonNull CarValue<Float> rawSpeed) {
- mRawSpeedMetersPerSecond = requireNonNull(rawSpeed);
- return this;
- }
-
/**
* Sets the raw speed in meters per second.
*
@@ -217,21 +147,6 @@
return this;
}
- // TODO(b/192106888): Remove when new values fully supported by Android Auto Host.
-
- /**
- * Sets the display speed in meters per second. *
- *
- * @throws NullPointerException if {@code displaySpeedMetersPerSecond} is {@code null}
- * @deprecated use {@link #setDisplaySpeedMetersPerSecond}
- */
- @Deprecated
- @NonNull
- public Builder setDisplaySpeed(@NonNull CarValue<Float> displaySpeed) {
- mDisplaySpeedMetersPerSecond = requireNonNull(displaySpeed);
- return this;
- }
-
/**
* Sets the display speed in meters per second. *
*
diff --git a/car/app/app/src/main/java/androidx/car/app/notification/CarPendingIntent.java b/car/app/app/src/main/java/androidx/car/app/notification/CarPendingIntent.java
index 42399eb..f321557 100644
--- a/car/app/app/src/main/java/androidx/car/app/notification/CarPendingIntent.java
+++ b/car/app/app/src/main/java/androidx/car/app/notification/CarPendingIntent.java
@@ -61,7 +61,10 @@
/**
* Creates a {@link PendingIntent} that can be sent in a notification action which will allow
- * your car app to be started when the user clicks on the action.
+ * the targeted car app to be started when the user clicks on the action.
+ *
+ * <p>See {@link CarContext#startCarApp} for the supported intents that can be passed to this
+ * method.
*
* <p>Here is an example of usage of this method when setting a notification's intent:
*
@@ -86,11 +89,11 @@
* @throws InvalidParameterException if the {@code intent} is not for starting a navigation
* or a phone call and does not have the target car app's
* component name
- * @throws SecurityException if the {@code intent} does not follow the allowed values
- * as is defined in {@link CarContext#startCarApp(Intent)}
+ * @throws SecurityException if the {@code intent} is for a different component than the
+ * one associated with the input {@code context}
*
- * @return an existing or new PendingIntent matching the given parameters. May return null
- * only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
+ * @return an existing or new PendingIntent matching the given parameters. May return {@code
+ * null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
*/
@NonNull
public static PendingIntent getCarApp(@NonNull Context context, int requestCode,
diff --git a/car/app/app/src/main/java/androidx/car/app/versioning/CarAppApiLevels.java b/car/app/app/src/main/java/androidx/car/app/versioning/CarAppApiLevels.java
index 7accc41..bdc8821 100644
--- a/car/app/app/src/main/java/androidx/car/app/versioning/CarAppApiLevels.java
+++ b/car/app/app/src/main/java/androidx/car/app/versioning/CarAppApiLevels.java
@@ -16,9 +16,16 @@
package androidx.car.app.versioning;
+import static java.util.Objects.requireNonNull;
+
import androidx.annotation.RestrictTo;
import androidx.car.app.CarContext;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
/**
* API levels supported by this library.
*
@@ -65,6 +72,8 @@
@CarAppApiLevel
public static final int UNKNOWN = 0;
+ private static final String CAR_API_LEVEL_FILE = "car-app-api.level";
+
/**
* Returns whether the given integer is a valid {@link CarAppApiLevel}
*
@@ -80,7 +89,37 @@
*/
@CarAppApiLevel
public static int getLatest() {
- return LEVEL_3;
+ // The latest Car API level is defined as java resource, generated via build.gradle. This
+ // has to be read through the class loader because we do not have access to the context
+ // to retrieve an Android resource.
+ ClassLoader classLoader = requireNonNull(CarAppApiLevels.class.getClassLoader());
+ InputStream inputStream = classLoader.getResourceAsStream(CAR_API_LEVEL_FILE);
+
+ if (inputStream == null) {
+ throw new IllegalStateException(String.format("Car API level file %s not found",
+ CAR_API_LEVEL_FILE));
+ }
+
+ try {
+ InputStreamReader streamReader = new InputStreamReader(inputStream);
+ BufferedReader reader = new BufferedReader(streamReader);
+ String line = reader.readLine();
+
+ switch (Integer.parseInt(line)) {
+ case 0:
+ return UNKNOWN;
+ case 1:
+ return LEVEL_1;
+ case 2:
+ return LEVEL_2;
+ case 3:
+ return LEVEL_3;
+ default:
+ throw new IllegalStateException("Undefined Car API level: " + line);
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to read Car API level file");
+ }
}
/**
diff --git a/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java b/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java
index b46b476..faf1163 100644
--- a/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java
@@ -103,7 +103,8 @@
mCarContext = TestCarContext.createCarContext(
ApplicationProvider.getApplicationContext());
Session session = createTestSession();
- SessionController.of(session, mCarContext);
+ // Injects the TestCarContext into the test session.
+ new SessionController(session, mCarContext);
return session;
}
};
diff --git a/car/app/app/src/test/java/androidx/car/app/ScreenTest.java b/car/app/app/src/test/java/androidx/car/app/ScreenTest.java
index cdda07c..13084ad 100644
--- a/car/app/app/src/test/java/androidx/car/app/ScreenTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/ScreenTest.java
@@ -83,7 +83,7 @@
}
};
- ScreenController.of(mCarContext, mScreen).create().start().resume();
+ new ScreenController(mCarContext, mScreen).create().start().resume();
}
@Test
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt
index 806f096..f842dd9 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt
@@ -21,10 +21,12 @@
import androidx.compose.desktop.DesktopMaterialTheme
import androidx.compose.desktop.LocalAppWindow
import androidx.compose.desktop.Window
+import androidx.compose.foundation.ExperimentalDesktopApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.VerticalScrollbar
+import androidx.compose.foundation.mouseClickable
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -82,10 +84,6 @@
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.plus
import androidx.compose.ui.input.key.shortcuts
-import androidx.compose.ui.input.pointer.PointerEvent
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerInputFilter
-import androidx.compose.ui.input.pointer.PointerInputModifier
import androidx.compose.ui.input.pointer.pointerMoveFilter
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.imageResource
@@ -107,7 +105,6 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
-import java.awt.event.MouseEvent
private const val title = "Desktop Compose Elements"
@@ -186,7 +183,10 @@
)
}
-@OptIn(ExperimentalComposeUiApi::class)
+@OptIn(
+ ExperimentalComposeUiApi::class,
+ ExperimentalDesktopApi::class
+)
@Composable
private fun ScrollableContent(scrollState: ScrollState) {
val amount = remember { mutableStateOf(0f) }
@@ -351,7 +351,6 @@
}
Row(verticalAlignment = Alignment.CenterVertically) {
- var lastEvent by remember { mutableStateOf<MouseEvent?>(null) }
Button(
modifier = Modifier.padding(4.dp),
onClick = {
@@ -361,22 +360,27 @@
Text("Base")
}
- Text(
- modifier = object : PointerInputModifier {
- override val pointerInputFilter = object : PointerInputFilter() {
- override fun onPointerEvent(
- pointerEvent: PointerEvent,
- pass: PointerEventPass,
- bounds: IntSize
- ) {
- lastEvent = pointerEvent.mouseEvent
- }
+ var clickableText by remember { mutableStateOf("Click me!") }
- override fun onCancel() {
+ Text(
+ modifier = Modifier.mouseClickable(
+ onClick = {
+ clickableText = buildString {
+ append("Buttons pressed:\n")
+ append("primary: ${buttons.isPrimaryPressed}\t")
+ append("secondary: ${buttons.isSecondaryPressed}\t")
+ append("tertiary: ${buttons.isTertiaryPressed}\t")
+
+ append("\n\nKeyboard modifiers pressed:\n")
+
+ append("alt: ${keyboardModifiers.isAltPressed}\t")
+ append("ctrl: ${keyboardModifiers.isCtrlPressed}\t")
+ append("meta: ${keyboardModifiers.isMetaPressed}\t")
+ append("shift: ${keyboardModifiers.isShiftPressed}\t")
}
}
- },
- text = "Custom mouse event button: ${lastEvent?.button}"
+ ),
+ text = clickableText
)
}
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyListScrollingBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyListScrollingBenchmark.kt
index a3e36fe..036d159 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyListScrollingBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/lazy/LazyListScrollingBenchmark.kt
@@ -45,9 +45,9 @@
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.dp
import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
+import org.junit.Assume
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -107,11 +107,11 @@
}
}
- // this test makes sense only when run on the Android version which supports RenderNodes
- // as this tests how efficiently we move RenderNodes.
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
@Test
fun drawAfterScroll_noNewItems() {
+ // this test makes sense only when run on the Android version which supports RenderNodes
+ // as this tests how efficiently we move RenderNodes.
+ Assume.assumeTrue(supportsRenderNode || supportsMRenderNode)
benchmarkRule.toggleStateBenchmarkDraw {
ListRemeasureTestCase(
addNewItemOnToggle = false,
@@ -121,11 +121,11 @@
}
}
- // this test makes sense only when run on the Android version which supports RenderNodes
- // as this tests how efficiently we move RenderNodes.
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
@Test
fun drawAfterScroll_newItemComposed() {
+ // this test makes sense only when run on the Android version which supports RenderNodes
+ // as this tests how efficiently we move RenderNodes.
+ Assume.assumeTrue(supportsRenderNode || supportsMRenderNode)
benchmarkRule.toggleStateBenchmarkDraw {
ListRemeasureTestCase(
addNewItemOnToggle = true,
@@ -143,6 +143,11 @@
LazyColumn,
LazyRow
)
+
+ // Copied from AndroidComposeTestCaseRunner
+ private val supportsRenderNode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
+ private val supportsMRenderNode = Build.VERSION.SDK_INT < Build.VERSION_CODES.P &&
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt
index 5e11763..a4e93b2 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt
@@ -16,13 +16,19 @@
package androidx.compose.foundation.lazy
+import androidx.compose.foundation.AutoTestFrameClock
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
@@ -34,6 +40,8 @@
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -304,4 +312,38 @@
.assertTopPositionInRootIsEqualTo(itemSize)
.assertLeftPositionInRootIsEqualTo(itemSize)
}
+
+ @Test
+ fun changeItemsCountAndScrollImmediately() {
+ lateinit var state: LazyListState
+ var count by mutableStateOf(100)
+ val composedIndexes = mutableListOf<Int>()
+ rule.setContent {
+ state = rememberLazyListState()
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ Modifier.fillMaxWidth().height(10.dp),
+ state
+ ) {
+ items(count) { index ->
+ composedIndexes.add(index)
+ Box(Modifier.size(20.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ composedIndexes.clear()
+ count = 10
+ runBlocking(AutoTestFrameClock()) {
+ // we try to scroll to the index after 10, but we expect that the component will
+ // already be aware there is a new count and not compose items with index > 10
+ state.scrollToItem(50)
+ }
+ composedIndexes.forEach {
+ Truth.assertThat(it).isLessThan(count)
+ }
+ Truth.assertThat(state.firstVisibleItemIndex).isEqualTo(9)
+ }
+ }
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt
index 6288c86..b0a860e 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerFocusTest.kt
@@ -36,6 +36,7 @@
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.center
import androidx.compose.ui.test.click
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.createAndroidComposeRule
@@ -49,7 +50,6 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.mock
@@ -78,11 +78,9 @@
private val hapticFeedback = mock<HapticFeedback>()
- @FlakyTest(bugId = 179770443)
@Test
- fun click_anywhere_to_cancel() {
+ fun tap_to_cancel() {
// Setup. Long press to create a selection.
- // A reasonable number.
createSelectionContainer()
// Touch position. In this test, each character's width and height equal to fontSize.
// Position is computed so that (position, position) is the center of the first character.
@@ -95,14 +93,9 @@
assertThat(selection1.value).isNotNull()
}
- // Touch position. In this test, each character's width and height equal to fontSize.
- // Position is computed so that (position, position) is the center of the first character.
- val positionInBox = with(Density(view.context)) {
- boxSize.toPx() / 2
- }
// Act.
rule.onNode(hasTestTag("box"))
- .performGesture { click(Offset(x = positionInBox, y = positionInBox)) }
+ .performGesture { click(center) }
// Assert.
rule.runOnIdle {
@@ -118,7 +111,6 @@
@Test
fun select_anotherContainer_cancelOld() {
// Setup. Long press to create a selection.
- // A reasonable number.
createSelectionContainer()
// Touch position. In this test, each character's width and height equal to fontSize.
// Position is computed so that (position, position) is the center of the first character.
@@ -168,16 +160,19 @@
selection1.value = it
}
) {
- CoreText(
- AnnotatedString(textContent),
- Modifier.fillMaxWidth(),
- style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
- softWrap = true,
- overflow = TextOverflow.Clip,
- maxLines = Int.MAX_VALUE,
- inlineContent = mapOf(),
- onTextLayout = {}
- )
+ Column {
+ CoreText(
+ AnnotatedString(textContent),
+ Modifier.fillMaxWidth(),
+ style = TextStyle(fontFamily = fontFamily, fontSize = fontSize),
+ softWrap = true,
+ overflow = TextOverflow.Clip,
+ maxLines = Int.MAX_VALUE,
+ inlineContent = mapOf(),
+ onTextLayout = {}
+ )
+ Box(Modifier.size(boxSize, boxSize).testTag("box"))
+ }
}
SelectionContainer(
@@ -200,8 +195,6 @@
onTextLayout = {}
)
}
-
- Box(Modifier.size(boxSize, boxSize).testTag("box"))
}
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
index f0dcd0d..2ec5116 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
@@ -47,9 +47,6 @@
contentPadding: PaddingValues = PaddingValues(0.dp),
content: LazyGridScope.() -> Unit
) {
- val scope = LazyGridScopeImpl()
- scope.apply(content)
-
when (cells) {
is GridCells.Fixed ->
FixedLazyGrid(
@@ -57,7 +54,7 @@
modifier = modifier,
state = state,
contentPadding = contentPadding,
- scope = scope
+ content = content
)
is GridCells.Adaptive ->
BoxWithConstraints(
@@ -68,7 +65,7 @@
nColumns = nColumns,
state = state,
contentPadding = contentPadding,
- scope = scope
+ content = content
)
}
}
@@ -185,14 +182,17 @@
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
- scope: LazyGridScopeImpl
+ content: LazyGridScope.() -> Unit
) {
- val rows = (scope.totalSize + nColumns - 1) / nColumns
LazyColumn(
modifier = modifier,
state = state,
contentPadding = contentPadding
) {
+ val scope = LazyGridScopeImpl()
+ scope.apply(content)
+
+ val rows = (scope.totalSize + nColumns - 1) / nColumns
items(rows) { rowIndex ->
Row {
for (columnIndex in 0 until nColumns) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
index 1ed3722..3b0261d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
@@ -33,6 +33,7 @@
import androidx.compose.runtime.DisposableEffectScope
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@@ -127,9 +128,12 @@
// When text is updated, the selection on this CoreText becomes invalid. It can be treated
// as a brand new CoreText.
// When SelectionRegistrar is updated, CoreText have to request a new ID to avoid ID collision.
- val selectableId = rememberSaveable(text, selectionRegistrar) {
- selectionRegistrar?.nextSelectableId() ?: SelectionRegistrar.InvalidSelectableId
- }
+
+ val selectableId =
+ rememberSaveable(text, selectionRegistrar, saver = selectionIdSaver(selectionRegistrar)) {
+ selectionRegistrar?.nextSelectableId() ?: SelectionRegistrar.InvalidSelectableId
+ }
+
val state = remember {
TextState(
TextDelegate(
@@ -404,9 +408,13 @@
dragTotalDistance += delta
if (!outOfBoundary(dragBeginPosition, dragBeginPosition + dragTotalDistance)) {
+ // Notice that only the end position needs to be updated here.
+ // Start position is left unchanged. This is typically important when
+ // long-press is using SelectionAdjustment.WORD or
+ // SelectionAdjustment.PARAGRAPH that updates the start handle position from
+ // the dragBeginPosition.
selectionRegistrar?.notifySelectionUpdate(
layoutCoordinates = it,
- startPosition = dragBeginPosition,
endPosition = dragBeginPosition + dragTotalDistance,
adjustment = SelectionAdjustment.CHARACTER
)
@@ -639,3 +647,11 @@
}
return Pair(placeholders, inlineComposables)
}
+
+/**
+ * A custom saver that won't save if no selection is active.
+ */
+private fun selectionIdSaver(selectionRegistrar: SelectionRegistrar?) = Saver<Long, Long>(
+ save = { if (selectionRegistrar.hasSelection(it)) it else null },
+ restore = { it }
+)
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
index dc4b353..acba3a7 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
@@ -16,5 +16,137 @@
package androidx.compose.foundation
+import androidx.compose.foundation.gestures.forEachGesture
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.input.pointer.AwaitPointerEventScope
+import androidx.compose.ui.input.pointer.PointerButtons
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
+import androidx.compose.ui.input.pointer.changedToDown
+import androidx.compose.ui.input.pointer.changedToUp
+import androidx.compose.ui.input.pointer.consumeDownChange
+import androidx.compose.ui.input.pointer.isOutOfBounds
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.util.fastAll
+import kotlinx.coroutines.coroutineScope
+
// TODO: b/168524931 - should this depend on the input device?
-internal actual val TapIndicationDelay: Long = 0L
\ No newline at end of file
+internal actual val TapIndicationDelay: Long = 0L
+
+@Immutable @ExperimentalDesktopApi
+class MouseClickScope constructor(
+ val buttons: PointerButtons,
+ val keyboardModifiers: PointerKeyboardModifiers
+)
+
+@ExperimentalDesktopApi
+internal val EmptyClickContext = MouseClickScope(
+ PointerButtons(0), PointerKeyboardModifiers(0)
+)
+
+/**
+ * Creates modifier similar to [Modifier.clickable] but provides additional context with
+ * information about pressed buttons and keyboard modifiers
+ *
+ */
+@ExperimentalDesktopApi
+fun Modifier.mouseClickable(
+ enabled: Boolean = true,
+ onClickLabel: String? = null,
+ role: Role? = null,
+ onClick: MouseClickScope.() -> Unit
+) = composed(
+ factory = {
+ val onClickState = rememberUpdatedState(onClick)
+ val gesture = if (enabled) {
+ Modifier.pointerInput(Unit) {
+ detectTapWithContext(
+ onTap = { down, _ ->
+ onClickState.value.invoke(
+ MouseClickScope(
+ down.buttons,
+ down.keyboardModifiers
+ )
+ )
+ }
+ )
+ }
+ } else {
+ Modifier
+ }
+ Modifier
+ .genericClickableWithoutGesture(
+ gestureModifiers = gesture,
+ enabled = enabled,
+ onClickLabel = onClickLabel,
+ role = role,
+ onLongClickLabel = null,
+ onLongClick = null,
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() },
+ onClick = { onClick(EmptyClickContext) }
+ )
+ },
+ inspectorInfo = debugInspectorInfo {
+ name = "clickable"
+ properties["enabled"] = enabled
+ properties["onClickLabel"] = onClickLabel
+ properties["role"] = role
+ properties["onClick"] = onClick
+ }
+)
+
+@OptIn(ExperimentalDesktopApi::class)
+internal suspend fun PointerInputScope.detectTapWithContext(
+ onTap: ((PointerEvent, PointerEvent) -> Unit)? = null
+) {
+ forEachGesture {
+ coroutineScope {
+ awaitPointerEventScope {
+
+ val down = awaitEventFirstDown().also {
+ it.changes.forEach { it.consumeDownChange() }
+ }
+
+ val up = waitForFirstInboundUp()
+ if (up != null) {
+ up.changes.forEach { it.consumeDownChange() }
+ onTap?.invoke(down, up)
+ }
+ }
+ }
+ }
+}
+
+@ExperimentalDesktopApi
+suspend fun AwaitPointerEventScope.awaitEventFirstDown(): PointerEvent {
+ var event: PointerEvent
+ do {
+ event = awaitPointerEvent()
+ } while (
+ !event.changes.fastAll { it.changedToDown() }
+ )
+ return event
+}
+
+private suspend fun AwaitPointerEventScope.waitForFirstInboundUp(): PointerEvent? {
+ while (true) {
+ val event = awaitPointerEvent()
+ val change = event.changes[0]
+ if (change.changedToUp()) {
+ return if (change.isOutOfBounds(size)) {
+ null
+ } else {
+ event
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ExperimentalDesktopApi.desktop.kt
similarity index 72%
rename from benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt
rename to compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ExperimentalDesktopApi.desktop.kt
index 88234c7..2ab52fb 100644
--- a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ExperimentalDesktopApi.desktop.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,10 +14,7 @@
* limitations under the License.
*/
-package androidx.benchmark.macro
+package androidx.compose.foundation
-import android.platform.test.rule.DropCachesRule
-
-class BenchmarkClass {
- val rule = DropCachesRule()
-}
+@RequiresOptIn("This API is experimental and is likely to change in the future.")
+annotation class ExperimentalDesktopApi
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
index 9c742d1..dae3c2e 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
@@ -160,7 +160,6 @@
verify(selectionRegistrar, times(1))
.notifySelectionUpdate(
layoutCoordinates = layoutCoordinates,
- startPosition = beginPosition2,
endPosition = beginPosition2 + dragDistance2,
adjustment = SelectionAdjustment.CHARACTER
)
@@ -181,7 +180,6 @@
verify(selectionRegistrar, times(1))
.notifySelectionUpdate(
layoutCoordinates = layoutCoordinates,
- startPosition = beginPosition,
endPosition = beginPosition + dragDistance,
adjustment = SelectionAdjustment.CHARACTER
)
@@ -202,7 +200,6 @@
verify(selectionRegistrar, times(0))
.notifySelectionUpdate(
layoutCoordinates = layoutCoordinates,
- startPosition = beginPosition,
endPosition = beginPosition + dragDistance,
adjustment = SelectionAdjustment.CHARACTER
)
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index e1e3e04..4332f18 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -72,7 +72,8 @@
}
public final class BadgeKt {
- method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void BadgeBox(optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? badgeContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+ method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void Badge(optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? content);
+ method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void BadgedBox(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> badge, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}
@androidx.compose.material.ExperimentalMaterialApi public final class BottomDrawerState extends androidx.compose.material.SwipeableState<androidx.compose.material.BottomDrawerValue> {
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/BadgeDemo.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/BadgeDemo.kt
index 7d39a3b..10c0360 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/BadgeDemo.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/BadgeDemo.kt
@@ -22,7 +22,8 @@
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.BadgeBox
+import androidx.compose.material.Badge
+import androidx.compose.material.BadgedBox
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.Button
@@ -104,7 +105,7 @@
navigationIcon = {
IconButton(onClick = { showNavigationIconBadge = false }) {
if (showNavigationIconBadge) {
- DemoBadgeBox(null) {
+ DemoBadgedBox(null) {
Icon(Icons.Filled.Menu, contentDescription = "Localized description")
}
} else {
@@ -118,7 +119,7 @@
onClick = onActionIcon1BadgeClick
) {
if (actionIcon1BadgeCount > 0) {
- DemoBadgeBox(actionIcon1BadgeCount.toString()) {
+ DemoBadgedBox(actionIcon1BadgeCount.toString()) {
Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
}
} else {
@@ -129,7 +130,7 @@
onClick = { showActionIcon2Badge = false }
) {
if (showActionIcon2Badge) {
- DemoBadgeBox("99+") {
+ DemoBadgedBox("99+") {
Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
}
} else {
@@ -172,7 +173,7 @@
} else {
when (item) {
"Artists" -> {
- DemoBadgeBox(artistsBadgeCount.toString()) {
+ DemoBadgedBox(artistsBadgeCount.toString()) {
Icon(
Icons.Filled.Favorite,
contentDescription = "Localized description"
@@ -180,7 +181,7 @@
}
}
else -> {
- DemoBadgeBox(
+ DemoBadgedBox(
when (index) {
2 -> "99+"
else -> null
@@ -241,7 +242,7 @@
if (!showBadge) {
Text(title)
} else {
- DemoBadgeBox(
+ DemoBadgedBox(
when (index) {
1 -> tab1BadgeCount.toString()
2 -> "99+"
@@ -305,7 +306,7 @@
contentDescription = "Localized description"
)
} else {
- DemoBadgeBox(
+ DemoBadgedBox(
when (index) {
1 -> tab1BadgeCount.toString()
2 -> "99+"
@@ -341,25 +342,28 @@
@OptIn(ExperimentalMaterialApi::class)
@Composable
-private fun DemoBadgeBox(
+private fun DemoBadgedBox(
badgeText: String?,
content: @Composable () -> Unit
) {
- if (badgeText.isNullOrEmpty()) {
- BadgeBox { content() }
- } else {
- BadgeBox(
- badgeContent = {
- Text(
- badgeText,
- textAlign = TextAlign.Center,
- modifier = Modifier.semantics {
- this.contentDescription = "$badgeText notifications"
- }
- )
- }
- ) {
- content()
+ BadgedBox(
+ badge = {
+ Badge(
+ content =
+ if (!badgeText.isNullOrEmpty()) {
+ {
+ Text(
+ badgeText,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.semantics {
+ this.contentDescription = "$badgeText notifications"
+ }
+ )
+ }
+ } else null
+ )
}
+ ) {
+ content()
}
}
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BadgeSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BadgeSamples.kt
index bdcda98..78666af 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BadgeSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BadgeSamples.kt
@@ -17,7 +17,8 @@
package androidx.compose.material.samples
import androidx.annotation.Sampled
-import androidx.compose.material.BadgeBox
+import androidx.compose.material.Badge
+import androidx.compose.material.BadgedBox
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.ExperimentalMaterialApi
@@ -34,7 +35,7 @@
BottomNavigation {
BottomNavigationItem(
icon = {
- BadgeBox(badgeContent = { Text("8") }) {
+ BadgedBox(badge = { Badge { Text("8") } }) {
Icon(
Icons.Filled.Favorite,
contentDescription = "Favorite"
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeScreenshotTest.kt
index 4bb77c2..791a081 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeScreenshotTest.kt
@@ -60,7 +60,7 @@
Modifier.size(56.dp).semantics(mergeDescendants = true) {}.testTag(TestTag),
contentAlignment = Alignment.Center
) {
- BadgeBox(badgeContent = { Text("8") }) {
+ BadgedBox(badge = { Badge { Text("8") } }) {
Icon(Icons.Filled.Favorite, null)
}
}
@@ -81,7 +81,7 @@
) {
BottomNavigationItem(
icon = {
- BadgeBox {
+ BadgedBox(badge = { Badge() }) {
Icon(Icons.Filled.Favorite, null)
}
},
@@ -106,12 +106,14 @@
) {
BottomNavigationItem(
icon = {
- BadgeBox(
- badgeContent = {
- Text(
- "8",
- textAlign = TextAlign.Center
- )
+ BadgedBox(
+ badge = {
+ Badge {
+ Text(
+ "8",
+ textAlign = TextAlign.Center
+ )
+ }
}
) {
Icon(Icons.Filled.Favorite, null)
@@ -138,12 +140,14 @@
) {
BottomNavigationItem(
icon = {
- BadgeBox(
- badgeContent = {
- Text(
- "99+",
- textAlign = TextAlign.Center
- )
+ BadgedBox(
+ badge = {
+ Badge {
+ Text(
+ "99+",
+ textAlign = TextAlign.Center
+ )
+ }
}
) {
Icon(Icons.Filled.Favorite, null)
@@ -167,7 +171,7 @@
MaterialTheme(lightColors()) {
Tab(
text = {
- BadgeBox {
+ BadgedBox(badge = { Badge() }) {
Text("TAB")
}
},
@@ -190,12 +194,14 @@
// A round badge with the text `8` attached to a tab.
Tab(
text = {
- BadgeBox(
- badgeContent = {
- Text(
- "8",
- textAlign = TextAlign.Center
- )
+ BadgedBox(
+ badge = {
+ Badge {
+ Text(
+ "8",
+ textAlign = TextAlign.Center
+ )
+ }
}
) {
Text("TAB")
@@ -220,12 +226,14 @@
// Tab with a pilled shape badge with the text `99+`.
Tab(
text = {
- BadgeBox(
- badgeContent = {
- Text(
- "99+",
- textAlign = TextAlign.Center
- )
+ BadgedBox(
+ badge = {
+ Badge {
+ Text(
+ "99+",
+ textAlign = TextAlign.Center
+ )
+ }
}
) {
Text("TAB")
@@ -250,12 +258,14 @@
// A round badge with the text `8` attached to a leading icon tab.
LeadingIconTab(
icon = {
- BadgeBox(
- badgeContent = {
- Text(
- "8",
- textAlign = TextAlign.Center
- )
+ BadgedBox(
+ badge = {
+ Badge {
+ Text(
+ "8",
+ textAlign = TextAlign.Center
+ )
+ }
}
) {
Icon(Icons.Filled.Favorite, null)
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeTest.kt
index 9dade12..1160bf1 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeTest.kt
@@ -132,10 +132,33 @@
}
@Test
+ fun badgeBox_noContent_position() {
+ rule
+ .setMaterialContent {
+ BadgedBox(badge = { Badge(Modifier.testTag(TestBadgeTag)) }) {
+ Icon(
+ icon,
+ null,
+ modifier = Modifier.testTag(TestAnchorTag)
+ )
+ }
+ }
+ val badge = rule.onNodeWithTag(TestBadgeTag)
+ val anchorBounds = rule.onNodeWithTag(TestAnchorTag).getUnclippedBoundsInRoot()
+ val badgeBounds = badge.getUnclippedBoundsInRoot()
+ badge.assertPositionInRootIsEqualTo(
+ expectedLeft =
+ anchorBounds.right + BadgeHorizontalOffset +
+ max((BadgeRadius - badgeBounds.width) / 2, 0.dp),
+ expectedTop = -badgeBounds.height / 2
+ )
+ }
+
+ @Test
fun badgeBox_shortContent_position() {
rule
.setMaterialContent {
- BadgeBox(badgeContent = { Text("8") }) {
+ BadgedBox(badge = { Badge { Text("8") } }) {
Icon(
icon,
null,
@@ -162,7 +185,7 @@
fun badgeBox_longContent_position() {
rule
.setMaterialContent {
- BadgeBox(badgeContent = { Text("999+") }) {
+ BadgedBox(badge = { Badge { Text("999+") } }) {
Icon(
icon,
null,
@@ -253,12 +276,12 @@
@Test
fun badge_notMergingDescendants_withOwnContentDescription() {
rule.setMaterialContent {
- BadgeBox(
+ BadgedBox(
+ badge = {
+ Badge { Text("99+") }
+ },
modifier = Modifier.testTag(TestBadgeTag).semantics {
this.contentDescription = "more than 99 new email"
- },
- badgeContent = {
- Text("99+")
}
) {
Text(
@@ -277,7 +300,7 @@
@Test
fun badgeBox_size() {
rule.setMaterialContentForSizeAssertions {
- BadgeBox(badgeContent = { Text("999+") }) {
+ BadgedBox(badge = { Badge { Text("999+") } }) {
Icon(icon, null)
}
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt
new file mode 100644
index 0000000..2efce3a
--- /dev/null
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.material
+
+import android.os.Build
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.center
+import androidx.compose.ui.test.down
+import androidx.compose.ui.test.hasClickAction
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performGesture
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@OptIn(ExperimentalTestApi::class)
+class FloatingActionButtonScreenshotTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @get:Rule
+ val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL)
+
+ @Test
+ fun icon() {
+ rule.setMaterialContent {
+ FloatingActionButton(onClick = { }) {
+ Icon(Icons.Filled.Favorite, contentDescription = null)
+ }
+ }
+
+ rule.onNode(hasClickAction())
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "fab_icon")
+ }
+
+ @Test
+ fun text() {
+ rule.setMaterialContent {
+ ExtendedFloatingActionButton(
+ text = { Text("EXTENDED") },
+ onClick = {}
+ )
+ }
+
+ rule.onNode(hasClickAction())
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "fab_text")
+ }
+
+ @Test
+ fun textAndIcon() {
+ rule.setMaterialContent {
+ ExtendedFloatingActionButton(
+ text = { Text("EXTENDED") },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
+ onClick = {}
+ )
+ }
+
+ rule.onNode(hasClickAction())
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "fab_textAndIcon")
+ }
+
+ @Test
+ fun ripple() {
+ rule.mainClock.autoAdvance = false
+
+ rule.setMaterialContent {
+ Box(Modifier.requiredSize(100.dp, 100.dp).wrapContentSize()) {
+ FloatingActionButton(onClick = { }) {
+ Icon(Icons.Filled.Favorite, contentDescription = null)
+ }
+ }
+ }
+
+ // Start ripple
+ rule.onNode(hasClickAction())
+ .performGesture { down(center) }
+
+ // Advance past the tap timeout
+ rule.mainClock.advanceTimeBy(100)
+
+ rule.waitForIdle()
+ // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't
+ // properly wait for synchronization. Instead just wait until after the ripples are
+ // finished animating.
+ Thread.sleep(300)
+
+ rule.onRoot()
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "fab_ripple")
+ }
+}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
index 1fda1bc..071c5f2 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
@@ -44,6 +44,7 @@
import androidx.compose.ui.test.performGesture
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.up
+import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
@@ -82,8 +83,9 @@
fun outlinedTextField_withInput() {
rule.setMaterialContent {
Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
+ val text = "Text"
OutlinedTextField(
- value = "Text",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
label = { Text("Label") },
modifier = Modifier.requiredWidth(280.dp)
@@ -152,8 +154,9 @@
fun outlinedTextField_error_focused() {
rule.setMaterialContent {
Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
+ val text = "Input"
OutlinedTextField(
- value = "Input",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
label = { Text("Label") },
isError = true,
@@ -188,8 +191,9 @@
fun outlinedTextField_textColor_fallbackToContentColor() {
rule.setMaterialContent {
CompositionLocalProvider(LocalContentColor provides Color.Magenta) {
+ val text = "Hello, world!"
OutlinedTextField(
- value = "Hello, world!",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
)
@@ -202,8 +206,9 @@
@Test
fun outlinedTextField_multiLine_withLabel_textAlignedToTop() {
rule.setMaterialContent {
+ val text = "Text"
OutlinedTextField(
- value = "Text",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
label = { Text("Label") },
modifier = Modifier.requiredHeight(300.dp)
@@ -218,8 +223,9 @@
@Test
fun outlinedTextField_multiLine_withoutLabel_textAlignedToTop() {
rule.setMaterialContent {
+ val text = "Text"
OutlinedTextField(
- value = "Text",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
modifier = Modifier.requiredHeight(300.dp)
.requiredWidth(280.dp)
@@ -286,8 +292,9 @@
@Test
fun outlinedTextField_singleLine_withLabel_textAlignedToTop() {
rule.setMaterialContent {
+ val text = "Text"
OutlinedTextField(
- value = "Text",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
singleLine = true,
label = { Text("Label") },
@@ -301,8 +308,9 @@
@Test
fun outlinedTextField_singleLine_withoutLabel_textCenteredVertically() {
rule.setMaterialContent {
+ val text = "Text"
OutlinedTextField(
- value = "Text",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
singleLine = true,
modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
@@ -484,8 +492,9 @@
@Test
fun outlinedTextField_textCenterAligned() {
rule.setMaterialContent {
+ val text = "Hello world"
OutlinedTextField(
- value = "Hello world",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
modifier = Modifier.width(300.dp).testTag(TextFieldTag),
textStyle = TextStyle(textAlign = TextAlign.Center),
@@ -499,8 +508,9 @@
@Test
fun outlinedTextField_textAlignedToEnd() {
rule.setMaterialContent {
+ val text = "Hello world"
OutlinedTextField(
- value = "Hello world",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
modifier = Modifier.fillMaxWidth().testTag(TextFieldTag),
textStyle = TextStyle(textAlign = TextAlign.End),
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt
index ee34435..42a0998 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldScreenshotTest.kt
@@ -44,6 +44,7 @@
import androidx.compose.ui.test.performGesture
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.up
+import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
@@ -81,8 +82,9 @@
fun textField_withInput() {
rule.setMaterialContent {
Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
+ val text = "Text"
TextField(
- value = "Text",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
label = { Text("Label") },
modifier = Modifier.requiredWidth(280.dp)
@@ -150,8 +152,9 @@
@Test
fun textField_error_focused() {
rule.setMaterialContent {
+ val text = "Input"
TextField(
- value = "Input",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
label = { Text("Label") },
isError = true,
@@ -183,8 +186,9 @@
fun textField_textColor_fallbackToContentColor() {
rule.setMaterialContent {
CompositionLocalProvider(LocalContentColor provides Color.Green) {
+ val text = "Hello, world!"
TextField(
- value = "Hello, world!",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
modifier = Modifier.requiredWidth(280.dp).testTag(TextFieldTag)
)
@@ -197,8 +201,9 @@
@Test
fun textField_multiLine_withLabel_textAlignedToTop() {
rule.setMaterialContent {
+ val text = "Text"
TextField(
- value = "Text",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
label = { Text("Label") },
modifier = Modifier.requiredHeight(300.dp)
@@ -213,8 +218,9 @@
@Test
fun textField_multiLine_withoutLabel_textAlignedToTop() {
rule.setMaterialContent {
+ val text = "Text"
TextField(
- value = "Text",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
modifier = Modifier.requiredHeight(300.dp)
.requiredWidth(280.dp)
@@ -281,8 +287,9 @@
@Test
fun textField_singleLine_withLabel_textAlignedToTop() {
rule.setMaterialContent {
+ val text = "Text"
TextField(
- value = "Text",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
singleLine = true,
label = { Text("Label") },
@@ -296,8 +303,9 @@
@Test
fun textField_singleLine_withoutLabel_textCenteredVertically() {
rule.setMaterialContent {
+ val text = "Text"
TextField(
- value = "Text",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
singleLine = true,
modifier = Modifier.requiredWidth(280.dp).testTag(TextFieldTag)
@@ -472,8 +480,9 @@
@Test
fun textField_textCenterAligned() {
rule.setMaterialContent {
+ val text = "Hello world"
TextField(
- value = "Hello world",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
modifier = Modifier.width(300.dp).testTag(TextFieldTag),
textStyle = TextStyle(textAlign = TextAlign.Center),
@@ -487,8 +496,9 @@
@Test
fun textField_textAlignedToEnd() {
rule.setMaterialContent {
+ val text = "Hello world"
TextField(
- value = "Hello world",
+ value = TextFieldValue(text = text, selection = TextRange(text.length)),
onValueChange = {},
modifier = Modifier.fillMaxWidth().testTag(TextFieldTag),
textStyle = TextStyle(textAlign = TextAlign.End),
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Badge.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Badge.kt
index 185fa4e..780bd0f 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Badge.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Badge.kt
@@ -39,34 +39,29 @@
import androidx.compose.ui.unit.sp
/**
- * A BadgeBox is used to decorate [content] with a badge that can contain dynamic information, such
+ * A BadgeBox is used to decorate [content] with a [badge] that can contain dynamic information,
+ * such
* as the presence of a new notification or a number of pending requests. Badges can be icon only
* or contain short text.
*
- * A common use case is to display a badge in the upper right corner of bottom navigation items.
+ * A common use case is to display a badge with bottom navigation items.
* For more information, see [Bottom Navigation](https://material.io/components/bottom-navigation#behavior)
*
* A simple icon with badge example looks like:
* @sample androidx.compose.material.samples.BottomNavigationItemWithBadge
*
+ * @param badge the badge to be displayed - typically a [Badge]
* @param modifier optional [Modifier] for this item
- * @param backgroundColor the background color for the badge
- * @param contentColor the color of label text rendered in the badge
- * @param badgeContent optional content to be rendered inside the badge
* @param content the anchor to which this badge will be positioned
*
*/
@ExperimentalMaterialApi
@Composable
-fun BadgeBox(
+fun BadgedBox(
+ badge: @Composable BoxScope.() -> Unit,
modifier: Modifier = Modifier,
- backgroundColor: Color = MaterialTheme.colors.error,
- contentColor: Color = contentColorFor(backgroundColor),
- badgeContent: @Composable (RowScope.() -> Unit)? = null,
content: @Composable BoxScope.() -> Unit,
) {
- val badgeHorizontalOffset =
- if (badgeContent != null) BadgeWithContentHorizontalOffset else BadgeHorizontalOffset
Layout(
{
Box(
@@ -74,11 +69,9 @@
contentAlignment = Alignment.Center,
content = content
)
- Badge(
+ Box(
modifier = Modifier.layoutId("badge"),
- backgroundColor = backgroundColor,
- contentColor = contentColor,
- content = badgeContent
+ content = badge
)
},
modifier = modifier
@@ -107,6 +100,12 @@
LastBaseline to lastBaseline
)
) {
+ // Use the width of the badge to infer whether it has any content (based on radius used
+ // in [Badge]) and determine its horizontal offset.
+ val hasContent = badgePlaceable.width > (2 * BadgeRadius.roundToPx())
+ val badgeHorizontalOffset =
+ if (hasContent) BadgeWithContentHorizontalOffset else BadgeHorizontalOffset
+
anchorPlaceable.placeRelative(0, 0)
val badgeX = anchorPlaceable.width + badgeHorizontalOffset.roundToPx()
val badgeY = -badgePlaceable.height / 2
@@ -116,18 +115,21 @@
}
/**
- * Internal badge implementation to help [BadgeBox].
+ * Badge is a component that can contain dynamic information, such as the presence of a new
+ * notification or a number of pending requests. Badges can be icon only or contain short text.
+ *
+ * See [BadgedBox] for a top level layout that will properly place the badge relative to content
+ * such as text or an icon.
*
* @param modifier optional [Modifier] for this item
* @param backgroundColor the background color for the badge
* @param contentColor the color of label text rendered in the badge
* @param content optional content to be rendered inside the badge
*
- * @see BadgeBox
*/
@ExperimentalMaterialApi
@Composable
-internal fun Badge(
+fun Badge(
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colors.error,
contentColor: Color = contentColorFor(backgroundColor),
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
index 50d21a7..9dffc43 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
@@ -168,22 +168,19 @@
contentColor = contentColor,
elevation = elevation
) {
- Box(
+ val startPadding = if (icon == null) ExtendedFabTextPadding else ExtendedFabIconPadding
+ Row(
modifier = Modifier.padding(
- start = ExtendedFabTextPadding,
+ start = startPadding,
end = ExtendedFabTextPadding
),
- contentAlignment = Alignment.Center
+ verticalAlignment = Alignment.CenterVertically
) {
- if (icon == null) {
- text()
- } else {
- Row(verticalAlignment = Alignment.CenterVertically) {
- icon()
- Spacer(Modifier.width(ExtendedFabIconPadding))
- text()
- }
+ if (icon != null) {
+ icon()
+ Spacer(Modifier.width(ExtendedFabIconPadding))
}
+ text()
}
}
}
diff --git a/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/ListSaver.kt b/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/ListSaver.kt
index bcdf4be..761fe64 100644
--- a/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/ListSaver.kt
+++ b/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/ListSaver.kt
@@ -27,7 +27,7 @@
*
* @sample androidx.compose.runtime.saveable.samples.ListSaverSample
*/
-fun <Original : Any, Saveable : Any> listSaver(
+fun <Original, Saveable> listSaver(
save: SaverScope.(value: Original) -> List<Saveable>,
restore: (list: List<Saveable>) -> Original?
): Saver<Original, Any> = @Suppress("UNCHECKED_CAST") Saver(
@@ -35,7 +35,9 @@
val list = save(it)
for (index in list.indices) {
val item = list[index]
- require(canBeSaved(item))
+ if (item != null) {
+ require(canBeSaved(item))
+ }
}
if (list.isNotEmpty()) ArrayList(list) else null
},
diff --git a/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/MapSaver.kt b/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/MapSaver.kt
index 6ad0912..9bfa09b 100644
--- a/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/MapSaver.kt
+++ b/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/MapSaver.kt
@@ -27,12 +27,12 @@
*
* @sample androidx.compose.runtime.saveable.samples.MapSaverSample
*/
-fun <T : Any> mapSaver(
- save: SaverScope.(value: T) -> Map<String, Any>,
- restore: (Map<String, Any>) -> T?
-) = listSaver<T, Any>(
+fun <T> mapSaver(
+ save: SaverScope.(value: T) -> Map<String, Any?>,
+ restore: (Map<String, Any?>) -> T?
+) = listSaver<T, Any?>(
save = {
- mutableListOf<Any>().apply {
+ mutableListOf<Any?>().apply {
save(it).forEach { entry ->
add(entry.key)
add(entry.value)
@@ -40,7 +40,7 @@
}
},
restore = { list ->
- val map = mutableMapOf<String, Any>()
+ val map = mutableMapOf<String, Any?>()
check(list.size.rem(2) == 0)
var index = 0
while (index < list.size) {
diff --git a/compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/ListSaverTest.kt b/compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/ListSaverTest.kt
index ffd8d9a..99d6d4d 100644
--- a/compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/ListSaverTest.kt
+++ b/compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/ListSaverTest.kt
@@ -58,11 +58,41 @@
assertThat(savedList).isInstanceOf(ArrayList::class.java)
assertThat(savedList).isEqualTo(listOf("One", "Two"))
}
+
+ @Test
+ fun nullableListItemsAreSupported() {
+ val original = NullableSize(null, 3)
+ val saved = with(NullableSizeSaver) {
+ allowingScope.save(original)
+ }
+
+ assertThat(saved).isNotNull()
+ assertThat(NullableSizeSaver.restore(saved!!))
+ .isEqualTo(original)
+ }
+
+ @Test
+ fun nullableTypeIsSupported() {
+ val saved = with(NullableSizeSaver) {
+ allowingScope.save(null)
+ }
+
+ assertThat(saved).isNotNull()
+ assertThat(NullableSizeSaver.restore(saved!!))
+ .isEqualTo(NullableSize(null, null))
+ }
}
private data class Size(val x: Int, val y: Int)
+private data class NullableSize(val x: Int?, val y: Int?)
+
private val SizeSaver = listSaver<Size, Int>(
save = { listOf(it.x, it.y) },
restore = { Size(it[0], it[1]) }
)
+
+private val NullableSizeSaver = listSaver<NullableSize?, Int?>(
+ save = { listOf(it?.x, it?.y) },
+ restore = { NullableSize(it[0], it[1]) }
+)
diff --git a/compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/MapSaverTest.kt b/compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/MapSaverTest.kt
index 8c939d2..375b6b9 100644
--- a/compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/MapSaverTest.kt
+++ b/compose/runtime/runtime-saveable/src/test/java/androidx/compose/runtime/saveable/MapSaverTest.kt
@@ -53,6 +53,29 @@
onlyInts.save(User("John", 30))
}
}
+
+ @Test
+ fun nullableMapItemsAreSupported() {
+ val original = NullableUser(null, 30)
+ val saved = with(NullableUserSaver) {
+ allowingScope.save(original)
+ }
+
+ assertThat(saved).isNotNull()
+ assertThat(NullableUserSaver.restore(saved!!))
+ .isEqualTo(original)
+ }
+
+ @Test
+ fun nullableTypeIsSupported() {
+ val saved = with(NullableUserSaver) {
+ allowingScope.save(null)
+ }
+
+ assertThat(saved).isNotNull()
+ assertThat(NullableUserSaver.restore(saved!!))
+ .isEqualTo(NullableUser(null, null))
+ }
}
private data class User(val name: String, val age: Int)
@@ -64,4 +87,15 @@
save = { mapOf(nameKey to it.name, ageKey to it.age) },
restore = { User(it[nameKey] as String, it[ageKey] as Int) }
)
+}
+
+private data class NullableUser(val name: String?, val age: Int?)
+
+private val NullableUserSaver = run {
+ val nameKey = "Name"
+ val ageKey = "Age"
+ mapSaver<NullableUser?>(
+ save = { mapOf(nameKey to it?.name, ageKey to it?.age) },
+ restore = { NullableUser(it[nameKey] as String?, it[ageKey] as Int?) }
+ )
}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index 864628b..23b73ce 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -1216,7 +1216,9 @@
parentProvider = parentContext.getCompositionLocalScope()
providersInvalidStack.push(providersInvalid.asInt())
providersInvalid = changed(parentProvider)
- collectParameterInformation = parentContext.collectingParameterInformation
+ if (!collectParameterInformation) {
+ collectParameterInformation = parentContext.collectingParameterInformation
+ }
resolveCompositionLocal(LocalInspectionTables, parentProvider)?.let {
it.add(slotTable)
parentContext.recordInspectionTable(it)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
index aec9166..8affa82 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
@@ -53,7 +53,9 @@
*/
private val readObserver: (Any) -> Unit = { state ->
if (!isPaused) {
- currentMap!!.addValue(state)
+ synchronized(applyMaps) {
+ currentMap!!.addValue(state)
+ }
}
}
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTests.kt
index 66b607b..af48238 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTests.kt
@@ -324,6 +324,44 @@
assertNull(threadException)
}
+ @Test // regression test for 192677711, second case
+ fun tryToReproduceSecondRaceCondtion() {
+ var running = true
+ var threadException: Exception? = null
+ try {
+ thread {
+ try {
+ while (running) {
+ Snapshot.sendApplyNotifications()
+ }
+ } catch (e: Exception) {
+ threadException = e
+ }
+ }
+
+ for (i in 1..10000) {
+ val state1 by mutableStateOf(0)
+ var state2 by mutableStateOf(true)
+ val observer = SnapshotStateObserver({}).apply {
+ start()
+ }
+ observer.observeReads(Unit, {}) {
+ repeat(1000) {
+ @Suppress("UNUSED_EXPRESSION")
+ state1
+ if (state2) {
+ state2 = false
+ }
+ }
+ }
+ assertNull(threadException)
+ }
+ } finally {
+ running = false
+ }
+ assertNull(threadException)
+ }
+
private fun runSimpleTest(
block: (modelObserver: SnapshotStateObserver, data: MutableState<Int>) -> Unit
) {
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidCanvasTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidCanvasTest.kt
index 3bdf133..001f11a 100644
--- a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidCanvasTest.kt
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/AndroidCanvasTest.kt
@@ -29,21 +29,21 @@
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
+import androidx.compose.testutils.captureToImage
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
-import androidx.compose.testutils.captureToImage
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
-import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import org.junit.Assert.assertTrue
@MediumTest
@RunWith(AndroidJUnit4::class)
@@ -270,6 +270,21 @@
assertEquals(bg, pixelMap[75, 76])
}
+ /**
+ * Verify the difference in rgb channels is within the given threshold. This assumes
+ * a fully blended color within a bitmap so only RGB channels are verified
+ */
+ private fun verifyPixelWithThreshold(
+ color: Color,
+ expectedColor: Color,
+ threshold: Int
+ ): Boolean {
+ val diff = Math.abs(color.red - expectedColor.red) +
+ Math.abs(color.green - expectedColor.green) +
+ Math.abs(color.blue - expectedColor.blue)
+ return (diff * 255) <= threshold
+ }
+
@Test
fun testCornerPathEffect() {
val width = 80
@@ -305,12 +320,15 @@
)
val composePixels = imageBitmap.toPixelMap()
- for (i in 0 until 80) {
- for (j in 0 until 80) {
- assertEquals(
+ for (i in 0 until width) {
+ for (j in 0 until height) {
+ assertTrue(
"invalid color at i: " + i + ", " + j,
- composePixels[i, j].toArgb(),
- androidBitmap.getPixel(i, j)
+ verifyPixelWithThreshold(
+ composePixels[i, j],
+ Color(androidBitmap.getPixel(i, j)),
+ 3
+ )
)
}
}
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
index 61b4ae8..35d0218 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
@@ -51,7 +51,7 @@
*
* @param colorStops Colors and their offset in the gradient area
* @param start Starting position of the linear gradient. This can be set to
- * [Offset.Infinite] to position at the far right and bottom of the drawing area
+ * [Offset.Zero] to position at the far left and top of the drawing area
* @param end Ending position of the linear gradient. This can be set to
* [Offset.Infinite] to position at the far right and bottom of the drawing area
* @param tileMode Determines the behavior for how the shader is to fill a region outside
@@ -87,7 +87,7 @@
*
* @param colors Colors to be rendered as part of the gradient
* @param start Starting position of the linear gradient. This can be set to
- * [Offset.Infinite] to position at the far right and bottom of the drawing area
+ * [Offset.Zero] to position at the far left and top of the drawing area
* @param end Ending position of the linear gradient. This can be set to
* [Offset.Infinite] to position at the far right and bottom of the drawing area
* @param tileMode Determines the behavior for how the shader is to fill a region outside
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
index 6904996..6370dc3 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
@@ -49,6 +49,7 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.runtime.tooling.CompositionData
import androidx.compose.runtime.tooling.LocalInspectionTables
import androidx.compose.ui.Alignment
@@ -66,6 +67,8 @@
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.text
import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextDecoration
@@ -763,12 +766,14 @@
}
@Composable
- fun First() {
+ @Suppress("UNUSED_PARAMETER")
+ fun First(p1: Int) {
Text("First")
}
@Composable
- fun Second() {
+ @Suppress("UNUSED_PARAMETER")
+ fun Second(p2: Int) {
Text("Second")
}
@@ -778,11 +783,16 @@
show {
Inspectable(slotTableRecord) {
- val showFirst by remember { mutableStateOf(true) }
- Crossfade(showFirst) {
- when (it) {
- true -> First()
- false -> Second()
+ Column {
+ var showFirst by remember { mutableStateOf(true) }
+ Button(onClick = { showFirst = !showFirst }) {
+ Text("Button")
+ }
+ Crossfade(showFirst) {
+ when (it) {
+ true -> First(p1 = 1)
+ false -> Second(p2 = 2)
+ }
}
}
}
@@ -797,6 +807,17 @@
val hash = packageNameHash(this.javaClass.name.substringBeforeLast('.'))
assertThat(first.fileName).isEqualTo("LayoutInspectorTreeTest.kt")
assertThat(first.packageHash).isEqualTo(hash)
+ assertThat(first.parameters.map { it.name }).contains("p1")
+
+ composeTestRule.onNodeWithText("Button").performClick()
+ composeTestRule.runOnIdle {
+ val second = builder.convert(androidComposeView)
+ .flatMap { flatten(it) }
+ .first { it.name == "Second" }
+ assertThat(second.fileName).isEqualTo("LayoutInspectorTreeTest.kt")
+ assertThat(second.packageHash).isEqualTo(hash)
+ assertThat(second.parameters.map { it.name }).contains("p2")
+ }
}
@Composable
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
index ba91108..9bf8205 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
@@ -106,7 +106,6 @@
import androidx.compose.ui.unit.offset
import androidx.compose.ui.unit.toOffset
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import org.junit.Assert.assertEquals
@@ -2916,12 +2915,11 @@
validateSquareColors(outerColor = Color.Blue, innerColor = Color.White, size = 10)
}
- @FlakyTest
@Test
fun makingItemLarger() {
var height by mutableStateOf(30)
- var actualHeight = 0
var latch = CountDownLatch(1)
+ var composeView: View? = null
activityTestRule.runOnUiThread {
val linearLayout = LinearLayout(activity)
linearLayout.orientation = LinearLayout.VERTICAL
@@ -2943,29 +2941,30 @@
10000f
)
)
- child.viewTreeObserver.addOnPreDrawListener {
- actualHeight = child.measuredHeight
- latch.countDown()
- true
- }
child.setContent {
- Layout({}) { _, constraints ->
+ Layout(
+ {},
+ Modifier.onGloballyPositioned {
+ latch.countDown()
+ }
+ ) { _, constraints ->
layout(constraints.maxWidth, height.coerceAtMost(constraints.maxHeight)) {}
}
}
+ composeView = child
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
latch = CountDownLatch(1)
activityTestRule.runOnUiThread {
- assertEquals(height, actualHeight)
+ assertEquals(height, composeView!!.measuredHeight)
height = 60
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
activityTestRule.runOnUiThread {
- assertEquals(height, actualHeight)
+ assertEquals(height, composeView!!.measuredHeight)
}
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.desktop.kt
index 215c40a..eb603a3 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.desktop.kt
@@ -16,6 +16,8 @@
package androidx.compose.ui.input.pointer
+import androidx.compose.ui.platform.DesktopPlatform
+import java.awt.event.InputEvent
import java.awt.event.MouseEvent
/**
@@ -37,8 +39,46 @@
internalPointerEvent: InternalPointerEvent?
) : this(changes, internalPointerEvent?.mouseEvent)
+ val buttons = PointerButtons(mouseEvent?.modifiersEx ?: 0)
+
+ val keyboardModifiers = PointerKeyboardModifiers(mouseEvent?.modifiersEx ?: 0)
+
/**
* @param changes The changes.
*/
actual constructor(changes: List<PointerInputChange>) : this(changes, mouseEvent = null)
+}
+
+@Suppress("INLINE_CLASS_DEPRECATED")
+inline class PointerButtons(val value: Int) {
+ private val isMacOsCtrlClick
+ get() = (
+ DesktopPlatform.Current == DesktopPlatform.MacOS &&
+ ((value and InputEvent.BUTTON1_DOWN_MASK) != 0) &&
+ ((value and InputEvent.CTRL_DOWN_MASK) != 0)
+ )
+
+ val isPrimaryPressed
+ get() = (value and InputEvent.BUTTON1_DOWN_MASK) != 0 && !isMacOsCtrlClick
+
+ val isSecondaryPressed: Boolean
+ get() = ((value and InputEvent.BUTTON3_DOWN_MASK) != 0) || isMacOsCtrlClick
+
+ val isTertiaryPressed: Boolean
+ get() = (value and InputEvent.BUTTON2_DOWN_MASK) != 0
+}
+
+@Suppress("INLINE_CLASS_DEPRECATED")
+inline class PointerKeyboardModifiers(val value: Int) {
+ val isAltPressed
+ get() = value and InputEvent.ALT_DOWN_MASK != 0
+
+ val isCtrlPressed
+ get() = value and InputEvent.CTRL_DOWN_MASK != 0
+
+ val isMetaPressed
+ get() = value and InputEvent.META_DOWN_MASK != 0
+
+ val isShiftPressed
+ get() = value and InputEvent.SHIFT_DOWN_MASK != 0
}
\ No newline at end of file
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index cbc8cc2..d3880fa 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -989,20 +989,12 @@
public final class IntentCompat {
method public static android.content.Intent createManageUnusedAppRestrictionsIntent(android.content.Context, String);
- method public static int getUnusedAppRestrictionsStatus(android.content.Context);
method public static android.content.Intent makeMainSelectorActivity(String, String);
field public static final String ACTION_CREATE_REMINDER = "android.intent.action.CREATE_REMINDER";
- field public static final String ACTION_UNUSED_APP_RESTRICTIONS = "android.intent.action.AUTO_REVOKE_PERMISSIONS";
- field public static final int APP_HIBERNATION_DISABLED = 5; // 0x5
- field public static final int APP_HIBERNATION_ENABLED = 4; // 0x4
field public static final String CATEGORY_LEANBACK_LAUNCHER = "android.intent.category.LEANBACK_LAUNCHER";
field public static final String EXTRA_HTML_TEXT = "android.intent.extra.HTML_TEXT";
field public static final String EXTRA_START_PLAYBACK = "android.intent.extra.START_PLAYBACK";
field public static final String EXTRA_TIME = "android.intent.extra.TIME";
- field public static final int PERMISSION_REVOCATION_DISABLED = 3; // 0x3
- field public static final int PERMISSION_REVOCATION_ENABLED = 2; // 0x2
- field public static final int UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE = 1; // 0x1
- field public static final int UNUSED_APP_RESTRICTION_STATUS_UNKNOWN = 0; // 0x0
}
public final class LocusIdCompat {
@@ -1019,6 +1011,17 @@
method public static String![] matchesMany(String![]?, String);
}
+ public final class PackageManagerCompat {
+ method public static int getUnusedAppRestrictionsStatus(android.content.Context);
+ field public static final String ACTION_PERMISSION_REVOCATION_SETTINGS = "android.intent.action.AUTO_REVOKE_PERMISSIONS";
+ field public static final int APP_HIBERNATION_DISABLED = 5; // 0x5
+ field public static final int APP_HIBERNATION_ENABLED = 4; // 0x4
+ field public static final int PERMISSION_REVOCATION_DISABLED = 3; // 0x3
+ field public static final int PERMISSION_REVOCATION_ENABLED = 2; // 0x2
+ field public static final int UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE = 1; // 0x1
+ field public static final int UNUSED_APP_RESTRICTION_STATUS_UNKNOWN = 0; // 0x0
+ }
+
public final class PermissionChecker {
method public static int checkCallingOrSelfPermission(android.content.Context, String);
method public static int checkCallingPermission(android.content.Context, String, String?);
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 334f7dd..bfe8097 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -989,20 +989,12 @@
public final class IntentCompat {
method public static android.content.Intent createManageUnusedAppRestrictionsIntent(android.content.Context, String);
- method public static int getUnusedAppRestrictionsStatus(android.content.Context);
method public static android.content.Intent makeMainSelectorActivity(String, String);
field public static final String ACTION_CREATE_REMINDER = "android.intent.action.CREATE_REMINDER";
- field public static final String ACTION_UNUSED_APP_RESTRICTIONS = "android.intent.action.AUTO_REVOKE_PERMISSIONS";
- field public static final int APP_HIBERNATION_DISABLED = 5; // 0x5
- field public static final int APP_HIBERNATION_ENABLED = 4; // 0x4
field public static final String CATEGORY_LEANBACK_LAUNCHER = "android.intent.category.LEANBACK_LAUNCHER";
field public static final String EXTRA_HTML_TEXT = "android.intent.extra.HTML_TEXT";
field public static final String EXTRA_START_PLAYBACK = "android.intent.extra.START_PLAYBACK";
field public static final String EXTRA_TIME = "android.intent.extra.TIME";
- field public static final int PERMISSION_REVOCATION_DISABLED = 3; // 0x3
- field public static final int PERMISSION_REVOCATION_ENABLED = 2; // 0x2
- field public static final int UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE = 1; // 0x1
- field public static final int UNUSED_APP_RESTRICTION_STATUS_UNKNOWN = 0; // 0x0
}
public final class LocusIdCompat {
@@ -1019,6 +1011,17 @@
method public static String![] matchesMany(String![]?, String);
}
+ public final class PackageManagerCompat {
+ method public static int getUnusedAppRestrictionsStatus(android.content.Context);
+ field public static final String ACTION_PERMISSION_REVOCATION_SETTINGS = "android.intent.action.AUTO_REVOKE_PERMISSIONS";
+ field public static final int APP_HIBERNATION_DISABLED = 5; // 0x5
+ field public static final int APP_HIBERNATION_ENABLED = 4; // 0x4
+ field public static final int PERMISSION_REVOCATION_DISABLED = 3; // 0x3
+ field public static final int PERMISSION_REVOCATION_ENABLED = 2; // 0x2
+ field public static final int UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE = 1; // 0x1
+ field public static final int UNUSED_APP_RESTRICTION_STATUS_UNKNOWN = 0; // 0x0
+ }
+
public final class PermissionChecker {
method public static int checkCallingOrSelfPermission(android.content.Context, String);
method public static int checkCallingPermission(android.content.Context, String, String?);
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 61cafa1..1712a00 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -1092,20 +1092,12 @@
public final class IntentCompat {
method public static android.content.Intent createManageUnusedAppRestrictionsIntent(android.content.Context, String);
- method public static int getUnusedAppRestrictionsStatus(android.content.Context);
method public static android.content.Intent makeMainSelectorActivity(String, String);
field public static final String ACTION_CREATE_REMINDER = "android.intent.action.CREATE_REMINDER";
- field public static final String ACTION_UNUSED_APP_RESTRICTIONS = "android.intent.action.AUTO_REVOKE_PERMISSIONS";
- field public static final int APP_HIBERNATION_DISABLED = 5; // 0x5
- field public static final int APP_HIBERNATION_ENABLED = 4; // 0x4
field public static final String CATEGORY_LEANBACK_LAUNCHER = "android.intent.category.LEANBACK_LAUNCHER";
field public static final String EXTRA_HTML_TEXT = "android.intent.extra.HTML_TEXT";
field public static final String EXTRA_START_PLAYBACK = "android.intent.extra.START_PLAYBACK";
field public static final String EXTRA_TIME = "android.intent.extra.TIME";
- field public static final int PERMISSION_REVOCATION_DISABLED = 3; // 0x3
- field public static final int PERMISSION_REVOCATION_ENABLED = 2; // 0x2
- field public static final int UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE = 1; // 0x1
- field public static final int UNUSED_APP_RESTRICTION_STATUS_UNKNOWN = 0; // 0x0
}
public final class LocusIdCompat {
@@ -1122,6 +1114,17 @@
method public static String![] matchesMany(String![]?, String);
}
+ public final class PackageManagerCompat {
+ method public static int getUnusedAppRestrictionsStatus(android.content.Context);
+ field public static final String ACTION_PERMISSION_REVOCATION_SETTINGS = "android.intent.action.AUTO_REVOKE_PERMISSIONS";
+ field public static final int APP_HIBERNATION_DISABLED = 5; // 0x5
+ field public static final int APP_HIBERNATION_ENABLED = 4; // 0x4
+ field public static final int PERMISSION_REVOCATION_DISABLED = 3; // 0x3
+ field public static final int PERMISSION_REVOCATION_ENABLED = 2; // 0x2
+ field public static final int UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE = 1; // 0x1
+ field public static final int UNUSED_APP_RESTRICTION_STATUS_UNKNOWN = 0; // 0x0
+ }
+
public final class PermissionChecker {
method @androidx.core.content.PermissionChecker.PermissionResult public static int checkCallingOrSelfPermission(android.content.Context, String);
method @androidx.core.content.PermissionChecker.PermissionResult public static int checkCallingPermission(android.content.Context, String, String?);
diff --git a/core/core/src/androidTest/java/androidx/core/content/IntentCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/IntentCompatTest.java
index 4685da9..0b98b7c 100644
--- a/core/core/src/androidTest/java/androidx/core/content/IntentCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/IntentCompatTest.java
@@ -26,13 +26,6 @@
import static android.os.Build.VERSION_CODES.R;
import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
-import static androidx.core.content.IntentCompat.APP_HIBERNATION_DISABLED;
-import static androidx.core.content.IntentCompat.APP_HIBERNATION_ENABLED;
-import static androidx.core.content.IntentCompat.PERMISSION_REVOCATION_DISABLED;
-import static androidx.core.content.IntentCompat.PERMISSION_REVOCATION_ENABLED;
-import static androidx.core.content.IntentCompat.UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE;
-import static androidx.core.content.IntentCompat.UNUSED_APP_RESTRICTION_STATUS_UNKNOWN;
-
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
@@ -50,7 +43,9 @@
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.net.Uri;
+import android.os.Build;
+import androidx.annotation.RequiresApi;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
@@ -201,105 +196,11 @@
mContext, PACKAGE_NAME));
}
- @Test
- @SdkSuppress(minSdkVersion = 31)
- public void getUnusedAppRestrictionsStatus_api31Plus_disabled_returnsAppHibernationDisabled() {
- // Mark the application as exempt from app hibernation, so the feature is disabled
- when(mPackageManager.isAutoRevokeWhitelisted()).thenReturn(true);
-
- assertThat(IntentCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
- APP_HIBERNATION_DISABLED);
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 31)
- public void getUnusedAppRestrictionsStatus_api31Plus_enabled_returnsAppHibernationEnabled() {
- // Mark the application as _not_ exempt from app hibernation, so the feature is enabled
- when(mPackageManager.isAutoRevokeWhitelisted()).thenReturn(false);
-
- assertThat(IntentCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
- APP_HIBERNATION_ENABLED);
- }
-
- @Test
- @SdkSuppress(minSdkVersion = R, maxSdkVersion = R)
- public void getUnusedAppRestrictionsStatus_api30_disabled_returnsPermRevocationDisabled() {
- // Mark the application as exempt from permission revocation, so the feature is disabled
- when(mPackageManager.isAutoRevokeWhitelisted()).thenReturn(true);
-
- assertThat(IntentCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
- PERMISSION_REVOCATION_DISABLED);
- }
-
- @Test
- @SdkSuppress(minSdkVersion = R)
- public void getUnusedAppRestrictionsStatus_api30Plus_enabled_returnsPermRevocationEnabled() {
- // Mark the application as _not_ exempt from permission revocation, so the feature is
- // enabled
- when(mPackageManager.isAutoRevokeWhitelisted()).thenReturn(false);
-
- assertThat(IntentCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
- PERMISSION_REVOCATION_ENABLED);
- }
-
- @Test
- @SdkSuppress(minSdkVersion = M, maxSdkVersion = Q)
- public void getUnusedAppRestrictionsStatus_preApi30_noRevocationApp_returnsNotAvailable() {
- // Don't install an app that can resolve the permission auto-revocation intent
-
- assertThat(IntentCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
- UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE);
- }
-
- @Test
- @SdkSuppress(minSdkVersion = M, maxSdkVersion = Q)
- public void getUnusedAppRestrictionsStatus_preApi30_noVerifierRevokeApp_returnsNotAvailable() {
- setupPermissionRevocationApps(Arrays.asList(NON_VERIFIER_PACKAGE_NAME));
- // Do not set this app as the Verifier on the device
- when(mPackageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
- NON_VERIFIER_PACKAGE_NAME)).thenReturn(PERMISSION_DENIED);
-
- assertThat(IntentCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
- UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE);
- }
-
- @Test
- @SdkSuppress(minSdkVersion = M, maxSdkVersion = Q)
- public void getUnusedAppRestrictionsStatus_preApi30_verifierRevocationApp_returnsUnknown() {
- setupPermissionRevocationApps(Arrays.asList(VERIFIER_PACKAGE_NAME));
- // Set this app as the Verifier on the device
- when(mPackageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
- VERIFIER_PACKAGE_NAME)).thenReturn(PERMISSION_GRANTED);
-
- assertThat(IntentCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
- UNUSED_APP_RESTRICTION_STATUS_UNKNOWN);
- }
-
- @Test
- @SdkSuppress(minSdkVersion = M, maxSdkVersion = Q)
- public void getUnusedAppRestrictionsStatus_preApi30_manyVerifierRevocationApps_doesNotThrow() {
- setupPermissionRevocationApps(Arrays.asList(VERIFIER_PACKAGE_NAME, VERIFIER_PACKAGE_NAME2));
- // Set both apps as the Verifier on the device, but we should have a graceful failure.
- when(mPackageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
- VERIFIER_PACKAGE_NAME)).thenReturn(PERMISSION_GRANTED);
- when(mPackageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
- VERIFIER_PACKAGE_NAME2)).thenReturn(PERMISSION_GRANTED);
-
- assertThat(IntentCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
- UNUSED_APP_RESTRICTION_STATUS_UNKNOWN);
- }
-
- @Test
- @SdkSuppress(maxSdkVersion = LOLLIPOP)
- public void getUnusedAppRestrictionsStatus_preApi23_returnsFeatureNotAvailable() {
- assertThat(IntentCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
- UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE);
- }
-
/**
* Setup applications that can handle unused app restriction features. In this case,
* they are permission revocation apps.
*/
+ @RequiresApi(api = Build.VERSION_CODES.KITKAT)
private void setupPermissionRevocationApps(List<String> packageNames) {
List<ResolveInfo> resolveInfos = new ArrayList<>();
diff --git a/core/core/src/androidTest/java/androidx/core/content/PackageManagerCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/PackageManagerCompatTest.java
new file mode 100644
index 0000000..bdf5820
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/content/PackageManagerCompatTest.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content;
+
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.os.Build.VERSION_CODES.M;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+
+import static androidx.core.content.PackageManagerCompat.APP_HIBERNATION_DISABLED;
+import static androidx.core.content.PackageManagerCompat.APP_HIBERNATION_ENABLED;
+import static androidx.core.content.PackageManagerCompat.PERMISSION_REVOCATION_DISABLED;
+import static androidx.core.content.PackageManagerCompat.PERMISSION_REVOCATION_ENABLED;
+import static androidx.core.content.PackageManagerCompat.UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE;
+import static androidx.core.content.PackageManagerCompat.UNUSED_APP_RESTRICTION_STATUS_UNKNOWN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ResolveInfo;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+/** Tests for {@link PackageManagerCompat}. */
+public class PackageManagerCompatTest {
+
+ private Context mContext;
+ private PackageManager mPackageManager = mock(PackageManager.class);
+ private static final String VERIFIER_PACKAGE_NAME = "verifier.package.name";
+ private static final String VERIFIER_PACKAGE_NAME2 = "verifier.package.name.2";
+ private static final String NON_VERIFIER_PACKAGE_NAME = "non.verifier.package.name";
+
+ @Before
+ public void setUp() {
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 31)
+ public void getUnusedAppRestrictionsStatus_api31Plus_disabled_returnsAppHibernationDisabled() {
+ // Mark the application as exempt from app hibernation, so the feature is disabled
+ when(mPackageManager.isAutoRevokeWhitelisted()).thenReturn(true);
+
+ assertThat(PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
+ APP_HIBERNATION_DISABLED);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 31)
+ public void getUnusedAppRestrictionsStatus_api31Plus_enabled_returnsAppHibernationEnabled() {
+ // Mark the application as _not_ exempt from app hibernation, so the feature is enabled
+ when(mPackageManager.isAutoRevokeWhitelisted()).thenReturn(false);
+
+ assertThat(PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
+ APP_HIBERNATION_ENABLED);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = R, maxSdkVersion = R)
+ public void getUnusedAppRestrictionsStatus_api30_disabled_returnsPermRevocationDisabled() {
+ // Mark the application as exempt from permission revocation, so the feature is disabled
+ when(mPackageManager.isAutoRevokeWhitelisted()).thenReturn(true);
+
+ assertThat(PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
+ PERMISSION_REVOCATION_DISABLED);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = R)
+ public void getUnusedAppRestrictionsStatus_api30Plus_enabled_returnsPermRevocationEnabled() {
+ // Mark the application as _not_ exempt from permission revocation, so the feature is
+ // enabled
+ when(mPackageManager.isAutoRevokeWhitelisted()).thenReturn(false);
+
+ assertThat(PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
+ PERMISSION_REVOCATION_ENABLED);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = M, maxSdkVersion = Q)
+ public void getUnusedAppRestrictionsStatus_preApi30_noRevocationApp_returnsNotAvailable() {
+ // Don't install an app that can resolve the permission auto-revocation intent
+
+ assertThat(PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
+ UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = M, maxSdkVersion = Q)
+ public void getUnusedAppRestrictionsStatus_preApi30_noVerifierRevokeApp_returnsNotAvailable() {
+ setupPermissionRevocationApps(Arrays.asList(NON_VERIFIER_PACKAGE_NAME));
+ // Do not set this app as the Verifier on the device
+ when(mPackageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
+ NON_VERIFIER_PACKAGE_NAME)).thenReturn(PERMISSION_DENIED);
+
+ assertThat(PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
+ UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = M, maxSdkVersion = Q)
+ public void getUnusedAppRestrictionsStatus_preApi30_verifierRevocationApp_returnsUnknown() {
+ setupPermissionRevocationApps(Arrays.asList(VERIFIER_PACKAGE_NAME));
+ // Set this app as the Verifier on the device
+ when(mPackageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
+ VERIFIER_PACKAGE_NAME)).thenReturn(PERMISSION_GRANTED);
+
+ assertThat(PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
+ UNUSED_APP_RESTRICTION_STATUS_UNKNOWN);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = M, maxSdkVersion = Q)
+ public void getUnusedAppRestrictionsStatus_preApi30_manyVerifierRevocationApps_doesNotThrow() {
+ setupPermissionRevocationApps(Arrays.asList(VERIFIER_PACKAGE_NAME, VERIFIER_PACKAGE_NAME2));
+ // Set both apps as the Verifier on the device, but we should have a graceful failure.
+ when(mPackageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
+ VERIFIER_PACKAGE_NAME)).thenReturn(PERMISSION_GRANTED);
+ when(mPackageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
+ VERIFIER_PACKAGE_NAME2)).thenReturn(PERMISSION_GRANTED);
+
+ assertThat(PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
+ UNUSED_APP_RESTRICTION_STATUS_UNKNOWN);
+ }
+
+ @Test
+ @SdkSuppress(maxSdkVersion = LOLLIPOP)
+ public void getUnusedAppRestrictionsStatus_preApi23_returnsFeatureNotAvailable() {
+ assertThat(PackageManagerCompat.getUnusedAppRestrictionsStatus(mContext)).isEqualTo(
+ UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE);
+ }
+
+ /**
+ * Setup applications that can handle unused app restriction features. In this case,
+ * they are permission revocation apps.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+ private void setupPermissionRevocationApps(List<String> packageNames) {
+ List<ResolveInfo> resolveInfos = new ArrayList<>();
+
+ for (String packageName : packageNames) {
+ ApplicationInfo appInfo = new ApplicationInfo();
+ appInfo.uid = 12345;
+ appInfo.packageName = packageName;
+
+ ActivityInfo activityInfo = new ActivityInfo();
+ activityInfo.packageName = packageName;
+ activityInfo.name = "Name needed to keep toString() happy :)";
+ activityInfo.applicationInfo = appInfo;
+
+ ResolveInfo resolveInfo = new ResolveInfo();
+ resolveInfo.activityInfo = activityInfo;
+ resolveInfo.providerInfo = new ProviderInfo();
+ resolveInfo.providerInfo.name = "Name needed to keep toString() happy :)";
+
+ resolveInfos.add(resolveInfo);
+ }
+
+ // Mark the applications as being able to resolve the AUTO_REVOKE_PERMISSIONS intent
+ when(mPackageManager.queryIntentActivities(
+ nullable(Intent.class), eq(0))).thenReturn(resolveInfos);
+ }
+}
diff --git a/core/core/src/main/java/androidx/core/content/IntentCompat.java b/core/core/src/main/java/androidx/core/content/IntentCompat.java
index 33a93db..9ba25b6 100644
--- a/core/core/src/main/java/androidx/core/content/IntentCompat.java
+++ b/core/core/src/main/java/androidx/core/content/IntentCompat.java
@@ -18,27 +18,19 @@
import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
-import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.core.content.PackageManagerCompat.ACTION_PERMISSION_REVOCATION_SETTINGS;
+import static androidx.core.content.PackageManagerCompat.UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE;
+import static androidx.core.content.PackageManagerCompat.getUnusedAppRestrictionsStatus;
+import static androidx.core.content.PackageManagerCompat.getVerifierRolePackageName;
import static androidx.core.util.Preconditions.checkNotNull;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.net.Uri;
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 java.lang.annotation.Retention;
-import java.util.List;
/**
* Helper for accessing features in {@link android.content.Intent}.
@@ -68,14 +60,6 @@
public static final String ACTION_CREATE_REMINDER = "android.intent.action.CREATE_REMINDER";
/**
- * Activity action: creates an intent to redirect the user to UI to turn on/off their
- * unused app restriction settings.
- */
- @SuppressLint("ActionValue")
- public static final String ACTION_UNUSED_APP_RESTRICTIONS =
- "android.intent.action.AUTO_REVOKE_PERMISSIONS";
-
- /**
* A constant String that is associated with the Intent, used with
* {@link android.content.Intent#ACTION_SEND} to supply an alternative to
* {@link android.content.Intent#EXTRA_TEXT}
@@ -107,55 +91,6 @@
*/
public static final String CATEGORY_LEANBACK_LAUNCHER = "android.intent.category.LEANBACK_LAUNCHER";
- /** The status of Unused App Restrictions features is unknown for this app. */
- public static final int UNUSED_APP_RESTRICTION_STATUS_UNKNOWN = 0;
-
- /** There are no available Unused App Restrictions features for this app. */
- public static final int UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE = 1;
-
- /**
- * Permission revocation is enabled for this app (i.e. permissions will be automatically
- * reset if the app is unused).
- *
- * Note: this also means that app hibernation is not available for this app.
- */
- public static final int PERMISSION_REVOCATION_ENABLED = 2;
-
- /**
- * Permission revocation is disabled for this app (i.e. this app is exempt from having
- * its permissions automatically removed).
- *
- * Note: this also means that app hibernation is not available for this app.
- */
- public static final int PERMISSION_REVOCATION_DISABLED = 3;
-
- /**
- * App hibernation is enabled for this app (i.e. this app will be hibernated and have its
- * permissions revoked if the app is unused).
- *
- * Note: this also means that permission revocation is enabled for this app.
- */
- public static final int APP_HIBERNATION_ENABLED = 4;
-
- /**
- * App hibernation is disabled for this app (i.e. this app is exempt from being hibernated).
- *
- * Note: this also means that permission revocation is disabled for this app.
- */
- public static final int APP_HIBERNATION_DISABLED = 5;
-
- /**
- * The status of Unused App Restrictions features for this app.
- * @hide
- */
- @IntDef({UNUSED_APP_RESTRICTION_STATUS_UNKNOWN, UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE,
- PERMISSION_REVOCATION_ENABLED, PERMISSION_REVOCATION_DISABLED,
- APP_HIBERNATION_ENABLED, APP_HIBERNATION_DISABLED})
- @Retention(SOURCE)
- @RestrictTo(LIBRARY)
- public @interface UnusedAppRestrictionsStatus {
- }
-
/**
* Make an Intent for the main activity of an application, without
* specifying a specific activity to run but giving a selector to find
@@ -195,13 +130,14 @@
* Make an Intent to redirect the user to UI to manage their unused app restriction settings
* for a particular app (e.g. permission revocation, app hibernation).
*
- * Note: developers must first call {@link #getUnusedAppRestrictionsStatus(Context)} to make
+ * Note: developers must first call
+ * {@link PackageManagerCompat#getUnusedAppRestrictionsStatus(Context)} to make
* sure that unused app restriction features are available on the device before attempting to
* create an intent using this method. Any return value of this method besides
- * {@link #UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE} indicates that at least one
- * unused app restriction feature is available on the device. If the return value _is_
- * {@link #UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE}, this method will throw an
- * {@link UnsupportedOperationException}.
+ * {@link PackageManagerCompat#UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE} indicates that at
+ * least one unused app restriction feature is available on the device. If the return value _is_
+ * {@link PackageManagerCompat#UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE}, this method will
+ * throw an {@link UnsupportedOperationException}.
*
* Compatibility behavior:
* <ul>
@@ -238,125 +174,22 @@
.setData(Uri.fromParts("package", packageName, /* fragment= */ null));
}
- Intent unusedAppRestrictionsIntent =
- new Intent(ACTION_UNUSED_APP_RESTRICTIONS)
+ Intent permissionRevocationSettingsIntent =
+ new Intent(ACTION_PERMISSION_REVOCATION_SETTINGS)
.setData(Uri.fromParts(
"package", packageName, /* fragment= */ null));
// If the OS version is R, then no need to add any other data or flags, since we're
// relying on the Android R system feature.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- return unusedAppRestrictionsIntent;
+ return permissionRevocationSettingsIntent;
} else {
// Only allow apps with the Verifier role to resolve the intent.
String verifierPackageName = getVerifierRolePackageName(context.getPackageManager());
// The Verifier package name shouldn't be null since we've already checked that there
// exists a Verifier on the device, but nonetheless we double-check here.
- return unusedAppRestrictionsIntent
+ return permissionRevocationSettingsIntent
.setPackage(checkNotNull(verifierPackageName));
}
}
-
- /**
- * Returns the package name of the one and only Verifier on the device. If none exist, this
- * will return {@code null}. Likewise, if multiple Verifiers exist, this method will return
- * the first Verifier's package name.
- */
- @Nullable
- private static String getVerifierRolePackageName(PackageManager packageManager) {
- Intent unusedAppRestrictionsIntent =
- new Intent(ACTION_UNUSED_APP_RESTRICTIONS)
- .setData(Uri.fromParts(
- "package", "com.example", /* fragment= */ null));
- List<ResolveInfo> intentResolvers =
- packageManager.queryIntentActivities(unusedAppRestrictionsIntent, /* flags= */ 0);
-
- String verifierPackageName = null;
-
- for (ResolveInfo intentResolver: intentResolvers) {
- String packageName = intentResolver.activityInfo.packageName;
- if (packageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
- packageName) != PackageManager.PERMISSION_GRANTED) {
- continue;
- }
-
- if (verifierPackageName != null) {
- // This shouldn't happen, but we fail gracefully nonetheless and avoid throwing an
- // exception, instead returning the first package name with the Verifier role
- // that we found.
- return verifierPackageName;
- }
- verifierPackageName = packageName;
- }
-
- return verifierPackageName;
- }
-
- /**
- * Returns the status of Unused App Restriction features for the current application, i.e.
- * whether the features are available and if so, enabled for the application.
- *
- * Compatibility behavior:
- * <ul>
- * <li>SDK 31 and above, if {@link PackageManager#isAutoRevokeWhitelisted()} is true, this
- * will return {@link #APP_HIBERNATION_ENABLED}. Else, it will return
- * {@link #APP_HIBERNATION_DISABLED}.</li>
- * <li>SDK 30, if {@link PackageManager#isAutoRevokeWhitelisted()} is true, this will return
- * {@link #PERMISSION_REVOCATION_ENABLED}. Else, it will return
- * {@link #PERMISSION_REVOCATION_DISABLED}.</li>
- * <li>SDK 23 through 29, if there exists an app with the Verifier role that can resolve the
- * {@code Intent.ACTION_AUTO_REVOKE_PERMISSIONS} action.
- * <li>SDK 22 and below, this method always returns
- * {@link #UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE} as runtime permissions did not exist
- * yet.
- * </ul>
- */
- public static @UnusedAppRestrictionsStatus int getUnusedAppRestrictionsStatus(
- @NonNull Context context) {
- // Return false if the Android OS version is before M, because Android M introduced runtime
- // permissions
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- return UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE;
- }
-
- // TODO: replace with VERSION_CODES.S once it's defined
- if (Build.VERSION.SDK_INT >= 31) {
- return Api30Impl.areUnusedAppRestrictionsEnabled(context)
- ? APP_HIBERNATION_ENABLED
- : APP_HIBERNATION_DISABLED;
- }
-
- if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
- return Api30Impl.areUnusedAppRestrictionsEnabled(context)
- ? PERMISSION_REVOCATION_ENABLED
- : PERMISSION_REVOCATION_DISABLED;
- }
-
- // Else, check for an app with the verifier role that can resolve the intent
- String verifierPackageName = getVerifierRolePackageName(context.getPackageManager());
- // Check that we were able to get the one Verifier's package name. If no Verifier or
- // more than one Verifier exists on the device, unused app restrictions are not available
- // on the device.
- return (verifierPackageName == null)
- ? UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE
- // TODO(b/177234481): Implement the backport behavior of this API
- : UNUSED_APP_RESTRICTION_STATUS_UNKNOWN;
- }
-
- /**
- * We create this static class to avoid Class Verification Failures from referencing a method
- * only added in Android R.
- *
- * <p>Gating references on SDK checks does not address class verification failures, hence the
- * need for this inner class.
- */
- @RequiresApi(Build.VERSION_CODES.R)
- private static class Api30Impl {
- private Api30Impl() {}
- static boolean areUnusedAppRestrictionsEnabled(@NonNull Context context) {
- // If the app is allowlisted, that means that it is exempt from unused app restriction
- // features, and thus the features are _disabled_.
- return !context.getPackageManager().isAutoRevokeWhitelisted();
- }
- }
}
diff --git a/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java b/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java
new file mode 100644
index 0000000..2303a65
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/content/PackageManagerCompat.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2011 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.core.content;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+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 java.lang.annotation.Retention;
+import java.util.List;
+
+/**
+ * Helper for accessing features in {@link PackageManager}.
+ */
+public final class PackageManagerCompat {
+ private PackageManagerCompat() {
+ /* Hide constructor */
+ }
+
+ /**
+ * Activity action: creates an intent to redirect the user to UI to turn on/off their
+ * permission revocation settings.
+ */
+ @SuppressLint("ActionValue")
+ public static final String ACTION_PERMISSION_REVOCATION_SETTINGS =
+ "android.intent.action.AUTO_REVOKE_PERMISSIONS";
+
+ /** The status of Unused App Restrictions features is unknown for this app. */
+ public static final int UNUSED_APP_RESTRICTION_STATUS_UNKNOWN = 0;
+
+ /** There are no available Unused App Restrictions features for this app. */
+ public static final int UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE = 1;
+
+ /**
+ * Permission revocation is enabled for this app (i.e. permissions will be automatically
+ * reset if the app is unused).
+ *
+ * Note: this also means that app hibernation is not available for this app.
+ */
+ public static final int PERMISSION_REVOCATION_ENABLED = 2;
+
+ /**
+ * Permission revocation is disabled for this app (i.e. this app is exempt from having
+ * its permissions automatically removed).
+ *
+ * Note: this also means that app hibernation is not available for this app.
+ */
+ public static final int PERMISSION_REVOCATION_DISABLED = 3;
+
+ /**
+ * App hibernation is enabled for this app (i.e. this app will be hibernated and have its
+ * permissions revoked if the app is unused).
+ *
+ * Note: this also means that permission revocation is enabled for this app.
+ */
+ public static final int APP_HIBERNATION_ENABLED = 4;
+
+ /**
+ * App hibernation is disabled for this app (i.e. this app is exempt from being hibernated).
+ *
+ * Note: this also means that permission revocation is disabled for this app.
+ */
+ public static final int APP_HIBERNATION_DISABLED = 5;
+
+ /**
+ * The status of Unused App Restrictions features for this app.
+ * @hide
+ */
+ @IntDef({UNUSED_APP_RESTRICTION_STATUS_UNKNOWN, UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE,
+ PERMISSION_REVOCATION_ENABLED, PERMISSION_REVOCATION_DISABLED,
+ APP_HIBERNATION_ENABLED, APP_HIBERNATION_DISABLED})
+ @Retention(SOURCE)
+ @RestrictTo(LIBRARY)
+ public @interface UnusedAppRestrictionsStatus {
+ }
+
+ /**
+ * Returns the status of Unused App Restriction features for the current application, i.e.
+ * whether the features are available and if so, enabled for the application.
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>SDK 31 and above, if {@link PackageManager#isAutoRevokeWhitelisted()} is true, this
+ * will return {@link #APP_HIBERNATION_ENABLED}. Else, it will return
+ * {@link #APP_HIBERNATION_DISABLED}.</li>
+ * <li>SDK 30, if {@link PackageManager#isAutoRevokeWhitelisted()} is true, this will return
+ * {@link #PERMISSION_REVOCATION_ENABLED}. Else, it will return
+ * {@link #PERMISSION_REVOCATION_DISABLED}.</li>
+ * <li>SDK 23 through 29, if there exists an app with the Verifier role that can resolve the
+ * {@code Intent.ACTION_AUTO_REVOKE_PERMISSIONS} action.
+ * <li>SDK 22 and below, this method always returns
+ * {@link #UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE} as runtime permissions did not exist
+ * yet.
+ * </ul>
+ */
+ public static @UnusedAppRestrictionsStatus int getUnusedAppRestrictionsStatus(
+ @NonNull Context context) {
+ // Return false if the Android OS version is before M, because Android M introduced runtime
+ // permissions
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ return UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE;
+ }
+
+ // TODO: replace with VERSION_CODES.S once it's defined
+ if (Build.VERSION.SDK_INT >= 31) {
+ return Api30Impl.areUnusedAppRestrictionsEnabled(context)
+ ? APP_HIBERNATION_ENABLED
+ : APP_HIBERNATION_DISABLED;
+ }
+
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
+ return Api30Impl.areUnusedAppRestrictionsEnabled(context)
+ ? PERMISSION_REVOCATION_ENABLED
+ : PERMISSION_REVOCATION_DISABLED;
+ }
+
+ // Else, check for an app with the verifier role that can resolve the intent
+ String verifierPackageName = getVerifierRolePackageName(context.getPackageManager());
+ // Check that we were able to get the one Verifier's package name. If no Verifier or
+ // more than one Verifier exists on the device, unused app restrictions are not available
+ // on the device.
+ return (verifierPackageName == null)
+ ? UNUSED_APP_RESTRICTION_FEATURE_NOT_AVAILABLE
+ // TODO(b/177234481): Implement the backport behavior of this API
+ : UNUSED_APP_RESTRICTION_STATUS_UNKNOWN;
+ }
+
+ /**
+ * Returns the package name of the one and only Verifier on the device. If none exist, this
+ * will return {@code null}. Likewise, if multiple Verifiers exist, this method will return
+ * the first Verifier's package name.
+ */
+ @Nullable
+ static String getVerifierRolePackageName(@NonNull PackageManager packageManager) {
+ Intent permissionRevocationSettingsIntent =
+ new Intent(ACTION_PERMISSION_REVOCATION_SETTINGS)
+ .setData(Uri.fromParts(
+ "package", "com.example", /* fragment= */ null));
+ List<ResolveInfo> intentResolvers =
+ packageManager.queryIntentActivities(
+ permissionRevocationSettingsIntent, /* flags= */ 0);
+
+ String verifierPackageName = null;
+
+ for (ResolveInfo intentResolver: intentResolvers) {
+ String packageName = intentResolver.activityInfo.packageName;
+ if (packageManager.checkPermission("android.permission.PACKAGE_VERIFICATION_AGENT",
+ packageName) != PackageManager.PERMISSION_GRANTED) {
+ continue;
+ }
+
+ if (verifierPackageName != null) {
+ // This shouldn't happen, but we fail gracefully nonetheless and avoid throwing an
+ // exception, instead returning the first package name with the Verifier role
+ // that we found.
+ return verifierPackageName;
+ }
+ verifierPackageName = packageName;
+ }
+
+ return verifierPackageName;
+ }
+
+ /**
+ * We create this static class to avoid Class Verification Failures from referencing a method
+ * only added in Android R.
+ *
+ * <p>Gating references on SDK checks does not address class verification failures, hence the
+ * need for this inner class.
+ */
+ @RequiresApi(Build.VERSION_CODES.R)
+ private static class Api30Impl {
+ private Api30Impl() {}
+ static boolean areUnusedAppRestrictionsEnabled(@NonNull Context context) {
+ // If the app is allowlisted, that means that it is exempt from unused app restriction
+ // features, and thus the features are _disabled_.
+ return !context.getPackageManager().isAutoRevokeWhitelisted();
+ }
+ }
+}
diff --git a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
index 3882a04..a24bcfa 100644
--- a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
+++ b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
@@ -46,6 +46,9 @@
* SharedPreferences to migrate from (limited to [keysToMigrate] and a T which represent
* the current data. The function must return the migrated data.
*
+ * If SharedPreferences is empty or does not contain any keys which you specified, this
+ * callback will not run.
+ *
* @param sharedPreferencesView the current state of the SharedPreferences
* @param currentData the most recently persisted data
* @return a Single of the updated data
diff --git a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
index 6b83eaa..0c64b55 100644
--- a/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
+++ b/datastore/datastore-rxjava3/src/main/java/androidx/datastore/rxjava3/RxSharedPreferencesMigration.kt
@@ -46,6 +46,9 @@
* SharedPreferences to migrate from (limited to [keysToMigrate]) and a T which represent
* the current data. The function must return the migrated data.
*
+ * If SharedPreferences is empty or does not contain any keys which you specified, this
+ * callback will not run.
+ *
* @param sharedPreferencesView the current state of the SharedPreferences
* @param currentData the most recently persisted data
* @return a Single of the updated data
diff --git a/datastore/datastore/src/androidTest/java/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt b/datastore/datastore/src/androidTest/java/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt
index 52604f60..0387583 100644
--- a/datastore/datastore/src/androidTest/java/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt
+++ b/datastore/datastore/src/androidTest/java/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt
@@ -154,6 +154,21 @@
}
@Test
+ fun testSharedPrefsViewWithAllKeysSpecified_doesntThrowErrorWhenKeyDoesntExist() =
+ runBlockingTest {
+ assertThat(sharedPrefs.edit().putInt("unrelated_key", -123).commit()).isTrue()
+
+ val migration = SharedPreferencesMigration(
+ produceSharedPreferences = { sharedPrefs },
+ ) { prefs: SharedPreferencesView, _: Byte ->
+ prefs.getInt("this_key_doesnt_exist_yet", 123).toByte()
+ }
+
+ val dataStore = getDataStoreWithMigrations(listOf(migration))
+ assertThat(dataStore.data.first()).isEqualTo(123)
+ }
+
+ @Test
fun producedSharedPreferencesIsUsed() = runBlockingTest {
assertThat(sharedPrefs.edit().putInt("integer_key", 123).commit()).isTrue()
diff --git a/datastore/datastore/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt b/datastore/datastore/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt
index 76cfae9..4fc7b7c 100644
--- a/datastore/datastore/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt
+++ b/datastore/datastore/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt
@@ -66,7 +66,8 @@
* since this may be called multiple times. See [DataMigration.migrate] for more
* information. The lambda accepts a SharedPreferencesView which is the view of the
* SharedPreferences to migrate from (limited to [keysToMigrate] and a T which represent
- * the current data. The function must return the migrated data.
+ * the current data. The function must return the migrated data. If SharedPreferences is
+ * empty or does not contain any keys which you specified, this callback will not run.
*/
@JvmOverloads // Generate constructors for default params for java users.
public constructor(
@@ -111,7 +112,8 @@
* since this may be called multiple times. See [DataMigration.migrate] for more
* information. The lambda accepts a SharedPreferencesView which is the view of the
* SharedPreferences to migrate from (limited to [keysToMigrate] and a T which represent
- * the current data. The function must return the migrated data.
+ * the current data. The function must return the migrated data. If SharedPreferences is
+ * empty or does not contain any keys which you specified, this callback will not run.
*/
@JvmOverloads // Generate constructors for default params for java users.
public constructor(
@@ -131,20 +133,26 @@
private val sharedPrefs: SharedPreferences by lazy(produceSharedPreferences)
- private val keySet: MutableSet<String> by lazy {
+ /**
+ * keySet is null if the user specified [MIGRATE_ALL_KEYS].
+ */
+ private val keySet: MutableSet<String>? =
if (keysToMigrate === MIGRATE_ALL_KEYS) {
- sharedPrefs.all.keys
+ null
} else {
- keysToMigrate
- }.toMutableSet()
- }
+ keysToMigrate.toMutableSet()
+ }
override suspend fun shouldMigrate(currentData: T): Boolean {
if (!shouldRunMigration(currentData)) {
return false
}
- return keySet.any(sharedPrefs::contains)
+ return if (keySet == null) {
+ sharedPrefs.all.isNotEmpty()
+ } else {
+ keySet.any(sharedPrefs::contains)
+ }
}
override suspend fun migrate(currentData: T): T =
@@ -160,8 +168,12 @@
override suspend fun cleanUp() {
val sharedPrefsEditor = sharedPrefs.edit()
- for (key in keySet) {
- sharedPrefsEditor.remove(key)
+ if (keySet == null) {
+ sharedPrefsEditor.clear()
+ } else {
+ keySet.forEach { key ->
+ sharedPrefsEditor.remove(key)
+ }
}
if (!sharedPrefsEditor.commit()) {
@@ -172,7 +184,7 @@
deleteSharedPreferences(context, name)
}
- keySet.clear()
+ keySet?.clear()
}
private fun deleteSharedPreferences(context: Context, name: String) {
@@ -211,12 +223,11 @@
}
/**
- * Read-only wrapper around SharedPreferences. This will be passed in to your migration. The
- * constructor is public to enable easier testing of migrations.
+ * Read-only wrapper around SharedPreferences. This will be passed in to your migration.
*/
public class SharedPreferencesView internal constructor(
private val prefs: SharedPreferences,
- private val keySet: Set<String>
+ private val keySet: Set<String>?
) {
/**
* Checks whether the preferences contains a preference.
@@ -285,18 +296,22 @@
prefs.getStringSet(checkKey(key), defValues)?.toMutableSet()
/** Retrieve all values from the preferences that are in the specified keySet. */
- public fun getAll(): Map<String, Any?> = prefs.all.filter { (key, _) ->
- key in keySet
- }.mapValues { (_, value) ->
- if (value is Set<*>) {
- value.toSet()
- } else {
- value
+ public fun getAll(): Map<String, Any?> =
+ prefs.all.filter { (key, _) ->
+ keySet?.contains(key) ?: true
+ }.mapValues { (_, value) ->
+ if (value is Set<*>) {
+ value.toSet()
+ } else {
+ value
+ }
}
- }
private fun checkKey(key: String): String {
- check(key in keySet) { "Can't access key outside migration: $key" }
+ keySet?.let {
+ check(key in it) { "Can't access key outside migration: $key" }
+ }
+
return key
}
}
diff --git a/development/build_log_simplifier/message-flakes.ignore b/development/build_log_simplifier/message-flakes.ignore
index 110863e..f9748fc 100644
--- a/development/build_log_simplifier/message-flakes.ignore
+++ b/development/build_log_simplifier/message-flakes.ignore
@@ -20,13 +20,18 @@
# > Task :compose:compiler:compiler-hosted:integration-tests:testDebugUnitTest
# If a test fails, we don't want the build to fail, we want to pass the test output to the tests server and for the tests server to report the failure
[0-9]+ test.*failed.*
+There were failing tests\. See the report at: .*.html
# Some status messages
Starting a Gradle Daemon, [0-9]+ busy and [0-9]+ incompatible Daemons could not be reused, use \-\-status for details
Starting a Gradle Daemon, [0-9]+ busy and [0-9]+ incompatible and [0-9]+ stopped Daemons could not be reused, use \-\-status for details
+Starting a Gradle Daemon \(subsequent builds will be faster\)
+To honour the JVM settings for this build a single\-use Daemon process will be forked\. See https://docs\.gradle\.org/[0-9]+\.[0-9]+\.[0-9]+/userguide/gradle_daemon\.html\#sec:disabling_the_daemon\.
Remote build cache is disabled when running with \-\-offline\.
[0-9]+ actionable tasks: [0-9]+ up\-to\-date
[0-9]+ actionable tasks: [0-9]+ executed
+[0-9]+ actionable task: [0-9]+ executed
[0-9]+ actionable tasks: [0-9]+ executed, [0-9]+ from cache, [0-9]+ up\-to\-date
+The remote build cache was disabled during the build due to errors\.
# Some messages that encode the number of a certain type of other error
[0-9]+ errors, [0-9]+ warnings \([0-9]+ warnings filtered by baseline lint\-baseline\.xml\)
[0-9]+ errors, [0-9]+ warnings \([0-9]+ warning filtered by baseline lint\-baseline\.xml\)
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 88d5612..3196854 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -9,7 +9,6 @@
PROGRESS: Rendering
No docs found on supertype with \{@inheritDoc\} method .*
Can't find node by signature .*
-Exception while resolving link to Module: Package:androidx\.compose\.material Function:animateElevation Receiver:<this>
java\.lang\.IllegalStateException: Unsupported Receiver
at org\.jetbrains\.dokka\.Formats\.JavaLayoutHtmlUriProvider\$DefaultImpls\.mainUri\(JavaLayoutHtmlFormat\.kt:[0-9]+\)
at org\.jetbrains\.dokka\.Formats\.JavaLayoutHtmlFormatGenerator\.mainUri\(JavaLayoutHtmlGenerator\.kt:[0-9]+\)
@@ -108,7 +107,6 @@
at kotlinx\.html\.Gen_tag_unionsKt\.code\$default\(gen\-tag\-unions\.kt:[0-9]+\)
at org\.jetbrains\.dokka\.Formats\.JavaLayoutHtmlFormatOutputBuilder\$functionLikeSummaryRow\$[0-9]+\$[0-9]+\$[0-9]+\.invoke\(JavaLayoutHtmlFormatOutputBuilder\.kt:[0-9]+\)
Exception while resolving link to Module: Package:androidx\.compose\.ui\.graphics Function:compositeOver Receiver:<this>
-Exception while resolving link to Module: Package:androidx\.compose\.ui\.graphics GroupNode:NativeTileMode
Exception while resolving link to Module: Package:androidx\.compose\.ui\.graphics GroupNode:NativeCanvas
at kotlinx\.html\.Gen_tag_groupsKt\.pre\(gen\-tag\-groups\.kt:[0-9]+\)
Exception while resolving link to Module: Package:androidx\.compose\.ui\.graphics GroupNode:NativePaint
@@ -116,12 +114,6 @@
at org\.jetbrains\.dokka\.Formats\.JavaLayoutHtmlFormatOutputBuilder\$propertyLikeSummaryRow\$[0-9]+\.invoke\(JavaLayoutHtmlFormatOutputBuilder\.kt:[0-9]+\)
at org\.jetbrains\.dokka\.Formats\.JavaLayoutHtmlFormatOutputBuilder\.propertyLikeSummaryRow\(JavaLayoutHtmlFormatOutputBuilder\.kt:[0-9]+\)
at org\.jetbrains\.dokka\.Formats\.JavaLayoutHtmlFormatOutputBuilder\.propertyLikeSummaryRow\$default\(JavaLayoutHtmlFormatOutputBuilder\.kt:[0-9]+\)
-at org\.jetbrains\.dokka\.Formats\.JavaLayoutHtmlFormatOutputBuilder\.qualifiedTypeReference\(JavaLayoutHtmlFormatOutputBuilder\.kt:[0-9]+\)
-at org\.jetbrains\.dokka\.Formats\.DevsiteLayoutHtmlFormatOutputBuilder\$classHierarchy\$[0-9]+\$\$special\$\$inlined\$forEach\$lambda\$[0-9]+\$[0-9]+\.invoke\(DacHtmlFormat\.kt:[0-9]+\)
-at org\.jetbrains\.dokka\.Formats\.DevsiteLayoutHtmlFormatOutputBuilder\$classHierarchy\$[0-9]+\$\$special\$\$inlined\$forEach\$lambda\$[0-9]+\.invoke\(DacHtmlFormat\.kt:[0-9]+\)
-at org\.jetbrains\.dokka\.Formats\.DevsiteLayoutHtmlFormatOutputBuilder\$classHierarchy\$[0-9]+\.invoke\(DacHtmlFormat\.kt:[0-9]+\)
-at org\.jetbrains\.dokka\.Formats\.DevsiteLayoutHtmlFormatOutputBuilder\.classHierarchy\(DacHtmlFormat\.kt:[0-9]+\)
-Exception while resolving link to Module: Package:androidx\.compose\.ui\.graphics GroupNode:NativeVertexMode
Exception while resolving link to Module: Package:androidx\.compose\.ui\.graphics\.vector Function:group Receiver:<this>
Exception while resolving link to Module: Package:androidx\.compose\.ui\.graphics\.vector Function:path Receiver:<this>
Exception while resolving link to Module: Package:androidx\.compose\.ui\.unit ExternalClass:kotlin\.Int Function:times Receiver:<this>
@@ -133,7 +125,6 @@
CHECKOUT=\$CHECKOUT
GRADLE_USER_HOME=\$GRADLE_USER_HOME
To honour the JVM settings for this build a single\-use Daemon process will be forked\. See https://docs\.gradle\.org/[0-9]+\.[0-9]+/userguide/gradle_daemon\.html\#sec:disabling_the_daemon\.
-Starting a Gradle Daemon \(subsequent builds will be faster\)
Downloading file\:\$SUPPORT\/gradle\/wrapper\/.*
\.\.\.\.\.\.\.\.\.\.10%\.\.\.\.\.\.\.\.\.\.\.20%\.\.\.\.\.\.\.\.\.\.\.30%\.\.\.\.\.\.\.\.\.\.40%\.\.\.\.\.\.\.\.\.\.\.50%\.\.\.\.\.\.\.\.\.\.\.60%\.\.\.\.\.\.\.\.\.\.70%\.\.\.\.\.\.\.\.\.\.\.80%\.\.\.\.\.\.\.\.\.\.\.90%\.\.\.\.\.\.\.\.\.\.\.100%
Welcome to Gradle .*
@@ -141,7 +132,6 @@
\- Faster incremental Java compilation
\- Easier source set configuration in the Kotlin DSL
For more details see .*
-To honour the JVM settings for this build a single\-use Daemon process will be forked\. See https://docs\.gradle\.org/[0-9]+\.[0-9]+\.[0-9]+/userguide/gradle_daemon\.html\#sec:disabling_the_daemon\.
Daemon will be stopped at the end of the build
# > Task :buildSrc:build
# > Configure project :appsearch:appsearch\-local\-backend
@@ -151,20 +141,12 @@
updated local\.properties
# > Configure project :compose:test\-utils
The following Kotlin source sets were configured but not added to any Kotlin compilation:
-\* iosArm[0-9]+Main
\* iosArm[0-9]+Test
-\* iosX[0-9]+Main
\* iosX[0-9]+Test
-\* jsMain
-\* jsTest
-\* linuxX[0-9]+Main
\* linuxX[0-9]+Test
-\* macosX[0-9]+Main
\* macosX[0-9]+Test
-\* mingwX[0-9]+Main
\* mingwX[0-9]+Test
\* nativeTest
-\* nonJvmMain
\* androidAndroidTestRelease
\* androidTestFixtures
\* androidTestFixturesDebug
@@ -173,22 +155,14 @@
\* test
You can add a source set to a target's compilation by connecting it with the compilation's default source set using 'dependsOn'\.
See https://kotlinlang\.org/docs/reference/building\-mpp\-with\-gradle\.html\#connecting\-source\-sets
-Some Kotlin/Native targets cannot be built on this macos_x[0-9]+ machine and are disabled:
-\* In project ':collection:collection[0-9]+':
-\* target 'mingwX[0-9]+' \(can be built with a mingw_x[0-9]+ host\)
-To hide this message, add 'kotlin\.native\.ignoreDisabledTargets=true' to the Gradle properties\.
\* jvmMain
# > Task :listTaskOutputs
Wrote \$DIST_DIR/task_outputs\.txt
Deprecated Gradle features were used in this build, making it incompatible with Gradle [0-9]+\.[0-9]+\.
-Use '\-\-warning\-mode all' to show the individual deprecation warnings\.
See https://docs\.gradle\.org/[0-9]+\.[0-9]+/userguide/command_line_interface\.html\#sec:command_line_warnings
Execution optimizations have been disabled for [0-9]+ invalid unit\(s\) of work during this build to ensure correctness\.
Please consult deprecation warnings for more details\.
-See https\:\/\/docs\.gradle\.org\/[0-9]+\.[0-9]+\.[0-9]+\/userguide\/command_line_interface\.html\#sec\:command_line_warnings
BUILD SUCCESSFUL in .*
-The remote build cache was disabled during the build due to errors\.
-[0-9]+ actionable task: [0-9]+ executed
# > Task :doclava:compileJava
Note\: Some input files use or override a deprecated API\.
Note: Some input files use or override a deprecated API that is marked for removal\.
@@ -202,8 +176,6 @@
application@android:debuggable was tagged at .*\.xml:[0-9]+ to replace other declarations but no other declaration present
\$OUT_DIR/androidx/wear/compose/compose\-material\-benchmark/build/intermediates/tmp/manifest/androidTest/release/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
\$OUT_DIR/androidx/profileinstaller/profileinstaller\-benchmark/build/intermediates/tmp/manifest/androidTest/release/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR/androidx/slice\-benchmark/build/intermediates/tmp/manifest/androidTest/release/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR/androidx/ads\-identifier\-benchmark/build/intermediates/tmp/manifest/androidTest/release/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
\$OUT_DIR/androidx/appcompat/appcompat\-benchmark/build/intermediates/tmp/manifest/androidTest/release/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
\$OUT_DIR/androidx/benchmark/benchmark\-benchmark/build/intermediates/tmp/manifest/androidTest/release/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
\$OUT_DIR/androidx/collection/collection\-benchmark/build/intermediates/tmp/manifest/androidTest/release/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
@@ -228,29 +200,16 @@
\$OUT_DIR/androidx/compose/ui/ui\-graphics/ui\-graphics\-benchmark/build/intermediates/tmp/manifest/androidTest/release/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Task :compose:foundation:foundation-layout:processDebugAndroidTestManifest
\$SUPPORT/compose/foundation/foundation\-layout/src/androidAndroidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR/androidx/compose/foundation/foundation\-layout/build/intermediates/tmp/manifest/androidTest/debug/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-# > Task :compose:integration-tests:benchmark:processReleaseAndroidTestManifest
-\$OUT_DIR/androidx/compose/integration\-tests/benchmark/build/intermediates/tmp/manifest/androidTest/release/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Task :compose:navigation:navigation:processDebugAndroidTestManifest
# > Task :compose:runtime:runtime-saveable:processDebugAndroidTestManifest
\$SUPPORT/compose/runtime/runtime\-saveable/src/androidAndroidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR/androidx/compose/runtime/runtime\-saveable/build/intermediates/tmp/manifest/androidTest/debug/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-# > Task :compose:ui:ui-graphics:compileKotlinMetadata
-w: \$SUPPORT/compose/ui/ui\-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color\.kt: \([0-9]+, [0-9]+\): The feature "inline classes" is experimental
-w: \$SUPPORT/compose/ui/ui\-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Float[0-9]+\.kt: \([0-9]+, [0-9]+\): The feature "inline classes" is experimental
# > Task :compose:runtime:runtime:benchmark:processReleaseAndroidTestManifest
\$SUPPORT/compose/runtime/runtime/compose\-runtime\-benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR/androidx/compose/runtime/runtime/benchmark/build/intermediates/tmp/manifest/androidTest/release/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-# > Task :compose:material:material-icons-extended:processDebugAndroidTestManifest
-\[:compose:material:material\-icons\-extended\] \$OUT_DIR/androidx/compose/material/material\-icons\-extended/build/intermediates/merged_manifest/debug/AndroidManifest\.xml Warning:
-Package name 'androidx\.compose\.material\.icons' used in: :compose:material:material\-icons\-extended, :compose:material:material\-icons\-core\.
# > Task :compose:ui:ui:compileKotlinMetadata
w: Runtime JAR files in the classpath should have the same version\. These files were found in the classpath:
\$CHECKOUT/prebuilts/androidx/external/org/jetbrains/kotlin/kotlin\-stdlib/[0-9]+\.[0-9]+\.[0-9]+/kotlin\-stdlib\-[0-9]+\.[0-9]+\.[0-9]+\.jar \(version [0-9]+\.[0-9]+\)
\$CHECKOUT/prebuilts/androidx/external/org/jetbrains/kotlin/kotlin\-stdlib\-common/[0-9]+\.[0-9]+\.[0-9]+/kotlin\-stdlib\-common\-[0-9]+\.[0-9]+\.[0-9]+\.jar \(version [0-9]+\.[0-9]+\)
w: Some runtime JAR files in the classpath have an incompatible version\. Consider removing them from the classpath
-w: \$SUPPORT/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/Key\.kt: \([0-9]+, [0-9]+\): The feature "inline classes" is experimental
-w: \$SUPPORT/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Measured\.kt: \([0-9]+, [0-9]+\): The feature "inline classes" is experimental
w: \$SUPPORT/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DepthSortedSet\.kt: \([0-9]+, [0-9]+\): The corresponding parameter in the supertype 'Comparator' is named 'a'\. This may cause problems when calling this function with named arguments\.
w: \$SUPPORT/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DepthSortedSet\.kt: \([0-9]+, [0-9]+\): The corresponding parameter in the supertype 'Comparator' is named 'b'\. This may cause problems when calling this function with named arguments\.
# > Task :benchmark:benchmark-common:runErrorProne
@@ -258,13 +217,6 @@
symbol\: static FLAG_MUTABLE
location\: class PendingIntent
\$OUT_DIR\/androidx\/docs\-public\/build\/unzippedDocsSources\/androidx\/work\/impl\/utils\/ForceStopRunnable\.java\:[0-9]+\: error\: cannot find symbol
-symbol: class ExtensionDeviceState
-location: package androidx\.window\.extensions
-DeviceState translate\(ExtensionDeviceState deviceState\) \{
-location: class ExtensionAdapter
-\$OUT_DIR/androidx/docs\-public/build/unzippedDocsSources/androidx/window/ExtensionTranslatingCallback\.java:[0-9]+: error: cannot find symbol
-public void onDeviceStateChanged\(@NonNull ExtensionDeviceState newDeviceState\) \{
-location: class ExtensionTranslatingCallback
# > Task :buildOnServer
[0-9]+ actionable tasks: [0-9]+ executed, [0-9]+ up\-to\-date
[0-9]+ actionable tasks: [0-9]+ executed, [0-9]+ from cache
@@ -281,28 +233,6 @@
# > Task :textclassifier:textclassifier:compileReleaseJavaWithJavac
# > Task :publicDocsTask
[0-9]+ warnings
-# > Task :appsearch:appsearch-local-storage:externalNativeBuildRelease
-ninja: .*
-Build icing_.*
-In file included from \$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/json_objectwriter\.cc:[0-9]+:
-\$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/json_objectwriter\.h:[0-9]+:[0-9]+: warning: 'StartObject' overrides a member function but is not marked 'override' \[\-Winconsistent\-missing\-override\]
-\$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/object_writer\.h:[0-9]+:[0-9]+: note: overridden virtual function is here
-\$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/json_objectwriter\.h:[0-9]+:[0-9]+: warning: 'EndObject' overrides a member function but is not marked 'override' \[\-Winconsistent\-missing\-override\]
-\$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/json_objectwriter\.h:[0-9]+:[0-9]+: warning: 'StartList' overrides a member function but is not marked 'override' \[\-Winconsistent\-missing\-override\]
-\$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/json_objectwriter\.h:[0-9]+:[0-9]+: warning: 'EndList' overrides a member function but is not marked 'override' \[\-Winconsistent\-missing\-override\]
-\$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/json_objectwriter\.h:[0-9]+:[0-9]+: warning: 'RenderBool' overrides a member function but is not marked 'override' \[\-Winconsistent\-missing\-override\]
-\$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/json_objectwriter\.h:[0-9]+:[0-9]+: warning: 'RenderInt[0-9]+' overrides a member function but is not marked 'override' \[\-Winconsistent\-missing\-override\]
-\$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/json_objectwriter\.h:[0-9]+:[0-9]+: warning: 'RenderUint[0-9]+' overrides a member function but is not marked 'override' \[\-Winconsistent\-missing\-override\]
-\$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/json_objectwriter\.h:[0-9]+:[0-9]+: warning: 'RenderDouble' overrides a member function but is not marked 'override' \[\-Winconsistent\-missing\-override\]
-\$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/json_objectwriter\.h:[0-9]+:[0-9]+: warning: 'RenderFloat' overrides a member function but is not marked 'override' \[\-Winconsistent\-missing\-override\]
-\$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/json_objectwriter\.h:[0-9]+:[0-9]+: warning: 'RenderString' overrides a member function but is not marked 'override' \[\-Winconsistent\-missing\-override\]
-\$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/json_objectwriter\.h:[0-9]+:[0-9]+: warning: 'RenderBytes' overrides a member function but is not marked 'override' \[\-Winconsistent\-missing\-override\]
-\$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/json_objectwriter\.h:[0-9]+:[0-9]+: warning: 'RenderNull' overrides a member function but is not marked 'override' \[\-Winconsistent\-missing\-override\]
-In file included from \$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/type_info_test_helper\.cc:[0-9]+:
-In file included from \$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/type_info_test_helper\.h:[0-9]+:
-\$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/default_value_objectwriter\.h:[0-9]+:[0-9]+: warning: 'RenderNull' overrides a member function but is not marked 'override' \[\-Winconsistent\-missing\-override\]
-In file included from \$CHECKOUT/external/protobuf/src/google/protobuf/util/json_util\.cc:[0-9]+:
-In file included from \$CHECKOUT/external/protobuf/src/google/protobuf/util/internal/default_value_objectwriter\.cc:[0-9]+:
# > Task :support-animation-demos:compileDebugJavaWithJavac
# > Task :lint-demos:lint-demo-appcompat:compileDebugJavaWithJavac
# > Task :support-transition-demos:compileDebugJavaWithJavac
@@ -311,7 +241,6 @@
# > Task :startup:integration-tests:first-library:processDebugManifest
\$SUPPORT/startup/integration\-tests/first\-library/src/main/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
meta\-data\#androidx\.work\.WorkManagerInitializer was tagged at AndroidManifest\.xml\:[0-9]+ to remove other declarations but no other declaration present
-provider\#androidx\.work\.WorkManagerInitializer was tagged at AndroidManifest\.xml:[0-9]+ to remove other declarations but no other declaration present
# > Task :camera:integration-tests:camera-testapp-extensions:compileDebugJavaWithJavac
# > Task :camera:integration-tests:camera-testapp-core:compileDebugJavaWithJavac
# > Task :room:integration-tests:room-testapp:processDebugMainManifest
@@ -320,63 +249,20 @@
# > Task :lifecycle:integration-tests:lifecycle-testapp:compileDebugJavaWithJavac
# > Task :support-slices-demos:compileDebugJavaWithJavac
Note: \$SUPPORT/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser\.java uses unchecked or unsafe operations\.
-# > Task :ads-identifier:processDebugAndroidTestManifest
-# > Task :sharetarget:integration-tests:testapp:compileDebugJavaWithJavac
-# > Task :ads-identifier-provider:processDebugAndroidTestManifest
-# > Task :hilt:integration-tests:hilt-testapp-viewmodel:compileDebugJavaWithJavac
-# > Task :ads-identifier-benchmark:processReleaseAndroidTestManifest
-\$SUPPORT/ads/ads\-identifier\-benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR/androidx/ads\-identifier\-benchmark/build/intermediates/tmp/manifest/androidTest/release/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-# > Task :internal-testutils-runtime:processDebugAndroidTestManifest
-# > Task :slice-benchmark:processReleaseAndroidTestManifest
-\$OUT_DIR/androidx/slice\-benchmark/build/intermediates/tmp/manifest/androidTest/release/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Task :activity:activity:processDebugAndroidTestManifest
# > Task :appcompat:appcompat:processDebugAndroidTestManifest
Package name 'androidx\.testutils' used in: :internal\-testutils\-appcompat, :internal\-testutils\-runtime\.
-# > Task :appcompat:appcompat-resources:processDebugAndroidTestManifest
-# > Task :appcompat:appcompat-benchmark:processReleaseAndroidTestManifest
-\$OUT_DIR/androidx/appcompat/appcompat\-benchmark/build/intermediates/tmp/manifest/androidTest/release/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-# > Task :benchmark:benchmark-benchmark:processReleaseAndroidTestManifest
-\$OUT_DIR/androidx/benchmark/benchmark\-benchmark/build/intermediates/tmp/manifest/androidTest/release/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-# > Task :camera:camera-camera2:processDebugAndroidTestManifest
-# > Task :camera:camera-core:processDebugAndroidTestManifest
-# > Task :camera:camera-extensions:processDebugAndroidTestManifest
-# > Task :camera:camera-lifecycle:processDebugAndroidTestManifest
-# > Task :camera:camera-testing:externalNativeBuildDebug
-Build testing_surface_format_.*
-# > Task :collection:collection-benchmark:processReleaseAndroidTestManifest
-\$OUT_DIR/androidx/collection/collection\-benchmark/build/intermediates/tmp/manifest/androidTest/release/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Task :emoji2:emoji2-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/emoji[0-9]+/emoji[0-9]+\-benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR/androidx/emoji2/emoji2\-benchmark/build/intermediates/tmp/manifest/androidTest/release/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Task :gridlayout:gridlayout:compileDebugAndroidTestJavaWithJavac
# > Task :leanback:leanback:processDebugAndroidTestManifest
Package name 'androidx\.testutils' used in: :internal\-testutils\-runtime, :internal\-testutils\-espresso\.
# > Task :leanback:leanback-tab:processDebugAndroidTestManifest
# > Task :lifecycle:lifecycle-extensions:processDebugAndroidTestManifest
# > Task :leanback:leanback:compileDebugAndroidTestJavaWithJavac
-warning: unknown enum constant AnnotationTarget\.CLASS
-warning: unknown enum constant AnnotationTarget\.PROPERTY
-warning: unknown enum constant AnnotationTarget\.LOCAL_VARIABLE
-warning: unknown enum constant AnnotationTarget\.VALUE_PARAMETER
-warning: unknown enum constant AnnotationTarget\.CONSTRUCTOR
reason: class file for kotlin\.annotation\.AnnotationTarget not found
-warning: unknown enum constant AnnotationTarget\.FUNCTION
-warning: unknown enum constant AnnotationTarget\.PROPERTY_GETTER
-warning: unknown enum constant AnnotationTarget\.PROPERTY_SETTER
-warning: unknown enum constant AnnotationTarget\.FILE
-warning: unknown enum constant AnnotationTarget\.TYPEALIAS
reason: class file for kotlin\.annotation\.AnnotationRetention not found
warning: unknown enum constant AnnotationTarget\.ANNOTATION_CLASS
-# > Task :lifecycle:lifecycle-service:processDebugAndroidTestManifest
-# > Task :lifecycle:lifecycle-viewmodel-savedstate:processDebugAndroidTestManifest
-# > Task :leanback:leanback-tab:compileDebugAndroidTestJavaWithJavac
-# > Task :media2:media2-common:processDebugAndroidTestManifest
-# > Task :media2:media2-player:processDebugAndroidTestManifest
-# > Task :media2:media2-session:processDebugAndroidTestManifest
-# > Task :media2:media2-common:compileDebugAndroidTestJavaWithJavac
-# > Task :navigation:navigation-benchmark:processReleaseAndroidTestManifest
-\$OUT_DIR/androidx/navigation/navigation\-benchmark/build/intermediates/tmp/manifest/androidTest/release/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Task :navigation:navigation-dynamic-features-runtime:processDebugAndroidTestManifest
# > Task :navigation:navigation-fragment:processDebugAndroidTestManifest
Package name 'androidx\.testutils' used in: :internal\-testutils\-runtime, :internal\-testutils\-navigation\.
@@ -386,31 +272,16 @@
# > Task :preference:preference-ktx:processDebugAndroidTestManifest
# > Task :recyclerview:recyclerview-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/recyclerview/recyclerview\-benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR/androidx/recyclerview/recyclerview\-benchmark/build/intermediates/tmp/manifest/androidTest/release/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Task :remotecallback:remotecallback:processDebugAndroidTestManifest
# > Task :recyclerview:recyclerview-selection:compileDebugAndroidTestJavaWithJavac
# > Task :savedstate:savedstate:processDebugAndroidTestManifest
# > Task :room:room-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/room/benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR/androidx/room/room\-benchmark/build/intermediates/tmp/manifest/androidTest/release/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Task :room:room-benchmark:kaptReleaseAndroidTestKotlin
\$OUT_DIR/androidx/room/room\-benchmark/build/tmp/kapt[0-9]+/stubs/releaseAndroidTest/androidx/room/benchmark/RelationBenchmark\.java:[0-9]+: warning: The return value includes a POJO with a @Relation\. It is usually desired to annotate this method with @Transaction to avoid possibility of inconsistent results between the POJO and its relations\. See https://developer\.android\.com/reference/androidx/room/Transaction\.html for details\.
public abstract java\.util\.List<androidx\.room\.benchmark\.RelationBenchmark\.UserWithItems> getUserWithItems\(\);
# > Task :viewpager2:viewpager2:processDebugAndroidTestManifest
Package name 'androidx\.testutils' used in: :internal\-testutils\-appcompat, :internal\-testutils\-runtime, :internal\-testutils\-espresso\.
-# > Task :wear:wear-watchface:processDebugAndroidTestManifest
-# > Task :window:window:processDebugAndroidTestManifest
-# > Task :webkit:webkit:compileDebugAndroidTestJavaWithJavac
-# > Task :work:work-benchmark:processReleaseAndroidTestManifest
-\$OUT_DIR/androidx/work/work\-benchmark/build/intermediates/tmp/manifest/androidTest/release/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-# > Task :benchmark:integration-tests:dry-run-benchmark:processReleaseAndroidTestManifest
-\$OUT_DIR/androidx/benchmark/integration\-tests/dry\-run\-benchmark/build/intermediates/tmp/manifest/androidTest/release/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-# > Task :benchmark:integration-tests:startup-benchmark:processReleaseAndroidTestManifest
-\$OUT_DIR/androidx/benchmark/integration\-tests/startup\-benchmark/build/intermediates/tmp/manifest/androidTest/release/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-# > Task :camera:integration-tests:camera-testapp-core:externalNativeBuildDebug
-Build opengl_renderer_jni_.*
-# > Task :camera:integration-tests:camera-testapp-view:minifyReleaseWithR8
-R[0-9]+: Missing class: java\.lang\.ClassValue
# > Task :room:integration-tests:room-testapp-noappcompat:compileDebugAndroidTestJavaWithJavac
\$SUPPORT/room/integration\-tests/noappcompattestapp/src/androidTest/java/androidx/room/integration/noappcompat/BareRelationDatabaseTest\.java:[0-9]+: warning: The return value includes a POJO with a @Relation\. It is usually desired to annotate this method with @Transaction to avoid possibility of inconsistent results between the POJO and its relations\. See https://developer\.android\.com/reference/androidx/room/Transaction\.html for details\.
UserAndPets getUserWithPets\(long id\);
@@ -418,15 +289,8 @@
# > Task :textclassifier:integration-tests:testapp:compileDebugAndroidTestJavaWithJavac
# > Task :room:integration-tests:room-testapp:compileDebugAndroidTestJavaWithJavac
Note: \$SUPPORT/room/integration\-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/TestUtil\.java uses unchecked or unsafe operations\.
-# > Task :hilt:integration-tests:hilt-testapp-worker:kaptDebugAndroidTestKotlin
-warning: The following options were not recognized by any processor: '\[dagger\.hilt\.android\.internal\.disableAndroidSuperclassValidation, kapt\.kotlin\.generated\]'
-# > Task :appsearch:appsearch-compiler:lint
-[0-9]+ errors, [0-9]+ warnings \([0-9]+ error filtered by baseline lint\-baseline\.xml\)
# > Task :activity:integration-tests:testapp:processDebugAndroidTestManifest
# b/166471969
-.*androidTest.*AndroidManifest.*xml Warning.*:
-Package name.*test' used in: .*AndroidManifest.*xml.*
-\$SUPPORT/slices/benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
\$SUPPORT/appcompat/appcompat\-benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
\$SUPPORT/benchmark/benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
\$SUPPORT/collection/collection\-benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
@@ -436,7 +300,6 @@
\$SUPPORT/benchmark/integration\-tests/startup\-benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
\[:internal\-testutils\-espresso\] \$OUT_DIR/androidx/internal\-testutils\-espresso/build/intermediates/merged_manifest/debug/AndroidManifest\.xml Warning:
Package name 'androidx\.testutils' used in: :internal\-testutils\-espresso, :internal\-testutils\-runtime\.
-\$OUT_DIR/androidx/compose/ui/ui\-tooling/build/intermediates/tmp/manifest/androidTest/debug/manifestMerger[0-9]+\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
\[:internal\-testutils\-runtime\] \$OUT_DIR/androidx/internal\-testutils\-runtime/build/intermediates/merged_manifest/debug/AndroidManifest\.xml Warning:
# > Task :leanback:leanback-paging:generateApi
w\: Runtime JAR files in the classpath have the version [0-9]+\.[0-9]+\, which is older than the API version [0-9]+\.[0-9]+\. Consider using the runtime of version [0-9]+\.[0-9]+\, or pass \'\-api\-version [0-9]+\.[0-9]+\' explicitly to restrict the available APIs to the runtime of version [0-9]+\.[0-9]+\. You can also pass \'\-language\-version [0-9]+\.[0-9]+\' instead\, which will restrict not only the APIs to the specified version\, but also the language features
@@ -448,57 +311,21 @@
WARNING: An illegal reflective access operation has occurred
WARNING: Please consider reporting this to the maintainers of org\.jetbrains\.kotlin\.kapt[0-9]+\.base\.javac\.KaptJavaFileManager
WARNING: Illegal reflective access by com\.intellij\.util\.ReflectionUtil \(file:\$CHECKOUT/prebuilts/androidx/external/com/google/devsite/dackka/[0-9]+\.[0-9]+\.[0-9]+/dackka\-[0-9]+\.[0-9]+\.[0-9]+\.jar\) to method java\.util\.ResourceBundle\.setParent\(java\.util\.ResourceBundle\)
-WARNING: Illegal reflective access by com\.intellij\.util\.ReflectionUtil \(file:\$OUT_DIR/androidx/compose/compiler/compiler\-hosted/integration\-tests/kotlin\-compiler\-repackaged/build/repackaged/kotlin\-compiler\-repackaged\.jar\) to method java\.util\.ResourceBundle\.setParent\(java\.util\.ResourceBundle\)
-WARNING\: Illegal reflective access using Lookup on org\.gradle\.internal\.classloader\.ClassLoaderUtils\$AbstractClassLoaderLookuper \(file\:\$GRADLE_USER_HOME\/caches\/[0-9]+\.[0-9]+\.[0-9]+\/generated\-gradle\-jars\/gradle\-api\-[0-9]+\.[0-9]+\.[0-9]+\.jar\) to class java\.lang\.ClassLoader
-WARNING\: Please consider reporting this to the maintainers of org\.gradle\.internal\.classloader\.ClassLoaderUtils\$AbstractClassLoaderLookuper
-WARNING\: Illegal reflective access by org\.robolectric\.util\.ReflectionHelpers\$[0-9]+ \(file\:\$CHECKOUT\/prebuilts\/androidx\/external\/org\/robolectric\/shadowapi\/[0-9]+\.[0-9]+\-alpha\-[0-9]+\/shadowapi\-[0-9]+\.[0-9]+\-alpha\-[0-9]+\.jar\) to method java\.lang\.ClassLoader\.getPackage\(java\.lang\.String\)
-WARNING\: Please consider reporting this to the maintainers of org\.robolectric\.util\.ReflectionHelpers\$[0-9]+
-WARNING\: Illegal reflective access by com\.intellij\.util\.ReflectionUtil \(file\:\$CHECKOUT\/prebuilts\/androidx\/external\/com\/android\/tools\/external\/com\-intellij\/intellij\-core\/[0-9]+\.[0-9]+\.[0-9]+\-beta[0-9]+\/intellij\-core\-[0-9]+\.[0-9]+\.[0-9]+\-beta[0-9]+\.jar\) to method java\.util\.ResourceBundle\.setParent\(java\.util\.ResourceBundle\)
-WARNING: Illegal reflective access by org\.jetbrains\.kotlin\.com\.intellij\.util\.ReflectionUtil \(file:\$CHECKOUT/prebuilts/androidx/external/org/jetbrains/kotlin/kotlin\-compiler\-embeddable/.+/kotlin\-compiler\-embeddable\-.+\.jar\) to method java\.util\.ResourceBundle\.setParent\(java\.util\.ResourceBundle\)
-WARNING\: Please consider reporting this to the maintainers of org\.jetbrains\.kotlin\.com\.intellij\.util\.ReflectionUtil
-WARNING: Illegal reflective access by com\.intellij\.util\.ReflectionUtil \(file:\$CHECKOUT/prebuilts/androidx/external/org/jetbrains/kotlin/kotlin\-compiler/.+/kotlin\-compiler\-.+\.jar\) to method java\.util\.ResourceBundle\.setParent\(java\.util\.ResourceBundle\)
WARNING: Please consider reporting this to the maintainers of com\.intellij\.util\.ReflectionUtil
WARNING: Use \-\-illegal\-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
-# > Task :compose:compiler:compiler-hosted:integration-tests:testDebugUnitTest
-WARNING: Illegal reflective access by org\.robolectric\.util\.ReflectionHelpers \(file:\$CHECKOUT/prebuilts/androidx/external/org/robolectric/shadowapi/[0-9]+\.[0-9]+\-alpha\-[0-9]+/shadowapi\-[0-9]+\.[0-9]+\-alpha\-[0-9]+\.jar\) to field java\.lang\.reflect\.Field\.modifiers
-WARNING: Please consider reporting this to the maintainers of org\.robolectric\.util\.ReflectionHelpers
-# > Task :compose:compiler:compiler-hosted:integration-tests:test
-wrote dependency log to \$DIST_DIR/affected_module_detector_log\.txt
# > Task :ipc:ipc-compiler:kaptTestKotlin
Annotation processors discovery from compile classpath is deprecated\.
Set 'kapt\.includeCompileClasspath = false' to disable discovery\.
Run the build with '\-\-info' for more details\.
-# > Task :wear:wear-input:compileDebugUnitTestJavaWithJavac
-# > Task :lifecycle:integration-tests:incrementality:compileTestKotlin
-\$GRADLE_USER_HOME\/wrapper\/dists\/gradle\-[^/]*-bin\/[0-9a-z]+\/gradle\-[^/]*/lib\/kotlin\-stdlib\-[0-9.]*.jar \(version [^/]*\)
-\$GRADLE_USER_HOME\/wrapper\/dists\/gradle\-[^/]*-bin\/[0-9a-z]+\/gradle\-[^/]*/lib\/kotlin\-stdlib\-common\-[0-9.]*.jar \(version [^/]*\)
-\$GRADLE_USER_HOME\/wrapper\/dists\/gradle\-[^/]*-bin\/[0-9a-z]+\/gradle\-[^/]*/lib\/kotlin\-stdlib\-jdk[0-9]*-[0-9.]*.jar \(version [^/]*\)
-\$GRADLE_USER_HOME\/wrapper\/dists\/gradle\-[^/]*-bin\/[0-9a-z]+\/gradle\-[^/]*/lib\/kotlin\-reflect\-[0-9.]*.jar \(version [^/]*\)
-w: Consider providing an explicit dependency on kotlin\-reflect [^/]* to prevent strange errors
# > Task :room:room-compiler:test
-WARNING: Illegal reflective access by androidx\.room\.compiler\.processing\.javac\.JavacProcessingEnvMessager\$Companion\$isFromCompiledClass\$[0-9]+ \(file:\$OUT_DIR/androidx/room/room\-compiler\-processing/build/libs/room\-compiler\-processing\-[0-9]+\.[0-9]+\.[0-9]+(\-(alpha|beta|rc)[0-9]+)?\.jar\) to field com\.sun\.tools\.javac\.code\.Symbol\$ClassSymbol\.classfile
WARNING: Illegal reflective access by androidx\.room\.compiler\.processing\.javac\.JavacProcessingEnvMessager\$Companion \(file:\$OUT_DIR/androidx/room/room\-compiler\-processing/build/libs/room\-compiler\-processing\-[0-9]+\.[0-9]+\.[0-9]+(\-(alpha|beta|rc)[0-9]+)?\.jar\) to field com\.sun\.tools\.javac\.code\.Symbol\$ClassSymbol\.classfile
-WARNING: Please consider reporting this to the maintainers of androidx\.room\.compiler\.processing\.javac\.JavacProcessingEnvMessager\$Companion\$isFromCompiledClass\$[0-9]+
WARNING: Please consider reporting this to the maintainers of androidx\.room\.compiler\.processing\.javac\.JavacProcessingEnvMessager\$Companion
-# > Task :wear:wear-watchface-complications-rendering:compileDebugUnitTestJavaWithJavac
-# > Task :wear:wear-watchface:testDebugUnitTest
-System\.logW\: A resource was acquired at attached stack trace but never released\. See java\.io\.Closeable for information on avoiding resource leaks\.java\.lang\.Throwable\: Explicit termination method \'dispose\' not called
-System\.logW\: A resource was acquired at attached stack trace but never released\. See java\.io\.Closeable for information on avoiding resource leaks\.java\.lang\.Throwable\: Explicit termination method \'release\' not called
-# > Task :benchmark:benchmark-perfetto:mergeDebugAndroidTestJavaResource
-More than one file was found with OS independent path '.*'\. This version of the Android Gradle Plugin chooses the file from the app or dynamic\-feature module, but this can cause unexpected behavior or errors at runtime\. Future versions of the Android Gradle Plugin will throw an error in this case\.
# > Task :docs-runner:dokkaJavaTipOfTreeDocs
-\$CHECKOUT\/prebuilts\/androidx\/external\/org\/jetbrains\/kotlin\/kotlin\-reflect\/[0-9]+\.[0-9]+\.[0-9]+\/kotlin\-reflect\-[0-9]+\.[0-9]+\.[0-9]+\.jar \(version [0-9]+\.[0-9]+\)
\$CHECKOUT/prebuilts/androidx/external/org/jetbrains/kotlin/kotlin\-stdlib\-jdk[0-9]+/[0-9]+\.[0-9]+\.[0-9]+/kotlin\-stdlib\-jdk[0-9]+\-[0-9]+\.[0-9]+\.[0-9]+\.jar \(version [0-9]+\.[0-9]+\)
-\$CHECKOUT/prebuilts/androidx/external/org/jetbrains/kotlin/kotlin\-stdlib\-jdk[0-9]+/[0-9]+\.[0-9]+\.[0-9]+\-M[0-9]+/kotlin\-stdlib\-jdk[0-9]+\-[0-9]+\.[0-9]+\.[0-9]+\-M[0-9]+\.jar \(version [0-9]+\.[0-9]+\)
-\$CHECKOUT/prebuilts/androidx/external/org/jetbrains/kotlin/kotlin\-stdlib/[0-9]+\.[0-9]+\.[0-9]+\-M[0-9]+/kotlin\-stdlib\-[0-9]+\.[0-9]+\.[0-9]+\-M[0-9]+\.jar \(version [0-9]+\.[0-9]+\)
-\$CHECKOUT/prebuilts/androidx/external/org/jetbrains/kotlin/kotlin\-stdlib\-common/[0-9]+\.[0-9]+\.[0-9]+\-M[0-9]+/kotlin\-stdlib\-common\-[0-9]+\.[0-9]+\.[0-9]+\-M[0-9]+\.jar \(version [0-9]+\.[0-9]+\)
-\$CHECKOUT/prebuilts/androidx/external/org/jetbrains/kotlin/kotlin\-reflect/[0-9]+\.[0-9]+\.[0-9]+\-M[0-9]+/kotlin\-reflect\-[0-9]+\.[0-9]+\.[0-9]+\-M[0-9]+\.jar \(version [0-9]+\.[0-9]+\)
# > Task :compose:ui:ui:processDebugAndroidTestManifest
\$OUT_DIR/androidx/compose/ui/ui/build/intermediates/tmp/manifest/androidTest/debug/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml Warning:
Package name 'androidx\.compose\.ui\.test' used in: tempFile[0-9]+ProcessTestManifest[0-9]+\.xml, :compose:ui:ui\-test\.
-\$OUT_DIR\/androidx\/compose\/ui\/ui\/build\/intermediates\/tmp\/manifest\/androidTest\/debug\/manifestMerger[0-9]+\.xml Warning\:
-Package name \'androidx\.compose\.ui\.test\' used in\: manifestMerger[0-9]+\.xml\, \:compose\:ui\:ui\-test\.
# > Task :buildSrc:build UP-TO-DATE
See the profiling report at\: file\:\/\/\$OUT_DIR\/buildSrc\/build\/reports\/profile\/profile\-[0-9]+\-[0-9]+\-[0-9]+\-[0-9]+\-[0-9]+\-[0-9]+\.html
A fine\-grained performance profile is available\: use the \-\-scan option\.
@@ -513,48 +340,14 @@
\-XXLanguage\:\+InlineClasses
# > Task :docs-public:lintDebug
Wrote HTML report to file://\$OUT_DIR/androidx/docs\-public/build/reports/lint\-results\-debug\.html
-Wrote XML report to file://\$OUT_DIR/androidx/docs\-public/build/reports/lint\-results\-debug\.xml
# > Task :internal-testutils-appcompat:processDebugAndroidTestManifest
\[:internal\-testutils\-appcompat\] \$OUT_DIR/androidx/internal\-testutils\-appcompat/build/intermediates/merged_manifest/debug/AndroidManifest\.xml Warning:
-# > Task :support-wear-demos:packageDebugAndroidTest
-PackagingOptions\.jniLibs\.useLegacyPackaging should be set to true because android:extractNativeLibs is set to "true" in AndroidManifest\.xml\.
-# > Task :camera:integration-tests:camera-testapp-camera2-pipe:dexBuilderDebug
-WARNING:\$OUT_DIR/androidx/camera/integration\-tests/camera\-testapp\-camera[0-9]+\-pipe/build/intermediates/jacoco_instrumented_classes/debug/out/androidx/camera/integration/camera[0-9]+/pipe/DebugKt\.class: D[0-9]+: Invalid stack map table at [0-9]+: aload [0-9]+, error: The expected type Initialized\(double\) is not assignable to java\.lang\.Object\.
-\$OUT_DIR/androidx/camera/integration\-tests/camera\-testapp\-camera[0-9]+\-pipe/build/intermediates/jacoco_instrumented_classes/debug/out/androidx/camera/integration/camera[0-9]+/pipe/DebugKt\.class: D[0-9]+: Invalid stack map table at [0-9]+: aload [0-9]+, error: The expected type Initialized\(double\) is not assignable to java\.lang\.Object\.
-# > Task :support-remotecallback-demos:lintDebug
-[0-9]+ errors\, [0-9]+ warnings \([0-9]+ errors filtered by baseline lint\-baseline\.xml\)
-# > Task :camera:camera-camera2-pipe-integration:lintDebug
-[0-9]+ errors, [0-9]+ warnings
# > Task :camera:camera-core:compileDebugJavaWithJavac
warning: unknown enum constant AnnotationRetention\.BINARY
-# > Task :navigation:navigation-dynamic-features-fragment:compileDebugKotlin
-w: \$SUPPORT/navigation/navigation\-dynamic\-features\-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment\.kt: \([0-9]+, [0-9]+\): 'startIntentSenderForResult\(IntentSender!, Int, Intent\?, Int, Int, Int, Bundle\?\): Unit' is deprecated\. Deprecated in Java
-w: \$SUPPORT/navigation/navigation\-dynamic\-features\-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment\.kt: \([0-9]+, [0-9]+\): 'onActivityResult\(Int, Int, Intent\?\): Unit' is deprecated\. Deprecated in Java
-# > Task :inspection:inspection-gradle-plugin:test
-There were failing tests\. See the report at: .*.html
# > Task :compose:ui:ui:processDebugUnitTestManifest
\$OUT_DIR/androidx/compose/ui/ui/build/intermediates/tmp/manifest/test/debug/tempFile[0-9]+ProcessTestManifest[0-9]+\.xml Warning:
-\$OUT_DIR/androidx/compose/ui/ui/build/intermediates/tmp/manifest/test/debug/manifestMerger[0-9]+\.xml Warning:
# > Task :compose:foundation:foundation:reportLibraryMetrics
-Stripped invalid locals information from [0-9]+ methods\.
-Methods with invalid locals information:
-java\.lang\.Object androidx\.compose\.foundation\.gestures\.ZoomableKt\$detectZoom\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
-Type information in locals\-table is inconsistent\. Cannot constrain type: @Nullable java\.util\.List \{\} for value: v[0-9]+\(\$this\$fastAny\$iv\) by constraint FLOAT\.
-java\.lang\.Object androidx\.compose\.foundation\.gestures\.DragGestureDetectorKt\$awaitLongPressOrCancellation\$[0-9]+\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
-java\.lang\.Object androidx\.compose\.foundation\.gestures\.DragGestureDetectorKt\.awaitVerticalTouchSlopOrCancellation\-jO[0-9]+t[0-9]+\(androidx\.compose\.ui\.input\.pointer\.AwaitPointerEventScope, long, kotlin\.jvm\.functions\.Function[0-9]+, kotlin\.coroutines\.Continuation\)
-Type information in locals\-table is inconsistent\. Cannot constrain type: BOTTOM \(empty\) for value: v[0-9]+ by constraint INT\.
-java\.lang\.Object androidx\.compose\.foundation\.gestures\.TransformableKt\$detectZoom\$[0-9]+\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
-java\.lang\.Object androidx\.compose\.foundation\.gestures\.DraggableKt\$draggable\$[0-9]+\$[0-9]+\$[0-9]+\$[0-9]+\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
-Type information in locals\-table is inconsistent\. Cannot constrain type: BOTTOM \(empty\) for value: v[0-9]+\(\$this\$fastAny\$iv\) by constraint FLOAT\.
-Some warnings are typically a sign of using an outdated Java toolchain\. To fix, recompile the source with an updated toolchain\.
-Type information in locals\-table is inconsistent\. Cannot constrain type: BOTTOM \(empty\) for value: v[0-9]+ by constraint FLOAT\.
-java\.lang\.Object androidx\.compose\.foundation\.gestures\.DragGestureDetectorKt\.awaitHorizontalTouchSlopOrCancellation\-jO[0-9]+t[0-9]+\(androidx\.compose\.ui\.input\.pointer\.AwaitPointerEventScope, long, kotlin\.jvm\.functions\.Function[0-9]+, kotlin\.coroutines\.Continuation\)
-java\.lang\.Object androidx\.compose\.foundation\.gestures\.TransformGestureDetectorKt\$detectTransformGestures\$[0-9]+\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
java\.lang\.Object androidx\.compose\.foundation\.lazy\.LazyListScrollingKt\$doSmoothScrollToItem\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
-Information in locals\-table is invalid with respect to the stack map table\. Local refers to non\-present stack map type for register: [0-9]+ with constraint FLOAT\.
-Type information in locals\-table is inconsistent\. Cannot constrain type: INT for value: v[0-9]+\(index\$iv\$iv\) by constraint FLOAT\.
-java\.lang\.Object androidx\.compose\.foundation\.gestures\.TapGestureDetectorKt\.translatePointerEventsToChannel\(androidx\.compose\.ui\.input\.pointer\.AwaitPointerEventScope, kotlinx\.coroutines\.CoroutineScope, kotlinx\.coroutines\.channels\.SendChannel, androidx\.compose\.runtime\.State, androidx\.compose\.runtime\.MutableState, kotlin\.coroutines\.Continuation\)
-java\.lang\.Object androidx\.compose\.foundation\.gestures\.TapGestureDetectorKt\.waitForUpOrCancellation\(androidx\.compose\.ui\.input\.pointer\.AwaitPointerEventScope, kotlin\.coroutines\.Continuation\)
java\.lang\.Object androidx\.compose\.foundation\.gestures\.ForEachGestureKt\.forEachGesture\(androidx\.compose\.ui\.input\.pointer\.PointerInputScope, kotlin\.jvm\.functions\.Function[0-9]+, kotlin\.coroutines\.Continuation\)
# > Task :preference:preference:compileDebugAndroidTestKotlin
w\: \$SUPPORT\/preference\/preference\/src\/androidTest\/java\/androidx\/preference\/tests\/PreferenceDialogFragmentCompatTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setTargetFragment\(Fragment\?\, Int\)\: Unit\' is deprecated\. Deprecated in Java
@@ -592,11 +385,6 @@
Found an unresolved type in androidx\.compose\.ui\.graphics\.vector\$group\(androidx\.compose\.ui\.graphics\.vector\.ImageVector\.Builder, kotlin\.String, kotlin\.Float, kotlin\.Float, kotlin\.Float, kotlin\.Float, kotlin\.Float, kotlin\.Float, kotlin\.Float, kotlin\.collections\.List\(\(androidx\.compose\.ui\.graphics\.vector\.PathNode\)\), kotlin\.Function[0-9]+\(\(androidx\.compose\.ui\.graphics\.vector\.ImageVector\.Builder, kotlin\.Unit\)\)\) \(ImageVector\.kt:[0-9]+\)
Found an unresolved type in androidx\.compose\.ui\.unit\$constrainWidth\(androidx\.compose\.ui\.unit\.Constraints, kotlin\.Int\) \(Constraints\.kt:[0-9]+\)
Found an unresolved type in androidx\.compose\.ui\.unit\$constrainHeight\(androidx\.compose\.ui\.unit\.Constraints, kotlin\.Int\) \(Constraints\.kt:[0-9]+\)
-Found an unresolved type in androidx\.navigation\$ActivityNavigatorExtras\(androidx\.core\.app\.ActivityOptionsCompat, kotlin\.Int\) \(ActivityNavigatorExtras\.kt:[0-9]+\)
-Found an unresolved type in androidx\.navigation\$navigation\(androidx\.navigation\.NavigatorProvider, kotlin\.Int, kotlin\.Int, kotlin\.Function[0-9]+\(\(androidx\.navigation\.NavGraphBuilder, kotlin\.Unit\)\)\) \(NavGraphBuilder\.kt:[0-9]+\)
-Found an unresolved type in androidx\.navigation\.dynamicfeatures\.DynamicActivityNavigatorDestinationBuilder\$build\(\) \(DynamicActivityNavigatorDestinationBuilder\.kt:[0-9]+\)
-Found an unresolved type in androidx\.navigation\.dynamicfeatures\.fragment\.DynamicFragmentNavigatorDestinationBuilder\$build\(\) \(DynamicFragmentNavigatorDestinationBuilder\.kt:[0-9]+\)
-Found an unresolved type in androidx\.navigation\.fragment\$FragmentNavigatorExtras\(kotlin\.Array\(\(kotlin\.Pair\(\(android\.view\.View, kotlin\.String\)\)\)\)\) \(FragmentNavigatorExtras\.kt:[0-9]+\)
Found an unresolved type in androidx\.paging\.RxPagedListBuilder\$setInitialLoadKey\(androidx\.paging\.RxPagedListBuilder\.Key\) \(RxPagedListBuilder\.kt:[0-9]+\)
Found an unresolved type in androidx\.paging\.RxPagedListBuilder\$setBoundaryCallback\(androidx\.paging\.PagedList\.BoundaryCallback\(\(androidx\.paging\.RxPagedListBuilder\.Value\)\)\) \(RxPagedListBuilder\.kt:[0-9]+\)
Found an unresolved type in androidx\.paging\.RxPagedListBuilder\$setNotifyScheduler\(io\.reactivex\.Scheduler\) \(RxPagedListBuilder\.kt:[0-9]+\)
@@ -604,7 +392,6 @@
Found an unresolved type in androidx\.security\.crypto\$EncryptedFile\(android\.content\.Context, java\.io\.File, androidx\.security\.crypto\.MasterKey, androidx\.security\.crypto\.EncryptedFile\.FileEncryptionScheme, kotlin\.String, kotlin\.String\) \(EncryptedFile\.kt:[0-9]+\)
Found an unresolved type in androidx\.slice\.builders\$list\(android\.content\.Context, android\.net\.Uri, kotlin\.Long, kotlin\.Function[0-9]+\(\(androidx\.slice\.builders\.ListBuilderDsl, kotlin\.Unit\)\)\) \(ListBuilder\.kt:[0-9]+\)
# See b/180023439 for hiltNavGraphViewModel warning.
-Found an unresolved type in androidx\.hilt\.navigation\.compose\$hiltNavGraphViewModel\(androidx\.navigation\.NavController, kotlin\.String\) \(NavHostController\.kt:[0-9]+\)
Unresolved link to .*
Found an unresolved type in androidx\.compose\.foundation\.MutatorMutex\$mutate\(androidx\.compose\.foundation\.MutatePriority, kotlin\.coroutines\.SuspendFunction[0-9]+\(\(androidx\.compose\.foundation\.MutatorMutex\.mutate\.R\)\)\) \(MutatorMutex\.kt:[0-9]+\)
Found an unresolved type in androidx\.compose\.foundation\.MutatorMutex\$mutateWith\(androidx\.compose\.foundation\.MutatorMutex\.mutateWith\.T, androidx\.compose\.foundation\.MutatePriority, kotlin\.coroutines\.SuspendFunction[0-9]+\(\(androidx\.compose\.foundation\.MutatorMutex\.mutateWith\.T, androidx\.compose\.foundation\.MutatorMutex\.mutateWith\.R\)\)\) \(MutatorMutex\.kt:[0-9]+\)
@@ -618,44 +405,29 @@
Found an unresolved type in androidx\.compose\.runtime\$mutableStateMapOf\(kotlin\.Array\(\(kotlin\.Pair\(\(androidx\.compose\.runtime\.mutableStateMapOf\.K, androidx\.compose\.runtime\.mutableStateMapOf\.V\)\)\)\)\) \(SnapshotState\.kt:[0-9]+\)
Found an unresolved type in androidx\.compose\.runtime\$toMutableStateMap\(kotlin\.collections\.Iterable\(\(kotlin\.Pair\(\(androidx\.compose\.runtime\.toMutableStateMap\.K, androidx\.compose\.runtime\.toMutableStateMap\.V\)\)\)\)\) \(SnapshotState\.kt:[0-9]+\)
Found an unresolved type in androidx\.compose\.runtime\.collection\.MutableVector\$sortWith\(\(\(androidx\.compose\.runtime\.collection\.MutableVector\.T\)\)\) \(MutableVector\.kt:[0-9]+\)
-Found an unresolved type in androidx\.navigation\.dynamicfeatures\$navigation\(androidx\.navigation\.NavigatorProvider, kotlin\.Int, kotlin\.Int, kotlin\.Function[0-9]+\(\(androidx\.navigation\.dynamicfeatures\.DynamicNavGraphBuilder, kotlin\.Unit\)\)\) \(DynamicNavGraphBuilder\.kt:[0-9]+\)
-Found an unresolved type in androidx\.navigation\.dynamicfeatures\$createGraph\(androidx\.navigation\.NavController, kotlin\.Int, kotlin\.Int, kotlin\.Function[0-9]+\(\(androidx\.navigation\.dynamicfeatures\.DynamicNavGraphBuilder, kotlin\.Unit\)\)\) \(NavController\.kt:[0-9]+\)
Found an unresolved type in androidx\.paging\.LivePagedListBuilder\$setCoroutineScope\(kotlinx\.coroutines\.CoroutineScope\) \(LivePagedListBuilder\.kt:[0-9]+\)
Found an unresolved type in androidx\.paging\.LivePagedListBuilder\$setInitialLoadKey\(androidx\.paging\.LivePagedListBuilder\.Key\) \(LivePagedListBuilder\.kt:[0-9]+\)
Found an unresolved type in androidx\.paging\.LivePagedListBuilder\$setBoundaryCallback\(androidx\.paging\.PagedList\.BoundaryCallback\(\(androidx\.paging\.LivePagedListBuilder\.Value\)\)\) \(LivePagedListBuilder\.kt:[0-9]+\)
Found an unresolved type in androidx\.paging\.LivePagedListBuilder\$setFetchExecutor\(java\.util\.concurrent\.Executor\) \(LivePagedListBuilder\.kt:[0-9]+\)
-Unresolved function .*
-# > Task :compose:ui:ui-inspection:externalNativeBuildDebug
-Build compose_inspection_jni_.*
# > Task :compose:foundation:foundation-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/compose/foundation/foundation/benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR\/androidx\/compose\/foundation\/foundation\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
# > Task :compose:material:material-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/compose/material/material/benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR\/androidx\/compose\/material\/material\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
# > Task :compose:foundation:foundation-layout:foundation-layout-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/compose/foundation/foundation\-layout/benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR\/androidx\/compose\/foundation\/foundation\-layout\/foundation\-layout\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
# > Task :compose:ui:ui-text:ui-text-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/compose/ui/ui\-text/benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR\/androidx\/compose\/ui\/ui\-text\/ui\-text\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
# > Task :compose:benchmark-utils:benchmark-utils-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/compose/benchmark\-utils/benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR\/androidx\/compose\/benchmark\-utils\/benchmark\-utils\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
# > Task :compose:ui:ui-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/compose/ui/ui/benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR\/androidx\/compose\/ui\/ui\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
# > Task :compose:animation:animation-core:animation-core-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/compose/animation/animation\-core/benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR\/androidx\/compose\/animation\/animation\-core\/animation\-core\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
# > Task :compose:ui:ui-graphics:ui-graphics-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/compose/ui/ui\-graphics/benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-\$OUT_DIR\/androidx\/compose\/ui\/ui\-graphics\/ui\-graphics\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
# > Task :docs-public:doclavaDocs
\$OUT_DIR\/androidx\/docs\-public\/build\/unzippedDocsSources\/androidx\/work\/impl\/WorkManagerImpl\.java\:[0-9]+\: error\: cannot find symbol
import static android\.app\.PendingIntent\.FLAG_MUTABLE\;
-\$OUT_DIR/androidx/docs\-public/build/unzippedDocsSources/androidx/window/ExtensionAdapter\.java:[0-9]+: error: cannot find symbol
-import androidx\.window\.extensions\.ExtensionDeviceState;
# > Task :docs-public:dackkaDocs
PROGRESS: Initializing plugins
Loaded plugins: \[org\.jetbrains\.dokka\.base\.DokkaBase, com\.google\.devsite\.DevsitePlugin\]
@@ -733,6 +505,7 @@
PROGRESS: Validity check
PROGRESS: Creating documentation models
ERROR: An attempt to write .*
+WARN: Unable to find what is referred to by
Generation completed with.*
# > Task :docs-tip-of-tree:dackkaDocs
Conflicting documentation for .*
@@ -742,7 +515,6 @@
PROGRESS: Creating pages
PROGRESS: Transforming pages
Unused extension points found:.*
-generation completed successfully
PROGRESS:
=== TIME MEASUREMENT ===
Initializing plugins\: *[0-9]+
@@ -754,371 +526,44 @@
Creating pages\: *[0-9]+
Transforming pages\: *[0-9]+
Rendering\: *[0-9]+
-WARNING\: unable to find what is referred to by
@param _value
in DClass ObservableWatchData
@param currentUserStyleRepository,
@param watchState
in DClass Renderer
@param bounds
-in DClass Complication
in DClass ComplicationSlot
-@param maxSlotsToRetainForReuse
-in DClass SubcomposeLayoutState
-@param initVal
-in DClass AnimationVector[0-9]+D
-@param a,
-@param b,
-@param c,
-@param d
-in DClass CubicBezierEasing
-@param v[0-9]+,
-@param v[0-9]+
-@param frictionMultiplier,
-@param absVelocityThreshold
-in DClass FloatExponentialDecaySpec
-@param duration,
-@param delay,
-@param easing
-in DClass FloatTweenSpec
-@param dampingRatio,
-@param stiffness,
-@param visibilityThreshold
-in DClass SpringSpec
-@param durationMillis,
-in DClass TweenSpec
-in DClass FloatSpringSpec
-@param iterations,
-@param animation,
-@param repeatMode
-in DClass RepeatableSpec
-in DClass VectorizedInfiniteRepeatableSpec
-@param keyframes,
-@param delayMillis
-in DClass VectorizedKeyframesSpec
-@param delayMillis,
-in DClass VectorizedTweenSpec
-@param animationSpec,
-@param initialValue,
-@param targetValue,
-@param typeConverter,
-@param initialVelocityVector
-in DClass TargetBasedAnimation
-@param delay
-in DClass SnapSpec
-in DClass InfiniteRepeatableSpec
-in DClass VectorizedRepeatableSpec
-@param initialVelocityVector,
-@param lastFrameTimeNanos,
-@param finishedTimeNanos,
-@param isRunning
-in DClass AnimationState
-in DClass VectorizedSnapSpec
-in DClass Animatable
-@param width,
-@param brush
-in DClass BorderStroke
-@param initial
-in DClass ScrollState
-@param firstVisibleItemIndex,
-@param firstVisibleItemScrollOffset
-in DClass LazyListState
-@param builder
-in DClass GenericShape
-@param topLeft,
-@param topRight,
-@param bottomRight,
-@param bottomLeft
-in DClass AbsoluteCutCornerShape
-@param topStart,
-@param topEnd,
-@param bottomEnd,
-@param bottomStart
-in DClass RoundedCornerShape
-in DClass CutCornerShape
-in DClass AbsoluteRoundedCornerShape
-in DClass CornerBasedShape
-@param capitalization,
-@param autoCorrect,
-@param keyboardType,
-@param imeAction
-in DClass KeyboardOptions
-@param drawerState,
-@param bottomSheetState,
-@param snackbarHostState
-in DClass BottomSheetScaffoldState
-@param confirmStateChange,
-in DClass BackdropScaffoldState
-@param confirmStateChange
-in DClass DismissState
-in DClass BottomDrawerState
-in DClass ScaffoldState
-@param basis,
-@param factorAtMin,
-@param factorAtMax
-in DClass ResistanceConfig
-in DClass BottomSheetState
-in DClass DrawerState
-@param offset
-in DClass FixedThreshold
-@param fraction
-in DClass FractionalThreshold
-in DClass ModalBottomSheetState
-@param from,
-@param to,
-in DClass SwipeProgress
-in DClass SwipeableState
-@param file
-in DAnnotation LiveLiteralFileInfo
-@param name,
-@param id
-in DAnnotation DecoyImplementation
-@param key,
-in DAnnotation LiveLiteralInfo
-@param parameters
-in DAnnotation StabilityInferred
-@param targetName,
-@param signature
-in DAnnotation Decoy
-@param left,
-@param top,
-@param right,
-@param bottom
-in DClass MutableRect
-@param buffer,
-@param bufferOffset,
-@param height,
-@param stride
-in DClass PixelMap
-@param image,
-@param srcOffset,
-@param srcSize
-in DClass BitmapPainter
-@param imageVector
-in DClass AnimatedImageVector
-@param keyCode
-in DClass Key
-@param value
-in DClass PointerId
-@param positionChange,
-@param downChange
-in DClass ConsumedData
@param id,
@param canvasComplicationFactory,
@param supportedTypes,
\@param defaultProviderPolicy\,
@param defaultDataSourcePolicy,
-@param defaultPolicy,
@param boundsType,
@param bounds,
\@param defaultPolicy
@param complicationTapFilter
-@param uptimeMillis,
-@param position,
-@param pressed,
-@param previousUptimeMillis,
-@param previousPosition,
-@param previousPressed,
-@param consumed,
-@param type
-in DClass PointerInputChange
-@param merger
-in DClass VerticalAlignmentLine
-in DClass HorizontalAlignmentLine
-@param value,
-@param maxValue,
-@param reverseScrolling
-in DClass ScrollAxisRange
-@param current,
-@param range,
-@param steps
-in DClass ProgressBarRangeInfo
-@param label,
-@param action
-in DClass AccessibilityAction
-in DClass CustomAccessibilityAction
-@param selectedNodes,
-@param customErrorOnNoMatch
-in DClass SelectionResult
-@param description,
-@param requiresExactlyOneNode,
-@param chainedInputSelector,
-@param selector
-in DClass SemanticsSelector
-@param activityRule,
-@param activityProvider
-in DClass AndroidComposeTestRule
-@param placeholderVerticalAlign
-in DClass Placeholder
-@param item,
-@param start,
-@param end,
-@param tag
-in DClass Range
-@param verbatim
-in DClass VerbatimTtsAnnotation
-@param annotatedString,
-@param style,
-@param placeholders,
-@param density,
-@param resourceLoader
-in DClass MultiParagraphIntrinsics
-@param textAlign,
-@param textDirection,
-@param textIndent,
-@param lineHeight
-in DClass ParagraphStyle
-@param color,
-@param fontSize,
-@param fontWeight,
-@param fontStyle,
-@param fontSynthesis,
-@param fontFamily,
-@param fontFeatureSettings,
-@param letterSpacing,
-@param baselineShift,
-@param textGeometricTransform,
-@param localeList,
-@param background,
-@param textDecoration,
-@param shadow
-in DClass SpanStyle
-@param shadow,
-in DClass TextStyle
-@param intrinsics,
-@param maxLines,
-@param ellipsis,
-@param width
-in DClass MultiParagraph
-@param capacity
in DClass Builder
-@param typeface
-in DClass LoadedFontFamily
-@param resId,
-@param weight,
-@param style
-in DClass ResourceFont
-@param name
-in DClass GenericFontFamily
-@param weight
-in DClass FontWeight
-@param singleLine,
-in DClass ImeOptions
-@param selection,
-@param composition
-in DClass TextFieldValue
-@param mask
-in DClass PasswordVisualTransformation
-@param multiplier
-in DClass BaselineShift
-@param firstLine,
-@param restLine
-in DClass TextIndent
-@param scaleX,
-@param skewX
-in DClass TextGeometricTransform
-@param provider,
-@param limit
-in DAnnotation PreviewParameter
-@param group,
-@param apiLevel,
-@param widthDp,
-@param heightDp,
-@param locale,
-@param fontScale,
-@param showSystemUi,
-@param showBackground,
-@param backgroundColor,
-@param uiMode,
-@param device
-in DAnnotation Preview
-@param words
-in DClass LoremIpsum
-@param securePolicy
-in DClass PopupProperties
-@param rowCount,
-@param columnCount
-in DClass CollectionInfo
-@param rowIndex,
-@param rowSpan,
-@param columnIndex,
-@param columnSpan
-in DClass CollectionItemInfo
-@param endOfPaginationReached
-in DClass LoadState
-in DClass NotLoading
-@param error
-in DClass Error
-@param pagingSourceFactory
-in DClass InvalidatingPagingSourceFactory
-@param flow
-in DClass LazyPagingItems
-@param density
-in DClass SplineBasedFloatDecayAnimationSpec
Did you make a typo\? Are you trying to refer to something not visible to users\?
# Wire proto generation, task :generateDebugProtos
Writing .* to \$OUT_DIR/.*/build/generated/source/wire
# > Task :docs-tip-of-tree:doclavaDocs
javadoc\: warning \- Multiple sources of package comments found for package \"javax\.annotation\"
[0-9]+ warning
-# > Task :profileinstaller:profileinstaller:integration-tests:testapp:compileReleaseKotlin
-w: Flag is not supported by this version of the compiler: \-Xallow\-jvm\-ir\-dependencies
-# > Task :room:room-compiler-processing-testing:sourceJar
-Execution optimizations have been disabled for task ':room:room\-compiler\-processing\-testing:sourceJar' to ensure correctness due to the following reasons:
-\- Gradle detected a problem with the following location: '\$OUT_DIR/androidx/room/room\-compiler\-processing\-testing/build/test\-config'\. Reason: Task ':room:room\-compiler\-processing\-testing:sourceJar' uses this output of task ':room:room\-compiler\-processing\-testing:prepareTestConfiguration' without declaring an explicit or implicit dependency\. This can lead to incorrect results being produced, depending on what order the tasks are executed\. Please refer to https://docs\.gradle\.org/[0-9]+\.[0-9]+/userguide/validation_problems\.html\#implicit_dependency for more details about this problem\.
-# > Task :wear:tiles:tiles-proto:sourceJar
-Execution optimizations have been disabled for task ':wear:tiles:tiles\-proto:sourceJar' to ensure correctness due to the following reasons:
-\- Gradle detected a problem with the following location: '\$OUT_DIR/androidx/wear/tiles/tiles\-proto/build/generated/source/proto'\. Reason: Task ':wear:tiles:tiles\-proto:sourceJar' uses this output of task ':wear:tiles:tiles\-proto:generateProto' without declaring an explicit or implicit dependency\. This can lead to incorrect results being produced, depending on what order the tasks are executed\. Please refer to https://docs\.gradle\.org/[0-9]+\.[0-9]+/userguide/validation_problems\.html\#implicit_dependency for more details about this problem\.
-# > Task :appsearch:appsearch-local-storage:desugarDebugAndroidTestFileDependencies
-Execution optimizations have been disabled for task ':appsearch:appsearch\-local\-storage:desugarDebugAndroidTestFileDependencies' to ensure correctness due to the following reasons:
-# > Task :appsearch:appsearch:desugarDebugAndroidTestFileDependencies
-Execution optimizations have been disabled for task ':appsearch:appsearch:desugarDebugAndroidTestFileDependencies' to ensure correctness due to the following reasons:
-# > Task :appsearch:appsearch-debug-view:desugarDebugAndroidTestFileDependencies
-Execution optimizations have been disabled for task ':appsearch:appsearch-debug-view:desugar.*AndroidTestFileDependencies' to ensure correctness due to the following reasons:
-# > Task :appsearch:appsearch-debug-view:samples:desugarDebugFileDependencies
-Execution optimizations have been disabled for task ':appsearch:appsearch-debug-view:samples:desugar.*FileDependencies' to ensure correctness due to the following reasons:
# > Task :stripArchiveForPartialDejetification
Execution optimizations have been disabled for task ':stripArchiveForPartialDejetification' to ensure correctness due to the following reasons:
\- Gradle detected a problem with the following location: '\$DIST_DIR/top\-of\-tree\-m2repository\-all\-.*.zip'\. Reason: Task ':stripArchiveForPartialDejetification' uses this output of task ':benchmark:benchmark\-gradle\-plugin:buildOnServer' without declaring an explicit or implicit dependency\. This can lead to incorrect results being produced, depending on what order the tasks are executed\. Please refer to https://docs\.gradle\.org/[0-9]+\.[0-9]+/userguide/validation_problems\.html\#implicit_dependency for more details about this problem\.
# > Task :icing:lintDebug
Wrote HTML report to file://\$OUT_DIR/androidx/icing/build/reports/lint\-results\-debug\.html
-Wrote XML report to file://\$OUT_DIR/androidx/icing/build/reports/lint\-results\-debug\.xml
-# > Task :zipConstrainedTestConfigsWithApks
-Execution optimizations have been disabled for task ':zipConstrainedTestConfigsWithApks' to ensure correctness due to the following reasons:
-\- Gradle detected a problem with the following location: '\$DIST_DIR/constrained\-test\-xml\-configs'\. Reason: Task ':zipConstrainedTestConfigsWithApks' uses this output of task ':benchmark:benchmark\-gradle\-plugin:buildOnServer' without declaring an explicit or implicit dependency\. This can lead to incorrect results being produced, depending on what order the tasks are executed\. Please refer to https://docs\.gradle\.org/[0-9]+\.[0-9]+/userguide/validation_problems\.html\#implicit_dependency for more details about this problem\.
-# > Task :zipTestConfigsWithApks
-Execution optimizations have been disabled for task ':zipTestConfigsWithApks' to ensure correctness due to the following reasons:
-\- Gradle detected a problem with the following location: '\$DIST_DIR/test\-xml\-configs'\. Reason: Task ':zipTestConfigsWithApks' uses this output of task ':benchmark:benchmark\-gradle\-plugin:buildOnServer' without declaring an explicit or implicit dependency\. This can lead to incorrect results being produced, depending on what order the tasks are executed\. Please refer to https://docs\.gradle\.org/[0-9]+\.[0-9]+/userguide/validation_problems\.html\#implicit_dependency for more details about this problem\.
-# > Task :room:room-compiler:checkArtifactTask
-Execution optimizations have been disabled for task ':room:room\-compiler:checkArtifactTask' to ensure correctness due to the following reasons:
-\- Gradle detected a problem with the following location: '\$OUT_DIR/androidx/room/room\-compiler/build/libs/room\-compiler\-[0-9]+\.[0-9]+\.[0-9]+\-alpha[0-9]+\.jar'\. Reason: Task ':room:room\-compiler:checkArtifactTask' uses this output of task ':room:room\-compiler:shadowJar' without declaring an explicit or implicit dependency\. This can lead to incorrect results being produced, depending on what order the tasks are executed\. Please refer to https://docs\.gradle\.org/[0-9]+\.[0-9]+/userguide/validation_problems\.html\#implicit_dependency for more details about this problem\.
-\- Gradle detected a problem with the following location: '\$OUT_DIR/androidx/room/room\-compiler/build/publications/maven/pom\-default\.xml'\. Reason: Task ':room:room\-compiler:checkArtifactTask' uses this output of task ':room:room\-compiler:generatePomFileForMavenPublication' without declaring an explicit or implicit dependency\. This can lead to incorrect results being produced, depending on what order the tasks are executed\. Please refer to https://docs\.gradle\.org/[0-9]+\.[0-9]+/userguide/validation_problems\.html\#implicit_dependency for more details about this problem\.
-\- Gradle detected a problem with the following location: '\$OUT_DIR'\. Reason: Task ':zipEcFiles' uses this output of task.*
-Execution optimizations have been disabled for task ':zipEcFiles' to ensure correctness due to the following reasons:
-\- Gradle detected a problem with the following location: '\$OUT_DIR/.*/build/repackaged/repackaged\-Debug\.jar'\. Reason:.*
-\- Gradle detected a problem with the following location: '\$OUT_DIR/.*/build/repackaged/repackaged\-Release\.jar'\. Reason:.*
-Execution optimizations have been disabled for task ':emoji.*
-Execution optimizations have been disabled for task ':support-emoji-demos:.*
-# https://issues.apache.org/jira/browse/GROOVY-8843
-WARNING: Illegal reflective access by org\.codehaus\.groovy\.vmplugin\.v7\.Java7\$1 .* to constructor java\.lang\.invoke\.MethodHandles\$Lookup\(java\.lang\.Class,int\)
-WARNING: Please consider reporting this to the maintainers of org\.codehaus\.groovy\.vmplugin\.v7\.Java7\$1
# https://youtrack.jetbrains.com/issue/KT-30589
WARNING: Illegal reflective access by org\.jetbrains\.kotlin\.kapt3\.base\.javac\.KaptJavaFileManager .* to method com\.sun\.tools\.javac\.file\.BaseFileManager\.handleOption\(com\.sun\.tools\.javac\.main\.Option,java\.lang\.String\)
# > Task :benchmark:benchmark-macro:compileReleaseKotlin
Execution optimizations have been disabled for task ':benchmark:benchmark\-macro:.*' to ensure correctness due to the following reasons:
\- Gradle detected a problem with the following location: '\$OUT_DIR/androidx/benchmark/benchmark\-macro/build/generated/source/wire'\. Reason: Task ':benchmark:benchmark\-macro:.*' uses this output of task ':benchmark:benchmark\-macro:.*' without declaring an explicit or implicit dependency\. This can lead to incorrect results being produced, depending on what order the tasks are executed\. Please refer to https://docs\.gradle\.org/[0-9]+\.[0-9]+/userguide/validation_problems\.html\#implicit_dependency for more details about this problem\.
-# > Task :support-emoji-demos:lintDebug
-Error processing .* broken class file\? \(This feature requires ASM[0-9]+\)
# > Task :profileinstaller:profileinstaller:processDebugUnitTestManifest
-\$SUPPORT/profileinstaller/profileinstaller/src/test/AndroidManifest\.xml Warning:
-Unknown flag --class-dir: Use --help for usage information
Scanning .+: \.*
-\.+
# > Task :compose:ui:ui-tooling:processDebugAndroidTestManifest
\$SUPPORT/compose/ui/ui\-tooling/src/androidAndroidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
No issues found.*
-Incorrect detector reported disabled issue.*
-Scanning .* \(Phase 2\):
dagger\.lint\.DaggerIssueRegistry in .*/lint\.jar does not specify a vendor; see IssueRegistry#vendor
# > Task :camera:camera-camera2-pipe:reportLibraryMetrics
Info: Stripped invalid locals information from [0-9]+ methods\.
@@ -1126,14 +571,10 @@
java\.lang\.Object androidx\.wear\.complications\.ComplicationDataSourceInfoRetriever\.retrievePreviewComplicationData\(android\.content\.ComponentName, androidx\.wear\.complications\.data\.ComplicationType, kotlin\.coroutines\.Continuation\)
java\.lang\.Object androidx\.wear\.watchface\.editor\.BaseEditorSession\.openComplicationDataSourceChooser\$suspendImpl\(androidx\.wear\.watchface\.editor\.BaseEditorSession, int, kotlin\.coroutines\.Continuation\)
java\.lang\.Object androidx\.compose\.ui\.platform\.GlobalSnapshotManager\$ensureStarted\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
-java\.lang\.Object androidx\.wear\.watchface\.WatchFaceService\$EngineWrapper\.createWatchFaceImpl\(android\.icu\.util\.Calendar, androidx\.wear\.watchface\.ComplicationSlotsManager, androidx\.wear\.watchface\.style\.CurrentUserStyleRepository, kotlinx\.coroutines\.CompletableDeferred, kotlinx\.coroutines\.CompletableDeferred, androidx\.wear\.watchface\.WatchState, kotlin\.coroutines\.Continuation\)
-java\.lang\.Object androidx\.wear\.watchface\.WatchFaceService\$EngineWrapper\.setUserStyle\$wear_watchface_release\(androidx\.wear\.watchface\.style\.data\.UserStyleWireFormat, kotlin\.coroutines\.Continuation\)
java\.lang\.Object androidx\.wear\.watchface\.editor\.BaseEditorSession\$fetchComplicationsData\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
java\.lang\.Object androidx\.camera\.camera[0-9]+\.pipe\.CameraDevicesKt\$find\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
java\.lang\.Object androidx\.camera\.camera[0-9]+\.pipe\.compat\.VirtualCameraManager\$requestLoop\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
Information in locals\-table is invalid with respect to the stack map table\. Local refers to non\-present stack map type for register: [0-9]+ with constraint OBJECT\.
-java\.lang\.Object androidx\.camera\.camera[0-9]+\.pipe\.graph\.Controller[0-9]+A\.submit[0-9]+A\(androidx\.camera\.camera[0-9]+\.pipe\.AeMode, androidx\.camera\.camera[0-9]+\.pipe\.AfMode, androidx\.camera\.camera[0-9]+\.pipe\.AwbMode, java\.util\.List, java\.util\.List, java\.util\.List, kotlin\.coroutines\.Continuation\)
-java\.lang\.Object androidx\.camera\.camera[0-9]+\.pipe\.graph\.Controller[0-9]+A\.lock[0-9]+A\(java\.util\.List, java\.util\.List, java\.util\.List, androidx\.camera\.camera[0-9]+\.pipe\.Lock[0-9]+ABehavior, androidx\.camera\.camera[0-9]+\.pipe\.Lock[0-9]+ABehavior, androidx\.camera\.camera[0-9]+\.pipe\.Lock[0-9]+ABehavior, int, java\.lang\.Long, kotlin\.coroutines\.Continuation\)
java\.lang\.Object androidx\.camera\.camera[0-9]+\.pipe\.compat\.VirtualCameraManager\.openCameraWithRetry\-RzXb[0-9]+QE\(java\.lang\.String, kotlinx\.coroutines\.CoroutineScope, kotlin\.coroutines\.Continuation\)
Information in locals\-table is invalid with respect to the stack map table\. Local refers to non\-present stack map type for register: [0-9]+ with constraint INT\.
Info: Some warnings are typically a sign of using an outdated Java toolchain\. To fix, recompile the source with an updated toolchain\.
@@ -1142,40 +583,21 @@
java\.lang\.Object androidx\.paging\.samples\.RemoteMediatorSampleKt\$remoteMediatorPageKeyedSample\$ExampleRemoteMediator\.load\(androidx\.paging\.LoadType, androidx\.paging\.PagingState, kotlin\.coroutines\.Continuation\)
# > Task :wear:wear-complications-data:reportLibraryMetrics
Info: Stripped invalid locals information from [0-9]+ method\.
-java\.lang\.Object androidx\.wear\.complications\.ProviderInfoRetriever\.retrievePreviewComplicationData\(android\.content\.ComponentName, androidx\.wear\.complications\.data\.ComplicationType, kotlin\.coroutines\.Continuation\)
# > Task :wear:wear-watchface-editor:reportLibraryMetrics
java\.lang\.Object androidx\.wear\.watchface\.editor\.BaseEditorSession\.getPreviewData\$wear_watchface_editor_release\(androidx\.wear\.complications\.ComplicationDataSourceInfoRetriever, androidx\.wear\.complications\.ComplicationDataSourceInfo, kotlin\.coroutines\.Continuation\)
# > Task :work:work-runtime-ktx:reportLibraryMetrics
java\.lang\.Object androidx\.work\.OperationKt\.await\(androidx\.work\.Operation, kotlin\.coroutines\.Continuation\)
# > Task :compose:material:material:reportLibraryMetrics
java\.lang\.Object androidx\.compose\.material\.SnackbarHostState\.showSnackbar\(java\.lang\.String, java\.lang\.String, androidx\.compose\.material\.SnackbarDuration, kotlin\.coroutines\.Continuation\)
-java\.lang\.Object androidx\.compose\.material\.SwipeableState\.processNewAnchors\$material_release\(java\.util\.Map, java\.util\.Map, kotlin\.coroutines\.Continuation\)
-# > Task :car:app:app-samples:helloworld-automotive:processReleaseMainManifest
-\[:car:app:app\-automotive\] \$OUT_DIR/androidx/car/app/app\-automotive/build/intermediates/merged_manifest/release/AndroidManifest\.xml Warning:
-Package name 'androidx\.car\.app' used in: :car:app:app\-automotive, :car:app:app\.
-# > Task :car:app:app-automotive:processDebugUnitTestManifest
-\[:car:app:app\-automotive\] \$OUT_DIR/androidx/car/app/app\-automotive/build/intermediates/merged_manifest/debug/AndroidManifest\.xml Warning:
-# > Task :camera:camera-video:lintDebug
-Error processing \$CHECKOUT/prebuilts/androidx/external/org/jetbrains/kotlin/kotlin\-stdlib/[0-9]+\.[0-9]+\.[0-9]+\-M[0-9]+/kotlin\-stdlib\-[0-9]+\.[0-9]+\.[0-9]+\-M[0-9]+\.jar:META\-INF/versions/[0-9]+/module\-info\.class: broken class file\? \(This feature requires ASM[0-9]+\)
# > Task :compose:foundation:foundation:androidReleaseSourcesJar
Encountered duplicate path "android[a-zA-Z]*/.+" during copy operation configured with DuplicatesStrategy\.WARN
# > Task :camera:camera-camera2-pipe:reportLibraryMetrics
java\.lang\.Object androidx\.camera\.camera[0-9]+\.pipe\.compat\.Camera[0-9]+DeviceCache\$getCameras\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
Type information in locals\-table is inconsistent\. Cannot constrain type: @Nullable androidx\.camera\.camera[0-9]+\.pipe\.core\.Debug \{\} for value: v[0-9]+\(this_\$iv\$iv\) by constraint INT\.
# > Task :compose:foundation:foundation:integration-tests:foundation-demos:reportLibraryMetrics
-Stripped invalid locals information from [0-9]+ method\.
java\.lang\.Object androidx\.compose\.foundation\.demos\.ListDemosKt\$ListHoistedStateDemo\$[0-9]+\$[0-9]+\$[0-9]+\$[0-9]+\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
-# > Task :compose:runtime:runtime-saveable-lint:compileKotlin
-at org\.jetbrains\.kotlin\.compilerRunner\.GradleCompilerRunnerWithWorkers\$GradleKotlinCompilerWorkAction\.execute\(GradleCompilerRunnerWithWorkers\.kt\:[0-9]+\)
-# > Task :buildSrc:jetpad-integration:compileJava
-Could not store entry [0-9a-f]{32} in remote build cache: Storing entry at 'http://gradle\-remote\-cache\.uplink\.goog:[0-9]+/cache/[0-9a-f]{32}' response status [0-9]+: Uplink Bad Gateway
# > Task :profileinstaller:profileinstaller-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/profileinstaller/profileinstaller\-benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-WARNING:Software Components will not be created automatically for Maven publishing from Android Gradle Plugin 8\.0\..*
-# > Task :wear:wear-watchface:compileReleaseKotlin
-w: \$SUPPORT/wear/wear\-watchface/src/main/java/androidx/wear/watchface/WatchFaceService\.kt: \([0-9]+, [0-9]+\): Parameter 'surfaceHolder' is never used
-# > Task :lint-checks:integration-tests:copyDebugAndroidLintReports
-Copying lint text report to .*
# > Task :wear:compose:compose-material-benchmark:processReleaseAndroidTestManifest
\$SUPPORT/wear/compose/material/benchmark/src/androidTest/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Configure project :ads-identifier
@@ -1190,3 +612,5 @@
\$SUPPORT/emoji[0-9]+/emoji[0-9]+\-bundled/src/main/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
# > Configure project :emoji2:emoji2-bundled
WARNING:The option setting 'android\.disableAutomaticComponentCreation=true' is experimental\.
+# > Task :compose:ui:ui-test-junit4:testDebugUnitTest
+System\.logW: A resource was acquired at attached stack trace but never released\. See java\.io\.Closeable for information on avoiding resource leaks\.java\.lang\.Throwable: Explicit termination method 'dispose' not called
\ No newline at end of file
diff --git a/development/referenceDocs/stageReferenceDocs.sh b/development/referenceDocs/stageReferenceDocs.sh
index 0907583..d3c430e 100755
--- a/development/referenceDocs/stageReferenceDocs.sh
+++ b/development/referenceDocs/stageReferenceDocs.sh
@@ -44,12 +44,10 @@
# Remove directories we never publish
rm en -rf
-rm reference/android -rf
rm reference/java -rf
rm reference/org -rf
rm reference/hierarchy.html
rm reference/kotlin/org -rf
-rm reference/kotlin/android -rf
# Move package list into the correct location
mv reference/kotlin/package-list reference/kotlin/androidx/package-list
diff --git a/development/referenceDocs/stageReferenceDocsWithDackka.sh b/development/referenceDocs/stageReferenceDocsWithDackka.sh
index d3e12d7..1916086 100755
--- a/development/referenceDocs/stageReferenceDocsWithDackka.sh
+++ b/development/referenceDocs/stageReferenceDocsWithDackka.sh
@@ -94,12 +94,10 @@
# Remove directories we never publish
rm en -rf
-rm reference/android -rf
rm reference/java -rf
rm reference/org -rf
rm reference/hierarchy.html
rm reference/kotlin/org -rf
-rm reference/kotlin/android -rf
# Move package list into the correct location
mv reference/kotlin/package-list reference/kotlin/androidx/package-list
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 587c7cc..2383267 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -125,6 +125,7 @@
docs("androidx.hilt:hilt-common:1.0.0-beta01")
docs("androidx.hilt:hilt-navigation:1.0.0-beta01")
docs("androidx.hilt:hilt-navigation-compose:1.0.0-alpha03")
+ samples("androidx.hilt:hilt-navigation-compose-samples:1.0.0-alpha04")
docs("androidx.hilt:hilt-navigation-fragment:1.0.0-beta01")
docs("androidx.hilt:hilt-work:1.0.0-beta01")
docs("androidx.interpolator:interpolator:1.0.0")
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
index 5ce276d..45093a9 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
@@ -234,8 +234,8 @@
*/
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
mFragments.noteStateNotSaved();
+ super.onConfigurationChanged(newConfig);
mFragments.dispatchConfigurationChanged(newConfig);
}
@@ -382,8 +382,8 @@
@Override
@CallSuper
protected void onNewIntent(@SuppressLint("UnknownNullness") Intent intent) {
- super.onNewIntent(intent);
mFragments.noteStateNotSaved();
+ super.onNewIntent(intent);
}
/**
@@ -406,9 +406,9 @@
*/
@Override
protected void onResume() {
+ mFragments.noteStateNotSaved();
super.onResume();
mResumed = true;
- mFragments.noteStateNotSaved();
mFragments.execPendingActions();
}
@@ -468,6 +468,7 @@
*/
@Override
protected void onStart() {
+ mFragments.noteStateNotSaved();
super.onStart();
mStopped = false;
@@ -477,7 +478,6 @@
mFragments.dispatchActivityCreated();
}
- mFragments.noteStateNotSaved();
mFragments.execPendingActions();
// NOTE: HC onStart goes here.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index caf3af8..d44fe56 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -33,7 +33,7 @@
ksp = "1.5.10-1.0.0-beta01"
leakcanary = "2.2"
mockito = "2.25.0"
-skiko = "0.3.5"
+skiko = "0.3.6"
sqldelight = "1.3.0"
wire = "3.6.0"
diff --git a/navigation/navigation-fragment/api/current.txt b/navigation/navigation-fragment/api/current.txt
index bfb1930..fb744d2 100644
--- a/navigation/navigation-fragment/api/current.txt
+++ b/navigation/navigation-fragment/api/current.txt
@@ -13,7 +13,7 @@
ctor public AbstractListDetailFragment();
method public androidx.navigation.fragment.NavHostFragment onCreateDetailPaneNavHostFragment();
method public abstract android.view.View onCreateListPaneView(android.view.LayoutInflater inflater, android.view.ViewGroup? container, android.os.Bundle? savedInstanceState);
- method @CallSuper public final android.view.View? onCreateView(android.view.LayoutInflater inflater, android.view.ViewGroup? container, android.os.Bundle? savedInstanceState);
+ method @CallSuper public final android.view.View onCreateView(android.view.LayoutInflater inflater, android.view.ViewGroup? container, android.os.Bundle? savedInstanceState);
method public void onListPaneViewCreated(android.view.View view, android.os.Bundle? savedInstanceState);
method @CallSuper public final void onViewCreated(android.view.View view, android.os.Bundle? savedInstanceState);
method public final androidx.navigation.fragment.NavHostFragment requireDetailPaneNavHostFragment();
diff --git a/navigation/navigation-fragment/api/public_plus_experimental_current.txt b/navigation/navigation-fragment/api/public_plus_experimental_current.txt
index bfb1930..fb744d2 100644
--- a/navigation/navigation-fragment/api/public_plus_experimental_current.txt
+++ b/navigation/navigation-fragment/api/public_plus_experimental_current.txt
@@ -13,7 +13,7 @@
ctor public AbstractListDetailFragment();
method public androidx.navigation.fragment.NavHostFragment onCreateDetailPaneNavHostFragment();
method public abstract android.view.View onCreateListPaneView(android.view.LayoutInflater inflater, android.view.ViewGroup? container, android.os.Bundle? savedInstanceState);
- method @CallSuper public final android.view.View? onCreateView(android.view.LayoutInflater inflater, android.view.ViewGroup? container, android.os.Bundle? savedInstanceState);
+ method @CallSuper public final android.view.View onCreateView(android.view.LayoutInflater inflater, android.view.ViewGroup? container, android.os.Bundle? savedInstanceState);
method public void onListPaneViewCreated(android.view.View view, android.os.Bundle? savedInstanceState);
method @CallSuper public final void onViewCreated(android.view.View view, android.os.Bundle? savedInstanceState);
method public final androidx.navigation.fragment.NavHostFragment requireDetailPaneNavHostFragment();
diff --git a/navigation/navigation-fragment/api/restricted_current.txt b/navigation/navigation-fragment/api/restricted_current.txt
index bfb1930..fb744d2 100644
--- a/navigation/navigation-fragment/api/restricted_current.txt
+++ b/navigation/navigation-fragment/api/restricted_current.txt
@@ -13,7 +13,7 @@
ctor public AbstractListDetailFragment();
method public androidx.navigation.fragment.NavHostFragment onCreateDetailPaneNavHostFragment();
method public abstract android.view.View onCreateListPaneView(android.view.LayoutInflater inflater, android.view.ViewGroup? container, android.os.Bundle? savedInstanceState);
- method @CallSuper public final android.view.View? onCreateView(android.view.LayoutInflater inflater, android.view.ViewGroup? container, android.os.Bundle? savedInstanceState);
+ method @CallSuper public final android.view.View onCreateView(android.view.LayoutInflater inflater, android.view.ViewGroup? container, android.os.Bundle? savedInstanceState);
method public void onListPaneViewCreated(android.view.View view, android.os.Bundle? savedInstanceState);
method @CallSuper public final void onViewCreated(android.view.View view, android.os.Bundle? savedInstanceState);
method public final androidx.navigation.fragment.NavHostFragment requireDetailPaneNavHostFragment();
diff --git a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/AbstractListDetailFragment.kt b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/AbstractListDetailFragment.kt
index e7cb787..d62f9df 100644
--- a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/AbstractListDetailFragment.kt
+++ b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/AbstractListDetailFragment.kt
@@ -22,12 +22,15 @@
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import androidx.activity.OnBackPressedCallback
import androidx.annotation.CallSuper
import androidx.core.content.res.use
import androidx.core.view.doOnLayout
import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.commit
import androidx.slidingpanelayout.widget.SlidingPaneLayout
@@ -42,7 +45,7 @@
* be overridden by [AbstractListDetailFragment.onCreateDetailPaneNavHostFragment] and provide
* custom NavHostFragment.
*/
-abstract class AbstractListDetailFragment : Fragment(R.layout.abstract_list_detail_fragment) {
+abstract class AbstractListDetailFragment : Fragment() {
private var onBackPressedCallback: OnBackPressedCallback? = null
private var detailPaneNavHostFragment: NavHostFragment? = null
private var graphId = 0
@@ -133,18 +136,42 @@
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
- ): View? {
+ ): View {
if (savedInstanceState != null) {
graphId = savedInstanceState.getInt(NavHostFragment.KEY_GRAPH_ID)
}
- val slidingPaneLayout = super.onCreateView(inflater, container, savedInstanceState) as
- SlidingPaneLayout
- val listContainer = slidingPaneLayout.findViewById<FrameLayout>(R.id.list_container)
+ val slidingPaneLayout = SlidingPaneLayout(inflater.context).apply {
+ id = R.id.sliding_pane_layout
+ }
+
+ // Set up the list container
+ val listContainer = FrameLayout(inflater.context).apply {
+ id = R.id.sliding_pane_list_container
+ }
+ val listLayoutParams = SlidingPaneLayout.LayoutParams(WRAP_CONTENT, MATCH_PARENT)
+ slidingPaneLayout.addView(listContainer, listLayoutParams)
+
+ // Now create and add the list pane itself
val listPaneView = onCreateListPaneView(inflater, listContainer, savedInstanceState)
if (listPaneView.parent != listContainer) {
listContainer.addView(listPaneView)
}
- val existingNavHostFragment = childFragmentManager.findFragmentById(R.id.detail_nav_host)
+
+ // Set up the detail container
+ val detailContainer = FragmentContainerView(inflater.context).apply {
+ id = R.id.sliding_pane_detail_container
+ }
+ val detailWidth = inflater.context.resources.getDimensionPixelSize(
+ R.dimen.sliding_pane_detail_pane_width
+ )
+ val detailLayoutParams = SlidingPaneLayout.LayoutParams(detailWidth, MATCH_PARENT).apply {
+ weight = 1F
+ }
+ slidingPaneLayout.addView(detailContainer, detailLayoutParams)
+
+ // Now create the NavHostFragment for the detail container
+ val existingNavHostFragment =
+ childFragmentManager.findFragmentById(R.id.sliding_pane_detail_container)
detailPaneNavHostFragment = if (existingNavHostFragment != null) {
existingNavHostFragment as NavHostFragment
} else {
@@ -152,7 +179,7 @@
childFragmentManager
.commit {
setReorderingAllowed(true)
- add(R.id.detail_nav_host, newNavHostFragment)
+ add(R.id.sliding_pane_detail_container, newNavHostFragment)
}
}
}
@@ -207,8 +234,7 @@
@CallSuper
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- val listContainer =
- requireSlidingPaneLayout().findViewById<FrameLayout>(R.id.list_container)
+ val listContainer = view.findViewById<FrameLayout>(R.id.sliding_pane_list_container)
val listPaneView = listContainer.getChildAt(0)
onListPaneViewCreated(listPaneView, savedInstanceState)
}
diff --git a/navigation/navigation-fragment/src/main/res/layout/abstract_list_detail_fragment.xml b/navigation/navigation-fragment/src/main/res/layout/abstract_list_detail_fragment.xml
deleted file mode 100644
index 50df6c0..0000000
--- a/navigation/navigation-fragment/src/main/res/layout/abstract_list_detail_fragment.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
- Copyright 2021 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<androidx.slidingpanelayout.widget.SlidingPaneLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <FrameLayout
- android:id="@+id/list_container"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"/>
- <androidx.fragment.app.FragmentContainerView
- android:id="@+id/detail_nav_host"
- android:layout_width="300dp"
- android:layout_height="match_parent"
- android:layout_weight="1"/>
-
-</androidx.slidingpanelayout.widget.SlidingPaneLayout>
\ No newline at end of file
diff --git a/navigation/navigation-fragment/src/main/res/values/dimens.xml b/navigation/navigation-fragment/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..b1906d9
--- /dev/null
+++ b/navigation/navigation-fragment/src/main/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <dimen name="sliding_pane_detail_pane_width">300dp</dimen>
+</resources>
diff --git a/navigation/navigation-fragment/src/main/res/values/ids.xml b/navigation/navigation-fragment/src/main/res/values/ids.xml
index 28a8854..8ab2900 100644
--- a/navigation/navigation-fragment/src/main/res/values/ids.xml
+++ b/navigation/navigation-fragment/src/main/res/values/ids.xml
@@ -16,4 +16,7 @@
-->
<resources>
<item type="id" name="nav_host_fragment_container" />
+ <item type="id" name="sliding_pane_layout" />
+ <item type="id" name="sliding_pane_list_container" />
+ <item type="id" name="sliding_pane_detail_container" />
</resources>
diff --git a/paging/common/api/current.txt b/paging/common/api/current.txt
index 0da1a47..e006221 100644
--- a/paging/common/api/current.txt
+++ b/paging/common/api/current.txt
@@ -364,6 +364,10 @@
property public final Throwable throwable;
}
+ public static final class PagingSource.LoadResult.Invalid<Key, Value> extends androidx.paging.PagingSource.LoadResult<Key,Value> {
+ ctor public PagingSource.LoadResult.Invalid();
+ }
+
public static final class PagingSource.LoadResult.Page<Key, Value> extends androidx.paging.PagingSource.LoadResult<Key,Value> {
ctor public PagingSource.LoadResult.Page(java.util.List<? extends Value> data, Key? prevKey, Key? nextKey, optional @IntRange(from=COUNT_UNDEFINED.toLong()) int itemsBefore, optional @IntRange(from=COUNT_UNDEFINED.toLong()) int itemsAfter);
ctor public PagingSource.LoadResult.Page(java.util.List<? extends Value> data, Key? prevKey, Key? nextKey);
diff --git a/paging/common/api/public_plus_experimental_current.txt b/paging/common/api/public_plus_experimental_current.txt
index 38fc7c5..62e15c2 100644
--- a/paging/common/api/public_plus_experimental_current.txt
+++ b/paging/common/api/public_plus_experimental_current.txt
@@ -368,6 +368,10 @@
property public final Throwable throwable;
}
+ public static final class PagingSource.LoadResult.Invalid<Key, Value> extends androidx.paging.PagingSource.LoadResult<Key,Value> {
+ ctor public PagingSource.LoadResult.Invalid();
+ }
+
public static final class PagingSource.LoadResult.Page<Key, Value> extends androidx.paging.PagingSource.LoadResult<Key,Value> {
ctor public PagingSource.LoadResult.Page(java.util.List<? extends Value> data, Key? prevKey, Key? nextKey, optional @IntRange(from=COUNT_UNDEFINED.toLong()) int itemsBefore, optional @IntRange(from=COUNT_UNDEFINED.toLong()) int itemsAfter);
ctor public PagingSource.LoadResult.Page(java.util.List<? extends Value> data, Key? prevKey, Key? nextKey);
diff --git a/paging/common/api/restricted_current.txt b/paging/common/api/restricted_current.txt
index 0da1a47..e006221 100644
--- a/paging/common/api/restricted_current.txt
+++ b/paging/common/api/restricted_current.txt
@@ -364,6 +364,10 @@
property public final Throwable throwable;
}
+ public static final class PagingSource.LoadResult.Invalid<Key, Value> extends androidx.paging.PagingSource.LoadResult<Key,Value> {
+ ctor public PagingSource.LoadResult.Invalid();
+ }
+
public static final class PagingSource.LoadResult.Page<Key, Value> extends androidx.paging.PagingSource.LoadResult<Key,Value> {
ctor public PagingSource.LoadResult.Page(java.util.List<? extends Value> data, Key? prevKey, Key? nextKey, optional @IntRange(from=COUNT_UNDEFINED.toLong()) int itemsBefore, optional @IntRange(from=COUNT_UNDEFINED.toLong()) int itemsAfter);
ctor public PagingSource.LoadResult.Page(java.util.List<? extends Value> data, Key? prevKey, Key? nextKey);
diff --git a/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt b/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt
index 6f942fc..47cce21 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt
@@ -329,6 +329,7 @@
pageEventCh.send(LoadStateUpdate(REFRESH, false, loadState))
}
}
+ is LoadResult.Invalid -> onInvalidLoad()
}
}
@@ -443,6 +444,10 @@
}
return
}
+ is LoadResult.Invalid -> {
+ onInvalidLoad()
+ return
+ }
}
val dropType = when (loadType) {
@@ -529,6 +534,12 @@
pages.last().nextKey
}
}
+
+ // the handler for LoadResult.Invalid for both doInitialLoad and doLoad
+ private fun onInvalidLoad() {
+ close()
+ pagingSource.invalidate()
+ }
}
/**
diff --git a/paging/common/src/main/kotlin/androidx/paging/PagedList.kt b/paging/common/src/main/kotlin/androidx/paging/PagedList.kt
index 149958d..35ca80a 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PagedList.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PagedList.kt
@@ -185,6 +185,15 @@
when (initialResult) {
is PagingSource.LoadResult.Page -> initialResult
is PagingSource.LoadResult.Error -> throw initialResult.throwable
+ is PagingSource.LoadResult.Invalid ->
+ throw IllegalStateException(
+ "Failed to create PagedList. The provided PagingSource " +
+ "returned LoadResult.Invalid, but a LoadResult.Page was " +
+ "expected. To use a PagingSource which supports " +
+ "invalidation, use a PagedList builder that accepts a " +
+ "factory method for PagingSource or DataSource.Factory, " +
+ "such as LivePagedList."
+ )
}
}
}
diff --git a/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt b/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt
index 05aa243..f5c2707 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt
@@ -204,6 +204,24 @@
) : LoadResult<Key, Value>()
/**
+ * Invalid result object for [PagingSource.load]
+ *
+ * This return type can be used to terminate future load requests on this [PagingSource]
+ * when the [PagingSource] is not longer valid due to changes in the underlying dataset.
+ *
+ * For example, if the underlying database gets written into but the [PagingSource] does
+ * not invalidate in time, it may return inconsistent results if its implementation depends
+ * on the immutability of the backing dataset it loads from (e.g., LIMIT OFFSET style db
+ * implementations). In this scenario, it is recommended to check for invalidation after
+ * loading and to return LoadResult.Invalid, which causes Paging to discard any
+ * pending or future load requests to this PagingSource and invalidate it.
+ *
+ * Returning [Invalid] will trigger Paging to [invalidate] this [PagingSource] and
+ * terminate any future attempts to [load] from this [PagingSource]
+ */
+ public class Invalid<Key : Any, Value : Any> : LoadResult<Key, Value>()
+
+ /**
* Success result object for [PagingSource.load].
*
* @sample androidx.paging.samples.pageKeyedPage
diff --git a/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt b/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
index e1562f4..9f4fdf3 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
@@ -29,6 +29,7 @@
import androidx.paging.PageEvent.Insert.Companion.Refresh
import androidx.paging.PageEvent.LoadStateUpdate
import androidx.paging.PagingSource.LoadResult.Page
+import androidx.paging.PagingSource.LoadResult
import androidx.paging.RemoteMediatorMock.LoadEvent
import androidx.paging.TestPagingSource.Companion.LOAD_ERROR
import com.google.common.truth.Truth.assertThat
@@ -3437,6 +3438,138 @@
assertFalse { initialHint.shouldPrioritizeOver(accessHint, APPEND) }
}
+ @Test
+ fun close_cancelsCollectionFromLoadResultInvalid() = testScope.runBlockingTest {
+ val pagingSource = object : PagingSource<Int, Int>() {
+ override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Int> {
+ return LoadResult.Invalid()
+ }
+
+ override fun getRefreshKey(state: PagingState<Int, Int>): Int? {
+ fail("should not reach here")
+ }
+ }
+ val pager = PageFetcherSnapshot(50, pagingSource, config, retryFlow = retryBus.flow)
+
+ collectSnapshotData(pager) { _, job ->
+
+ // Start initial load but this load should return LoadResult.Invalid
+ // wait some time for the invalid result handler to close the page event flow
+ advanceTimeBy(1000)
+
+ assertTrue { !job.isActive }
+ }
+ }
+
+ @Test
+ fun refresh_cancelsCollectionFromLoadResultInvalid() = testScope.runBlockingTest {
+ val pagingSource = TestPagingSource()
+ pagingSource.nextLoadResult = LoadResult.Invalid()
+
+ val pager = PageFetcherSnapshot(50, pagingSource, config, retryFlow = retryBus.flow)
+
+ collectSnapshotData(pager) { state, job ->
+
+ // Start initial load but this load should return LoadResult.Invalid
+ // Wait some time for the result handler to close the page event flow
+ advanceUntilIdle()
+
+ // The flow's last page event should be the original Loading event before it
+ // was closed by the invalid result handler
+ assertThat(state.newEvents()).containsExactly(
+ LoadStateUpdate<Int>(
+ loadType = REFRESH,
+ fromMediator = false,
+ loadState = Loading
+ ),
+ )
+ // make sure no more new events are sent to UI
+ assertThat(state.newEvents()).isEmpty()
+ assertTrue(pagingSource.invalid)
+ assertTrue { !job.isActive }
+ }
+ }
+
+ @Test
+ fun append_cancelsCollectionFromLoadResultInvalid() = testScope.runBlockingTest {
+ val pagingSource = TestPagingSource()
+ val pager = PageFetcherSnapshot(50, pagingSource, config, retryFlow = retryBus.flow)
+
+ collectSnapshotData(pager) { state, job ->
+
+ advanceUntilIdle()
+
+ assertThat(state.newEvents()).containsExactly(
+ LoadStateUpdate<Int>(REFRESH, false, Loading),
+ createRefresh(50..51)
+ )
+ // append a page
+ pager.accessHint(
+ ViewportHint.Access(
+ pageOffset = 0,
+ indexInPage = 1,
+ presentedItemsBefore = 1,
+ presentedItemsAfter = 0,
+ originalPageOffsetFirst = 0,
+ originalPageOffsetLast = 0
+ )
+ )
+ // now return LoadResult.Invalid
+ pagingSource.nextLoadResult = LoadResult.Invalid()
+
+ advanceUntilIdle()
+
+ // Only a LoadStateUpdate for Append with loading status should be sent and it should
+ // not complete
+ assertThat(state.newEvents()).containsExactly(
+ LoadStateUpdate<Int>(APPEND, false, Loading),
+ )
+ assertTrue(pagingSource.invalid)
+ assertThat(state.newEvents()).isEmpty()
+ assertThat(!job.isActive)
+ }
+ }
+
+ @Test
+ fun prepend_cancelsCollectionFromLoadResultInvalid() = testScope.runBlockingTest {
+ val pagingSource = TestPagingSource()
+ val pager = PageFetcherSnapshot(50, pagingSource, config, retryFlow = retryBus.flow)
+
+ collectSnapshotData(pager) { state, job ->
+
+ advanceUntilIdle()
+
+ assertThat(state.newEvents()).containsExactly(
+ LoadStateUpdate<Int>(REFRESH, false, Loading),
+ createRefresh(50..51)
+ )
+ // now prepend
+ pager.accessHint(
+ ViewportHint.Access(
+ pageOffset = 0,
+ indexInPage = -1,
+ presentedItemsBefore = 0,
+ presentedItemsAfter = 1,
+ originalPageOffsetFirst = 0,
+ originalPageOffsetLast = 0
+ )
+ )
+ // now return LoadResult.Invalid.
+ pagingSource.nextLoadResult = LoadResult.Invalid()
+
+ advanceUntilIdle()
+
+ // Only a LoadStateUpdate for Prepend with loading status should be sent and it should
+ // not complete
+ assertThat(state.newEvents()).containsExactly(
+ LoadStateUpdate<Int>(PREPEND, false, Loading),
+ )
+ assertTrue(pagingSource.invalid)
+ assertThat(state.newEvents()).isEmpty()
+ assertThat(!job.isActive)
+ }
+ }
+
internal class CollectedPageEvents<T : Any>(val pageEvents: ArrayList<PageEvent<T>>) {
var lastIndex = 0
fun newEvents(): List<PageEvent<T>> = when {
diff --git a/paging/common/src/test/kotlin/androidx/paging/PageFetcherTest.kt b/paging/common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
index b095750..97258f1 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
@@ -19,7 +19,9 @@
import androidx.paging.LoadState.Loading
import androidx.paging.LoadType.APPEND
import androidx.paging.LoadType.REFRESH
+import androidx.paging.LoadType.PREPEND
import androidx.paging.PageEvent.LoadStateUpdate
+import androidx.paging.PagingSource.LoadResult
import androidx.paging.RemoteMediator.InitializeAction.LAUNCH_INITIAL_REFRESH
import androidx.paging.RemoteMediator.InitializeAction.SKIP_INITIAL_REFRESH
import com.google.common.truth.Truth.assertThat
@@ -44,6 +46,7 @@
import kotlin.test.assertFailsWith
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
+import java.util.ArrayList
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(JUnit4::class)
@@ -195,6 +198,153 @@
}
@Test
+ fun refresh_invalidatePropagatesThroughLoadResultInvalid() =
+ testScope.runBlockingTest {
+ val pagingSources = mutableListOf<TestPagingSource>()
+ val pageFetcher = PageFetcher(
+ pagingSourceFactory = {
+ TestPagingSource().also {
+ // make this initial load return LoadResult.Invalid to see if new paging
+ // source is generated
+ if (pagingSources.size == 0) it.nextLoadResult = LoadResult.Invalid()
+ pagingSources.add(it)
+ }
+ },
+ initialKey = 50,
+ config = config,
+ )
+
+ val fetcherState = collectFetcherState(pageFetcher)
+ advanceUntilIdle()
+
+ // should have two PagingData returned, one for each paging source
+ assertThat(fetcherState.pagingDataList.size).isEqualTo(2)
+
+ // First PagingData only returns a loading state because invalidation prevents load
+ // completion
+ assertTrue(pagingSources[0].invalid)
+ assertThat(fetcherState.pageEventLists[0]).containsExactly(
+ LoadStateUpdate<Int>(REFRESH, false, Loading)
+ )
+ // previous load() returning LoadResult.Invalid should trigger a new paging source
+ // retrying with the same load params, this should return a refresh starting
+ // from key = 50
+ assertTrue(!pagingSources[1].invalid)
+ assertThat(fetcherState.pageEventLists[1]).containsExactly(
+ LoadStateUpdate<Int>(REFRESH, false, Loading),
+ createRefresh(50..51)
+ )
+
+ assertThat(pagingSources[0]).isNotEqualTo(pagingSources[1])
+ fetcherState.job.cancel()
+ }
+
+ @Test
+ fun append_invalidatePropagatesThroughLoadResultInvalid() =
+ testScope.runBlockingTest {
+ val pagingSources = mutableListOf<TestPagingSource>()
+ val pageFetcher = PageFetcher(
+ pagingSourceFactory = { TestPagingSource().also { pagingSources.add(it) } },
+ initialKey = 50,
+ config = config,
+ )
+ val fetcherState = collectFetcherState(pageFetcher)
+ advanceUntilIdle()
+
+ assertThat(fetcherState.pageEventLists.size).isEqualTo(1)
+ assertThat(fetcherState.newEvents()).containsExactly(
+ LoadStateUpdate<Int>(REFRESH, false, Loading),
+ createRefresh(50..51),
+ )
+
+ // append a page
+ fetcherState.pagingDataList[0].receiver.accessHint(
+ ViewportHint.Access(
+ pageOffset = 0,
+ indexInPage = 1,
+ presentedItemsBefore = 1,
+ presentedItemsAfter = 0,
+ originalPageOffsetFirst = 0,
+ originalPageOffsetLast = 0
+ )
+ )
+ // now return LoadResult.Invalid
+ pagingSources[0].nextLoadResult = LoadResult.Invalid()
+
+ advanceUntilIdle()
+
+ // make sure the append load never completes
+ assertThat(fetcherState.pageEventLists[0].last()).isEqualTo(
+ LoadStateUpdate<Int>(APPEND, false, Loading)
+ )
+
+ // the invalid result handler should exit the append load loop gracefully and allow
+ // fetcher to generate a new paging source
+ assertThat(pagingSources.size).isEqualTo(2)
+ assertTrue(pagingSources[0].invalid)
+
+ // second generation should load refresh with cached append load params
+ assertThat(fetcherState.newEvents()).containsExactly(
+ LoadStateUpdate<Int>(REFRESH, false, Loading),
+ createRefresh(51..52)
+ )
+
+ fetcherState.job.cancel()
+ }
+
+ @Test
+ fun prepend_invalidatePropagatesThroughLoadResultInvalid() =
+ testScope.runBlockingTest {
+ val pagingSources = mutableListOf<TestPagingSource>()
+ val pageFetcher = PageFetcher(
+ pagingSourceFactory = { TestPagingSource().also { pagingSources.add(it) } },
+ initialKey = 50,
+ config = config,
+ )
+ val fetcherState = collectFetcherState(pageFetcher)
+ advanceUntilIdle()
+
+ assertThat(fetcherState.pageEventLists.size).isEqualTo(1)
+ assertThat(fetcherState.newEvents()).containsExactly(
+ LoadStateUpdate<Int>(REFRESH, false, Loading),
+ createRefresh(50..51),
+ )
+ // prepend a page
+ fetcherState.pagingDataList[0].receiver.accessHint(
+ ViewportHint.Access(
+ pageOffset = 0,
+ indexInPage = -1,
+ presentedItemsBefore = 0,
+ presentedItemsAfter = 1,
+ originalPageOffsetFirst = 0,
+ originalPageOffsetLast = 0
+ )
+ )
+ // now return LoadResult.Invalid
+ pagingSources[0].nextLoadResult = LoadResult.Invalid()
+
+ advanceUntilIdle()
+
+ // make sure the prepend load never completes
+ assertThat(fetcherState.pageEventLists[0].last()).isEqualTo(
+ LoadStateUpdate<Int>(PREPEND, false, Loading)
+ )
+
+ // the invalid result should exit the prepend load loop gracefully and allow fetcher to
+ // generate a new paging source
+ assertThat(pagingSources.size).isEqualTo(2)
+ assertTrue(pagingSources[0].invalid)
+
+ // second generation should load refresh with cached prepend load params
+ assertThat(fetcherState.newEvents()).containsExactly(
+ LoadStateUpdate<Int>(REFRESH, false, Loading),
+ createRefresh(49..50)
+ )
+
+ fetcherState.job.cancel()
+ }
+
+ @Test
fun refresh_closesCollection() = testScope.runBlockingTest {
val pageFetcher = PageFetcher(pagingSourceFactory, 50, config)
diff --git a/paging/common/src/test/kotlin/androidx/paging/PagedListTest.kt b/paging/common/src/test/kotlin/androidx/paging/PagedListTest.kt
index 7e81475..3d7479b 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PagedListTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PagedListTest.kt
@@ -20,6 +20,7 @@
import androidx.paging.LoadType.REFRESH
import androidx.testutils.TestDispatcher
import androidx.testutils.TestExecutor
+import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
@@ -32,6 +33,7 @@
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
+import kotlin.test.fail
@RunWith(JUnit4::class)
class PagedListTest {
@@ -132,6 +134,42 @@
}
@Test
+ fun createNoInitialPageInvalidResult() {
+ runBlocking {
+ val pagingSource = object : PagingSource<Int, String>() {
+ override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
+ return LoadResult.Invalid()
+ }
+
+ override fun getRefreshKey(state: PagingState<Int, String>): Int? {
+ fail("should not reach here")
+ }
+ }
+
+ val expectedException = assertFailsWith<IllegalStateException> {
+ @Suppress("DEPRECATION")
+ PagedList.create(
+ pagingSource,
+ initialPage = null,
+ testCoroutineScope,
+ Dispatchers.Default,
+ Dispatchers.IO,
+ boundaryCallback = null,
+ Config(10),
+ key = 0
+ )
+ }
+ assertThat(expectedException.message).isEqualTo(
+ "Failed to create PagedList. The provided PagingSource returned " +
+ "LoadResult.Invalid, but a LoadResult.Page was expected. To use a " +
+ "PagingSource which supports invalidation, use a PagedList builder that " +
+ "accepts a factory method for PagingSource or DataSource.Factory, such as " +
+ "LivePagedList."
+ )
+ }
+ }
+
+ @Test
fun defaults() = runBlocking {
val initialPage = pagingSource.load(
PagingSource.LoadParams.Refresh(
diff --git a/paging/common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt b/paging/common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
index fa7152d..aaa1ee0 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
@@ -26,11 +26,13 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.consumeAsFlow
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -42,6 +44,7 @@
import org.junit.runners.JUnit4
import kotlin.coroutines.ContinuationInterceptor
import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
@@ -758,6 +761,57 @@
job.cancel()
}
}
+
+ @Test
+ fun uncaughtException() = testScope.runBlockingTest {
+ val differ = SimpleDiffer(dummyDifferCallback)
+ val pager = Pager(
+ PagingConfig(1),
+ ) {
+ object : PagingSource<Int, Int>() {
+ override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Int> {
+ throw IllegalStateException()
+ }
+
+ override fun getRefreshKey(state: PagingState<Int, Int>): Int? = null
+ }
+ }
+
+ val pagingData = pager.flow.first()
+ val deferred = testScope.async {
+ differ.collectFrom(pagingData)
+ }
+
+ advanceUntilIdle()
+ assertFailsWith<IllegalStateException> { deferred.await() }
+ }
+
+ @Test
+ fun handledLoadResultInvalid() = testScope.runBlockingTest {
+ val differ = SimpleDiffer(dummyDifferCallback)
+ var generation = 0
+ val pager = Pager(
+ PagingConfig(1),
+ ) {
+ TestPagingSource().also {
+ if (generation == 0) {
+ it.nextLoadResult = PagingSource.LoadResult.Invalid()
+ }
+ generation++
+ }
+ }
+
+ val pagingData = pager.flow.first()
+ val deferred = testScope.async {
+ // only returns if flow is closed, or work canclled, or exception thrown
+ // in this case it should cancel due LoadResult.Invalid causing collectFrom to return
+ differ.collectFrom(pagingData)
+ }
+
+ advanceUntilIdle()
+ // this will return only if differ.collectFrom returns
+ deferred.await()
+ }
}
private fun infinitelySuspendingPagingData(receiver: UiReceiver = dummyReceiver) = PagingData(
diff --git a/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
index 6f90f6b..c3fc14b 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
@@ -58,12 +58,15 @@
assertEquals(ITEMS_BY_NAME_ID.subList(45, 55), result.data)
assertEquals(45, result.itemsAfter)
+ val errorParams = LoadParams.Refresh(key, 10, false)
// Verify error is propagated correctly.
pagingSource.enqueueError()
- val errorParams = LoadParams.Refresh(key, 10, false)
assertFailsWith<CustomException> {
pagingSource.load(errorParams)
}
+ // Verify LoadResult.Invalid is returned
+ pagingSource.invalidateLoad()
+ assertTrue(pagingSource.load(errorParams) is LoadResult.Invalid)
}
}
@@ -197,12 +200,15 @@
assertEquals(ITEMS_BY_NAME_ID.subList(0, 5), observed)
+ val errorParams = LoadParams.Prepend(key, 5, false)
// Verify error is propagated correctly.
dataSource.enqueueError()
assertFailsWith<CustomException> {
- val errorParams = LoadParams.Prepend(key, 5, false)
dataSource.load(errorParams)
}
+ // Verify LoadResult.Invalid is returned
+ dataSource.invalidateLoad()
+ assertTrue(dataSource.load(errorParams) is LoadResult.Invalid)
}
}
@@ -217,12 +223,15 @@
assertEquals(ITEMS_BY_NAME_ID.subList(6, 11), observed)
+ val errorParams = LoadParams.Append(key, 5, false)
// Verify error is propagated correctly.
dataSource.enqueueError()
assertFailsWith<CustomException> {
- val errorParams = LoadParams.Append(key, 5, false)
dataSource.load(errorParams)
}
+ // Verify LoadResult.Invalid is returned
+ dataSource.invalidateLoad()
+ assertTrue(dataSource.load(errorParams) is LoadResult.Invalid)
}
}
@@ -256,6 +265,8 @@
private var error = false
+ private var invalidLoadResult = false
+
override fun getRefreshKey(state: PagingState<Key, Item>): Key? {
return state.anchorPosition
?.let { anchorPosition -> state.closestItemToPosition(anchorPosition) }
@@ -274,6 +285,9 @@
if (error) {
error = false
throw EXCEPTION
+ } else if (invalidLoadResult) {
+ invalidLoadResult = false
+ return LoadResult.Invalid()
}
val key = params.key ?: Key("", Int.MAX_VALUE)
@@ -292,6 +306,9 @@
if (error) {
error = false
throw EXCEPTION
+ } else if (invalidLoadResult) {
+ invalidLoadResult = false
+ return LoadResult.Invalid()
}
val start = findFirstIndexAfter(params.key!!)
@@ -304,6 +321,9 @@
if (error) {
error = false
throw EXCEPTION
+ } else if (invalidLoadResult) {
+ invalidLoadResult = false
+ return LoadResult.Invalid()
}
val firstIndexBefore = findFirstIndexBefore(params.key!!)
@@ -327,6 +347,10 @@
fun enqueueError() {
error = true
}
+
+ fun invalidateLoad() {
+ invalidLoadResult = true
+ }
}
class CustomException : Exception()
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RemoteMediator.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RemoteMediator.kt
index 761a3d8..fa3204e 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RemoteMediator.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RemoteMediator.kt
@@ -30,8 +30,17 @@
@OptIn(ExperimentalPagingApi::class)
internal class V3RemoteMediator(
private val database: SampleDatabase,
- private val networkSource: NetworkCustomerPagingSource
+ private val networkSourceFactory: () -> NetworkCustomerPagingSource
) : RemoteMediator<Int, Customer>() {
+
+ private var networkSource: NetworkCustomerPagingSource
+ private val callBack = { newNetworkSource() }
+
+ init {
+ networkSource = networkSourceFactory.invoke()
+ networkSource.registerInvalidatedCallback(callBack)
+ }
+
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Customer>
@@ -73,6 +82,18 @@
is PagingSource.LoadResult.Error -> {
MediatorResult.Error(result.throwable)
}
+ is PagingSource.LoadResult.Invalid -> {
+ networkSource.invalidate()
+ load(loadType, state)
+ }
}
}
+
+ private fun newNetworkSource() {
+ val newNetworkSource = networkSourceFactory.invoke()
+ newNetworkSource.registerInvalidatedCallback { callBack }
+ networkSource.unregisterInvalidatedCallback { callBack }
+
+ networkSource = newNetworkSource
+ }
}
\ No newline at end of file
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomViewModel.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomViewModel.kt
index 8f927e0..9a3718d 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomViewModel.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomViewModel.kt
@@ -65,7 +65,7 @@
PagingConfig(10),
remoteMediator = V3RemoteMediator(
database,
- NetworkCustomerPagingSource.FACTORY()
+ NetworkCustomerPagingSource.FACTORY
)
) {
database.customerDao.loadPagedAgeOrderPagingSource()
diff --git a/paging/paging-compose/api/current.txt b/paging/paging-compose/api/current.txt
index 5859668..99d803e 100644
--- a/paging/paging-compose/api/current.txt
+++ b/paging/paging-compose/api/current.txt
@@ -16,8 +16,8 @@
public final class LazyPagingItemsKt {
method @androidx.compose.runtime.Composable public static <T> androidx.paging.compose.LazyPagingItems<T> collectAsLazyPagingItems(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<T>>);
- method public static <T> void items(androidx.compose.foundation.lazy.LazyListScope, androidx.paging.compose.LazyPagingItems<T> lazyPagingItems, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
- method public static <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyListScope, androidx.paging.compose.LazyPagingItems<T> lazyPagingItems, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method public static <T> void items(androidx.compose.foundation.lazy.LazyListScope, androidx.paging.compose.LazyPagingItems<T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+ method public static <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyListScope, androidx.paging.compose.LazyPagingItems<T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
}
}
diff --git a/paging/paging-compose/api/public_plus_experimental_current.txt b/paging/paging-compose/api/public_plus_experimental_current.txt
index 5859668..99d803e 100644
--- a/paging/paging-compose/api/public_plus_experimental_current.txt
+++ b/paging/paging-compose/api/public_plus_experimental_current.txt
@@ -16,8 +16,8 @@
public final class LazyPagingItemsKt {
method @androidx.compose.runtime.Composable public static <T> androidx.paging.compose.LazyPagingItems<T> collectAsLazyPagingItems(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<T>>);
- method public static <T> void items(androidx.compose.foundation.lazy.LazyListScope, androidx.paging.compose.LazyPagingItems<T> lazyPagingItems, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
- method public static <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyListScope, androidx.paging.compose.LazyPagingItems<T> lazyPagingItems, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method public static <T> void items(androidx.compose.foundation.lazy.LazyListScope, androidx.paging.compose.LazyPagingItems<T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+ method public static <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyListScope, androidx.paging.compose.LazyPagingItems<T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
}
}
diff --git a/paging/paging-compose/api/restricted_current.txt b/paging/paging-compose/api/restricted_current.txt
index 5859668..99d803e 100644
--- a/paging/paging-compose/api/restricted_current.txt
+++ b/paging/paging-compose/api/restricted_current.txt
@@ -16,8 +16,8 @@
public final class LazyPagingItemsKt {
method @androidx.compose.runtime.Composable public static <T> androidx.paging.compose.LazyPagingItems<T> collectAsLazyPagingItems(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<T>>);
- method public static <T> void items(androidx.compose.foundation.lazy.LazyListScope, androidx.paging.compose.LazyPagingItems<T> lazyPagingItems, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
- method public static <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyListScope, androidx.paging.compose.LazyPagingItems<T> lazyPagingItems, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method public static <T> void items(androidx.compose.foundation.lazy.LazyListScope, androidx.paging.compose.LazyPagingItems<T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+ method public static <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyListScope, androidx.paging.compose.LazyPagingItems<T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
}
}
diff --git a/paging/paging-compose/integration-tests/paging-demos/src/main/java/androidx/paging/compose/demos/room/PagingRoomSample.kt b/paging/paging-compose/integration-tests/paging-demos/src/main/java/androidx/paging/compose/demos/room/PagingRoomSample.kt
index 780a5ac..27ebfb1 100644
--- a/paging/paging-compose/integration-tests/paging-demos/src/main/java/androidx/paging/compose/demos/room/PagingRoomSample.kt
+++ b/paging/paging-compose/integration-tests/paging-demos/src/main/java/androidx/paging/compose/demos/room/PagingRoomSample.kt
@@ -16,13 +16,19 @@
package androidx.paging.compose.demos.room
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.sp
import androidx.paging.Pager
@@ -92,9 +98,10 @@
scope.launch(Dispatchers.IO) {
val randomUser = dao.getRandomUser()
if (randomUser != null) {
+ val newName = Names[Random.nextInt(Names.size)]
val updatedUser = User(
randomUser.id,
- randomUser.name + " updated"
+ newName
)
dao.update(updatedUser)
}
@@ -106,8 +113,16 @@
val lazyPagingItems = pager.flow.collectAsLazyPagingItems()
LazyColumn {
- itemsIndexed(lazyPagingItems) { index, user ->
- Text("$index " + user?.name, fontSize = 50.sp)
+ itemsIndexed(
+ items = lazyPagingItems,
+ key = { _, user -> user.id }
+ ) { index, user ->
+ var counter by rememberSaveable { mutableStateOf(0) }
+ Text(
+ text = "counter=$counter index=$index ${user?.name} ${user?.id}",
+ fontSize = 50.sp,
+ modifier = Modifier.clickable { counter++ }
+ )
}
}
}
diff --git a/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt b/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt
index 17d2e46..18af614 100644
--- a/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt
+++ b/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt
@@ -21,13 +21,16 @@
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.unit.dp
import androidx.paging.Pager
import androidx.paging.PagingConfig
@@ -497,4 +500,69 @@
rule.onNodeWithTag("1")
.assertDoesNotExist()
}
+
+ @Test
+ fun stateIsMovedWithItemWithCustomKey_items() {
+ val items = mutableListOf(1)
+ val pager = createPager {
+ TestPagingSource(items = items, loadDelay = 0)
+ }
+
+ lateinit var lazyPagingItems: LazyPagingItems<Int>
+ var counter = 0
+ rule.setContent {
+ lazyPagingItems = pager.flow.collectAsLazyPagingItems()
+ LazyColumn {
+ items(lazyPagingItems, key = { it }) {
+ BasicText(
+ "Item=$it. counter=${remember { counter++ }}"
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ items.clear()
+ items.addAll(listOf(0, 1))
+ lazyPagingItems.refresh()
+ }
+
+ rule.onNodeWithText("Item=0. counter=1")
+ .assertExists()
+
+ rule.onNodeWithText("Item=1. counter=0")
+ .assertExists()
+ }
+
+ @Test
+ fun stateIsMovedWithItemWithCustomKey_itemsIndexed() {
+ val items = mutableListOf(1)
+ val pager = createPager {
+ TestPagingSource(items = items, loadDelay = 0)
+ }
+
+ lateinit var lazyPagingItems: LazyPagingItems<Int>
+ rule.setContent {
+ lazyPagingItems = pager.flow.collectAsLazyPagingItems()
+ LazyColumn {
+ itemsIndexed(lazyPagingItems, key = { _, item -> item }) { index, item ->
+ BasicText(
+ "Item=$item. index=$index. remembered index=${remember { index }}"
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ items.clear()
+ items.addAll(listOf(0, 1))
+ lazyPagingItems.refresh()
+ }
+
+ rule.onNodeWithText("Item=0. index=0. remembered index=0")
+ .assertExists()
+
+ rule.onNodeWithText("Item=1. index=1. remembered index=0")
+ .assertExists()
+ }
}
diff --git a/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt b/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
index b53790f..ae13eab 100644
--- a/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
+++ b/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
@@ -17,6 +17,8 @@
package androidx.paging.compose
import android.annotation.SuppressLint
+import android.os.Parcel
+import android.os.Parcelable
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Composable
@@ -253,17 +255,34 @@
*
* @sample androidx.paging.compose.samples.ItemsDemo
*
- * @param lazyPagingItems the items received from a [Flow] of [PagingData].
+ * @param items the items received from a [Flow] of [PagingData].
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
* @param itemContent the content displayed by a single item. In case the item is `null`, the
* [itemContent] method should handle the logic of displaying a placeholder instead of the main
* content displayed by an item which is not `null`.
*/
public fun <T : Any> LazyListScope.items(
- lazyPagingItems: LazyPagingItems<T>,
+ items: LazyPagingItems<T>,
+ key: ((item: T) -> Any)? = null,
itemContent: @Composable LazyItemScope.(value: T?) -> Unit
) {
- items(lazyPagingItems.itemCount) { index ->
- itemContent(lazyPagingItems[index])
+ items(
+ count = items.itemCount,
+ key = if (key == null) null else { index ->
+ val item = items.peek(index)
+ if (item == null) {
+ PagingPlaceholderKey(index)
+ } else {
+ key(item)
+ }
+ }
+ ) { index ->
+ itemContent(items[index])
}
}
@@ -275,16 +294,56 @@
*
* @sample androidx.paging.compose.samples.ItemsIndexedDemo
*
- * @param lazyPagingItems the items received from a [Flow] of [PagingData].
+ * @param items the items received from a [Flow] of [PagingData].
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
* @param itemContent the content displayed by a single item. In case the item is `null`, the
* [itemContent] method should handle the logic of displaying a placeholder instead of the main
* content displayed by an item which is not `null`.
*/
public fun <T : Any> LazyListScope.itemsIndexed(
- lazyPagingItems: LazyPagingItems<T>,
+ items: LazyPagingItems<T>,
+ key: ((index: Int, item: T) -> Any)? = null,
itemContent: @Composable LazyItemScope.(index: Int, value: T?) -> Unit
) {
- items(lazyPagingItems.itemCount) { index ->
- itemContent(index, lazyPagingItems[index])
+ items(
+ count = items.itemCount,
+ key = if (key == null) null else { index ->
+ val item = items.peek(index)
+ if (item == null) {
+ PagingPlaceholderKey(index)
+ } else {
+ key(index, item)
+ }
+ }
+ ) { index ->
+ itemContent(index, items[index])
+ }
+}
+
+@SuppressLint("BanParcelableUsage")
+private data class PagingPlaceholderKey(private val index: Int) : Parcelable {
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.writeInt(index)
+ }
+
+ override fun describeContents(): Int {
+ return 0
+ }
+
+ companion object {
+ @Suppress("unused")
+ @JvmField
+ val CREATOR: Parcelable.Creator<PagingPlaceholderKey> =
+ object : Parcelable.Creator<PagingPlaceholderKey> {
+ override fun createFromParcel(parcel: Parcel) =
+ PagingPlaceholderKey(parcel.readInt())
+
+ override fun newArray(size: Int) = arrayOfNulls<PagingPlaceholderKey?>(size)
+ }
}
}
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/Encoding.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/Encoding.java
index 84ff365..c2d5a35 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/Encoding.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/Encoding.java
@@ -118,41 +118,49 @@
) throws IOException {
// Read the expected compressed data size.
Inflater inf = new Inflater();
- byte[] result = new byte[uncompressedDataSize];
- int totalBytesRead = 0;
- int totalBytesInflated = 0;
- byte[] input = new byte[2048]; // 2KB read window size;
- while (!inf.finished() && !inf.needsDictionary() && totalBytesRead < compressedDataSize) {
- int bytesRead = is.read(input);
- if (bytesRead < 0) {
+ try {
+ byte[] result = new byte[uncompressedDataSize];
+ int totalBytesRead = 0;
+ int totalBytesInflated = 0;
+ byte[] input = new byte[2048]; // 2KB read window size;
+ while (
+ !inf.finished() &&
+ !inf.needsDictionary() &&
+ totalBytesRead < compressedDataSize
+ ) {
+ int bytesRead = is.read(input);
+ if (bytesRead < 0) {
+ throw error(
+ "Invalid zip data. Stream ended after $totalBytesRead bytes. " +
+ "Expected " + compressedDataSize + " bytes"
+ );
+ }
+ inf.setInput(input, 0, bytesRead);
+ try {
+ totalBytesInflated += inf.inflate(
+ result,
+ totalBytesInflated,
+ uncompressedDataSize - totalBytesInflated
+ );
+ } catch (DataFormatException e) {
+ throw error(e.getMessage());
+ }
+ totalBytesRead += bytesRead;
+ }
+ if (totalBytesRead != compressedDataSize) {
throw error(
- "Invalid zip data. Stream ended after $totalBytesRead bytes. " +
- "Expected " + compressedDataSize + " bytes"
+ "Didn't read enough bytes during decompression." +
+ " expected=" + compressedDataSize +
+ " actual=" + totalBytesRead
);
}
- inf.setInput(input, 0, bytesRead);
- try {
- totalBytesInflated += inf.inflate(
- result,
- totalBytesInflated,
- uncompressedDataSize - totalBytesInflated
- );
- } catch (DataFormatException e) {
- throw error(e.getMessage());
+ if (!inf.finished()) {
+ throw error("Inflater did not finish");
}
- totalBytesRead += bytesRead;
+ return result;
+ } finally {
+ inf.end();
}
- if (totalBytesRead != compressedDataSize) {
- throw error(
- "Didn't read enough bytes during decompression." +
- " expected=" + compressedDataSize +
- " actual=" + totalBytesRead
- );
- }
- if (!inf.finished()) {
- throw error("Inflater did not finish");
- }
- return result;
}
static void writeAll(@NonNull InputStream is, @NonNull OutputStream os) throws IOException {
diff --git a/room/room-compiler-processing-testing/build.gradle b/room/room-compiler-processing-testing/build.gradle
index 6494d0c..0e8ddae 100644
--- a/room/room-compiler-processing-testing/build.gradle
+++ b/room/room-compiler-processing-testing/build.gradle
@@ -24,7 +24,6 @@
}
dependencies {
- implementation("androidx.annotation:annotation:1.1.0")
api(project(":room:room-compiler-processing"))
implementation(libs.kotlinStdlib)
implementation(libs.kspApi)
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticJavacProcessor.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticJavacProcessor.kt
index 95ede95..8613a86 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticJavacProcessor.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticJavacProcessor.kt
@@ -17,24 +17,35 @@
package androidx.room.compiler.processing
import androidx.room.compiler.processing.util.XTestInvocation
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
+import javax.lang.model.element.TypeElement
@Suppress("VisibleForTests")
@ExperimentalProcessingApi
class SyntheticJavacProcessor private constructor(
private val impl: SyntheticProcessorImpl
-) : JavacTestProcessor(), SyntheticProcessor by impl {
+) : AbstractProcessor(), SyntheticProcessor by impl {
constructor(handlers: List<(XTestInvocation) -> Unit>) : this(
SyntheticProcessorImpl(handlers)
)
- override fun doProcess(annotations: Set<XTypeElement>, roundEnv: XRoundEnv): Boolean {
+
+ override fun process(
+ annotations: MutableSet<out TypeElement>,
+ roundEnv: RoundEnvironment
+ ): Boolean {
+ if (roundEnv.processingOver()) {
+ return true
+ }
if (!impl.canRunAnotherRound()) {
return true
}
val xEnv = XProcessingEnv.create(processingEnv)
+ val xRoundEnv = XRoundEnv.create(xEnv, roundEnv)
val testInvocation = XTestInvocation(
processingEnv = xEnv,
- roundEnv = roundEnv
+ roundEnv = xRoundEnv
)
impl.runNextRound(testInvocation)
return impl.expectsAnotherRound()
diff --git a/room/room-compiler-processing/build.gradle b/room/room-compiler-processing/build.gradle
index f1c4286..d1ae9643 100644
--- a/room/room-compiler-processing/build.gradle
+++ b/room/room-compiler-processing/build.gradle
@@ -27,7 +27,6 @@
api(libs.kotlinStdlib)
api(libs.javapoet)
api(libs.kotlinPoet)
- implementation("androidx.annotation:annotation:1.1.0")
implementation(libs.guava)
implementation(libs.autoCommon)
implementation(libs.autoValueAnnotations)
@@ -37,6 +36,7 @@
implementation(libs.kspApi)
implementation(libs.kotlinStdlibJdk8) // KSP defines older version as dependency, force update.
+ testImplementation("androidx.annotation:annotation:1.1.0")
testImplementation(libs.googleCompileTesting)
testImplementation(libs.junit)
testImplementation(libs.jsr250)
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
index b850d16..64d89eb 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
@@ -15,7 +15,6 @@
*/
package androidx.room.compiler.processing
-import androidx.room.compiler.processing.javac.JavacExecutableElement
import androidx.room.compiler.processing.ksp.KspMethodElement
import androidx.room.compiler.processing.ksp.KspMethodType
import com.squareup.javapoet.ClassName
@@ -139,11 +138,8 @@
}
addAnnotation(Override::class.java)
varargs(executableElement.isVarArgs())
- if (executableElement is JavacExecutableElement) {
- // copy throws for java
- executableElement.element.thrownTypes.forEach {
- addException(TypeName.get(it))
- }
+ executableElement.thrownTypes.forEach {
+ addException(it.typeName)
}
returns(resolvedType.returnType.typeName)
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavacTestProcessor.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavacTestProcessor.kt
deleted file mode 100644
index aa9b92a..0000000
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/JavacTestProcessor.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.room.compiler.processing
-
-import androidx.annotation.VisibleForTesting
-import androidx.room.compiler.processing.javac.JavacProcessingEnv
-import androidx.room.compiler.processing.javac.JavacRoundEnv
-import javax.annotation.processing.AbstractProcessor
-import javax.annotation.processing.RoundEnvironment
-import javax.lang.model.element.TypeElement
-
-/**
- * Javac processor implementation that provides access to the round environment.
- *
- * This is only used in tests, the main processor uses an API similar to the processing step
- * in Auto Common.
- */
-@VisibleForTesting
-@ExperimentalProcessingApi
-abstract class JavacTestProcessor : AbstractProcessor() {
- val xProcessingEnv by lazy {
- // lazily create this as it is not available on construction time
- XProcessingEnv.create(super.processingEnv)
- }
-
- final override fun process(
- annotations: MutableSet<out TypeElement>,
- roundEnv: RoundEnvironment
- ): Boolean {
- if (roundEnv.processingOver()) {
- return true
- }
- val env = xProcessingEnv as JavacProcessingEnv
- val javacRoundEnv = JavacRoundEnv(env, roundEnv)
- val xAnnotations = annotations.mapTo(mutableSetOf()) {
- env.wrapTypeElement(it)
- }
- return doProcess(xAnnotations, javacRoundEnv)
- }
-
- abstract fun doProcess(
- annotations: Set<XTypeElement>,
- roundEnv: XRoundEnv
- ): Boolean
-}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XExecutableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XExecutableElement.kt
index 0bc4dd0..815f45e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XExecutableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XExecutableElement.kt
@@ -40,6 +40,11 @@
* @see [isVarArgs]
*/
val parameters: List<XExecutableParameterElement>
+
+ /**
+ * The list of `Throwable`s that are declared in this executable's signature.
+ */
+ val thrownTypes: List<XType>
/**
* Returns true if this method receives a vararg parameter.
*/
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
index 14e4b06..aed0202 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
@@ -21,19 +21,21 @@
import javax.lang.model.element.Element
private val NONNULL_ANNOTATIONS = arrayOf(
- androidx.annotation.NonNull::class.java,
- org.jetbrains.annotations.NotNull::class.java
+ "androidx.annotation.NonNull",
+ "org.jetbrains.annotations.NotNull"
)
private val NULLABLE_ANNOTATIONS = arrayOf(
- androidx.annotation.Nullable::class.java,
- org.jetbrains.annotations.Nullable::class.java
+ "androidx.annotation.Nullable",
+ "org.jetbrains.annotations.Nullable"
)
@Suppress("UnstableApiUsage")
-private fun Element.hasAnyOf(annotations: Array<Class<out Annotation>>) = annotations.any {
- MoreElements.isAnnotationPresent(this, it)
-}
+private fun Element.hasAnyOf(annotations: Array<String>) =
+ annotationMirrors.any { annotationMirror ->
+ val annotationTypeElement = MoreElements.asType(annotationMirror.annotationType.asElement())
+ annotations.any { annotationTypeElement.qualifiedName.contentEquals(it) }
+ }
internal val Element.nullability: XNullability
get() = if (asType().kind.isPrimitive || hasAnyOf(NONNULL_ANNOTATIONS)) {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacBasicAnnotationProcessor.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacBasicAnnotationProcessor.kt
index f8926ad..f41a368 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacBasicAnnotationProcessor.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacBasicAnnotationProcessor.kt
@@ -18,6 +18,7 @@
import androidx.room.compiler.processing.XBasicAnnotationProcessor
import androidx.room.compiler.processing.XElement
+import androidx.room.compiler.processing.XProcessingStep
import androidx.room.compiler.processing.XRoundEnv
import com.google.auto.common.BasicAnnotationProcessor
import com.google.common.collect.ImmutableSetMultimap
@@ -31,43 +32,45 @@
abstract class JavacBasicAnnotationProcessor :
BasicAnnotationProcessor(), XBasicAnnotationProcessor {
- final override fun steps(): Iterable<Step> {
- // Execute all processing steps in a single auto-common Step. This is done to share the
- // XProcessingEnv and its cached across steps in the same round.
- val steps = processingSteps()
- val parentStep = object : Step {
- override fun annotations() = steps.flatMap { it.annotations() }.toSet()
+ // This state is cached here so that it can be shared by all steps in a given processing round.
+ // The state is initialized at beginning of each round using the InitializingStep, and
+ // the state is cleared at the end of each round in BasicAnnotationProcessor#postRound()
+ private var cachedXEnv: JavacProcessingEnv? = null
- override fun process(
- elementsByAnnotation: ImmutableSetMultimap<String, Element>
- ): Set<Element> {
- val xEnv = JavacProcessingEnv(processingEnv)
- val convertedElementsByAnnotation = mutableMapOf<String, Set<XElement>>()
- annotations().forEach { annotation ->
- convertedElementsByAnnotation[annotation] =
- elementsByAnnotation[annotation].mapNotNull { element ->
- xEnv.wrapAnnotatedElement(element, annotation)
- }.toSet()
- }
- val results = steps.flatMap { step ->
- step.process(
- env = xEnv,
- elementsByAnnotation = step.annotations().associateWith {
- convertedElementsByAnnotation[it] ?: emptySet()
- }
- )
- }
- return results.map { (it as JavacElement).element }.toSet()
+ final override fun steps(): Iterable<Step> {
+ return processingSteps().map { DelegatingStep(it) }
+ }
+
+ /** A [Step] that delegates to an [XProcessingStep]. */
+ private inner class DelegatingStep(val xStep: XProcessingStep) : Step {
+ override fun annotations() = xStep.annotations()
+
+ override fun process(
+ elementsByAnnotation: ImmutableSetMultimap<String, Element>
+ ): Set<Element> {
+ // The first step in a round initializes the cachedXEnv. Note: the "first" step can
+ // change each round depending on which annotations are present in the current round and
+ // which elements were deferred in the previous round.
+ val xEnv = cachedXEnv ?: JavacProcessingEnv(processingEnv).also { cachedXEnv = it }
+ val xElementsByAnnotation = mutableMapOf<String, Set<XElement>>()
+ xStep.annotations().forEach { annotation ->
+ xElementsByAnnotation[annotation] =
+ elementsByAnnotation[annotation].mapNotNull { element ->
+ xEnv.wrapAnnotatedElement(element, annotation)
+ }.toSet()
}
+ return xStep.process(xEnv, xElementsByAnnotation).map {
+ (it as JavacElement).element
+ }.toSet()
}
- return listOf(parentStep)
}
final override fun postRound(roundEnv: RoundEnvironment) {
- // Due to BasicAnnotationProcessor taking over AbstractProcessor#process() we can't
- // share the same XProcessingEnv from the steps, but that might be ok...
- val xEnv = JavacProcessingEnv(processingEnv)
+ // The cachedXEnv can be null if none of the steps were processed in the round.
+ // In this case, we just create a new one since there is no cached one to share.
+ val xEnv = cachedXEnv ?: JavacProcessingEnv(processingEnv)
val xRound = XRoundEnv.create(xEnv, roundEnv)
postRound(xEnv, xRound)
+ cachedXEnv = null // Reset after every round to allow GC
}
}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt
index ea9c18a..ffca24b 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacExecutableElement.kt
@@ -18,6 +18,7 @@
import androidx.room.compiler.processing.XExecutableElement
import androidx.room.compiler.processing.XHasModifiers
+import androidx.room.compiler.processing.XNullability
import androidx.room.compiler.processing.javac.kotlin.KmExecutable
import androidx.room.compiler.processing.javac.kotlin.descriptor
import javax.lang.model.element.ExecutableElement
@@ -59,6 +60,16 @@
return element.isVarArgs
}
+ override val thrownTypes by lazy {
+ element.thrownTypes.map {
+ env.wrap<JavacType>(
+ typeMirror = it,
+ kotlinType = null,
+ elementNullability = XNullability.UNKNOWN
+ )
+ }
+ }
+
companion object {
internal const val DEFAULT_IMPLS_CLASS_NAME = "DefaultImpls"
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
index 2a150d9..68af580 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
@@ -24,6 +24,7 @@
import com.google.devtools.ksp.symbol.AnnotationUseSiteTarget
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSAnnotation
+import com.google.devtools.ksp.symbol.KSTypeAlias
import kotlin.reflect.KClass
@OptIn(KspExperimental::class)
@@ -90,7 +91,10 @@
ksAnnotation: KSAnnotation,
annotationClass: KClass<out Annotation>
): Boolean {
- val declaration = ksAnnotation.annotationType.resolve().declaration
+ var declaration = ksAnnotation.annotationType.resolve().declaration
+ while (declaration is KSTypeAlias) {
+ declaration = declaration.type.resolve().declaration
+ }
val qualifiedName = declaration.qualifiedName?.asString() ?: return false
return qualifiedName == annotationClass.qualifiedName
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspBasicAnnotationProcessor.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspBasicAnnotationProcessor.kt
index 1fc7b4d..43ea091 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspBasicAnnotationProcessor.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspBasicAnnotationProcessor.kt
@@ -46,14 +46,24 @@
val round = XRoundEnv.create(processingEnv)
val deferredElements = processingSteps().flatMap { step ->
val invalidElements = mutableSetOf<XElement>()
- val elementsByAnnotation = step.annotations().associateWith { annotation ->
+ val elementsByAnnotation = step.annotations().mapNotNull { annotation ->
val annotatedElements = round.getElementsAnnotatedWith(annotation)
- annotatedElements
+ val validElements = annotatedElements
.filter { (it as KspElement).declaration.validateExceptLocals() }
- .also { invalidElements.addAll(annotatedElements - it) }
.toSet()
+ invalidElements.addAll(annotatedElements - validElements)
+ if (validElements.isNotEmpty()) {
+ annotation to validElements
+ } else {
+ null
+ }
+ }.toMap()
+ // Only process the step if there are annotated elements found for this step.
+ if (elementsByAnnotation.isNotEmpty()) {
+ invalidElements + step.process(processingEnv, elementsByAnnotation)
+ } else {
+ invalidElements
}
- invalidElements + step.process(processingEnv, elementsByAnnotation)
}
postRound(processingEnv, round)
return deferredElements.map { (it as KspElement).declaration }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
index f2a52bb..5e620f7 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
@@ -20,8 +20,10 @@
import androidx.room.compiler.processing.XExecutableElement
import androidx.room.compiler.processing.XExecutableParameterElement
import androidx.room.compiler.processing.XHasModifiers
+import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE
import androidx.room.compiler.processing.util.ISSUE_TRACKER_LINK
+import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.isConstructor
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.Modifier
@@ -60,6 +62,16 @@
}
}
+ @OptIn(KspExperimental::class)
+ override val thrownTypes: List<XType> by lazy {
+ env.resolver.getJvmCheckedException(declaration).map {
+ env.wrap(
+ ksType = it,
+ allowPrimitives = false
+ )
+ }.toList()
+ }
+
override fun isVarArgs(): Boolean {
// in java, only the last argument can be a vararg so for suspend functions, it is never
// a vararg function. this would change if room generated kotlin code
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBox.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBox.kt
index 3bdd7ae..d07a36e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBox.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBox.kt
@@ -16,7 +16,6 @@
package androidx.room.compiler.processing.ksp
-import androidx.annotation.VisibleForTesting
import androidx.room.compiler.processing.XAnnotationBox
import androidx.room.compiler.processing.XType
@@ -25,7 +24,7 @@
* handles those cases.
* see: https://github.com/google/ksp/issues/53
*/
-internal class KspReflectiveAnnotationBox<T : Annotation> @VisibleForTesting constructor(
+internal class KspReflectiveAnnotationBox<T : Annotation> constructor(
private val env: KspProcessingEnv,
private val annotationClass: Class<T>,
private val annotation: T
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
index 4c0226b..d01cec9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
@@ -93,6 +93,16 @@
override val docComment: String?
get() = null
+ override val thrownTypes: List<XType>
+ get() {
+ // TODO replace with the Resolver method when it is available. This solution works only
+ // in sources.
+ // https://github.com/google/ksp/issues/505
+ return getAnnotation(Throws::class)
+ ?.getAsTypeList("exceptionClasses")
+ ?: emptyList()
+ }
+
final override fun asMemberOf(other: XType): XMethodType {
return KspSyntheticPropertyMethodType.create(
element = this,
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/JavacTestProcessorTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/JavacTestProcessorTest.kt
deleted file mode 100644
index 7b42e74..0000000
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/JavacTestProcessorTest.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.compiler.processing
-
-import androidx.room.compiler.processing.testcode.OtherAnnotation
-import androidx.room.compiler.processing.util.Source
-import androidx.room.compiler.processing.util.getField
-import androidx.room.compiler.processing.util.getMethod
-import com.google.common.truth.Truth.assertAbout
-import com.google.common.truth.Truth.assertThat
-import com.google.testing.compile.JavaSourcesSubjectFactory
-import org.junit.Test
-import java.util.concurrent.atomic.AtomicBoolean
-import kotlin.reflect.KClass
-
-class JavacTestProcessorTest {
-
- @Test
- fun getElementsAnnotatedWith() {
- val source = Source.java(
- "foo.bar.Baz",
- """
- package foo.bar;
- import androidx.room.compiler.processing.testcode.OtherAnnotation;
- @OtherAnnotation(value="xx")
- class Baz {
- @OtherAnnotation(value="xx")
- int myField = 0;
- @OtherAnnotation(value="xx")
- void myFunction() { }
- }
- """.trimIndent()
- )
- testProcessor(listOf(source), listOf(OtherAnnotation::class)) { roundEnv ->
- val annotatedElementsByClass = roundEnv.getElementsAnnotatedWith(
- OtherAnnotation::class
- )
-
- val annotatedElementsByName = roundEnv.getElementsAnnotatedWith(
- OtherAnnotation::class.qualifiedName!!
- )
-
- val targetElement = xProcessingEnv.requireTypeElement("foo.bar.Baz")
- assertThat(
- annotatedElementsByClass
- ).apply {
- containsExactlyElementsIn(annotatedElementsByName)
- hasSize(3)
- contains(targetElement)
- contains(targetElement.getMethod("myFunction"))
- contains(targetElement.getField("myField"))
- }
- }
- }
-
- private fun testProcessor(
- sources: List<Source>,
- annotations: List<KClass<out Annotation>>,
- doProcessTest: JavacTestProcessor.(XRoundEnv) -> Unit
- ) {
- val invoked = AtomicBoolean(false)
-
- val testProcessor = object : JavacTestProcessor() {
- override fun doProcess(annotations: Set<XTypeElement>, roundEnv: XRoundEnv): Boolean {
- invoked.set(true)
-
- doProcessTest(roundEnv)
-
- return true
- }
-
- override fun getSupportedAnnotationTypes(): Set<String> {
- return annotations.map { it.java.canonicalName }.toSet()
- }
- }
- assertAbout(
- JavaSourcesSubjectFactory.javaSources()
- ).that(
- sources.map { it.toJFO() }
- ).processedWith(
- testProcessor
- ).compilesWithoutError()
-
- assertThat(invoked.get()).isTrue()
- }
-}
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
index 2032454..f76ad64 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
@@ -442,8 +442,9 @@
) { invocation ->
val (target, methods) = invocation.getOverrideTestTargets(ignoreInheritedMethods)
methods.forEachIndexed { index, method ->
- if (invocation.isKsp && method.name == "throwsException") {
+ if (invocation.isKsp && method.name == "throwsException" && preCompiledCode) {
// TODO b/171572318
+ // https://github.com/google/ksp/issues/507
} else {
val subject = MethodSpecHelper.overridingWithFinalParams(
method,
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
index 6f0b907..05526ea 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationTest.kt
@@ -40,6 +40,8 @@
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
+// used in typealias test
+typealias OtherAnnotationTypeAlias = OtherAnnotation
@RunWith(Parameterized::class)
class XAnnotationTest(
private val preCompiled: Boolean
@@ -662,6 +664,33 @@
}
}
+ @Test
+ fun typealiasAnnotation() {
+ val source = Source.kotlin(
+ "Subject.kt",
+ """
+ typealias SourceTypeAlias = ${OtherAnnotation::class.qualifiedName}
+ @SourceTypeAlias("x")
+ class Subject {
+ }
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(source)
+ ) { invocation ->
+ // TODO use getSymbolsWithAnnotation after
+ // https://github.com/google/ksp/issues/506 is fixed
+ val subject = invocation.processingEnv.requireTypeElement("Subject")
+ val annotation = subject.getAnnotation(OtherAnnotation::class)
+ assertThat(annotation).isNotNull()
+ assertThat(annotation?.value?.value).isEqualTo("x")
+
+ val annotation2 = subject.getAnnotation(OtherAnnotationTypeAlias::class)
+ assertThat(annotation2).isNotNull()
+ assertThat(annotation2?.value?.value).isEqualTo("x")
+ }
+ }
+
// helper function to read what we need
private fun XAnnotated.getSuppressValues(): List<String>? {
return this.findAnnotation<TestSuppressWarnings>()
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
index 3f8a36a..78e2a11 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
@@ -20,6 +20,7 @@
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.UNIT_CLASS_NAME
import androidx.room.compiler.processing.util.className
+import androidx.room.compiler.processing.util.compileFiles
import androidx.room.compiler.processing.util.getDeclaredMethod
import androidx.room.compiler.processing.util.getMethod
import androidx.room.compiler.processing.util.getParameter
@@ -27,12 +28,16 @@
import androidx.room.compiler.processing.util.typeName
import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import com.squareup.javapoet.ClassName
import com.squareup.javapoet.ParameterizedTypeName
import com.squareup.javapoet.TypeName
import com.squareup.javapoet.WildcardTypeName
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import java.io.IOException
+import java.lang.IllegalStateException
@RunWith(JUnit4::class)
class XExecutableElementTest {
@@ -604,6 +609,122 @@
genericToPrimitiveOverrides(asMemberOf = true)
}
+ @Test
+ fun thrownTypes() {
+ fun buildSources(pkg: String) = listOf(
+ Source.java(
+ "$pkg.JavaSubject",
+ """
+ package $pkg;
+ import java.io.*;
+ public class JavaSubject {
+ public JavaSubject() throws IllegalArgumentException {}
+
+ public void multipleThrows() throws IOException, IllegalStateException {
+ }
+ }
+ """.trimIndent()
+ ),
+ Source.kotlin(
+ "KotlinSubject.kt",
+ """
+ package $pkg
+ import java.io.*
+ public class KotlinSubject {
+ @Throws(IllegalArgumentException::class)
+ constructor() {
+ }
+
+ @Throws(IOException::class, IllegalStateException::class)
+ fun multipleThrows() {
+ }
+ }
+ """.trimIndent()
+ ),
+ Source.kotlin(
+ "AccessorThrows.kt",
+ """
+ package $pkg
+ import java.io.*
+ public class KotlinAccessors {
+ @get:Throws(IllegalArgumentException::class)
+ val getterThrows: Int = 3
+ @set:Throws(IllegalStateException::class)
+ var setterThrows: Int = 3
+ @get:Throws(IOException::class)
+ @set:Throws(IllegalStateException::class, IllegalArgumentException::class)
+ var bothThrows: Int = 3
+ }
+ """.trimIndent()
+ )
+ )
+ runProcessorTest(
+ sources = buildSources("app"),
+ classpath = compileFiles(sources = buildSources("lib"))
+ ) { invocation ->
+ fun collectExceptions(subject: XTypeElement): List<Pair<String, Set<TypeName>>> {
+ return (subject.getConstructors() + subject.getDeclaredMethods()).mapNotNull {
+ val throwTypes = it.thrownTypes
+ val name = if (it is XMethodElement) {
+ it.name
+ } else {
+ "<init>"
+ }
+ if (throwTypes.isEmpty()) {
+ null
+ } else {
+ name to throwTypes.map { it.typeName }.toSet()
+ }
+ }
+ }
+ // TODO
+ // add lib here once https://github.com/google/ksp/issues/507 is fixed
+ listOf("app").forEach { pkg ->
+ invocation.processingEnv.requireTypeElement("$pkg.KotlinSubject").let { subject ->
+ assertWithMessage(subject.qualifiedName).that(
+ collectExceptions(subject)
+ ).containsExactly(
+ "<init>" to setOf(ClassName.get(IllegalArgumentException::class.java)),
+ "multipleThrows" to setOf(
+ ClassName.get(IOException::class.java),
+ ClassName.get(IllegalStateException::class.java)
+ )
+ )
+ }
+ invocation.processingEnv.requireTypeElement("$pkg.JavaSubject").let { subject ->
+ assertWithMessage(subject.qualifiedName).that(
+ collectExceptions(subject)
+ ).containsExactly(
+ "<init>" to setOf(ClassName.get(IllegalArgumentException::class.java)),
+ "multipleThrows" to setOf(
+ ClassName.get(IOException::class.java),
+ ClassName.get(IllegalStateException::class.java)
+ )
+ )
+ }
+ invocation.processingEnv.requireTypeElement("$pkg.KotlinAccessors").let { subject ->
+ assertWithMessage(subject.qualifiedName).that(
+ collectExceptions(subject)
+ ).containsExactly(
+ "getGetterThrows" to setOf(
+ ClassName.get(IllegalArgumentException::class.java)
+ ),
+ "setSetterThrows" to setOf(
+ ClassName.get(IllegalStateException::class.java)
+ ),
+ "getBothThrows" to setOf(
+ ClassName.get(IOException::class.java)
+ ),
+ "setBothThrows" to setOf(
+ ClassName.get(IllegalStateException::class.java),
+ ClassName.get(IllegalArgumentException::class.java)
+ ),
+ )
+ }
+ }
+ }
+ }
+
// see b/160258066
private fun genericToPrimitiveOverrides(asMemberOf: Boolean) {
val source = Source.kotlin(
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt
index b0af4d3..fbd36fc 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt
@@ -268,6 +268,68 @@
}
@Test
+ fun cachingBetweenSteps() {
+ val main = JavaFileObjects.forSourceString(
+ "foo.bar.Main",
+ """
+ package foo.bar;
+ import androidx.room.compiler.processing.testcode.*;
+ @MainAnnotation(
+ typeList = {},
+ singleType = Object.class,
+ intMethod = 3,
+ singleOtherAnnotation = @OtherAnnotation("y")
+ )
+ class Main {}
+ """.trimIndent()
+ )
+ val other = JavaFileObjects.forSourceString(
+ "foo.bar.Other",
+ """
+ package foo.bar;
+ import androidx.room.compiler.processing.testcode.*;
+ @OtherAnnotation("x")
+ class Other {
+ }
+ """.trimIndent()
+ )
+ val elementsByStep = mutableMapOf<XProcessingStep, XTypeElement>()
+ // create a scenario where we can test caching between steps
+ val mainStep = object : XProcessingStep {
+ override fun annotations(): Set<String> = setOf(MainAnnotation::class.qualifiedName!!)
+ override fun process(
+ env: XProcessingEnv,
+ elementsByAnnotation: Map<String, Set<XElement>>
+ ): Set<XTypeElement> {
+ elementsByStep[this] = env.requireTypeElement("foo.bar.Main")
+ return emptySet()
+ }
+ }
+ val otherStep = object : XProcessingStep {
+ override fun annotations(): Set<String> = setOf(OtherAnnotation::class.qualifiedName!!)
+ override fun process(
+ env: XProcessingEnv,
+ elementsByAnnotation: Map<String, Set<XElement>>
+ ): Set<XTypeElement> {
+ elementsByStep[this] = env.requireTypeElement("foo.bar.Main")
+ return emptySet()
+ }
+ }
+ assertAbout(
+ JavaSourcesSubjectFactory.javaSources()
+ ).that(
+ listOf(main, other)
+ ).processedWith(
+ object : JavacBasicAnnotationProcessor() {
+ override fun processingSteps() = listOf(mainStep, otherStep)
+ }
+ ).compilesWithoutError()
+ assertThat(elementsByStep.keys).containsExactly(mainStep, otherStep)
+ // make sure elements between steps are the same instances
+ assertThat(elementsByStep[mainStep]).isSameInstanceAs(elementsByStep[otherStep])
+ }
+
+ @Test
fun kspReturnsUnprocessed() {
CompilationTestCapabilities.assumeKspIsEnabled()
var returned: Set<XElement>? = null
@@ -388,6 +450,112 @@
}
@Test
+ fun javacDeferredStep() {
+ // create a scenario where we defer the first round of processing
+ val main = JavaFileObjects.forSourceString(
+ "foo.bar.Main",
+ """
+ package foo.bar;
+ import androidx.room.compiler.processing.testcode.*;
+ @MainAnnotation(
+ typeList = {},
+ singleType = Object.class,
+ intMethod = 3,
+ singleOtherAnnotation = @OtherAnnotation("y")
+ )
+ class Main {}
+ """.trimIndent()
+ )
+ val stepsProcessed = mutableListOf<XProcessingStep>()
+ val mainStep = object : XProcessingStep {
+ var round = 0
+ override fun annotations() = setOf(MainAnnotation::class.qualifiedName!!)
+ override fun process(
+ env: XProcessingEnv,
+ elementsByAnnotation: Map<String, Set<XElement>>
+ ): Set<XElement> {
+ stepsProcessed.add(this)
+ val deferredElements = if (round++ == 0) {
+ // Generate a random class to trigger another processing round
+ val className = ClassName.get("foo.bar", "Main_Impl")
+ val spec = TypeSpec.classBuilder(className).build()
+ JavaFile.builder(className.packageName(), spec)
+ .build()
+ .writeTo(env.filer)
+
+ // Defer all processing to the next round
+ elementsByAnnotation.values.flatten().toSet()
+ } else {
+ emptySet()
+ }
+ return deferredElements
+ }
+ }
+ assertAbout(
+ JavaSourcesSubjectFactory.javaSources()
+ ).that(
+ listOf(main)
+ ).processedWith(
+ object : JavacBasicAnnotationProcessor() {
+ override fun processingSteps() = listOf(mainStep)
+ }
+ ).compilesWithoutError()
+
+ // Assert that mainStep was processed twice due to deferring
+ assertThat(stepsProcessed).containsExactly(mainStep, mainStep)
+ }
+
+ @Test
+ fun javacStepOnlyCalledIfElementsToProcess() {
+ val main = JavaFileObjects.forSourceString(
+ "foo.bar.Main",
+ """
+ package foo.bar;
+ import androidx.room.compiler.processing.testcode.*;
+ @MainAnnotation(
+ typeList = {},
+ singleType = Object.class,
+ intMethod = 3,
+ singleOtherAnnotation = @OtherAnnotation("y")
+ )
+ class Main {
+ }
+ """.trimIndent()
+ )
+ val stepsProcessed = mutableListOf<XProcessingStep>()
+ val mainStep = object : XProcessingStep {
+ override fun annotations() = setOf(MainAnnotation::class.qualifiedName!!)
+ override fun process(
+ env: XProcessingEnv,
+ elementsByAnnotation: Map<String, Set<XElement>>
+ ): Set<XElement> {
+ stepsProcessed.add(this)
+ return emptySet()
+ }
+ }
+ val otherStep = object : XProcessingStep {
+ override fun annotations() = setOf(OtherAnnotation::class.qualifiedName!!)
+ override fun process(
+ env: XProcessingEnv,
+ elementsByAnnotation: Map<String, Set<XElement>>
+ ): Set<XElement> {
+ stepsProcessed.add(this)
+ return emptySet()
+ }
+ }
+ assertAbout(
+ JavaSourcesSubjectFactory.javaSources()
+ ).that(
+ listOf(main)
+ ).processedWith(
+ object : JavacBasicAnnotationProcessor() {
+ override fun processingSteps() = listOf(mainStep, otherStep)
+ }
+ ).compilesWithoutError()
+ assertThat(stepsProcessed).containsExactly(mainStep)
+ }
+
+ @Test
fun kspAnnotatedElementsByStep() {
val main = SourceFile.kotlin(
"Classes.kt",
@@ -449,4 +617,121 @@
assertThat(elementsByStep[otherStep])
.containsExactly("foo.bar.Other")
}
+
+ @Test
+ fun kspDeferredStep() {
+ // create a scenario where we defer the first round of processing
+ val main = SourceFile.kotlin(
+ "Classes.kt",
+ """
+ package foo.bar
+ import androidx.room.compiler.processing.testcode.*
+ @MainAnnotation(
+ typeList = [],
+ singleType = Any::class,
+ intMethod = 3,
+ singleOtherAnnotation = OtherAnnotation("y")
+ )
+ class Main {}
+ """.trimIndent()
+ )
+ val stepsProcessed = mutableListOf<XProcessingStep>()
+ val mainStep = object : XProcessingStep {
+ var round = 0
+ override fun annotations() = setOf(MainAnnotation::class.qualifiedName!!)
+ override fun process(
+ env: XProcessingEnv,
+ elementsByAnnotation: Map<String, Set<XElement>>
+ ): Set<XElement> {
+ stepsProcessed.add(this)
+ val deferredElements = if (round++ == 0) {
+ // Generate a random class to trigger another processing round
+ val className = ClassName.get("foo.bar", "Main_Impl")
+ val spec = TypeSpec.classBuilder(className).build()
+ JavaFile.builder(className.packageName(), spec)
+ .build()
+ .writeTo(env.filer)
+
+ // Defer all processing to the next round
+ elementsByAnnotation.values.flatten().toSet()
+ } else {
+ emptySet()
+ }
+ return deferredElements
+ }
+ }
+
+ val processorProvider = object : SymbolProcessorProvider {
+ override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
+ return object : KspBasicAnnotationProcessor(environment) {
+ override fun processingSteps() = listOf(mainStep)
+ }
+ }
+ }
+ KotlinCompilation().apply {
+ workingDir = temporaryFolder.root
+ inheritClassPath = true
+ symbolProcessorProviders = listOf(processorProvider)
+ sources = listOf(main)
+ verbose = false
+ }.compile()
+
+ // Assert that mainStep was processed twice due to deferring
+ assertThat(stepsProcessed).containsExactly(mainStep, mainStep)
+ }
+
+ @Test
+ fun kspStepOnlyCalledIfElementsToProcess() {
+ val main = SourceFile.kotlin(
+ "Classes.kt",
+ """
+ package foo.bar
+ import androidx.room.compiler.processing.testcode.*
+ @MainAnnotation(
+ typeList = [],
+ singleType = Any::class,
+ intMethod = 3,
+ singleOtherAnnotation = OtherAnnotation("y")
+ )
+ class Main {
+ }
+ """.trimIndent()
+ )
+ val stepsProcessed = mutableListOf<XProcessingStep>()
+ val mainStep = object : XProcessingStep {
+ override fun annotations() = setOf(MainAnnotation::class.qualifiedName!!)
+ override fun process(
+ env: XProcessingEnv,
+ elementsByAnnotation: Map<String, Set<XElement>>
+ ): Set<XElement> {
+ stepsProcessed.add(this)
+ return emptySet()
+ }
+ }
+ val otherStep = object : XProcessingStep {
+ override fun annotations() = setOf(OtherAnnotation::class.qualifiedName!!)
+ override fun process(
+ env: XProcessingEnv,
+ elementsByAnnotation: Map<String, Set<XElement>>
+ ): Set<XElement> {
+ stepsProcessed.add(this)
+ return emptySet()
+ }
+ }
+ val processorProvider = object : SymbolProcessorProvider {
+ override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
+ return object : KspBasicAnnotationProcessor(environment) {
+ override fun processingSteps() = listOf(mainStep, otherStep)
+ }
+ }
+ }
+ KotlinCompilation().apply {
+ workingDir = temporaryFolder.root
+ inheritClassPath = true
+ symbolProcessorProviders = listOf(processorProvider)
+ sources = listOf(main)
+ verbose = false
+ }.compile()
+ assertThat(stepsProcessed).containsExactly(mainStep)
+ }
}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index a78875b..9904a62 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -216,7 +216,6 @@
includeProject(":benchmark:benchmark-junit4", "benchmark/junit4")
includeProject(":benchmark:benchmark-macro", "benchmark/macro", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":benchmark:benchmark-macro-junit4", "benchmark/macro-junit4", [BuildType.MAIN, BuildType.COMPOSE])
-includeProject(":benchmark:integration-tests:crystalball-experiment", "benchmark/integration-tests/crystalball-experiment", [BuildType.MAIN])
includeProject(":benchmark:integration-tests:dry-run-benchmark", "benchmark/integration-tests/dry-run-benchmark", [BuildType.MAIN])
includeProject(":benchmark:integration-tests:macrobenchmark", "benchmark/integration-tests/macrobenchmark", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":benchmark:integration-tests:macrobenchmark-target", "benchmark/integration-tests/macrobenchmark-target", [BuildType.MAIN, BuildType.COMPOSE])
@@ -623,6 +622,8 @@
includeProject(":wear:wear-complications-data", "wear/wear-complications-data", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:wear-complications-data-source", "wear/wear-complications-data-source", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:wear-complications-data-source-samples", "wear/wear-complications-data-source-samples", [BuildType.MAIN, BuildType.WEAR])
+includeProject(":wear:benchmark:integration-tests:macrobenchmark-target", "wear/benchmark/integration-tests/macrobenchmark-target", [BuildType.MAIN, BuildType.COMPOSE])
+includeProject(":wear:benchmark:integration-tests:macrobenchmark", "wear/benchmark/integration-tests/macrobenchmark", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":wear:compose:compose-foundation", "wear/compose/foundation", [BuildType.COMPOSE])
includeProject(":wear:compose:compose-foundation-samples", "wear/compose/foundation/samples", [BuildType.COMPOSE])
includeProject(":wear:compose:compose-material", "wear/compose/material", [BuildType.COMPOSE])
diff --git a/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
new file mode 100644
index 0000000..76ae77a
--- /dev/null
+++ b/wear/benchmark/integration-tests/macrobenchmark-target/build.gradle
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id 'com.android.application'
+ id 'kotlin-android'
+}
+
+android {
+ buildTypes {
+ release {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile("proguard-android-optimize.txt")
+ }
+ }
+}
+
+dependencies {
+ implementation(libs.kotlinStdlib)
+ implementation(libs.constraintLayout)
+ implementation projectOrArtifact(":activity:activity-ktx")
+ implementation 'androidx.core:core-ktx'
+ implementation(libs.material)
+}
diff --git a/wear/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/wear/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0aaac14
--- /dev/null
+++ b/wear/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="androidx.wear.benchmark.integration.macrobenchmark.target">
+
+ <application
+ android:label="wear Macrobenchmark Target"
+ android:allowBackup="false"
+ android:supportsRtl="true"
+ android:theme="@android:style/Theme.DeviceDefault"
+ tools:ignore="MissingApplicationIcon">
+
+ <!-- Profileable to enable macrobenchmark profiling -->
+ <!--suppress AndroidElementNotAllowed -->
+ <profileable android:shell="true"/>
+
+ <!--
+ Activities need to be exported so the macrobenchmark can discover them
+ under the new package visibility changes for Android 11.
+ -->
+ <activity
+ android:name=".StartActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action
+ android:name=
+ "androidx.wear.benchmark.integration.macrobenchmark.target.STARTUP_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
\ No newline at end of file
diff --git a/wear/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/target/StartActivity.kt b/wear/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/target/StartActivity.kt
new file mode 100644
index 0000000..d6e4c18
--- /dev/null
+++ b/wear/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/benchmark/integration/macrobenchmark/target/StartActivity.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.benchmark.integration.macrobenchmark.target
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+
+class StartActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_start)
+
+ val txt = findViewById<TextView>(R.id.text)
+ txt.setText("Wear Macrobenchmark Target")
+ }
+}
\ No newline at end of file
diff --git a/wear/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/activity_start.xml b/wear/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/activity_start.xml
new file mode 100644
index 0000000..8ca9e47
--- /dev/null
+++ b/wear/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/activity_start.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="0dp"
+ tools:context=".StartActivity"
+ tools:deviceIds="wear">
+
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="Preview text" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/wear/benchmark/integration-tests/macrobenchmark/build.gradle b/wear/benchmark/integration-tests/macrobenchmark/build.gradle
new file mode 100644
index 0000000..67fa2c2
--- /dev/null
+++ b/wear/benchmark/integration-tests/macrobenchmark/build.gradle
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("AndroidXPlugin")
+ id 'com.android.library'
+ id 'kotlin-android'
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 29
+ }
+}
+
+dependencies {
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":benchmark:benchmark-macro-junit4"))
+ androidTestImplementation(project(":internal-testutils-macrobenchmark"))
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testUiautomator)
+}
+
+// Define a task dependency so the app is installed before we run macro benchmarks.
+tasks.getByPath(":wear:benchmark:integration-tests:macrobenchmark:connectedCheck")
+ .dependsOn(tasks.getByPath(
+ ":wear:benchmark:integration-tests:macrobenchmark-target:installRelease"))
\ No newline at end of file
diff --git a/wear/benchmark/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml b/wear/benchmark/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..c9becec4
--- /dev/null
+++ b/wear/benchmark/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest package="androidx.wear.benchmark.integration.macrobenchmark.test"/>
diff --git a/wear/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/benchmark/integration/macrobenchmark/StartupBenchmark.kt b/wear/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/benchmark/integration/macrobenchmark/StartupBenchmark.kt
new file mode 100644
index 0000000..87063f6
--- /dev/null
+++ b/wear/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/benchmark/integration/macrobenchmark/StartupBenchmark.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.benchmark.integration.macrobenchmark
+
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.filters.LargeTest
+import androidx.testutils.createStartupCompilationParams
+import androidx.testutils.measureStartup
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class StartupBenchmark(
+ private val startupMode: StartupMode,
+ private val compilationMode: CompilationMode
+) {
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
+ @Test
+ fun startup() = benchmarkRule.measureStartup(
+ compilationMode = compilationMode,
+ startupMode = startupMode,
+ packageName = "androidx.wear.benchmark.integration.macrobenchmark.target"
+ ) {
+ action = "androidx.wear.benchmark.integration.macrobenchmark.target" + ".STARTUP_ACTIVITY"
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "startup={0},compilation={1}")
+ @JvmStatic
+ fun parameters() = createStartupCompilationParams()
+ }
+}
\ No newline at end of file
diff --git a/wear/benchmark/integration-tests/macrobenchmark/src/main/AndroidManifest.xml b/wear/benchmark/integration-tests/macrobenchmark/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..86fc508
--- /dev/null
+++ b/wear/benchmark/integration-tests/macrobenchmark/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest package="androidx.wear.benchmark.integration.macrobenchmark" />
\ No newline at end of file
diff --git a/wear/compose/material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt b/wear/compose/material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
index d1d93f4..1c4df6df 100644
--- a/wear/compose/material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
+++ b/wear/compose/material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
@@ -112,7 +112,7 @@
.matchParentSize()
.paint(
painter = backgroundPainter,
- contentScale = ContentScale.FillBounds
+ contentScale = ContentScale.Crop
)
val contentBoxModifier = Modifier
diff --git a/wear/compose/material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt b/wear/compose/material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt
index 5dff26b..c24ec40 100644
--- a/wear/compose/material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt
+++ b/wear/compose/material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt
@@ -114,7 +114,7 @@
Modifier
.paint(
painter = colors.background(enabled = enabled).value,
- contentScale = ContentScale.FillBounds
+ contentScale = ContentScale.Crop
)
.fillMaxSize()
diff --git a/wear/wear-input-testing/api/1.1.0-beta01.txt b/wear/wear-input-testing/api/1.1.0-beta01.txt
new file mode 100644
index 0000000..154d309
--- /dev/null
+++ b/wear/wear-input-testing/api/1.1.0-beta01.txt
@@ -0,0 +1,18 @@
+// Signature format: 4.0
+package androidx.wear.input.testing {
+
+ public class TestWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public TestWearableButtonsProvider(java.util.Map<java.lang.Integer!,androidx.wear.input.testing.TestWearableButtonsProvider.TestWearableButtonLocation!>);
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ public static class TestWearableButtonsProvider.TestWearableButtonLocation {
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float);
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float, float, float);
+ method public android.graphics.PointF getLocation();
+ method public android.graphics.PointF? getRotatedLocation();
+ }
+
+}
+
diff --git a/wear/wear-input-testing/api/public_plus_experimental_1.1.0-beta01.txt b/wear/wear-input-testing/api/public_plus_experimental_1.1.0-beta01.txt
new file mode 100644
index 0000000..154d309
--- /dev/null
+++ b/wear/wear-input-testing/api/public_plus_experimental_1.1.0-beta01.txt
@@ -0,0 +1,18 @@
+// Signature format: 4.0
+package androidx.wear.input.testing {
+
+ public class TestWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public TestWearableButtonsProvider(java.util.Map<java.lang.Integer!,androidx.wear.input.testing.TestWearableButtonsProvider.TestWearableButtonLocation!>);
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ public static class TestWearableButtonsProvider.TestWearableButtonLocation {
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float);
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float, float, float);
+ method public android.graphics.PointF getLocation();
+ method public android.graphics.PointF? getRotatedLocation();
+ }
+
+}
+
diff --git a/wear/wear-input-testing/api/res-1.1.0-beta01.txt b/wear/wear-input-testing/api/res-1.1.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/wear-input-testing/api/res-1.1.0-beta01.txt
diff --git a/wear/wear-input-testing/api/restricted_1.1.0-beta01.txt b/wear/wear-input-testing/api/restricted_1.1.0-beta01.txt
new file mode 100644
index 0000000..154d309
--- /dev/null
+++ b/wear/wear-input-testing/api/restricted_1.1.0-beta01.txt
@@ -0,0 +1,18 @@
+// Signature format: 4.0
+package androidx.wear.input.testing {
+
+ public class TestWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public TestWearableButtonsProvider(java.util.Map<java.lang.Integer!,androidx.wear.input.testing.TestWearableButtonsProvider.TestWearableButtonLocation!>);
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ public static class TestWearableButtonsProvider.TestWearableButtonLocation {
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float);
+ ctor public TestWearableButtonsProvider.TestWearableButtonLocation(float, float, float, float);
+ method public android.graphics.PointF getLocation();
+ method public android.graphics.PointF? getRotatedLocation();
+ }
+
+}
+
diff --git a/wear/wear-input/api/1.1.0-beta01.txt b/wear/wear-input/api/1.1.0-beta01.txt
new file mode 100644
index 0000000..5909312
--- /dev/null
+++ b/wear/wear-input/api/1.1.0-beta01.txt
@@ -0,0 +1,67 @@
+// Signature format: 4.0
+package androidx.wear.input {
+
+ public final class DeviceWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public DeviceWearableButtonsProvider();
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ @RequiresApi(android.os.Build.VERSION_CODES.N) public final class RemoteInputIntentHelper {
+ method public static android.content.Intent createActionRemoteInputIntent();
+ method public static CharSequence? getCancelLabelExtra(android.content.Intent intent);
+ method public static CharSequence? getConfirmLabelExtra(android.content.Intent intent);
+ method public static CharSequence? getInProgressLabelExtra(android.content.Intent intent);
+ method public static java.util.List<android.app.RemoteInput>? getRemoteInputsExtra(android.content.Intent intent);
+ method public static java.util.List<java.lang.CharSequence>? getSmartReplyContextExtra(android.content.Intent intent);
+ method public static CharSequence? getTitleExtra(android.content.Intent intent);
+ method public static boolean hasRemoteInputsExtra(android.content.Intent intent);
+ method public static boolean isActionRemoteInput(android.content.Intent intent);
+ method public static android.content.Intent putCancelLabelExtra(android.content.Intent intent, CharSequence label);
+ method public static android.content.Intent putConfirmLabelExtra(android.content.Intent intent, CharSequence label);
+ method public static android.content.Intent putInProgressLabelExtra(android.content.Intent intent, CharSequence label);
+ method public static android.content.Intent putRemoteInputsExtra(android.content.Intent intent, java.util.List<android.app.RemoteInput> remoteInputs);
+ method public static android.content.Intent putSmartReplyContextExtra(android.content.Intent intent, java.util.List<? extends java.lang.CharSequence> smartReplyContext);
+ method public static android.content.Intent putTitleExtra(android.content.Intent intent, CharSequence title);
+ field public static final androidx.wear.input.RemoteInputIntentHelper.Companion Companion;
+ }
+
+ public static final class RemoteInputIntentHelper.Companion {
+ method public android.content.Intent createActionRemoteInputIntent();
+ method public CharSequence? getCancelLabelExtra(android.content.Intent intent);
+ method public CharSequence? getConfirmLabelExtra(android.content.Intent intent);
+ method public CharSequence? getInProgressLabelExtra(android.content.Intent intent);
+ method public java.util.List<android.app.RemoteInput>? getRemoteInputsExtra(android.content.Intent intent);
+ method public java.util.List<java.lang.CharSequence>? getSmartReplyContextExtra(android.content.Intent intent);
+ method public CharSequence? getTitleExtra(android.content.Intent intent);
+ method public boolean hasRemoteInputsExtra(android.content.Intent intent);
+ method public boolean isActionRemoteInput(android.content.Intent intent);
+ method public android.content.Intent putCancelLabelExtra(android.content.Intent intent, CharSequence label);
+ method public android.content.Intent putConfirmLabelExtra(android.content.Intent intent, CharSequence label);
+ method public android.content.Intent putInProgressLabelExtra(android.content.Intent intent, CharSequence label);
+ method public android.content.Intent putRemoteInputsExtra(android.content.Intent intent, java.util.List<android.app.RemoteInput> remoteInputs);
+ method public android.content.Intent putSmartReplyContextExtra(android.content.Intent intent, java.util.List<? extends java.lang.CharSequence> smartReplyContext);
+ method public android.content.Intent putTitleExtra(android.content.Intent intent, CharSequence title);
+ }
+
+ public final class WearableButtons {
+ method public static int getButtonCount(android.content.Context);
+ method public static android.graphics.drawable.Drawable? getButtonIcon(android.content.Context, int);
+ method public static androidx.wear.input.WearableButtons.ButtonInfo? getButtonInfo(android.content.Context, int);
+ method public static CharSequence? getButtonLabel(android.content.Context, int);
+ }
+
+ public static final class WearableButtons.ButtonInfo {
+ method public int getKeycode();
+ method public int getLocationZone();
+ method public float getX();
+ method public float getY();
+ }
+
+ public interface WearableButtonsProvider {
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+}
+
diff --git a/wear/wear-input/api/public_plus_experimental_1.1.0-beta01.txt b/wear/wear-input/api/public_plus_experimental_1.1.0-beta01.txt
new file mode 100644
index 0000000..5909312
--- /dev/null
+++ b/wear/wear-input/api/public_plus_experimental_1.1.0-beta01.txt
@@ -0,0 +1,67 @@
+// Signature format: 4.0
+package androidx.wear.input {
+
+ public final class DeviceWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public DeviceWearableButtonsProvider();
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ @RequiresApi(android.os.Build.VERSION_CODES.N) public final class RemoteInputIntentHelper {
+ method public static android.content.Intent createActionRemoteInputIntent();
+ method public static CharSequence? getCancelLabelExtra(android.content.Intent intent);
+ method public static CharSequence? getConfirmLabelExtra(android.content.Intent intent);
+ method public static CharSequence? getInProgressLabelExtra(android.content.Intent intent);
+ method public static java.util.List<android.app.RemoteInput>? getRemoteInputsExtra(android.content.Intent intent);
+ method public static java.util.List<java.lang.CharSequence>? getSmartReplyContextExtra(android.content.Intent intent);
+ method public static CharSequence? getTitleExtra(android.content.Intent intent);
+ method public static boolean hasRemoteInputsExtra(android.content.Intent intent);
+ method public static boolean isActionRemoteInput(android.content.Intent intent);
+ method public static android.content.Intent putCancelLabelExtra(android.content.Intent intent, CharSequence label);
+ method public static android.content.Intent putConfirmLabelExtra(android.content.Intent intent, CharSequence label);
+ method public static android.content.Intent putInProgressLabelExtra(android.content.Intent intent, CharSequence label);
+ method public static android.content.Intent putRemoteInputsExtra(android.content.Intent intent, java.util.List<android.app.RemoteInput> remoteInputs);
+ method public static android.content.Intent putSmartReplyContextExtra(android.content.Intent intent, java.util.List<? extends java.lang.CharSequence> smartReplyContext);
+ method public static android.content.Intent putTitleExtra(android.content.Intent intent, CharSequence title);
+ field public static final androidx.wear.input.RemoteInputIntentHelper.Companion Companion;
+ }
+
+ public static final class RemoteInputIntentHelper.Companion {
+ method public android.content.Intent createActionRemoteInputIntent();
+ method public CharSequence? getCancelLabelExtra(android.content.Intent intent);
+ method public CharSequence? getConfirmLabelExtra(android.content.Intent intent);
+ method public CharSequence? getInProgressLabelExtra(android.content.Intent intent);
+ method public java.util.List<android.app.RemoteInput>? getRemoteInputsExtra(android.content.Intent intent);
+ method public java.util.List<java.lang.CharSequence>? getSmartReplyContextExtra(android.content.Intent intent);
+ method public CharSequence? getTitleExtra(android.content.Intent intent);
+ method public boolean hasRemoteInputsExtra(android.content.Intent intent);
+ method public boolean isActionRemoteInput(android.content.Intent intent);
+ method public android.content.Intent putCancelLabelExtra(android.content.Intent intent, CharSequence label);
+ method public android.content.Intent putConfirmLabelExtra(android.content.Intent intent, CharSequence label);
+ method public android.content.Intent putInProgressLabelExtra(android.content.Intent intent, CharSequence label);
+ method public android.content.Intent putRemoteInputsExtra(android.content.Intent intent, java.util.List<android.app.RemoteInput> remoteInputs);
+ method public android.content.Intent putSmartReplyContextExtra(android.content.Intent intent, java.util.List<? extends java.lang.CharSequence> smartReplyContext);
+ method public android.content.Intent putTitleExtra(android.content.Intent intent, CharSequence title);
+ }
+
+ public final class WearableButtons {
+ method public static int getButtonCount(android.content.Context);
+ method public static android.graphics.drawable.Drawable? getButtonIcon(android.content.Context, int);
+ method public static androidx.wear.input.WearableButtons.ButtonInfo? getButtonInfo(android.content.Context, int);
+ method public static CharSequence? getButtonLabel(android.content.Context, int);
+ }
+
+ public static final class WearableButtons.ButtonInfo {
+ method public int getKeycode();
+ method public int getLocationZone();
+ method public float getX();
+ method public float getY();
+ }
+
+ public interface WearableButtonsProvider {
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+}
+
diff --git a/wear/wear-input/api/res-1.1.0-beta01.txt b/wear/wear-input/api/res-1.1.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/wear-input/api/res-1.1.0-beta01.txt
diff --git a/wear/wear-input/api/restricted_1.1.0-beta01.txt b/wear/wear-input/api/restricted_1.1.0-beta01.txt
new file mode 100644
index 0000000..5909312
--- /dev/null
+++ b/wear/wear-input/api/restricted_1.1.0-beta01.txt
@@ -0,0 +1,67 @@
+// Signature format: 4.0
+package androidx.wear.input {
+
+ public final class DeviceWearableButtonsProvider implements androidx.wear.input.WearableButtonsProvider {
+ ctor public DeviceWearableButtonsProvider();
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+ @RequiresApi(android.os.Build.VERSION_CODES.N) public final class RemoteInputIntentHelper {
+ method public static android.content.Intent createActionRemoteInputIntent();
+ method public static CharSequence? getCancelLabelExtra(android.content.Intent intent);
+ method public static CharSequence? getConfirmLabelExtra(android.content.Intent intent);
+ method public static CharSequence? getInProgressLabelExtra(android.content.Intent intent);
+ method public static java.util.List<android.app.RemoteInput>? getRemoteInputsExtra(android.content.Intent intent);
+ method public static java.util.List<java.lang.CharSequence>? getSmartReplyContextExtra(android.content.Intent intent);
+ method public static CharSequence? getTitleExtra(android.content.Intent intent);
+ method public static boolean hasRemoteInputsExtra(android.content.Intent intent);
+ method public static boolean isActionRemoteInput(android.content.Intent intent);
+ method public static android.content.Intent putCancelLabelExtra(android.content.Intent intent, CharSequence label);
+ method public static android.content.Intent putConfirmLabelExtra(android.content.Intent intent, CharSequence label);
+ method public static android.content.Intent putInProgressLabelExtra(android.content.Intent intent, CharSequence label);
+ method public static android.content.Intent putRemoteInputsExtra(android.content.Intent intent, java.util.List<android.app.RemoteInput> remoteInputs);
+ method public static android.content.Intent putSmartReplyContextExtra(android.content.Intent intent, java.util.List<? extends java.lang.CharSequence> smartReplyContext);
+ method public static android.content.Intent putTitleExtra(android.content.Intent intent, CharSequence title);
+ field public static final androidx.wear.input.RemoteInputIntentHelper.Companion Companion;
+ }
+
+ public static final class RemoteInputIntentHelper.Companion {
+ method public android.content.Intent createActionRemoteInputIntent();
+ method public CharSequence? getCancelLabelExtra(android.content.Intent intent);
+ method public CharSequence? getConfirmLabelExtra(android.content.Intent intent);
+ method public CharSequence? getInProgressLabelExtra(android.content.Intent intent);
+ method public java.util.List<android.app.RemoteInput>? getRemoteInputsExtra(android.content.Intent intent);
+ method public java.util.List<java.lang.CharSequence>? getSmartReplyContextExtra(android.content.Intent intent);
+ method public CharSequence? getTitleExtra(android.content.Intent intent);
+ method public boolean hasRemoteInputsExtra(android.content.Intent intent);
+ method public boolean isActionRemoteInput(android.content.Intent intent);
+ method public android.content.Intent putCancelLabelExtra(android.content.Intent intent, CharSequence label);
+ method public android.content.Intent putConfirmLabelExtra(android.content.Intent intent, CharSequence label);
+ method public android.content.Intent putInProgressLabelExtra(android.content.Intent intent, CharSequence label);
+ method public android.content.Intent putRemoteInputsExtra(android.content.Intent intent, java.util.List<android.app.RemoteInput> remoteInputs);
+ method public android.content.Intent putSmartReplyContextExtra(android.content.Intent intent, java.util.List<? extends java.lang.CharSequence> smartReplyContext);
+ method public android.content.Intent putTitleExtra(android.content.Intent intent, CharSequence title);
+ }
+
+ public final class WearableButtons {
+ method public static int getButtonCount(android.content.Context);
+ method public static android.graphics.drawable.Drawable? getButtonIcon(android.content.Context, int);
+ method public static androidx.wear.input.WearableButtons.ButtonInfo? getButtonInfo(android.content.Context, int);
+ method public static CharSequence? getButtonLabel(android.content.Context, int);
+ }
+
+ public static final class WearableButtons.ButtonInfo {
+ method public int getKeycode();
+ method public int getLocationZone();
+ method public float getX();
+ method public float getY();
+ }
+
+ public interface WearableButtonsProvider {
+ method public int[]? getAvailableButtonKeyCodes(android.content.Context);
+ method public android.os.Bundle getButtonInfo(android.content.Context, int);
+ }
+
+}
+
diff --git a/wear/wear-ongoing/api/1.0.0-beta01.txt b/wear/wear-ongoing/api/1.0.0-beta01.txt
new file mode 100644
index 0000000..9eb1123
--- /dev/null
+++ b/wear/wear-ongoing/api/1.0.0-beta01.txt
@@ -0,0 +1,100 @@
+// Signature format: 4.0
+package androidx.wear.ongoing {
+
+ @RequiresApi(24) public final class OngoingActivity {
+ method public void apply(android.content.Context);
+ method public android.graphics.drawable.Icon? getAnimatedIcon();
+ method public String? getCategory();
+ method public androidx.core.content.LocusIdCompat? getLocusId();
+ method public int getNotificationId();
+ method public int getOngoingActivityId();
+ method public android.graphics.drawable.Icon getStaticIcon();
+ method public androidx.wear.ongoing.Status? getStatus();
+ method public String? getTag();
+ method public long getTimestamp();
+ method public String? getTitle();
+ method public android.app.PendingIntent getTouchIntent();
+ method public static androidx.wear.ongoing.OngoingActivity? recoverOngoingActivity(android.content.Context, java.util.function.Predicate<androidx.wear.ongoing.OngoingActivity!>);
+ method public static androidx.wear.ongoing.OngoingActivity? recoverOngoingActivity(android.content.Context);
+ method public static androidx.wear.ongoing.OngoingActivity? recoverOngoingActivity(android.content.Context, int);
+ method public void update(android.content.Context, androidx.wear.ongoing.Status);
+ }
+
+ public static final class OngoingActivity.Builder {
+ ctor public OngoingActivity.Builder(android.content.Context, int, androidx.core.app.NotificationCompat.Builder);
+ ctor public OngoingActivity.Builder(android.content.Context, String, int, androidx.core.app.NotificationCompat.Builder);
+ method public androidx.wear.ongoing.OngoingActivity build();
+ method public androidx.wear.ongoing.OngoingActivity.Builder setAnimatedIcon(android.graphics.drawable.Icon);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setAnimatedIcon(@DrawableRes int);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setCategory(String);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setLocusId(androidx.core.content.LocusIdCompat);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setOngoingActivityId(int);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setStaticIcon(android.graphics.drawable.Icon);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setStaticIcon(@DrawableRes int);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setStatus(androidx.wear.ongoing.Status);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setTitle(String);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setTouchIntent(android.app.PendingIntent);
+ }
+
+ public class SerializationHelper {
+ method public static void copy(android.os.Bundle, android.os.Bundle);
+ method public static androidx.wear.ongoing.OngoingActivity? create(android.app.Notification);
+ method public static androidx.wear.ongoing.OngoingActivity? create(android.os.Bundle);
+ method public static boolean hasOngoingActivity(android.app.Notification);
+ }
+
+ public final class Status implements androidx.wear.ongoing.TimeDependentText {
+ method public static androidx.wear.ongoing.Status forPart(androidx.wear.ongoing.Status.Part);
+ method public long getNextChangeTimeMillis(long);
+ method public androidx.wear.ongoing.Status.Part? getPart(String);
+ method public java.util.Set<java.lang.String!> getPartNames();
+ method public java.util.List<java.lang.CharSequence!> getTemplates();
+ method public CharSequence getText(android.content.Context, long);
+ }
+
+ public static final class Status.Builder {
+ ctor public Status.Builder();
+ method public androidx.wear.ongoing.Status.Builder addPart(String, androidx.wear.ongoing.Status.Part);
+ method public androidx.wear.ongoing.Status.Builder addTemplate(CharSequence);
+ method public androidx.wear.ongoing.Status build();
+ }
+
+ public abstract static class Status.Part implements androidx.wear.ongoing.TimeDependentText {
+ }
+
+ public static final class Status.StopwatchPart extends androidx.wear.ongoing.Status.TimerOrStopwatchPart {
+ ctor public Status.StopwatchPart(long, long, long);
+ ctor public Status.StopwatchPart(long, long);
+ ctor public Status.StopwatchPart(long);
+ }
+
+ public static final class Status.TextPart extends androidx.wear.ongoing.Status.Part {
+ ctor public Status.TextPart(String);
+ method public long getNextChangeTimeMillis(long);
+ method public CharSequence getText(android.content.Context, long);
+ }
+
+ public abstract static class Status.TimerOrStopwatchPart extends androidx.wear.ongoing.Status.Part {
+ method public long getNextChangeTimeMillis(long);
+ method public long getPausedAtMillis();
+ method public CharSequence getText(android.content.Context, long);
+ method public long getTimeZeroMillis();
+ method public long getTotalDurationMillis();
+ method public boolean hasTotalDuration();
+ method public boolean isCountDown();
+ method public boolean isPaused();
+ }
+
+ public static final class Status.TimerPart extends androidx.wear.ongoing.Status.TimerOrStopwatchPart {
+ ctor public Status.TimerPart(long, long, long);
+ ctor public Status.TimerPart(long, long);
+ ctor public Status.TimerPart(long);
+ }
+
+ public interface TimeDependentText {
+ method public long getNextChangeTimeMillis(long);
+ method public CharSequence getText(android.content.Context, long);
+ }
+
+}
+
diff --git a/wear/wear-ongoing/api/public_plus_experimental_1.0.0-beta01.txt b/wear/wear-ongoing/api/public_plus_experimental_1.0.0-beta01.txt
new file mode 100644
index 0000000..9eb1123
--- /dev/null
+++ b/wear/wear-ongoing/api/public_plus_experimental_1.0.0-beta01.txt
@@ -0,0 +1,100 @@
+// Signature format: 4.0
+package androidx.wear.ongoing {
+
+ @RequiresApi(24) public final class OngoingActivity {
+ method public void apply(android.content.Context);
+ method public android.graphics.drawable.Icon? getAnimatedIcon();
+ method public String? getCategory();
+ method public androidx.core.content.LocusIdCompat? getLocusId();
+ method public int getNotificationId();
+ method public int getOngoingActivityId();
+ method public android.graphics.drawable.Icon getStaticIcon();
+ method public androidx.wear.ongoing.Status? getStatus();
+ method public String? getTag();
+ method public long getTimestamp();
+ method public String? getTitle();
+ method public android.app.PendingIntent getTouchIntent();
+ method public static androidx.wear.ongoing.OngoingActivity? recoverOngoingActivity(android.content.Context, java.util.function.Predicate<androidx.wear.ongoing.OngoingActivity!>);
+ method public static androidx.wear.ongoing.OngoingActivity? recoverOngoingActivity(android.content.Context);
+ method public static androidx.wear.ongoing.OngoingActivity? recoverOngoingActivity(android.content.Context, int);
+ method public void update(android.content.Context, androidx.wear.ongoing.Status);
+ }
+
+ public static final class OngoingActivity.Builder {
+ ctor public OngoingActivity.Builder(android.content.Context, int, androidx.core.app.NotificationCompat.Builder);
+ ctor public OngoingActivity.Builder(android.content.Context, String, int, androidx.core.app.NotificationCompat.Builder);
+ method public androidx.wear.ongoing.OngoingActivity build();
+ method public androidx.wear.ongoing.OngoingActivity.Builder setAnimatedIcon(android.graphics.drawable.Icon);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setAnimatedIcon(@DrawableRes int);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setCategory(String);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setLocusId(androidx.core.content.LocusIdCompat);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setOngoingActivityId(int);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setStaticIcon(android.graphics.drawable.Icon);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setStaticIcon(@DrawableRes int);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setStatus(androidx.wear.ongoing.Status);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setTitle(String);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setTouchIntent(android.app.PendingIntent);
+ }
+
+ public class SerializationHelper {
+ method public static void copy(android.os.Bundle, android.os.Bundle);
+ method public static androidx.wear.ongoing.OngoingActivity? create(android.app.Notification);
+ method public static androidx.wear.ongoing.OngoingActivity? create(android.os.Bundle);
+ method public static boolean hasOngoingActivity(android.app.Notification);
+ }
+
+ public final class Status implements androidx.wear.ongoing.TimeDependentText {
+ method public static androidx.wear.ongoing.Status forPart(androidx.wear.ongoing.Status.Part);
+ method public long getNextChangeTimeMillis(long);
+ method public androidx.wear.ongoing.Status.Part? getPart(String);
+ method public java.util.Set<java.lang.String!> getPartNames();
+ method public java.util.List<java.lang.CharSequence!> getTemplates();
+ method public CharSequence getText(android.content.Context, long);
+ }
+
+ public static final class Status.Builder {
+ ctor public Status.Builder();
+ method public androidx.wear.ongoing.Status.Builder addPart(String, androidx.wear.ongoing.Status.Part);
+ method public androidx.wear.ongoing.Status.Builder addTemplate(CharSequence);
+ method public androidx.wear.ongoing.Status build();
+ }
+
+ public abstract static class Status.Part implements androidx.wear.ongoing.TimeDependentText {
+ }
+
+ public static final class Status.StopwatchPart extends androidx.wear.ongoing.Status.TimerOrStopwatchPart {
+ ctor public Status.StopwatchPart(long, long, long);
+ ctor public Status.StopwatchPart(long, long);
+ ctor public Status.StopwatchPart(long);
+ }
+
+ public static final class Status.TextPart extends androidx.wear.ongoing.Status.Part {
+ ctor public Status.TextPart(String);
+ method public long getNextChangeTimeMillis(long);
+ method public CharSequence getText(android.content.Context, long);
+ }
+
+ public abstract static class Status.TimerOrStopwatchPart extends androidx.wear.ongoing.Status.Part {
+ method public long getNextChangeTimeMillis(long);
+ method public long getPausedAtMillis();
+ method public CharSequence getText(android.content.Context, long);
+ method public long getTimeZeroMillis();
+ method public long getTotalDurationMillis();
+ method public boolean hasTotalDuration();
+ method public boolean isCountDown();
+ method public boolean isPaused();
+ }
+
+ public static final class Status.TimerPart extends androidx.wear.ongoing.Status.TimerOrStopwatchPart {
+ ctor public Status.TimerPart(long, long, long);
+ ctor public Status.TimerPart(long, long);
+ ctor public Status.TimerPart(long);
+ }
+
+ public interface TimeDependentText {
+ method public long getNextChangeTimeMillis(long);
+ method public CharSequence getText(android.content.Context, long);
+ }
+
+}
+
diff --git a/wear/wear-ongoing/api/res-1.0.0-beta01.txt b/wear/wear-ongoing/api/res-1.0.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/wear-ongoing/api/res-1.0.0-beta01.txt
diff --git a/wear/wear-ongoing/api/restricted_1.0.0-beta01.txt b/wear/wear-ongoing/api/restricted_1.0.0-beta01.txt
new file mode 100644
index 0000000..9eb1123
--- /dev/null
+++ b/wear/wear-ongoing/api/restricted_1.0.0-beta01.txt
@@ -0,0 +1,100 @@
+// Signature format: 4.0
+package androidx.wear.ongoing {
+
+ @RequiresApi(24) public final class OngoingActivity {
+ method public void apply(android.content.Context);
+ method public android.graphics.drawable.Icon? getAnimatedIcon();
+ method public String? getCategory();
+ method public androidx.core.content.LocusIdCompat? getLocusId();
+ method public int getNotificationId();
+ method public int getOngoingActivityId();
+ method public android.graphics.drawable.Icon getStaticIcon();
+ method public androidx.wear.ongoing.Status? getStatus();
+ method public String? getTag();
+ method public long getTimestamp();
+ method public String? getTitle();
+ method public android.app.PendingIntent getTouchIntent();
+ method public static androidx.wear.ongoing.OngoingActivity? recoverOngoingActivity(android.content.Context, java.util.function.Predicate<androidx.wear.ongoing.OngoingActivity!>);
+ method public static androidx.wear.ongoing.OngoingActivity? recoverOngoingActivity(android.content.Context);
+ method public static androidx.wear.ongoing.OngoingActivity? recoverOngoingActivity(android.content.Context, int);
+ method public void update(android.content.Context, androidx.wear.ongoing.Status);
+ }
+
+ public static final class OngoingActivity.Builder {
+ ctor public OngoingActivity.Builder(android.content.Context, int, androidx.core.app.NotificationCompat.Builder);
+ ctor public OngoingActivity.Builder(android.content.Context, String, int, androidx.core.app.NotificationCompat.Builder);
+ method public androidx.wear.ongoing.OngoingActivity build();
+ method public androidx.wear.ongoing.OngoingActivity.Builder setAnimatedIcon(android.graphics.drawable.Icon);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setAnimatedIcon(@DrawableRes int);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setCategory(String);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setLocusId(androidx.core.content.LocusIdCompat);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setOngoingActivityId(int);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setStaticIcon(android.graphics.drawable.Icon);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setStaticIcon(@DrawableRes int);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setStatus(androidx.wear.ongoing.Status);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setTitle(String);
+ method public androidx.wear.ongoing.OngoingActivity.Builder setTouchIntent(android.app.PendingIntent);
+ }
+
+ public class SerializationHelper {
+ method public static void copy(android.os.Bundle, android.os.Bundle);
+ method public static androidx.wear.ongoing.OngoingActivity? create(android.app.Notification);
+ method public static androidx.wear.ongoing.OngoingActivity? create(android.os.Bundle);
+ method public static boolean hasOngoingActivity(android.app.Notification);
+ }
+
+ public final class Status implements androidx.wear.ongoing.TimeDependentText {
+ method public static androidx.wear.ongoing.Status forPart(androidx.wear.ongoing.Status.Part);
+ method public long getNextChangeTimeMillis(long);
+ method public androidx.wear.ongoing.Status.Part? getPart(String);
+ method public java.util.Set<java.lang.String!> getPartNames();
+ method public java.util.List<java.lang.CharSequence!> getTemplates();
+ method public CharSequence getText(android.content.Context, long);
+ }
+
+ public static final class Status.Builder {
+ ctor public Status.Builder();
+ method public androidx.wear.ongoing.Status.Builder addPart(String, androidx.wear.ongoing.Status.Part);
+ method public androidx.wear.ongoing.Status.Builder addTemplate(CharSequence);
+ method public androidx.wear.ongoing.Status build();
+ }
+
+ public abstract static class Status.Part implements androidx.wear.ongoing.TimeDependentText {
+ }
+
+ public static final class Status.StopwatchPart extends androidx.wear.ongoing.Status.TimerOrStopwatchPart {
+ ctor public Status.StopwatchPart(long, long, long);
+ ctor public Status.StopwatchPart(long, long);
+ ctor public Status.StopwatchPart(long);
+ }
+
+ public static final class Status.TextPart extends androidx.wear.ongoing.Status.Part {
+ ctor public Status.TextPart(String);
+ method public long getNextChangeTimeMillis(long);
+ method public CharSequence getText(android.content.Context, long);
+ }
+
+ public abstract static class Status.TimerOrStopwatchPart extends androidx.wear.ongoing.Status.Part {
+ method public long getNextChangeTimeMillis(long);
+ method public long getPausedAtMillis();
+ method public CharSequence getText(android.content.Context, long);
+ method public long getTimeZeroMillis();
+ method public long getTotalDurationMillis();
+ method public boolean hasTotalDuration();
+ method public boolean isCountDown();
+ method public boolean isPaused();
+ }
+
+ public static final class Status.TimerPart extends androidx.wear.ongoing.Status.TimerOrStopwatchPart {
+ ctor public Status.TimerPart(long, long, long);
+ ctor public Status.TimerPart(long, long);
+ ctor public Status.TimerPart(long);
+ }
+
+ public interface TimeDependentText {
+ method public long getNextChangeTimeMillis(long);
+ method public CharSequence getText(android.content.Context, long);
+ }
+
+}
+
diff --git a/wear/wear-phone-interactions/api/current.txt b/wear/wear-phone-interactions/api/current.txt
index 310f3a1..6db66ac 100644
--- a/wear/wear-phone-interactions/api/current.txt
+++ b/wear/wear-phone-interactions/api/current.txt
@@ -64,7 +64,7 @@
method @UiThread public void close();
method public static androidx.wear.phone.interactions.authentication.RemoteAuthClient create(android.content.Context context);
method protected void finalize();
- method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
+ method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, java.util.concurrent.Executor executor, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
field public static final androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion Companion;
field public static final int ERROR_PHONE_UNAVAILABLE = 1; // 0x1
field public static final int ERROR_UNSUPPORTED = 0; // 0x0
@@ -73,7 +73,7 @@
public abstract static class RemoteAuthClient.Callback {
ctor public RemoteAuthClient.Callback();
- method @UiThread public abstract void onAuthorizationError(int errorCode);
+ method @UiThread public abstract void onAuthorizationError(androidx.wear.phone.interactions.authentication.OAuthRequest request, int errorCode);
method @UiThread public abstract void onAuthorizationResponse(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.OAuthResponse response);
}
@@ -130,9 +130,9 @@
method public androidx.wear.phone.interactions.notifications.BridgingManager fromContext(android.content.Context context);
}
- public final class BridgingManagerServiceBinder {
- ctor public BridgingManagerServiceBinder(android.content.Context context, androidx.wear.phone.interactions.notifications.BridgingConfigurationHandler bridgingConfigurationHandler);
- method public android.os.IBinder? getBinder();
+ public final class BridgingManagerService extends android.app.Service {
+ ctor public BridgingManagerService(android.content.Context context, androidx.wear.phone.interactions.notifications.BridgingConfigurationHandler bridgingConfigurationHandler);
+ method public android.os.IBinder? onBind(android.content.Intent? intent);
}
}
diff --git a/wear/wear-phone-interactions/api/public_plus_experimental_current.txt b/wear/wear-phone-interactions/api/public_plus_experimental_current.txt
index 310f3a1..6db66ac 100644
--- a/wear/wear-phone-interactions/api/public_plus_experimental_current.txt
+++ b/wear/wear-phone-interactions/api/public_plus_experimental_current.txt
@@ -64,7 +64,7 @@
method @UiThread public void close();
method public static androidx.wear.phone.interactions.authentication.RemoteAuthClient create(android.content.Context context);
method protected void finalize();
- method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
+ method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, java.util.concurrent.Executor executor, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
field public static final androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion Companion;
field public static final int ERROR_PHONE_UNAVAILABLE = 1; // 0x1
field public static final int ERROR_UNSUPPORTED = 0; // 0x0
@@ -73,7 +73,7 @@
public abstract static class RemoteAuthClient.Callback {
ctor public RemoteAuthClient.Callback();
- method @UiThread public abstract void onAuthorizationError(int errorCode);
+ method @UiThread public abstract void onAuthorizationError(androidx.wear.phone.interactions.authentication.OAuthRequest request, int errorCode);
method @UiThread public abstract void onAuthorizationResponse(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.OAuthResponse response);
}
@@ -130,9 +130,9 @@
method public androidx.wear.phone.interactions.notifications.BridgingManager fromContext(android.content.Context context);
}
- public final class BridgingManagerServiceBinder {
- ctor public BridgingManagerServiceBinder(android.content.Context context, androidx.wear.phone.interactions.notifications.BridgingConfigurationHandler bridgingConfigurationHandler);
- method public android.os.IBinder? getBinder();
+ public final class BridgingManagerService extends android.app.Service {
+ ctor public BridgingManagerService(android.content.Context context, androidx.wear.phone.interactions.notifications.BridgingConfigurationHandler bridgingConfigurationHandler);
+ method public android.os.IBinder? onBind(android.content.Intent? intent);
}
}
diff --git a/wear/wear-phone-interactions/api/restricted_current.txt b/wear/wear-phone-interactions/api/restricted_current.txt
index 310f3a1..6db66ac 100644
--- a/wear/wear-phone-interactions/api/restricted_current.txt
+++ b/wear/wear-phone-interactions/api/restricted_current.txt
@@ -64,7 +64,7 @@
method @UiThread public void close();
method public static androidx.wear.phone.interactions.authentication.RemoteAuthClient create(android.content.Context context);
method protected void finalize();
- method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
+ method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, java.util.concurrent.Executor executor, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
field public static final androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion Companion;
field public static final int ERROR_PHONE_UNAVAILABLE = 1; // 0x1
field public static final int ERROR_UNSUPPORTED = 0; // 0x0
@@ -73,7 +73,7 @@
public abstract static class RemoteAuthClient.Callback {
ctor public RemoteAuthClient.Callback();
- method @UiThread public abstract void onAuthorizationError(int errorCode);
+ method @UiThread public abstract void onAuthorizationError(androidx.wear.phone.interactions.authentication.OAuthRequest request, int errorCode);
method @UiThread public abstract void onAuthorizationResponse(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.OAuthResponse response);
}
@@ -130,9 +130,9 @@
method public androidx.wear.phone.interactions.notifications.BridgingManager fromContext(android.content.Context context);
}
- public final class BridgingManagerServiceBinder {
- ctor public BridgingManagerServiceBinder(android.content.Context context, androidx.wear.phone.interactions.notifications.BridgingConfigurationHandler bridgingConfigurationHandler);
- method public android.os.IBinder? getBinder();
+ public final class BridgingManagerService extends android.app.Service {
+ ctor public BridgingManagerService(android.content.Context context, androidx.wear.phone.interactions.notifications.BridgingConfigurationHandler bridgingConfigurationHandler);
+ method public android.os.IBinder? onBind(android.content.Intent? intent);
}
}
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt
index 6dd4162..842e9ac 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt
@@ -65,6 +65,7 @@
* .setAuthProviderUrl(Uri.parse("https://...."))
* .setCodeChallenge(CodeChallenge(codeVerifier))
* .build(),
+ * Executors.newSingleThreadExecutor()
* new MyAuthCallback()
* );
* }
@@ -80,7 +81,7 @@
* ...
* }
*
- * override public fun onAuthorizationError(errorCode: int) {
+ * override public fun onAuthorizationError(request: OAuthRequest, errorCode: int) {
* // Compare against codes available in RemoteAuthClient.ErrorCode
* // You'll also want to display an error UI.
* ...
@@ -205,7 +206,7 @@
* see [sendAuthorizationRequest]
*/
@UiThread
- public abstract fun onAuthorizationError(@ErrorCode errorCode: Int)
+ public abstract fun onAuthorizationError(request: OAuthRequest, @ErrorCode errorCode: Int)
}
/**
@@ -215,13 +216,18 @@
* completes.
*
* @param request Request that will be sent to the phone. The auth response should redirect
- * to the Wear OS companion. See [WEAR_REDIRECT_URL_PREFIX]
+ * to the Wear OS companion. See [OAuthRequest.WEAR_REDIRECT_URL_PREFIX]
+ * @param executor The executor that callback will called on.
+ * @param clientCallback The callback that will be notified when request is completed.
*
* @Throws RuntimeException if the service has error to open the request
*/
@UiThread
- @SuppressLint("ExecutorRegistration")
- public fun sendAuthorizationRequest(request: OAuthRequest, clientCallback: Callback) {
+ public fun sendAuthorizationRequest(
+ request: OAuthRequest,
+ executor: Executor,
+ clientCallback: Callback
+ ) {
require(packageName == request.getPackageName()) {
"The request's package name is different from the auth client's package name."
}
@@ -229,18 +235,16 @@
if (connectionState == STATE_DISCONNECTED) {
connect()
}
- whenConnected(
- Runnable {
- val callback = RequestCallback(request, clientCallback)
- outstandingRequests.add(callback)
- try {
- service!!.openUrl(request.toBundle(), callback)
- } catch (e: Exception) {
- removePendingCallback(callback)
- throw RuntimeException(e)
- }
+ whenConnected {
+ val callback = RequestCallback(request, clientCallback, executor)
+ outstandingRequests.add(callback)
+ try {
+ service!!.openUrl(request.toBundle(), callback)
+ } catch (e: Exception) {
+ removePendingCallback(callback)
+ throw RuntimeException(e)
}
- )
+ }
}
/**
@@ -320,7 +324,8 @@
/** Receives results of async requests to the remote auth service. */
internal inner class RequestCallback internal constructor(
private val request: OAuthRequest,
- private val clientCallback: Callback
+ private val clientCallback: Callback,
+ private val executor: Executor
) : IAuthenticationRequestCallback.Stub() {
override fun getApiVersion(): Int = IAuthenticationRequestCallback.API_VERSION
@@ -345,9 +350,13 @@
Runnable {
removePendingCallback(this@RequestCallback)
if (error == NO_ERROR) {
- clientCallback.onAuthorizationResponse(request, response)
+ executor.execute {
+ clientCallback.onAuthorizationResponse(request, response)
+ }
} else {
- clientCallback.onAuthorizationError(response.getErrorCode())
+ executor.execute {
+ clientCallback.onAuthorizationError(request, response.getErrorCode())
+ }
}
}
)
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthService.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthService.kt
index dadd75b..cadc0c3 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthService.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthService.kt
@@ -46,8 +46,8 @@
/**
* Handle the auth request by sending it to the phone.
* Typically, if the paired phone is not connected, send a response with error code of
- * ERROR_PHONE_UNAVAILABLE; otherwise listening for the response from the phone and send it
- * back to the 3p app.
+ * [RemoteAuthClient.ERROR_PHONE_UNAVAILABLE]; otherwise listening for the response from the
+ * phone and send it back to the 3p app.
*
* [RemoteAuthService.sendResponseToCallback] is provided for sending response back to the
* callback provided by the 3p app.
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/notifications/BridgingManager.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/notifications/BridgingManager.kt
index 7f8b241..6b07b6b 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/notifications/BridgingManager.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/notifications/BridgingManager.kt
@@ -121,7 +121,7 @@
}
public companion object {
- private const val ACTION_BIND_BRIDGING_MANAGER =
+ internal const val ACTION_BIND_BRIDGING_MANAGER =
"android.support.wearable.notifications.action.BIND_BRIDGING_MANAGER"
private const val BRIDGING_CONFIG_SERVICE_PACKAGE = "com.google.android.wearable.app"
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/notifications/BridgingManagerServiceBinder.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/notifications/BridgingManagerService.kt
similarity index 79%
rename from wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/notifications/BridgingManagerServiceBinder.kt
rename to wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/notifications/BridgingManagerService.kt
index 8961404..79e8336 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/notifications/BridgingManagerServiceBinder.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/notifications/BridgingManagerService.kt
@@ -18,6 +18,7 @@
import android.app.Service
import android.content.Context
+import android.content.Intent
import android.os.Binder
import android.os.Bundle
import android.os.IBinder
@@ -35,19 +36,21 @@
}
/**
- * Class for providing the binder that clients used to communicate with the service regarding
- * notification bridging configurations.
+ * Service class receiving notification bridging configurations.
+ *
+ * @param context The [Context] of the application.
+ * @param bridgingConfigurationHandler The handler for applying the notification bridging
+ * configuration.
*/
-public class BridgingManagerServiceBinder(
+public class BridgingManagerService(
private val context: Context,
private val bridgingConfigurationHandler: BridgingConfigurationHandler
-) {
- /**
- * Call this method in [Service.onBind] to provide the interface that clients
- * use to communicate with the service by returning an IBinder.
- */
- public fun getBinder(): IBinder? =
- BridgingManagerServiceImpl(context, bridgingConfigurationHandler)
+) : Service() {
+ override fun onBind(intent: Intent?): IBinder? =
+ if (intent?.action == BridgingManager.ACTION_BIND_BRIDGING_MANAGER)
+ BridgingManagerServiceImpl(context, bridgingConfigurationHandler)
+ else
+ null
}
internal class BridgingManagerServiceImpl(
diff --git a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/RemoteAuthTest.kt b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/RemoteAuthTest.kt
index 8d8bf82..4b2ec78 100644
--- a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/RemoteAuthTest.kt
+++ b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/RemoteAuthTest.kt
@@ -69,6 +69,7 @@
private var fakeService: FakeClockworkHomeAuthService = FakeClockworkHomeAuthService()
private var clientUnderTest: RemoteAuthClient =
RemoteAuthClient(fakeServiceBinder, DIRECT_EXECUTOR, appPackageName)
+ private val executor: Executor = SyncExecutor()
@Test
public fun doesntConnectUntilARequestIsMade() {
@@ -86,6 +87,7 @@
.setAuthProviderUrl(Uri.parse(requestUri))
.setCodeChallenge(CodeChallenge(CodeVerifier()))
.build(),
+ executor,
mockCallback
)
// THEN a connection is made to Clockwork Home's Auth service
@@ -95,7 +97,7 @@
@Test
public fun sendAuthorizationRequestShouldCallBinderMethod() {
// WHEN an authorization request is sent
- clientUnderTest.sendAuthorizationRequest(requestA, mockCallback)
+ clientUnderTest.sendAuthorizationRequest(requestA, executor, mockCallback)
fakeServiceBinder.completeConnection()
// THEN a request is made to Clockwork Home
val request = fakeService.requests[0]
@@ -114,8 +116,8 @@
@Test
public fun twoQueuedAuthorizationRequestsBeforeConnectCompletes() {
// GIVEN two authorization requests were made before connecting to Clockwork Home completes
- clientUnderTest.sendAuthorizationRequest(requestA, mockCallback)
- clientUnderTest.sendAuthorizationRequest(requestB, mockCallback)
+ clientUnderTest.sendAuthorizationRequest(requestA, executor, mockCallback)
+ clientUnderTest.sendAuthorizationRequest(requestB, executor, mockCallback)
// WHEN the connection does complete
fakeServiceBinder.completeConnection()
// THEN two requests are made to Clockwork Home
@@ -145,7 +147,7 @@
@Throws(RemoteException::class)
public fun requestCompletionShouldCallBackToClient() {
// GIVEN an authorization request was sent
- clientUnderTest.sendAuthorizationRequest(requestA, mockCallback)
+ clientUnderTest.sendAuthorizationRequest(requestA, executor, mockCallback)
fakeServiceBinder.completeConnection()
val request = fakeService.requests[0]
// WHEN the request completes
@@ -157,10 +159,10 @@
@Throws(RemoteException::class)
public fun doesntDisconnectWhenRequestStillInProgress() {
// GIVEN 2 authorization requests were sent
- clientUnderTest.sendAuthorizationRequest(requestA, mockCallback)
+ clientUnderTest.sendAuthorizationRequest(requestA, executor, mockCallback)
// GIVEN the async binding to Clockwork Home completed after the 1st but before the 2nd
fakeServiceBinder.completeConnection()
- clientUnderTest.sendAuthorizationRequest(requestB, mockCallback)
+ clientUnderTest.sendAuthorizationRequest(requestB, executor, mockCallback)
// WHEN the first one completes
RemoteAuthService.sendResponseToCallback(
response,
@@ -175,10 +177,10 @@
@Throws(RemoteException::class)
public fun disconnectsWhenAllRequestsComplete() {
// GIVEN 2 authorization requests were sent
- clientUnderTest.sendAuthorizationRequest(requestA, mockCallback)
+ clientUnderTest.sendAuthorizationRequest(requestA, executor, mockCallback)
// GIVEN the async binding to Clockwork Home completed after the 1st but before the 2nd
fakeServiceBinder.completeConnection()
- clientUnderTest.sendAuthorizationRequest(requestB, mockCallback)
+ clientUnderTest.sendAuthorizationRequest(requestB, executor, mockCallback)
RemoteAuthService.sendResponseToCallback(
response,
fakeService.requests[0].second
@@ -271,3 +273,9 @@
}
}
}
+
+private class SyncExecutor : Executor {
+ override fun execute(command: Runnable?) {
+ command?.run()
+ }
+}
diff --git a/wear/wear-watchface-complications-rendering/api/current.txt b/wear/wear-watchface-complications-rendering/api/current.txt
index 897ab96..f8923f6 100644
--- a/wear/wear-watchface-complications-rendering/api/current.txt
+++ b/wear/wear-watchface-complications-rendering/api/current.txt
@@ -6,13 +6,10 @@
method public void drawHighlight(android.graphics.Canvas canvas, android.graphics.Rect bounds, int boundsType, android.icu.util.Calendar calendar, @ColorInt int color);
method public androidx.wear.complications.data.ComplicationData? getData();
method public final androidx.wear.watchface.complications.rendering.ComplicationDrawable getDrawable();
- method public boolean isHighlighted();
method @CallSuper public void loadData(androidx.wear.complications.data.ComplicationData? complicationData, boolean loadDrawablesAsynchronous);
- method public void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters);
+ method public void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters, int slotId);
method public final void setDrawable(androidx.wear.watchface.complications.rendering.ComplicationDrawable value);
- method public void setHighlighted(boolean value);
property public final androidx.wear.watchface.complications.rendering.ComplicationDrawable drawable;
- property public boolean isHighlighted;
}
public final class ComplicationDrawable extends android.graphics.drawable.Drawable {
@@ -145,11 +142,11 @@
}
public final class GlesTextureComplication {
- ctor public GlesTextureComplication(androidx.wear.watchface.CanvasComplication canvasComplication, @Px int textureWidth, @Px int textureHeight, int textureType);
+ ctor public GlesTextureComplication(androidx.wear.watchface.ComplicationSlot complicationSlot, @Px int textureWidth, @Px int textureHeight, int textureType);
method public void bind();
- method public androidx.wear.watchface.CanvasComplication getCanvasComplication();
+ method public androidx.wear.watchface.ComplicationSlot getComplicationSlot();
method public void renderToTexture(android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters);
- property public final androidx.wear.watchface.CanvasComplication canvasComplication;
+ property public final androidx.wear.watchface.ComplicationSlot complicationSlot;
}
}
diff --git a/wear/wear-watchface-complications-rendering/api/public_plus_experimental_current.txt b/wear/wear-watchface-complications-rendering/api/public_plus_experimental_current.txt
index 897ab96..f8923f6 100644
--- a/wear/wear-watchface-complications-rendering/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-complications-rendering/api/public_plus_experimental_current.txt
@@ -6,13 +6,10 @@
method public void drawHighlight(android.graphics.Canvas canvas, android.graphics.Rect bounds, int boundsType, android.icu.util.Calendar calendar, @ColorInt int color);
method public androidx.wear.complications.data.ComplicationData? getData();
method public final androidx.wear.watchface.complications.rendering.ComplicationDrawable getDrawable();
- method public boolean isHighlighted();
method @CallSuper public void loadData(androidx.wear.complications.data.ComplicationData? complicationData, boolean loadDrawablesAsynchronous);
- method public void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters);
+ method public void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters, int slotId);
method public final void setDrawable(androidx.wear.watchface.complications.rendering.ComplicationDrawable value);
- method public void setHighlighted(boolean value);
property public final androidx.wear.watchface.complications.rendering.ComplicationDrawable drawable;
- property public boolean isHighlighted;
}
public final class ComplicationDrawable extends android.graphics.drawable.Drawable {
@@ -145,11 +142,11 @@
}
public final class GlesTextureComplication {
- ctor public GlesTextureComplication(androidx.wear.watchface.CanvasComplication canvasComplication, @Px int textureWidth, @Px int textureHeight, int textureType);
+ ctor public GlesTextureComplication(androidx.wear.watchface.ComplicationSlot complicationSlot, @Px int textureWidth, @Px int textureHeight, int textureType);
method public void bind();
- method public androidx.wear.watchface.CanvasComplication getCanvasComplication();
+ method public androidx.wear.watchface.ComplicationSlot getComplicationSlot();
method public void renderToTexture(android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters);
- property public final androidx.wear.watchface.CanvasComplication canvasComplication;
+ property public final androidx.wear.watchface.ComplicationSlot complicationSlot;
}
}
diff --git a/wear/wear-watchface-complications-rendering/api/restricted_current.txt b/wear/wear-watchface-complications-rendering/api/restricted_current.txt
index a1772fa..8781006 100644
--- a/wear/wear-watchface-complications-rendering/api/restricted_current.txt
+++ b/wear/wear-watchface-complications-rendering/api/restricted_current.txt
@@ -6,13 +6,10 @@
method public void drawHighlight(android.graphics.Canvas canvas, android.graphics.Rect bounds, int boundsType, android.icu.util.Calendar calendar, @ColorInt int color);
method public androidx.wear.complications.data.ComplicationData? getData();
method public final androidx.wear.watchface.complications.rendering.ComplicationDrawable getDrawable();
- method public boolean isHighlighted();
method @CallSuper public void loadData(androidx.wear.complications.data.ComplicationData? complicationData, boolean loadDrawablesAsynchronous);
- method public void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters);
+ method public void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters, int slotId);
method public final void setDrawable(androidx.wear.watchface.complications.rendering.ComplicationDrawable value);
- method public void setHighlighted(boolean value);
property public final androidx.wear.watchface.complications.rendering.ComplicationDrawable drawable;
- property public boolean isHighlighted;
}
public final class ComplicationDrawable extends android.graphics.drawable.Drawable {
@@ -148,11 +145,11 @@
}
public final class GlesTextureComplication {
- ctor public GlesTextureComplication(androidx.wear.watchface.CanvasComplication canvasComplication, @Px int textureWidth, @Px int textureHeight, int textureType);
+ ctor public GlesTextureComplication(androidx.wear.watchface.ComplicationSlot complicationSlot, @Px int textureWidth, @Px int textureHeight, int textureType);
method public void bind();
- method public androidx.wear.watchface.CanvasComplication getCanvasComplication();
+ method public androidx.wear.watchface.ComplicationSlot getComplicationSlot();
method public void renderToTexture(android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters);
- property public final androidx.wear.watchface.CanvasComplication canvasComplication;
+ property public final androidx.wear.watchface.ComplicationSlot complicationSlot;
}
}
diff --git a/wear/wear-watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/CanvasComplicationDrawable.kt b/wear/wear-watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/CanvasComplicationDrawable.kt
index fc971bc..dc02aaf 100644
--- a/wear/wear-watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/CanvasComplicationDrawable.kt
+++ b/wear/wear-watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/CanvasComplicationDrawable.kt
@@ -28,6 +28,7 @@
import androidx.wear.complications.data.ComplicationData
import androidx.wear.utility.TraceEvent
import androidx.wear.watchface.CanvasComplication
+import androidx.wear.watchface.DrawMode
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.WatchState
import androidx.wear.watchface.data.ComplicationSlotBoundsType
@@ -93,31 +94,25 @@
// update.
value.setComplicationData(field.complicationData, false)
field = value
- value.isInAmbientMode = watchState.isAmbient.value
value.isLowBitAmbient = watchState.hasLowBitAmbient
value.isBurnInProtectionOn = watchState.hasBurnInProtection
}
- init {
- // This observer needs to use the property drawable defined above, not the constructor
- // argument with the same name.
- watchState.isAmbient.addObserver {
- this.drawable.isInAmbientMode = it
- }
- }
-
override fun render(
canvas: Canvas,
bounds: Rect,
calendar: Calendar,
- renderParameters: RenderParameters
+ renderParameters: RenderParameters,
+ slotId: Int
) {
if (!renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
return
}
+ drawable.isInAmbientMode = renderParameters.drawMode == DrawMode.AMBIENT
drawable.bounds = bounds
drawable.currentTimeMillis = calendar.timeInMillis
+ drawable.isHighlighted = renderParameters.pressedComplicationSlotIds.contains(slotId)
drawable.draw(canvas)
}
@@ -137,12 +132,6 @@
}
}
- public override var isHighlighted: Boolean
- get() = drawable.isHighlighted
- set(value) {
- drawable.isHighlighted = value
- }
-
private var _data: ComplicationData? = null
/** Returns the [ComplicationData] to render with. */
@@ -170,4 +159,4 @@
loadDrawablesAsynchronous
)
}
-}
\ No newline at end of file
+}
diff --git a/wear/wear-watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/GlesTextureComplication.kt b/wear/wear-watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/GlesTextureComplication.kt
index 3c5287b..c7b9a45 100644
--- a/wear/wear-watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/GlesTextureComplication.kt
+++ b/wear/wear-watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/GlesTextureComplication.kt
@@ -24,20 +24,20 @@
import android.opengl.GLES20
import android.opengl.GLUtils
import androidx.annotation.Px
-import androidx.wear.watchface.CanvasComplication
+import androidx.wear.watchface.ComplicationSlot
import androidx.wear.watchface.RenderParameters
/**
- * Helper for rendering a [CanvasComplication] to a GLES20 texture. To use call [renderToTexture]
+ * Helper for rendering a [ComplicationSlot] to a GLES20 texture. To use call [renderToTexture]
* and then [bind] before drawing.
*
- * @param canvasComplication The [CanvasComplication] to render to texture.
+ * @param complicationSlot The [ComplicationSlot] to render to texture.
* @param textureWidth The width of the texture in pixels to create.
* @param textureHeight The height of the texture in pixels to create.
* @param textureType The texture type, e.g. [GLES20.GL_TEXTURE_2D].
*/
public class GlesTextureComplication(
- public val canvasComplication: CanvasComplication,
+ public val complicationSlot: ComplicationSlot,
@Px textureWidth: Int,
@Px textureHeight: Int,
private val textureType: Int
@@ -51,10 +51,16 @@
private val canvas = Canvas(bitmap)
private val bounds = Rect(0, 0, textureWidth, textureHeight)
- /** Renders [canvasComplication] to an OpenGL texture. */
+ /** Renders [complicationSlot] to an OpenGL texture. */
public fun renderToTexture(calendar: Calendar, renderParameters: RenderParameters) {
canvas.drawColor(Color.BLACK)
- canvasComplication.render(canvas, bounds, calendar, renderParameters)
+ complicationSlot.renderer.render(
+ canvas,
+ bounds,
+ calendar,
+ renderParameters,
+ complicationSlot.id
+ )
bind()
GLUtils.texImage2D(textureType, 0, bitmap, 0)
}
diff --git a/wear/wear-watchface-data/api/restricted_current.txt b/wear/wear-watchface-data/api/restricted_current.txt
index f0b0b20..0989e7d 100644
--- a/wear/wear-watchface-data/api/restricted_current.txt
+++ b/wear/wear-watchface-data/api/restricted_current.txt
@@ -301,7 +301,7 @@
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class RenderParametersWireFormat implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
- ctor public RenderParametersWireFormat(int, int, int, int, String?, @ColorInt int, @ColorInt int);
+ ctor public RenderParametersWireFormat(int, int, int, int, String?, @ColorInt int, @ColorInt int, int[]);
method public int describeContents();
method @ColorInt public int getBackgroundTint();
method public int getDrawMode();
@@ -309,6 +309,7 @@
method public int getElementType();
method public String? getElementUserStyleSettingId();
method @ColorInt public int getHighlightTint();
+ method public int[] getPressedComplicationSlotIds();
method public int getWatchFaceLayerSetBitfield();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<androidx.wear.watchface.data.RenderParametersWireFormat!>! CREATOR;
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/RenderParametersWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/RenderParametersWireFormat.java
index 03f3529..59359aa 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/RenderParametersWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/RenderParametersWireFormat.java
@@ -108,6 +108,11 @@
@ColorInt
int mBackgroundTint;
+ /** Optional set of ComplicationSlots to render as pressed. */
+ @ParcelField(8)
+ @NonNull
+ int[] mPressedComplicationSlotIds = new int[0];
+
RenderParametersWireFormat() {
}
@@ -118,7 +123,8 @@
int complicationSlotId,
@Nullable String elementUserStyleSettingId,
@ColorInt int highlightTint,
- @ColorInt int backgroundTint) {
+ @ColorInt int backgroundTint,
+ @NonNull int[] pressedComplicationSlotIds) {
mDrawMode = drawMode;
mWatchFaceLayerSetBitfield = watchFaceLayerSetBitfield;
mElementType = elementType;
@@ -126,6 +132,7 @@
mElementUserStyleSettingId = elementUserStyleSettingId;
mHighlightTint = highlightTint;
mBackgroundTint = backgroundTint;
+ mPressedComplicationSlotIds = pressedComplicationSlotIds;
if (elementType == ELEMENT_TYPE_USER_STYLE) {
if (elementUserStyleSettingId == null) {
throw new IllegalArgumentException(
@@ -172,6 +179,11 @@
return mBackgroundTint;
}
+ @NonNull
+ public int[] getPressedComplicationSlotIds() {
+ return mPressedComplicationSlotIds;
+ }
+
/** Serializes this IndicatorState to the specified {@link Parcel}. */
@Override
public void writeToParcel(@NonNull Parcel parcel, int flags) {
diff --git a/wear/wear-watchface/api/current.txt b/wear/wear-watchface/api/current.txt
index 2e16536..20299ac 100644
--- a/wear/wear-watchface/api/current.txt
+++ b/wear/wear-watchface/api/current.txt
@@ -9,12 +9,9 @@
public interface CanvasComplication {
method public void drawHighlight(android.graphics.Canvas canvas, android.graphics.Rect bounds, int boundsType, android.icu.util.Calendar calendar, @ColorInt int color);
method public androidx.wear.complications.data.ComplicationData? getData();
- method public boolean isHighlighted();
method public void loadData(androidx.wear.complications.data.ComplicationData? complicationData, boolean loadDrawablesAsynchronous);
method @WorkerThread public default void onRendererCreated(androidx.wear.watchface.Renderer renderer);
- method @UiThread public void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters);
- method public void setIsHighlighted(boolean isHighlighted);
- property public abstract boolean isHighlighted;
+ method @UiThread public void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters, int slotId);
}
public static interface CanvasComplication.InvalidateCallback {
@@ -89,8 +86,10 @@
method public androidx.wear.watchface.ComplicationSlot? getBackgroundComplicationSlot();
method public androidx.wear.watchface.ComplicationSlot? getComplicationSlotAt(@Px int x, @Px int y);
method public java.util.Map<java.lang.Integer,androidx.wear.watchface.ComplicationSlot> getComplicationSlots();
+ method public java.util.Set<java.lang.Integer> getPressedSlotIds();
method @UiThread public void removeTapListener(androidx.wear.watchface.ComplicationSlotsManager.TapCallback tapCallback);
property public final java.util.Map<java.lang.Integer,androidx.wear.watchface.ComplicationSlot> complicationSlots;
+ property public final java.util.Set<java.lang.Integer> pressedSlotIds;
}
public static interface ComplicationSlotsManager.TapCallback {
@@ -147,13 +146,16 @@
}
public final class RenderParameters {
+ ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Set<? extends androidx.wear.watchface.style.WatchFaceLayer> watchFaceLayers, optional androidx.wear.watchface.RenderParameters.HighlightLayer? highlightLayer, optional java.util.Set<java.lang.Integer> pressedComplicationSlotIds);
ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Set<? extends androidx.wear.watchface.style.WatchFaceLayer> watchFaceLayers, optional androidx.wear.watchface.RenderParameters.HighlightLayer? highlightLayer);
ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Set<? extends androidx.wear.watchface.style.WatchFaceLayer> watchFaceLayers);
method public androidx.wear.watchface.DrawMode getDrawMode();
method public androidx.wear.watchface.RenderParameters.HighlightLayer? getHighlightLayer();
+ method public java.util.Set<java.lang.Integer> getPressedComplicationSlotIds();
method public java.util.Set<androidx.wear.watchface.style.WatchFaceLayer> getWatchFaceLayers();
property public final androidx.wear.watchface.DrawMode drawMode;
property public final androidx.wear.watchface.RenderParameters.HighlightLayer? highlightLayer;
+ property public final java.util.Set<java.lang.Integer> pressedComplicationSlotIds;
property public final java.util.Set<androidx.wear.watchface.style.WatchFaceLayer> watchFaceLayers;
field public static final androidx.wear.watchface.RenderParameters.Companion Companion;
field public static final androidx.wear.watchface.RenderParameters DEFAULT_INTERACTIVE;
diff --git a/wear/wear-watchface/api/public_plus_experimental_current.txt b/wear/wear-watchface/api/public_plus_experimental_current.txt
index 2e16536..20299ac 100644
--- a/wear/wear-watchface/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface/api/public_plus_experimental_current.txt
@@ -9,12 +9,9 @@
public interface CanvasComplication {
method public void drawHighlight(android.graphics.Canvas canvas, android.graphics.Rect bounds, int boundsType, android.icu.util.Calendar calendar, @ColorInt int color);
method public androidx.wear.complications.data.ComplicationData? getData();
- method public boolean isHighlighted();
method public void loadData(androidx.wear.complications.data.ComplicationData? complicationData, boolean loadDrawablesAsynchronous);
method @WorkerThread public default void onRendererCreated(androidx.wear.watchface.Renderer renderer);
- method @UiThread public void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters);
- method public void setIsHighlighted(boolean isHighlighted);
- property public abstract boolean isHighlighted;
+ method @UiThread public void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters, int slotId);
}
public static interface CanvasComplication.InvalidateCallback {
@@ -89,8 +86,10 @@
method public androidx.wear.watchface.ComplicationSlot? getBackgroundComplicationSlot();
method public androidx.wear.watchface.ComplicationSlot? getComplicationSlotAt(@Px int x, @Px int y);
method public java.util.Map<java.lang.Integer,androidx.wear.watchface.ComplicationSlot> getComplicationSlots();
+ method public java.util.Set<java.lang.Integer> getPressedSlotIds();
method @UiThread public void removeTapListener(androidx.wear.watchface.ComplicationSlotsManager.TapCallback tapCallback);
property public final java.util.Map<java.lang.Integer,androidx.wear.watchface.ComplicationSlot> complicationSlots;
+ property public final java.util.Set<java.lang.Integer> pressedSlotIds;
}
public static interface ComplicationSlotsManager.TapCallback {
@@ -147,13 +146,16 @@
}
public final class RenderParameters {
+ ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Set<? extends androidx.wear.watchface.style.WatchFaceLayer> watchFaceLayers, optional androidx.wear.watchface.RenderParameters.HighlightLayer? highlightLayer, optional java.util.Set<java.lang.Integer> pressedComplicationSlotIds);
ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Set<? extends androidx.wear.watchface.style.WatchFaceLayer> watchFaceLayers, optional androidx.wear.watchface.RenderParameters.HighlightLayer? highlightLayer);
ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Set<? extends androidx.wear.watchface.style.WatchFaceLayer> watchFaceLayers);
method public androidx.wear.watchface.DrawMode getDrawMode();
method public androidx.wear.watchface.RenderParameters.HighlightLayer? getHighlightLayer();
+ method public java.util.Set<java.lang.Integer> getPressedComplicationSlotIds();
method public java.util.Set<androidx.wear.watchface.style.WatchFaceLayer> getWatchFaceLayers();
property public final androidx.wear.watchface.DrawMode drawMode;
property public final androidx.wear.watchface.RenderParameters.HighlightLayer? highlightLayer;
+ property public final java.util.Set<java.lang.Integer> pressedComplicationSlotIds;
property public final java.util.Set<androidx.wear.watchface.style.WatchFaceLayer> watchFaceLayers;
field public static final androidx.wear.watchface.RenderParameters.Companion Companion;
field public static final androidx.wear.watchface.RenderParameters DEFAULT_INTERACTIVE;
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index 2263f8d..ca81607 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -35,12 +35,9 @@
public interface CanvasComplication {
method public void drawHighlight(android.graphics.Canvas canvas, android.graphics.Rect bounds, @androidx.wear.watchface.data.ComplicationSlotBoundsType int boundsType, android.icu.util.Calendar calendar, @ColorInt int color);
method public androidx.wear.complications.data.ComplicationData? getData();
- method public boolean isHighlighted();
method public void loadData(androidx.wear.complications.data.ComplicationData? complicationData, boolean loadDrawablesAsynchronous);
method @WorkerThread public default void onRendererCreated(androidx.wear.watchface.Renderer renderer);
- method @UiThread public void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters);
- method public void setIsHighlighted(boolean isHighlighted);
- property public abstract boolean isHighlighted;
+ method @UiThread public void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, androidx.wear.watchface.RenderParameters renderParameters, int slotId);
}
public static interface CanvasComplication.InvalidateCallback {
@@ -138,8 +135,10 @@
method public androidx.wear.watchface.ComplicationSlot? getBackgroundComplicationSlot();
method public androidx.wear.watchface.ComplicationSlot? getComplicationSlotAt(@Px int x, @Px int y);
method public java.util.Map<java.lang.Integer,androidx.wear.watchface.ComplicationSlot> getComplicationSlots();
+ method public java.util.Set<java.lang.Integer> getPressedSlotIds();
method @UiThread public void removeTapListener(androidx.wear.watchface.ComplicationSlotsManager.TapCallback tapCallback);
property public final java.util.Map<java.lang.Integer,androidx.wear.watchface.ComplicationSlot> complicationSlots;
+ property public final java.util.Set<java.lang.Integer> pressedSlotIds;
field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @VisibleForTesting public androidx.wear.watchface.WatchState watchState;
}
@@ -228,15 +227,18 @@
}
public final class RenderParameters {
+ ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Set<? extends androidx.wear.watchface.style.WatchFaceLayer> watchFaceLayers, optional androidx.wear.watchface.RenderParameters.HighlightLayer? highlightLayer, optional java.util.Set<java.lang.Integer> pressedComplicationSlotIds);
ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Set<? extends androidx.wear.watchface.style.WatchFaceLayer> watchFaceLayers, optional androidx.wear.watchface.RenderParameters.HighlightLayer? highlightLayer);
ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Set<? extends androidx.wear.watchface.style.WatchFaceLayer> watchFaceLayers);
ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public RenderParameters(androidx.wear.watchface.data.RenderParametersWireFormat wireFormat);
method public androidx.wear.watchface.DrawMode getDrawMode();
method public androidx.wear.watchface.RenderParameters.HighlightLayer? getHighlightLayer();
+ method public java.util.Set<java.lang.Integer> getPressedComplicationSlotIds();
method public java.util.Set<androidx.wear.watchface.style.WatchFaceLayer> getWatchFaceLayers();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.data.RenderParametersWireFormat toWireFormat();
property public final androidx.wear.watchface.DrawMode drawMode;
property public final androidx.wear.watchface.RenderParameters.HighlightLayer? highlightLayer;
+ property public final java.util.Set<java.lang.Integer> pressedComplicationSlotIds;
property public final java.util.Set<androidx.wear.watchface.style.WatchFaceLayer> watchFaceLayers;
field public static final androidx.wear.watchface.RenderParameters.Companion Companion;
field public static final androidx.wear.watchface.RenderParameters DEFAULT_INTERACTIVE;
diff --git a/wear/wear-watchface/samples/app/build.gradle b/wear/wear-watchface/samples/app/build.gradle
index 6ac4a42..056b9a9 100644
--- a/wear/wear-watchface/samples/app/build.gradle
+++ b/wear/wear-watchface/samples/app/build.gradle
@@ -42,11 +42,19 @@
}
buildTypes {
- release {
- minifyEnabled true
- shrinkResources true
- proguardFiles getDefaultProguardFile('proguard-android.txt')
- }
+ release {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android.txt')
+ }
+
+ /*
+ Release and debug targets will have different package names. Package name for debug version
+ will have suffix .debug, so this should be appended to package name when i.e. uninstalling.
+ */
+ debug {
+ applicationIdSuffix ".debug"
+ }
}
compileOptions {
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
index 64d50b6..76a4aa2 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
@@ -337,7 +337,7 @@
) // up vector
complicationTexture = GlesTextureComplication(
- complicationSlot.renderer,
+ complicationSlot,
128,
128,
GLES20.GL_TEXTURE_2D
diff --git a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
index c718ba4..bcc5c10 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
@@ -244,7 +244,7 @@
ComplicationRenderParams(
EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
RenderParameters(
- DrawMode.AMBIENT,
+ DrawMode.INTERACTIVE,
WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
null,
).toWireFormat(),
diff --git a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
index 87d91b8..314edbe 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -16,6 +16,7 @@
package androidx.wear.watchface.test
+import android.annotation.SuppressLint
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
@@ -533,6 +534,7 @@
bitmap.assertAgainstGolden(screenshotRule, "ambient_screenshot2")
}
+ @SuppressLint("NewApi")
@Test
public fun testCommandTakeScreenShot() {
val latch = CountDownLatch(1)
@@ -562,10 +564,11 @@
assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue()
bitmap!!.assertAgainstGolden(
screenshotRule,
- "ambient_screenshot"
+ "testCommandTakeScreenShot"
)
}
+ @SuppressLint("NewApi")
@Test
public fun testCommandTakeOpenGLScreenShot() {
val latch = CountDownLatch(1)
@@ -619,6 +622,7 @@
bitmap.assertAgainstGolden(screenshotRule, "green_screenshot")
}
+ @SuppressLint("NewApi")
@Test
public fun testHighlightAllComplicationsInScreenshot() {
val latch = CountDownLatch(1)
@@ -657,6 +661,43 @@
)
}
+ @SuppressLint("NewApi")
+ @Test
+ public fun testRenderLeftComplicationPressed() {
+ val latch = CountDownLatch(1)
+
+ handler.post(this::initCanvasWatchFace)
+ assertThat(initLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue()
+ sendComplications()
+
+ var bitmap: Bitmap? = null
+ handler.post {
+ bitmap = SharedMemoryImage.ashmemReadImageBundle(
+ interactiveWatchFaceInstance.renderWatchFaceToBitmap(
+ WatchFaceRenderParams(
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+ null,
+ setOf(EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID)
+ ).toWireFormat(),
+ 123456789,
+ null,
+ null
+ )
+ )
+ )
+ latch.countDown()
+ }
+
+ assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue()
+ bitmap!!.assertAgainstGolden(
+ screenshotRule,
+ "left_complication_pressed"
+ )
+ }
+
+ @SuppressLint("NewApi")
@Test
public fun testHighlightRightComplicationInScreenshot() {
val latch = CountDownLatch(1)
@@ -697,6 +738,7 @@
)
}
+ @SuppressLint("NewApi")
@Test
public fun testScreenshotWithPreviewComplicationData() {
val latch = CountDownLatch(1)
@@ -812,6 +854,7 @@
}
}
+ @SuppressLint("NewApi")
@Test
public fun complicationTapLaunchesActivity() {
handler.post(this::initCanvasWatchFace)
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index f72ed54..da6551a 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -69,13 +69,15 @@
* @param bounds A [Rect] describing the bounds of the complication
* @param calendar The current [Calendar]
* @param renderParameters The current [RenderParameters]
+ * @param slotId The Id of the [ComplicationSlot] being rendered
*/
@UiThread
public fun render(
canvas: Canvas,
bounds: Rect,
calendar: Calendar,
- renderParameters: RenderParameters
+ renderParameters: RenderParameters,
+ slotId: Int
)
/**
@@ -97,15 +99,6 @@
@ColorInt color: Int
)
- /**
- * Whether the complication should be drawn highlighted. This is to provide visual feedback when
- * the user taps on a complication.
- */
- @Suppress("INAPPLICABLE_JVM_NAME") // https://stackoverflow.com/questions/47504279
- @get:JvmName("isHighlighted")
- @set:JvmName("setIsHighlighted")
- public var isHighlighted: Boolean
-
/** Returns the [ComplicationData] to render with. */
public fun getData(): ComplicationData?
@@ -612,7 +605,7 @@
renderParameters: RenderParameters
) {
val bounds = computeBounds(Rect(0, 0, canvas.width, canvas.height))
- renderer.render(canvas, bounds, calendar, renderParameters)
+ renderer.render(canvas, bounds, calendar, renderParameters, id)
}
/**
@@ -661,16 +654,6 @@
}
}
- /**
- * Sets whether the complication should be drawn highlighted or not. This is to provide visual
- * feedback when the user taps on a complication.
- *
- * @param highlight Whether or not the complication should be drawn highlighted.
- */
- internal fun setIsHighlighted(highlight: Boolean) {
- renderer.isHighlighted = highlight
- }
-
internal fun init(invalidateListener: InvalidateListener) {
this.invalidateListener = invalidateListener
}
@@ -699,7 +682,6 @@
writer.increaseIndent()
writer.println("fixedComplicationDataSource=$fixedComplicationDataSource")
writer.println("enabled=$enabled")
- writer.println("renderer.isHighlighted=${renderer.isHighlighted}")
writer.println("boundsType=$boundsType")
writer.println("configExtras=$configExtras")
writer.println("supportedTypes=${supportedTypes.joinToString { it.toString() }}")
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
index e056e16..9246c09 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
@@ -91,6 +91,9 @@
public val complicationSlots: Map<Int, ComplicationSlot> =
complicationSlotCollection.associateBy(ComplicationSlot::id)
+ /** The set of slot ids that are rendered as pressed. */
+ public val pressedSlotIds: Set<Int> = HashSet()
+
private class InitialComplicationConfig(
val complicationSlotBounds: ComplicationSlotBounds,
val enabled: Boolean,
@@ -283,17 +286,17 @@
*/
@UiThread
public fun displayPressedAnimation(complicationSlotId: Int) {
- val complication = requireNotNull(complicationSlots[complicationSlotId]) {
+ requireNotNull(complicationSlots[complicationSlotId]) {
"No complication found with ID $complicationSlotId"
}
- complication.setIsHighlighted(true)
+ (pressedSlotIds as HashSet<Int>).add(complicationSlotId)
val weakRef = WeakReference(this)
watchFaceHostApi.getUiThreadHandler().postDelayed(
{
// The watch face might go away before this can run.
if (weakRef.get() != null) {
- complication.setIsHighlighted(false)
+ pressedSlotIds.remove(complicationSlotId)
}
},
WatchFaceImpl.CANCEL_COMPLICATION_HIGHLIGHTED_DELAY_MS
@@ -379,6 +382,7 @@
@UiThread
internal fun dump(writer: IndentingPrintWriter) {
writer.println("ComplicationSlotsManager:")
+ writer.println("renderer.pressedSlotIds=${pressedSlotIds.joinToString()}")
writer.increaseIndent()
for ((_, complication) in complicationSlots) {
complication.dump(writer)
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/RenderParameters.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/RenderParameters.kt
index dd06ae0..8b66e7f 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/RenderParameters.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/RenderParameters.kt
@@ -81,11 +81,13 @@
* @param highlightLayer Optional [HighlightLayer] used by editors to visually highlight an
* aspect of the watch face. Rendered last on top of [watchFaceLayers]. If highlighting isn't needed
* this will be `null`.
+ * @param pressedComplicationSlotIds A set of [ComplicationSlot.id]s to render as pressed.
*/
public class RenderParameters @JvmOverloads constructor(
public val drawMode: DrawMode,
public val watchFaceLayers: Set<WatchFaceLayer>,
- public val highlightLayer: HighlightLayer? = null
+ public val highlightLayer: HighlightLayer? = null,
+ public val pressedComplicationSlotIds: Set<Int> = emptySet()
) {
init {
require(watchFaceLayers.isNotEmpty() || highlightLayer != null) {
@@ -240,7 +242,8 @@
}
else -> null
- }
+ },
+ wireFormat.pressedComplicationSlotIds.toSet()
)
/** @hide */
@@ -254,7 +257,8 @@
0,
null,
highlightLayer!!.highlightTint,
- highlightLayer.backgroundTint
+ highlightLayer.backgroundTint,
+ pressedComplicationSlotIds.toIntArray()
)
is HighlightedElement.ComplicationSlot -> RenderParametersWireFormat(
@@ -264,7 +268,8 @@
thingHighlighted.id,
null,
highlightLayer!!.highlightTint,
- highlightLayer.backgroundTint
+ highlightLayer.backgroundTint,
+ pressedComplicationSlotIds.toIntArray()
)
is HighlightedElement.UserStyle -> RenderParametersWireFormat(
@@ -274,7 +279,8 @@
0,
thingHighlighted.id.value,
highlightLayer!!.highlightTint,
- highlightLayer.backgroundTint
+ highlightLayer.backgroundTint,
+ pressedComplicationSlotIds.toIntArray()
)
else -> RenderParametersWireFormat(
@@ -284,7 +290,8 @@
0,
null,
Color.BLACK,
- Color.BLACK
+ Color.BLACK,
+ pressedComplicationSlotIds.toIntArray()
)
}
@@ -342,6 +349,7 @@
if (drawMode != other.drawMode) return false
if (watchFaceLayers != other.watchFaceLayers) return false
if (highlightLayer != other.highlightLayer) return false
+ if (pressedComplicationSlotIds != other.pressedComplicationSlotIds) return false
return true
}
@@ -350,6 +358,7 @@
var result = drawMode.hashCode()
result = 31 * result + watchFaceLayers.hashCode()
result = 31 * result + (highlightLayer?.hashCode() ?: 0)
+ result = 31 * result + pressedComplicationSlotIds.hashCode()
return result
}
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index 4af16d0..c19817e 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -435,29 +435,40 @@
// available programmatically. The value below is the default but it could be overridden
// by OEMs.
internal const val INITIAL_LOW_BATTERY_THRESHOLD = 15.0f
-
- internal val defaultRenderParametersForDrawMode: HashMap<DrawMode, RenderParameters> =
- hashMapOf(
- DrawMode.AMBIENT to
- RenderParameters(
- DrawMode.AMBIENT, WatchFaceLayer.ALL_WATCH_FACE_LAYERS, null
- ),
- DrawMode.INTERACTIVE to
- RenderParameters(
- DrawMode.INTERACTIVE, WatchFaceLayer.ALL_WATCH_FACE_LAYERS, null
- ),
- DrawMode.LOW_BATTERY_INTERACTIVE to
- RenderParameters(
- DrawMode.LOW_BATTERY_INTERACTIVE, WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
- null
- ),
- DrawMode.MUTE to
- RenderParameters(
- DrawMode.MUTE, WatchFaceLayer.ALL_WATCH_FACE_LAYERS, null
- ),
- )
}
+ private val defaultRenderParametersForDrawMode: HashMap<DrawMode, RenderParameters> =
+ hashMapOf(
+ DrawMode.AMBIENT to
+ RenderParameters(
+ DrawMode.AMBIENT,
+ WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+ null,
+ complicationSlotsManager.pressedSlotIds
+ ),
+ DrawMode.INTERACTIVE to
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+ null,
+ complicationSlotsManager.pressedSlotIds
+ ),
+ DrawMode.LOW_BATTERY_INTERACTIVE to
+ RenderParameters(
+ DrawMode.LOW_BATTERY_INTERACTIVE,
+ WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+ null,
+ complicationSlotsManager.pressedSlotIds
+ ),
+ DrawMode.MUTE to
+ RenderParameters(
+ DrawMode.MUTE,
+ WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+ null,
+ complicationSlotsManager.pressedSlotIds
+ ),
+ )
+
private val systemTimeProvider = watchface.systemTimeProvider
private val legacyWatchFaceStyle = watchface.legacyWatchFaceStyle
internal val renderer = watchface.renderer
@@ -1019,7 +1030,8 @@
Canvas(complicationBitmap),
Rect(0, 0, bounds.width(), bounds.height()),
calendar,
- RenderParameters(params.renderParametersWireFormat)
+ RenderParameters(params.renderParametersWireFormat),
+ params.complicationSlotId
)
// Restore previous ComplicationData & style if required.
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/RenderParametersTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/RenderParametersTest.kt
index 18a2cec..bc881a2 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/RenderParametersTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/RenderParametersTest.kt
@@ -252,4 +252,4 @@
assertThat(renderParameters3a).isNotEqualTo(renderParameters3c)
assertThat(renderParameters3a).isNotEqualTo(renderParameters4a)
}
-}
\ No newline at end of file
+}
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index ba939fa..1f72f17 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -720,27 +720,27 @@
UserStyleSchema(emptyList())
)
- assertThat(complicationDrawableLeft.isHighlighted).isFalse()
- assertThat(complicationDrawableRight.isHighlighted).isFalse()
+ assertThat(complicationSlotsManager.pressedSlotIds).doesNotContain(LEFT_COMPLICATION_ID)
+ assertThat(complicationSlotsManager.pressedSlotIds).doesNotContain(RIGHT_COMPLICATION_ID)
// Tap left complication.
tapAt(30, 50)
- assertThat(complicationDrawableLeft.isHighlighted).isTrue()
+ assertThat(complicationSlotsManager.pressedSlotIds).contains(LEFT_COMPLICATION_ID)
assertThat(testWatchFaceService.tappedComplicationSlotIds)
.isEqualTo(listOf(LEFT_COMPLICATION_ID))
runPostedTasksFor(WatchFaceImpl.CANCEL_COMPLICATION_HIGHLIGHTED_DELAY_MS)
- assertThat(complicationDrawableLeft.isHighlighted).isFalse()
+ assertThat(complicationSlotsManager.pressedSlotIds).doesNotContain(LEFT_COMPLICATION_ID)
// Tap right complication.
testWatchFaceService.reset()
tapAt(70, 50)
- assertThat(complicationDrawableRight.isHighlighted).isTrue()
+ assertThat(complicationSlotsManager.pressedSlotIds).contains(RIGHT_COMPLICATION_ID)
assertThat(testWatchFaceService.tappedComplicationSlotIds)
.isEqualTo(listOf(RIGHT_COMPLICATION_ID))
runPostedTasksFor(WatchFaceImpl.CANCEL_COMPLICATION_HIGHLIGHTED_DELAY_MS)
- assertThat(complicationDrawableLeft.isHighlighted).isFalse()
+ assertThat(complicationSlotsManager.pressedSlotIds).doesNotContain(LEFT_COMPLICATION_ID)
// Tap on blank space.
testWatchFaceService.reset()
@@ -748,7 +748,7 @@
assertThat(testWatchFaceService.tappedComplicationSlotIds).isEmpty()
runPostedTasksFor(WatchFaceImpl.CANCEL_COMPLICATION_HIGHLIGHTED_DELAY_MS)
- assertThat(complicationDrawableLeft.isHighlighted).isFalse()
+ assertThat(complicationSlotsManager.pressedSlotIds).doesNotContain(LEFT_COMPLICATION_ID)
assertThat(testWatchFaceService.tappedComplicationSlotIds).isEmpty()
}
@@ -760,21 +760,21 @@
UserStyleSchema(emptyList())
)
- assertThat(complicationDrawableLeft.isHighlighted).isFalse()
- assertThat(complicationDrawableRight.isHighlighted).isFalse()
+ assertThat(complicationSlotsManager.pressedSlotIds).doesNotContain(LEFT_COMPLICATION_ID)
+ assertThat(complicationSlotsManager.pressedSlotIds).doesNotContain(RIGHT_COMPLICATION_ID)
// Rapidly tap left then right complication.
tapAt(30, 50)
tapAt(70, 50)
// Both complicationSlots get temporarily highlighted.
- assertThat(complicationDrawableLeft.isHighlighted).isTrue()
- assertThat(complicationDrawableRight.isHighlighted).isTrue()
+ assertThat(complicationSlotsManager.pressedSlotIds).contains(LEFT_COMPLICATION_ID)
+ assertThat(complicationSlotsManager.pressedSlotIds).contains(RIGHT_COMPLICATION_ID)
// And the highlight goes away after a delay.
runPostedTasksFor(WatchFaceImpl.CANCEL_COMPLICATION_HIGHLIGHTED_DELAY_MS)
- assertThat(complicationDrawableLeft.isHighlighted).isFalse()
- assertThat(complicationDrawableRight.isHighlighted).isFalse()
+ assertThat(complicationSlotsManager.pressedSlotIds).doesNotContain(LEFT_COMPLICATION_ID)
+ assertThat(complicationSlotsManager.pressedSlotIds).doesNotContain(RIGHT_COMPLICATION_ID)
// Taps are registered on both complicationSlots.
assertThat(testWatchFaceService.tappedComplicationSlotIds)
@@ -804,7 +804,7 @@
tapListener = tapListener
)
- assertThat(complicationDrawableEdge.isHighlighted).isFalse()
+ assertThat(complicationSlotsManager.pressedSlotIds).doesNotContain(EDGE_COMPLICATION_ID)
`when`(
edgeComplicationHitTester.hitTest(
@@ -817,12 +817,12 @@
// Tap the edge complication.
tapAt(0, 50)
- assertThat(complicationDrawableEdge.isHighlighted).isTrue()
+ assertThat(complicationSlotsManager.pressedSlotIds).contains(EDGE_COMPLICATION_ID)
assertThat(testWatchFaceService.tappedComplicationSlotIds)
.isEqualTo(listOf(EDGE_COMPLICATION_ID))
runPostedTasksFor(WatchFaceImpl.CANCEL_COMPLICATION_HIGHLIGHTED_DELAY_MS)
- assertThat(complicationDrawableEdge.isHighlighted).isFalse()
+ assertThat(complicationSlotsManager.pressedSlotIds).doesNotContain(EDGE_COMPLICATION_ID)
}
@Test
diff --git a/wear/wear/api/current.txt b/wear/wear/api/current.txt
index da88d15..3b47016 100644
--- a/wear/wear/api/current.txt
+++ b/wear/wear/api/current.txt
@@ -114,6 +114,10 @@
field public static final String WATCH_FACE_PREVIEW_METADATA_NAME = "com.google.android.wearable.watchface.preview";
}
+ public final class WearTypeHelper {
+ method public static boolean isChinaDevice(android.content.Context);
+ }
+
}
package androidx.wear.widget {
diff --git a/wear/wear/api/public_plus_experimental_current.txt b/wear/wear/api/public_plus_experimental_current.txt
index da88d15..3b47016 100644
--- a/wear/wear/api/public_plus_experimental_current.txt
+++ b/wear/wear/api/public_plus_experimental_current.txt
@@ -114,6 +114,10 @@
field public static final String WATCH_FACE_PREVIEW_METADATA_NAME = "com.google.android.wearable.watchface.preview";
}
+ public final class WearTypeHelper {
+ method public static boolean isChinaDevice(android.content.Context);
+ }
+
}
package androidx.wear.widget {
diff --git a/wear/wear/api/restricted_current.txt b/wear/wear/api/restricted_current.txt
index 89aab1d..c16eeeb 100644
--- a/wear/wear/api/restricted_current.txt
+++ b/wear/wear/api/restricted_current.txt
@@ -114,6 +114,10 @@
field public static final String WATCH_FACE_PREVIEW_METADATA_NAME = "com.google.android.wearable.watchface.preview";
}
+ public final class WearTypeHelper {
+ method public static boolean isChinaDevice(android.content.Context);
+ }
+
}
package androidx.wear.widget {
diff --git a/wear/wear/build.gradle b/wear/wear/build.gradle
index 654eb8a..a379c0b 100644
--- a/wear/wear/build.gradle
+++ b/wear/wear/build.gradle
@@ -30,6 +30,10 @@
testImplementation(libs.testCore)
testImplementation(libs.testRunner)
testImplementation(libs.robolectric)
+ testImplementation(libs.testExtJunit)
+ testImplementation(libs.testRules)
+ testImplementation(libs.mockitoCore)
+ testImplementation(libs.mockitoKotlin)
implementation("androidx.core:core-ktx:1.5.0-alpha04")
diff --git a/wear/wear/src/main/java/androidx/wear/utils/WearTypeHelper.java b/wear/wear/src/main/java/androidx/wear/utils/WearTypeHelper.java
new file mode 100644
index 0000000..bac34b3
--- /dev/null
+++ b/wear/wear/src/main/java/androidx/wear/utils/WearTypeHelper.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.utils;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * Helper class for determining whether the given Wear OS device is for China or rest of the world.
+ */
+public final class WearTypeHelper {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String CHINA_SYSTEM_FEATURE = "cn.google";
+
+ /**
+ * Returns whether the given device is China device.
+ *
+ * This can be used together with
+ * {@code androidx.wear.phone.interactions.PhoneTypeHelper} to
+ * decide what Uri should be used when opening Play Store on connected phone.
+ *
+ * @return True if it is China device and false if it is rest of the world device.
+ */
+ public static boolean isChinaDevice(@NonNull Context context) {
+ return context.getPackageManager().hasSystemFeature(CHINA_SYSTEM_FEATURE);
+ }
+
+ private WearTypeHelper() {}
+}
diff --git a/wear/wear/src/test/java/androidx/wear/utils/WearTypeHelperTest.java b/wear/wear/src/test/java/androidx/wear/utils/WearTypeHelperTest.java
new file mode 100644
index 0000000..555a428
--- /dev/null
+++ b/wear/wear/src/test/java/androidx/wear/utils/WearTypeHelperTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.utils;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadows.ShadowPackageManager;
+
+@RunWith(WearUtilsTestRunner.class)
+@DoNotInstrument // Stop Robolectric instrumenting this class due to it being in package "android".
+public class WearTypeHelperTest {
+ private ShadowPackageManager mShadowPackageManager = null;
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = ApplicationProvider.getApplicationContext();
+ mShadowPackageManager = Shadows.shadowOf(mContext.getPackageManager());
+ }
+
+ private void setSystemFeatureChina(boolean value) {
+ mShadowPackageManager.setSystemFeature(WearTypeHelper.CHINA_SYSTEM_FEATURE, value);
+ }
+
+ @Test
+ @Config(sdk = 28)
+ public void test_isChinaDevice() {
+ setSystemFeatureChina(true);
+
+ assertTrue(WearTypeHelper.isChinaDevice(mContext));
+ }
+
+ @Test
+ @Config(sdk = 28)
+ public void test_isROWDevice() {
+ setSystemFeatureChina(false);
+
+ assertFalse(WearTypeHelper.isChinaDevice(mContext));
+ }
+}
diff --git a/wear/wear/src/test/java/androidx/wear/utils/WearUtilsTestRunner.kt b/wear/wear/src/test/java/androidx/wear/utils/WearUtilsTestRunner.kt
new file mode 100644
index 0000000..6e8c2c2
--- /dev/null
+++ b/wear/wear/src/test/java/androidx/wear/utils/WearUtilsTestRunner.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.wear.utils
+
+import org.junit.runners.model.FrameworkMethod
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.internal.bytecode.InstrumentationConfiguration
+
+/**
+ * A [RobolectricTestRunner] for [androidx.wear.utils] unit tests.
+ *
+ * It has instrumentation turned off for the [androidx.wear.utils] package.
+ *
+ * Robolectric tries to instrument Kotlin classes, and it throws errors when it encounters
+ * companion objects, constructors with default values for parameters, and data classes with
+ * inline classes. We don't need shadowing of our classes because we want to use the actual
+ * objects in our tests.
+ */
+class WearUtilsTestRunner(testClass: Class<*>) : RobolectricTestRunner(testClass) {
+ override fun createClassLoaderConfig(method: FrameworkMethod): InstrumentationConfiguration =
+ InstrumentationConfiguration.Builder(
+ super.createClassLoaderConfig(method)
+ )
+ .doNotInstrumentPackage("androidx.wear.utils")
+ .build()
+}
\ No newline at end of file
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
index 68a57ab..01a3a1a 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
@@ -507,7 +507,6 @@
* HOSTNAME_PATTERN [ ":" PORT ] ]}, each part is explained in the below table:
*
* <table>
- * <col width="25%">
* <tr><th>Rule</th><th>Description</th><th>Example</th></tr>
*
* <tr>
diff --git a/window/window/proguard-rules.pro b/window/window/proguard-rules.pro
index 957f9c6..63c9b44 100644
--- a/window/window/proguard-rules.pro
+++ b/window/window/proguard-rules.pro
@@ -21,7 +21,7 @@
# Keep the whole library for now since there is a crash with a missing method.
# TODO(b/165268619) Make a narrow rule
--keep class androidx.window.window.** { *; }
+-keep class androidx.window.** { *; }
# We also neep to keep sidecar.** for the same reason.
-keep class androidx.window.sidecar.** { *; }
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
index 37207a0..16a795f 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
@@ -24,6 +24,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -194,4 +195,12 @@
verify(runnable, times(MAX_ATTEMPTS)).forceStopRunnable();
verify(handler, times(1)).handleException(any(Throwable.class));
}
+
+ @Test
+ public void test_completeOnMultiProcessChecks() {
+ ForceStopRunnable runnable = spy(mRunnable);
+ doReturn(false).when(runnable).multiProcessChecks();
+ runnable.run();
+ verify(mWorkManager).onForceStopRunnableCompleted();
+ }
}
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java b/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
index c1cc106..4a31178 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
@@ -89,54 +89,59 @@
@Override
public void run() {
- if (!multiProcessChecks()) {
- return;
- }
-
- while (true) {
- // Migrate the database to the no-backup directory if necessary.
- WorkDatabasePathHelper.migrateDatabase(mContext);
- // Clean invalid jobs attributed to WorkManager, and Workers that might have been
- // interrupted because the application crashed (RUNNING state).
- Logger.get().debug(TAG, "Performing cleanup operations.");
- try {
- forceStopRunnable();
- break;
- } catch (SQLiteCantOpenDatabaseException
- | SQLiteDatabaseCorruptException
- | SQLiteDatabaseLockedException
- | SQLiteTableLockedException
- | SQLiteConstraintException
- | SQLiteAccessPermException exception) {
- mRetryCount++;
- if (mRetryCount >= MAX_ATTEMPTS) {
- // ForceStopRunnable is usually the first thing that accesses a database
- // (or an app's internal data directory). This means that weird
- // PackageManager bugs are attributed to ForceStopRunnable, which is
- // unfortunate. This gives the developer a better error
- // message.
- String message = "The file system on the device is in a bad state. "
- + "WorkManager cannot access the app's internal data store.";
- Logger.get().error(TAG, message, exception);
- IllegalStateException throwable = new IllegalStateException(message, exception);
- InitializationExceptionHandler exceptionHandler =
- mWorkManager.getConfiguration().getExceptionHandler();
- if (exceptionHandler != null) {
- Logger.get().debug(TAG,
- "Routing exception to the specified exception handler",
- throwable);
- exceptionHandler.handleException(throwable);
- break;
+ try {
+ if (!multiProcessChecks()) {
+ return;
+ }
+ while (true) {
+ // Migrate the database to the no-backup directory if necessary.
+ WorkDatabasePathHelper.migrateDatabase(mContext);
+ // Clean invalid jobs attributed to WorkManager, and Workers that might have been
+ // interrupted because the application crashed (RUNNING state).
+ Logger.get().debug(TAG, "Performing cleanup operations.");
+ try {
+ forceStopRunnable();
+ break;
+ } catch (SQLiteCantOpenDatabaseException
+ | SQLiteDatabaseCorruptException
+ | SQLiteDatabaseLockedException
+ | SQLiteTableLockedException
+ | SQLiteConstraintException
+ | SQLiteAccessPermException exception) {
+ mRetryCount++;
+ if (mRetryCount >= MAX_ATTEMPTS) {
+ // ForceStopRunnable is usually the first thing that accesses a database
+ // (or an app's internal data directory). This means that weird
+ // PackageManager bugs are attributed to ForceStopRunnable, which is
+ // unfortunate. This gives the developer a better error
+ // message.
+ String message = "The file system on the device is in a bad state. "
+ + "WorkManager cannot access the app's internal data store.";
+ Logger.get().error(TAG, message, exception);
+ IllegalStateException throwable = new IllegalStateException(message,
+ exception);
+ InitializationExceptionHandler exceptionHandler =
+ mWorkManager.getConfiguration().getExceptionHandler();
+ if (exceptionHandler != null) {
+ Logger.get().debug(TAG,
+ "Routing exception to the specified exception handler",
+ throwable);
+ exceptionHandler.handleException(throwable);
+ break;
+ } else {
+ throw throwable;
+ }
} else {
- throw throwable;
+ long duration = mRetryCount * BACKOFF_DURATION_MS;
+ Logger.get()
+ .debug(TAG, String.format("Retrying after %s", duration),
+ exception);
+ sleep(mRetryCount * BACKOFF_DURATION_MS);
}
- } else {
- long duration = mRetryCount * BACKOFF_DURATION_MS;
- Logger.get()
- .debug(TAG, String.format("Retrying after %s", duration), exception);
- sleep(mRetryCount * BACKOFF_DURATION_MS);
}
}
+ } finally {
+ mWorkManager.onForceStopRunnableCompleted();
}
}
@@ -187,7 +192,6 @@
mWorkManager.getWorkDatabase(),
mWorkManager.getSchedulers());
}
- mWorkManager.onForceStopRunnableCompleted();
}
/**